Why another PHP ORM (Object-Relational Mapping) Package?

This package aims to provide only the commonly used data access and manipulation features that most PHP applications need. ORM packages like Propel, Doctrine ORM, Eloquent, etc also provide these features and more features which may never be needed in many PHP applications. LeanOrm's code base is designed to be

For the most part, users of this package will only be interacting with the Model class, one or both of the two Record classes and the Collection class.

LeanOrm is an implementation of the Generic Data Objects (GDAO) package (a package containing an abstract Model class, two interfaces (CollectionInterface and RecordInterface), a RecordList class (a collection data structure that stores only instances of RecordInterface) and a File containing Exception class definitions). The GDAO package was also designed to be compact, easily comprehensible and extensible.

Paris is another PHP ORM that adheres to some of the same principles (a compact code-base and easy comprehensibility) as LeanORM. However, the Active Record pattern (where records contain data, data access & manipulation logic and business logic) implemented in the Paris package leads to tight coupling of database access and application / business logic.

LeanOrm implements the Table Data Gateway and Data Mapper patterns. This allows for very loose coupling. Business logic dealing with a single row of data in a database table can be put in the Record class for the table in question, business logic dealing with multiple rows of data in a database table can be put in the Collection class for the table in question while database access logic remains in the Model class for the table in question.

The Model class generates all the SQL for accessing and manipulating data in the database; it uses the DBConnector class to execute the SQL statements. The Model class together with the DBConnector class act as a Table Data Gateway.

The Model class also acts as a Data Mapper by being able to map:

Deliberately Omitted Features

Code snippets illustrating how to implement some of the deliberately omitted features will be included in this documentation.

Installation & Requirements

Running tests

./vendor/bin/phpunit --coverage-text

Getting Started

LeanOrm will create only one connection to a specific database (ie. one connection per unique dsn string). Creating one or more Model objects for one or more tables with the same dsn string will lead to the creation of only one connection to the database; that one connection will be shared amongst all model instances created with that same dsn string. If you use two different dsn strings, two connections to the database(s) will be created by LeanOrm. Consequently, using three different dsn strings will lead to the creation of three different database connections, and so on for four or more different dsn strings.

All examples are based on the schema below:

Sample Blog Database Schema

Defining and Creating Model Objects

There are two basic ways to use this package:

For both methods, you can decide if you want to use the base Record and Collection classes (\LeanOrm\Model\Collection and \LeanOrm\Model\Record (or \LeanOrm\Model\ReadOnlyRecord if needed) ) or if you want to extend the base Record and Collection classes eg:

<?php
class AuthorsCollection extends \LeanOrm\Model\Collection {
   //some custom code for collection objects that are meant to hold records belonging to the 'authors' database table
}

class AuthorRecord extends \LeanOrm\Model\Record {
   //some custom code for records belonging to the 'authors' database table
}
?>

Creating Records & Inserting Data into the Database

<?php
    //Method 1:
    $new_record = $authors_model->createNewRecord(); //create a blank new record
    $new_record->name = 'Joe Blow'; //set a value for the 'name' column
    $new_record->save();//save to the database

    //Method 2:
    //create a new record and set a value for the 'name' column
    $new_record = $authors_model->createNewRecord([ 'name' => 'Joe Blow']); 
    $new_record->save();//save to the database

    //NOTE: createNewRecord also accepts a second parameter, an associative array
    //      that can be used to supply values for the properties of the record
    //      object to be created. 
    //      Eg. $authors_model->createNewRecord([ 'name' => 'Joe Blow'], ['_is_new'=>false ])
    //      that is marked as not new by setting the _is_new property of the Record object
    //      that was created to false.

    //Method 3:
    //$inserted_data is an associative array of the data just inserted into the 
    //database. This data will include an auto-generated primary key value and 
    //values of the columns that keep track of the time the record was inserted 
    //and last updated (if the $_created_timestamp_column_name and 
    //$_updated_timestamp_column_name were correctly set).
    $inserted_data = $authors_model->insert([ 'name' => 'Joe Blow']);

    //Multiple Inserts:
    //Below is the most efficient way insert multiple rows to the database.
    //$all_successfully_inserted will be === true if all the inserts were
    //successful, otherwise it will be  === false which means the multiple
    //insert was unsuccessful (nothing is saved to the database in this case).
    $all_successfully_inserted = $authors_model->insertMany([ ['name' => 'Joe Blow'], ['name' => 'Jane Doe'] ]);

    //NOTE: if you have a collection (an instance of \LeanOrm\Model\Collection) 
    //      containing 2 or more records you can also efficiently save the records 
    //      by calling \LeanOrm\Model\Collection::saveAll(true). See the documentation
    //      for Collections for more details.
?>

Fetching data from the Database - Parameter Definition

There are eight Model methods for fetching data from the database:

They all accept one parameter; an associative array ($params) for specifying how the data should be retrieved (except for fetch(..) which accepts additional parameters). Below is the specification for the structure of the array:

Fetching data from the Database - Data Retreival

Updating Data

There are two ways to update data in the database:

  1. By fetching one or more existing records from the database into record objects, then assigning new value(s) to each of the fetched records and then finally calling the save method on each Record object. If the records were fetched into a Collection object, then the saveAll method on the Collection object could be called instead (this will cause all the records in the collection to be saved).
  2. By calling the updateMatchingDbTableRows method on a Model object. This method does not involve the retrieval of Record objects, rather only the conditions for matching the rows of data in the database table to be updated and the new data to be used for the update need to be supplied to updateMatchingDbTableRows.

    Note: there is also an updateSpecifiedRecord method in the Model class which accepts a Record object as parameter and saves it to the database. The updateSpecifiedRecord method does not really need to be called, since the save method in the Record class calls it internally when save is called on a Record object that needs to be updated.

    <?php
    //Method 1:
    $params = [
        'where' =>  [ [ 'col'=>'author_id', 'op'=>'=', 'val'=>2 ] ],
    ];
    
    //Fetch the first row of data with an author_id value of 2 from the authors 
    //table into a Record object.
    $author_record = $authors_model->fetchOneRecord($params);
    
    //Change the value of a field in the fetched record.
    $author_record->name = 'A new Name';
    
    //Save the change made to the Record object to the database.
    $author_record->save();
    
    //NOTE: that the steps above can be applied to the Record objects returned  
    //      by fetchRecordsIntoArray and fetchRecordsIntoCollection.
    
    //Method 2:
    //Update all rows of data with an author_id value of 2 in the authors 
    //table, set the value of the name column in each of these rows to
    //'Another new Name'.
    $authors_model
        ->updateMatchingDbTableRows(['name'=>'Another new Name'], ['author_id'=>2]);
    
    //NOTE: updateMatchingDbTableRows only supports conditions using the equality operator ('=')
    //      to match the rows to be updated. If you need to match rows using other operators like 
    //      >, <=, LIKE, etc. then use any of the fetch methods that return Record objects to fetch
    //      the data you want to update and either call the save method on each Record object or call
    //      the saveAll method on a Collection object containing the record objects. The fetch methods 
    //      allows you to specify more complex conditions for matching rows of data than 
    //      updateMatchingDbTableRows does.
    ?>

Deleting Data

There are two ways of deleting data from the database:

  1. By fetching one or more existing records from the database into record objects and then calling the delete method on each Record object (NOTE: the data is deleted from the database but the Record object still contains the data and is automatically marked as new. To make sure the data is both deleted from the database and cleared out of the Record object the delete method on the Record object must be called with a boolean value of true as its first parameter). If the records were fetched into a Collection object, then the deleteAll method on the Collection object could be called instead (this will cause all the records in the collection to be deleted from the database, but the Record objects will still be in the Collection object with their data intact. removeAll should additionally be called on the Collection object to clear the Record objects and their data from the Collection object; if the Record Objects are not referenced anywhere else outside of the Collection object, then the __destruct method will be automatically called on each Record object and all the memory used by the Record objects will be released).
  2. By calling the deleteMatchingDbTableRows method on a Model object. This method does not involve the retrieval of Record objects, rather only the conditions for matching the rows of data in the database table to be deleted needs to be supplied to deleteMatchingDbTableRows.

    Note: there is also a deleteSpecifiedRecord method in the Model class which accepts a Record object as parameter and deletes the database row associated with the Record object, sets the primary key value of the Record object to null and also sets the _is_new property of the Record object to the boolean value of true (NOTE: other data in the Record object remains). The deleteSpecifiedRecord method does not really need to be called, since the delete method in the Record class calls it internally when delete is called on a Record object that needs to be deleted.

    <?php
    //Method 1:
    $params = [
        'where' =>  [ [ 'col'=>'author_id', 'op'=>'=', 'val'=>2 ] ],
    ];
    
    //Fetch the first row of data with an author_id value of 2 from the authors 
    //table into a Record object.
    $author_record = $authors_model->fetchOneRecord($params);;
    
    //Delete the row corresponding to the Record object from the database.
    $author_record->delete(); // OR $author_record->delete(true); to both delete from the database 
                              //and set the Record object's data to an empty array internally
    
    //NOTE: that the steps above can be applied to the Record objects returned  
    //      by fetchRecordsIntoArray and fetchRecordsIntoCollection.
    
    //Method 2:
    //Delete all rows of data with an author_id value of 2 in the authors 
    //table.
    $authors_model->deleteMatchingDbTableRows(['author_id'=>2]);
    
    //NOTE: deleteMatchingDbTableRows only supports conditions using the equality operator ('=')
    //      to match the rows to be deleted. If you need to match rows using other operators like 
    //      >, <=, LIKE, etc. then use any of the fetch methods that return Record objects to fetch
    //      the data you want to delete and either call the delete method on each Record object or call
    //      the deleteAll method on a Collection object containing the record objects. The fetch methods 
    //      allow you to specify more complex conditions for matching rows of data than 
    //      deleteMatchingDbTableRows does.
    ?>

Support or Contact