Rabbit

PHP is fun again
Satori is an implementation of the ideas behind active record abstraction. It allows you to easily create classes that store data in a database and make it available to the frontend in several ways after filtering or modifying it, as well as allow manipulation of this data by the frontend programmer in specific ways. This page will help you understand the basic principles of active record and how it works, what models and finders are, how to implement a specific model and its associated finders using Satori and Rabbit in general, and how to make sure the API encapsulates the storage engine and other implementation details.

Use Satori or not?

Satori is a library that allows you to easily create other libraries. These other libraries are of a very particular type: they follow the active record pattern and use a database for storage. Not all your libraries need to be following this principle. For example, a library might be using files to store data instead of a database. Or another library might not be doing anything active-record-specific; for instance, a library that contains helper functinos for multibyte string handling, or a library that functions as a set of Agent Smiths for asserting that synoptical columns in denormalized tables always have correct values, using a cron, has nothing to do with active record. Don't let active record limit you in what you are allowed to do in your backend.

In addition, some specific implementation of active record might be very unique to be able to be abstracted using the Satori base. Fear not! You don't have to use Satori. You can implement it from scratch using your own ways if it's unique enough. The satori library exists to help you get the most of active record in simple, every-day libraries that fall under this general category. We've done our best to make Satori as extensible and optimized as we could, but sometimes abstraction is just not appropriate. Feel free to use it or not, depending on your needs.

Although it is not necessary to use it even if you do use the active record ideas, it will greatly simplify and encapsulate the logic of your code if you do. It will also ensure that all your classes follow the same abstraction API and hence are instantly usable by other developers without an additional learning curve.

Active record

Active record is a pattern in software development which naturally evolved from trying to create an object-oriented way of handling database records. It's been around since databases and object orientation got together, not always with the exactly same implementation details or even with that name. Some active record implementations are different from others. You'll notice that Rabbit uses a somewhat different approach than most frameworks in how it handles a lot of things in regards to active record, and hence requires you to go through a learning curve even if you've already used active record elsewhere. This page is designed to help you understand this concepts.

Often, library files contain one or more classes, each of which is directly associated with a database table. Instances of those classes are directly associated with records within that database table, and can be used to read information from the record, write information to the record, or delete the record. Instances can also be used to create new records. This collection of ideas is known as the active record pattern, and the respective classes are known as domain level classes or models.

Models

A model is a class an instance of which is directly mapped to a record in a database. It is important to note that this mapping to the database record is part of the implementation details and is encapsulated by the class -- the frontend programmer does not need to be aware of the fact that there is a database mapping or a database at all.

For example, when developing a system that supports multiple users, a common case would be to have a 'User' class and `users` table. An instance of the 'User' class would then represent a single user, for example the currently logged in user. If it is desired to gather information about multiple users, multiple instances of the class 'User' must be created. It is important that the mapping is one-to-one: an instance of the User class cannot be used to handle multiple users.

Creating a model

After you create an empty library (see Libraries) that will house your model, it's time to create your model. To create a model, create a class and name it after the singular version of your database table's name. This convention is not obligatory: it is a convention. But it makes more sense if you follow the naming schema so that other programmers catching up with your code can understand it easily. Your class should extend the base Satori class that is made available to you by Rabbit:

<?php
    class Example extends Satori {
    }
?>


Of course, since we're talking about active record here, you'll need a database table to map to. Assuming our table is named "examples", and we have create a database table alias (see Database) also named "examples", we can connect our model to that table. This is done by supplying a value for the protected member attribute $mDbTableAlias in our model class. It is mandatory to set this attribute to a valid database table alias, or we won't be able to instantiate our class. Notice that the alias only need exist while you use the model, not before or after using it. This important detail makes it possible to create table aliases on-the-fly prior to instantiating the model and getting rid of the afterwards, which is especially useful when writing unit tests.

Let's hook our model up:

<?php
    class Example extends Satori {
        protected $mDbTableAlias = 'examples';
    }
?>


Having done this, we can already get some basic functionality from our model. We can retrieve data, update data, create new entries, and delete existing entries. However, notice that all these operations are directly mapped to the database without any interference. This is not usually what you want: your frontend programmer should not care about implementational details, so you must make sure they are hidden by the model by specifically creating functionality as required.

Remember that this example shown above is a complete, working model. You don't need to implement anything else to make it work -- but you usually want to. All additional features and abilities described below are optional.

Persistency

An instance of a model is directly associated with a database record. This database record can already exist in the table, or can be a piece of data in memory that will result in the creation of a database record. When the object contains the data exactly as it is contained in the database table, it is called a persistent object. When the object refers to a record yet to be created, or contains modifications that have not yet been written to the database it is called a non-persistent object.

Database table compatibility

When using Satori, you have to make some compromises in your database tables. They all need to follow particular rulesets to be compatible with Rabbit. If you do not desire to use these conventions, you can still use your database with Rabbit, but not with Satori in particular. However, since Satori is an important part of Rabbit, it is highly recommended that you follow the compatibility recommendations, even if you do not use Satori at all.

Requirement: At least one field

Each of your database tables must have at least one field. This might seem obvious, but some database systems do not make it obligatory. It usually does not make sense to have no fields in your table, but if you do, be aware that you will not be able to use it with Satori.

Requirement: All fields must be named

There can be no anonymous columns in your table. Some database systems allow columns without names -- you cannot have those when using Satori. Pick a descriptive name to use for your column.

Requirement: All fields must be prepended by the table name

This is most likely the most restrictive rule when compatibility with Satori is concerned. All table fields must start with the table name in singular followed by an underscore, followed by the actual field name. For example, if your table is named "examples", your fields must be names must start with "example_" and must contain at least something after that underscore. Something to note here is that Rabbit doesn't really care whether in fact you use the singular version of the table before the underscore, or any other given string, but it is recommended that you use the table name in singular, for consistency.

Requirement: One primary key

Your table must have one primary key. It's up to you what you'll use as a primary key. You can use a numeric or string column, or an auto-increment column, or even multiple columns of the same or different types. But a primary key must exist.

Recommendation: Table names in plural

We recommend that you use plural when naming your table names. For example "users", or "comments". We recommend that if your table name consists of multiple words, that you do not separate them with an underscore, and only that the last part of the talbe name is in plural. For example, "userprofiles". There are circumstances where this is not useful: you might want to separate your table name parts using an underscore character as in "user_profiles" to allow client software to group similar tables together, for instance. It is possible to use table prefixes if desired.

Recommendation: Lower-case

Use lower case when naming tables and fields. This is usually what everybody else does, so it's useful to follow the convention.

Creating new entries

Let us look at how we can use our basic class to create a new entry in a "users" database table. First, we'll instantiate our model. The new instance will be mapped to the new entry in the database. By instantiating the model without any arguments, we're essentially telling Satori to not associate the model with a specific existing record, but with a record-to-be-created:

<?php
    $user = New User();
?>


This is a non-persistent instance of Satori. This means that it doesn't yet exist in the database. We can use the method to make it persistent, i.e. to store it in the database. This will create a new record in our database table:

<?php
    $user->Save();
?>


Check the users database table to see that in fact a record was created.

Setting attributes

All database fields are, by default, mapped to object attributes in our model. The attributes that are created by the database fields correspond to the field names directly. The first letter of such an attribute is always uppercase, while each subsequent letter is always lowercase, and this case is irrelevant to the field name. The table name and the first underscore are not included as an attribute name. For example, if a field is named "example_name", it will correspond to the attribute "Name".

We can set these attributes to arbitrary values in a model object to set the values as desired.

Assuming that there is a field named "user_name" in our "users" database table, we can set it to something prior to storing the object:

<?php
    $user = New Example();
    $user->Name = 'hello';
    $user->Save();
?>


A new record now exists in our database with the "user_name" field value set to "hello" for that record. Notice that in order to do that, we did not define any "Name" attribute in our model. These attributes are propagated automatically and must not be defined.

Reading attributes

Attributes can also be retrieved in the same way they are set. To retrieve the value of the "example_name" field of a specific record associated with the current model object, we can simply query it:

<?php
    echo $example->Name;
?>


Creating objects from existing records

It is possible to instantiate an object based on an existing record. To do so, simply pass the primary key value as an argument. For example, if your primary key for the `users` table is the field `user_id`, pass the value of the field in your desired record to retrieve the object that corresponds to that record. Recall that primary keys are capable of uniquely identifying each record:

<?php
    $user = New User( 5 ); // retrieve the user with user_id = 5
?>


The $user object is now associated with the entry which has a user_id equal to 5. This object can now be used to read additional information from the record, such as the user_name of the user.

If your primary key consists of multiple columns, pass each value as a separate argument to the constructor, in the order they are defined within the key.

Storing objects

Any changes made to a persistent object cause it to become non-persistent. This means that if you change an attribute in an object corresponding to a record in your database, these changes are not directly reflected by the database, but you need to explicitly tell your object to store them. You can do that using the "Save" method:

Let's see how we can modify a persistent object and then store these modifications.

<?php
    $user = New User( 5 );
    $user->Name = 'neo'; // temporarily sets the name to "neo"
    $user->Save(); // change the name to neo on the database -- further reads will now read the new name
?>


The same also applies to brand new objects:

<?php
    $user = New User();
    $user->Name = 'trinity';
    $user->Save(); // creates a brand new database record with the name set to "trinity"
?>


Deleting objects

Existing objects can be deleted using the "Delete" method. This method results to an immediate save, thus it is not required to issue "Save" on the target object afterwards.

<?php
    $user = New User( 5 ); // look up the desired user
    $user->Delete(); // delete the user who has user_id 5
?>


As probably expected, new objects cannot be deleted unless they are first made persistent.

Defining new attributes

It's often useful to define new attributes in your models. These attributes can be used by the frontend programmer as if they were normal database attributes, even if they in fact do not exist in the database. Let's see how we could create a "Fullname" attribute based on the "firstname" and "lastname" attributes that exist in our database. Assume that fields "user_firstname" and "user_lastname" are defined. We'll first create a getter to retrieve the first name of the user. Getters can be specified using the PHP5 magic __get function. We'll create a getter which will return the full name of the user. Simply enough, this getter concatenates the first and last names adding a string in the middle. Getters are always defined as of public visibility.

<?php
    class User extends Satori {
        protected $mDbTableAlias = 'users';
 
        public function __get( $key ) {
            switch ( $key ) {
                case 'Fullname':
                    return $this->Firstname . ' ' . $this->Lastname;
                default:
                    return parent::__get( $key );
            }
        }
    }
?>


Notice the default case, which exists to allow the object to fallback to the default overloaded attributes of Satori. To use the getter, we can directly access a new attribute, "Fullname" on our object externally:

<?php
    $user = New User( 5 );
    echo $user->Fullname;
?>


This attribute behaves for the frontend programmer just the same as if it was an actual database attribute. We can also define a similar setter. Our setter will split the first and last names based on the space character and assign the resulting values to the Firstname and Lastname attributes respectively.

Setters are defined using the PHP5 magic __set function. We'll create a setter which will set the attributes desired. Notice that the setter takes a value, the value passed by the user during the assignment.

<?php
    class User extends Satori {
        protected $mDbTableAlias = 'users';
 
        public function __get( $key ) {
            switch ( $key ) {
                case 'Fullname':
                    return $this->Firstname . ' ' . $this->Lastname;
                default:
                    return parent::__get( $key );
            }
        }
        public function __set( $key, $value ) {
            switch ( $key ) {
                case 'Fullname':
                    $split = explode( ' ', $value, 2 );
 
                    $this->Firstname = $split[ 0 ];
                    if ( !isset( $split[ 1 ] ) ) {
                        $split[ 1 ] = '';
                    }
                    $this->Lastname = $split[ 1 ];
                    break;
                default:
                    return parent::__set( $key, $value );
            }
        }
    }
?>


Simply enough, again this attribute is now available to us as a normal object attribute:

<?php
    $user = New User( 5 );
    $user->Fullname = 'Alan Turing';
    $user->Save();
?>


You can define as many custom attributes as you wish using this method.

Delayed mapping

In active record, objects and database tables are mapped together, but an update in one doesn't mean an immediate update in the other. Modifications to objects have to be explicitly saved (using the Save method) so that they are written to the database. This is of particular importance when optimizations come in place, as well as in code that utilizes the idea of active record prototypes (see bellow).

Updates to the database are not apparent in instasntiated objects unless they are re-instantiated, or unless the update was made from the same script that contains the instantiated objects. This should not bother you most of the time, unless you are particularily considered about race condition situations where staying up-to-date is of particular importance even when a few milliseconds are concerned. Keep in mind that when you're using database replication, you already have an offset between reads and writes that is sometimes much larger than what a common script based on Satori will result in. For example, consider two scripts, running simultaniously, the first of which is interested in reading some information about a particular non-replicated database table, while the second is interested in writting some information about the same database table. Now, if the first script instantiates an object and the second script then updates the database table associated with the same object, the data reflected by the first script, even if used after the second script has committed its changes, might be outdated.

Also, do not assume that all the data is read when in fact instantiating. Data may be propagated lazily to avoid unnecessary SQL queries. For example, if you instantiate an object based on a primary key value and then only read that very primary key value and nothing else, no SQL queries are required: Do not rely on them taking place!

Finders

It is often useful to have methods that return model instances based on criteria other than the primary key in the database. These constructs are known as finding methods. All finding methods related to a particular model are grouped under a common class that is called a Finder. To create a finder, simply create a class whose name starts with your model name and the word "Finder" follows and make it extend the existing class Finder. For example, to create a Finder for the model User, we'd write:

<?php
    class UserFinder extends Finder {
    }
?>


The Finder class can contain methods that are capable of returning instances or arrays of instances of the model class. These methods are public, usually start with the word "Find" (although that's not a requirement) and are known as "finding methods". Let us create a finding method that returns instances of the user class for the administrators of our site. For simplicity, let them be hard-coded:

<?php
    class UserFinder extends Finder {
        public function FindAdmins() {
            return array(
                New User( 5 ),
                New User( 1 ),
                New User( 26 ),
                New User( 3 )
            );
        }
    }
?>


Simple enough, we can now call this externally to retrieve these users:

<?php
    $finder = New UserFinder();
    $admins = $finder->FindAdmins();
 
    foreach ( $admins as $admin ) {
        echo $admin->Name;
    }
?>


Relations

Relations are a core principle in the active record pattern as of recently. They map, to your objects, the relational nature of your database. When tables are linked together using foreign keys, the actual relation can be mapped to your objects formation. Common types of relations include one-to-one, one-to-many, and many-to-many relations. Uncommon types of relations not supported natively by Rabbit can be implemented by manually extending the base Relation class. An example of an uncommon type could include, for instance a one-to-one relation where both models are one and the same, essential when creating trees. For this type of relation, you could define your own relation by extending the existing one-to-one relation, or you could simply implement it manually in your model.

In this section, we'll focus on the three common relation types.

For this example, assume a 'User' class is used to represent a User, an 'Avatar' class is used to represent an avatar (user display picutre), a 'Post' class is used to represent a blog or news post, and a 'Comment' class is used to represent a comment. Here are the types of relations that could be created:



Satori allows you to easily implement all these types of relations without having to write any SQL. It is important that you understand the underlying SQL and database table joins that will be automatically generated for you, however, even if you don't have to write any SQL statements yourself. This is essential to be able to build a fast and scalable web application, or to customize classes the way you want them to, when you want to do things that Satori doesn't natively support.

Defining relations in Satori is easy. Each relation of a model to another is stored as an attribute within the model. Relations are defined in the Relations() function in your Rabbit which must be defined with protected visibility. Here's how it looks like:

<?php
    class User extends Satori {
        ...
 
        protected function Relations() {
            $this->Avatar = $this->HasOne( 'Avatar', 'Avatarid' );
            $this->Comments = $this->HasMany( 'CommentFinder', 'FindByUser', $this );
        }
    }
?>


You'll notice that the Relations function defines a few model attributes. Keep in mind that these attributes become public, but should not be declared at the top of your class at all! Also notice how we have called internal functions to retrieve the relations such as HasOne and HasMany. These functions return a pointer depending on the relation type and store it in the variable they are assigned to. It is meaningless to use the return value of these functions in any way other than assigning it to a variable. In addition, calls to these functions are only valid within the scope of the Relations() function.

Also keep in mind that relations are often lazy. This means that a relation will not actually be evaluated until it is needed, when it's used by the class itself or by an external caller.

We'll now look into each relation type in more depth.

One-to-one relations

One-to-one relations are the most simple to define and use. They are used when each object corresponds to another object one-to-one. They can also be used when a correspondance is one-to-one when it exists, but may not exist under certain circumstances. Assume we have two models, User and Avatar that we want to link with a one-to-one relation. We will define this relation from both sides:

<?php
    class User extends Satori {
        ...
 
        protected function Relations() {
            $this->Avatar = $this->HasOne( 'Avatar', 'Avatarid' );
        }
    }
 
    class Avatar extends Satori {
        ...
 
        protected function Relations() {
            $this->Owner = $this->HasOne( 'User', 'Userid' );
        }
    }
?>


You'll notice that we're calling the internal function HasOne to define this relation. The HasOne function takes two parameters: The target model that the relation points to, and a local attribute that is used as a foreign key towards the target model. The foreign key will be used as the primary key of the target object to look it up. In our first example, the local attribute avatarid of the User object, defined as a field in the `users` table named `user_avatarid` corresponds to the primary key in the `avatars` table, `avatar_id`. On the other hand, in our second example, the `avatar_userid` field is a foreign key in the `avatars` table mapping to the primary key `user_id` of the `users` table. Keep in mind that the argument passed to the HasOne method is the name of the local attribute, not the actual field name!

The HasOne function returns a value representing the remote object and it is stored in the defined attribute. Accessing the newly created attribute allows us to directly manipulate the target object, as it will give us an instance of the desired object immediately. Assumimg the "Avatar" class exposes an attribute named "Source", we can then do the following:

<?php
    $user = New User( 5 );
    echo $user->Avatar->Source;
?>


One-to-many relations

One-to-many relations are the second most common type, after one-to-one relations. They take a bit more effort to define them. Assuming we have a Comment class and each user can own multiple comments, we can do:

<?php
    class User extends Satori {
        ...
 
        protected function Relations() {
            ...
            $this->Comments = $this->HasMany( 'CommentFinder', 'FindByUser', $this );
        }
    }
 
    class Comment extends Satori {
        ...
 
        protected function Relations() {
            $this->Owner = $this->HasOne( 'User', 'Userid' );
        }
    }
?>


You should already be familiar with the second example. As long as the first is concerned, we used the function HasMany to define a one-to-many relation. This function takes three arguments. First, the name of the finder that will return the desired results. Second, the name of the finding method within that finder to use. Third, the foreign key to use for the lookup as a member attribute if provided as a string, or the value to pass to the finder if else. It's often useful to provide an attribute to allow for optimizations, but here we have chosen to provide a value -- the value of the $this variable. The finder needs to be predefined and accept the relevant argument accordingly (see defining finders above).

Now we can use this relation externally or internally directly, as it will return an array of objects in the HasMany relation, or an empty array if none match, essentially the same way the finding method will return it. Assuming that the "Comment" class exposes an attribute "Text", we can then do the following:

<?php
    $user = New User( 5 );
    foreach ( $user->Comments as $comment ) {
        echo $comment->Text;
    }
?>


^ Back to top