Table of Contents
The PersistentObject component provides object persistence for PHP 5 using a database. PersistentObject uses the Database component to provide database abstraction. It does not rely on code generation and does not force a specific inheritance structure to work.
PersistentObject is built to be fast and flexible, allowing you to build persistent classes in the same way as any other class in your application. It also utilizes the query abstraction layer of the Database component to make it easy to build queries. Please refer to the API documentation of ezcQuerySelect for detailed examples on how to use the query abstraction layer.
The PersistentObjectDatabaseSchemaTiein component allows you to automatically generate the definition files needed by PersistentObject from a database schema or the structure of your database. For more information, please refer to the API documentation on ezcPersistentObjectSchemaGenerator.
ezcPersistentSession is the main API for interacting with the persistent objects. Loading, saving and deleting persistent objects is done through this class. ezcPersistentSessionIdentityDecorator can be used to add identity mapping support to it (see Identity mapping).
This chapter describes the typical usage of PersistentObject with a single persistent class, using MySQL as the persistence storage.
We want to make a simple class, representing a person, persistent using a persistent object. This is a simple class with only a few members:
The id member will map to the required persistent identifier. It has to default to null. This is not required for any of the other mapped members. The id field is a required unique identifier for this persistent object. It is generated by the identifier generator and usually maps to an auto increment column in the database.
For simplicity, we have made the name and age members of the Person class public. However, this is not required and in a real application you can use any access method you like. You can even make the data completely private.
All persistent objects must implement the getState() and setState() methods. They are used to retrieve the state of the object when saving it and to set it when loading it. The getState() method should always return the complete state of the object while the setState() method should set only one member at the time.
The state arrays used by getState() and setState() must be indexed by the property names, as defined in the persistence mapping below. They do not act on the database columns names.
We are going to map the Person class onto the following SQL table:
CREATE TABLE persons ( id integer unsigned not null auto_increment, full_name varchar(255), age integer, PRIMARY KEY (id) ) TYPE=InnoDB;
The fields map one to one to the members of the Person class. Using the InnoDB type is not required. We strongly recommend it however, since it supports transactions. The id column is of the type auto_increment. This is required for the id generator that we will use. Other id generators may have other requirements to work as expected.
In order for PersistentObject to be able to store objects of the Person class into the persons table, we need to tell it how the columns are mapped to class members. We will use ezcPersistentCodeManager to fetch the definitions when required.
The first block of code creates the definition object and sets the database table and the name of the class to map. The second block defines the mapping of the identifier member and the algorithm that should be used to create identifiers for new objects. We will use ezcPersistentNativeGenerator, which simply retrieves the new id generated by auto_increment. If you rely on a database backend that does not support auto_increment (e.g. Oracle), ezcPersistentSequenceGenerator is the class to choose here.
The next two code blocks define the mapping between the database columns and the class members. It is possible to use the same name in the class and the database for a field.
The members must be inserted into the properties member, which is an associative array, using the name of the member as the key name. Note that this must not necessarily be the same name as the database column the property corresponds to. It is required that you use the property name here!
If you look at the API for ezcPersistentObjectDefinition, it also has a property named "columns" that is the same array as the "properties", except that it is mapped to the column names instead of the property names. This reverse mapping is set up by ezcPersistentCodeManager automatically.
Finally, we return the complete definition. Your definition will not work unless you return it to the manager.
To make the definition work with the ezcPersistentCodeManager, it must be put in a separate PHP file and given the name of the class in lowercase letters. In our example, the filename should be person.php.
The session object is in charge of the actual loading and saving of persistent objects. A session can be created by simply instantiating it:
The session takes two arguments: a pointer to the database instance to use and the manager from which to retrieve persistent object definitions. You can also use a Database instance by using the ezcDbInstance class. You can then simply use ezcDbInstance::get(); instead of passing $db as argument to the ezcPersistentSession's constructor.
We are using ezcPersistentCodeManager to load the definitions directly from file. You should point it to the location where you saved the person.php file. If you have several directories containing definitions, you can use the ezcPersistentMultiManager class to add as many as you like. In addition to the code manager we use a cache manager. The cache manager makes sure the definition is loaded from disk only once.
While it is possible to create a new session each time you want to manipulate a persistent object, you will probably want to use the same session each time. This functionality can be achieved by using the ezcPersistentSessionInstance class:
Lazy initialization is a mechanism to load and configure a component, only when it is really used in your application. This mechanism saves time for parsing the classes and configuration, when the component is not used at all during one request. You can find a description how you can use it for your own components and how it works in the ezcBase tutorial. The keyword for the database component is ezcInitPersistentSessionInstance.
ezcBaseInit::setCallback accepts as a first parameter a component specific key, which lets the component later request the right configuration callback. The second parameter is the name of the class to perform the static callback on. This class must implement the ezcBaseConfigurationInitializer class. Each component's lazy initialization calls the static method configureObject() on the referenced class.
This example shows a way to configure multiple database handlers, only when they are really requested in your application. The example does basically the same like the first example in this tutorial, but creates the connection not before it is really required.
In line 32 the default persistent session is first requested in this example, which does not exist yet, so that the configuration class earlier referenced through the setCallback() call will be asked for a new instance for the current instance name, which is 'null' for the default instance.
In the configureObject() method in line 8 we switch on the instance name and create and return the right newly created database handler. Line 35 shows, that this will also work with multiple database instances, creating an additional persistent session that reads the definition files from another directory (additionalPersistent).
Creating a new Person object and making it persistent is straightforward:
This code saves our newly created object to the database and generates an id for it. The id is set to the id property of the object. Since Guybrush is our first person, he is assigned the id of 1.
Of course, the age of Guybrush Threepwood is the source of much debate, and he is probably younger than 31. To change his age, simply edit the object and tell the session to update it.
Note that we used update() to store the object this time. This is because we want to trigger an UPDATE query instead of an INSERT query.
There are several ways to retrieve persistent objects from the database. The simplest is to fetch one object by its identifier.
This code retrieves the Guybrush object created above.
If you have stored a lot of persistent objects to the database and want to retrieve a list, you can use the find() method. The find() method requires a query parameter that can first be retrieved from the session.
This code will fetch a maximum of 10 Person objects where the age is higher than 15, sorted by name.
This is achieved by manipulating the query object returned by ezcPersistentSession->createFindQuery(). To learn more about query abstraction and how to use it, please refer to the specific subsection of the Database components tutorial.
The find() method will fetch the complete result set and instantiate it for you. This is not desirable if you are fetching large numbers of objects and you want it to be fast and efficient. For this you can use the findIterator() method:
This code will produce the same result as the first find() example. However, only one object will be instantiated and the data will be transferred from the database only when it is needed.
The final example uses a find query with a logical and to find objects:
The easiest way to delete persistent objects is to use the delete() method on the session:
Of course, you can only delete instantiated objects this way. If you want to delete an object or a whole series of objects that are not instantiated, you can use the deleteFromQuery() method:
The above code will remove all persons from the database who are more than 15 years old.
All persistent objects must have an identifier field. The identifier generation algorithm defines how the system will generate ids for new objects. This chapter describes the available generators.
The sequence generator relies on the PDO::lastInsertId() method to retrieve the ids for newly created persistent objects.
For databases supporting auto_increment (like MySQL and SQLite), use ezcPersistentNativeGenerator. Other databases must use a sequence. For example, the PostgreSQL person table definition should be as follows:
CREATE TABLE persons ( id integer unsigned not null, full_name varchar(255), age integer, PRIMARY KEY (id) ); CREATE SEQUENCE person_seq START 1;
If your database requires you to use a sequence, this parameter should be provided to ezcPersistentSequenceGenerator in the mapping definition.
The native generator relies on auto_increment, which is supported by, among others, MySQL and SQLite. An example table definition looks like this:
CREATE TABLE persons ( id integer unsigned not null auto_increment, full_name varchar(255), age integer, PRIMARY KEY (id) );
The corresponding generator definition is below:
If you do not rely on a database mechanism to generate values for a primary key column, you have to use the ezcPersistentManualGenerator class. You can then set the value of the id property by hand and save the object afterwards.
CREATE TABLE persons ( login varchar(25), full_name varchar(255), age integer, PRIMARY KEY (login) );
In this table, the string value is used as the primary key. Therefore, we have to generate id values manually. Use the following definition:
For saving a new instance, use the following code:
The session object needs to be able to fetch the persistent object definitions in order to function properly. The task of fetching the definitions is performed by a definition loader, which is provided to the session when it is instantiated.
This is currently the only manager available. It simply reads the definition from a file with the same name as the class from the specified directory. It does not perform any error checking on the definition and simply assumes that it is correct.
It is very easy to create your own definition loader. Simply extend the ezcPersistentDefinitionManager abstract class and implement the fetchDefinition() method:
The fetchDefinition() method should create the definition structure for the requested class or throw an exception.
Relations are defined within the persistence mapping.
The following definition classes are available to realize object relations:
All of these classes extend the abstract class ezcPersistentRelation.
For the examples in this section, we will reuse the Person class, defined in The persistent class. In addition, we will use an Address class, which looks as follows:
The Address class will be extended later on to include relations. The following basic persistence mapping is used and extended for each example:
The following extensions are necessary for the given class and persistence mapping, to realize a simple 1:n relation. Each person will be able to have multiple addresses, but one address may only refer to one person.
The Address class needs to be enhanced as follows, to store the id of the Person it is related to.
Additionally, we need to define the new property $person in the persistence mapping of the Address class:
The relation definition takes place in the persistence mapping of the Person class in The persistent class. It needs to be extended as follows:
A relation to another persistent object is defined in the property ezcPersistentObjectDefinition $relations, which is an array. Each relation must have the name of the persistent object class it refers to as the key in this array. An instance of one of the classes shown in Class overview must be the value. In this case, it is ezcPersistentOneToManyRelation. The parameter to its constructor are the names of the tables that the relation refers to. The first table is the table of the current object, and the second one refers to the related object.
To define which properties are used to realize the relation mapping, the property ezcPersistentOneToManyRelation->columnMap is used. It contains an array of (in this case) ezcPersistentSingleTableMap, which maps one column of each of the tables to one column of another. In the above case, the database column "id" from the table "persons" would be mapped to the column "person_id" in the table "addresses". In general, this means, that "id" is the primary key from the "persons" table and "person_id" is the foreign key in the "addresses" table, that refers to the "persons" table. Please note that the relation mappings are done on the table name/column name and not on the class name/property name.
If you want to map using several columns, you can add more ezcPersistentSingleTableMap instances to the columnMap array. For example, if you are using a person's first and last name as the primary key for the "persons" table, you could define the relation like this:
To use the previously defined 1:n relation, ezcPersistentSession offers several new methods:
Using these methods, we can now retrieve all addresses that are related to one person:
The variable $addresses will then contain an array of all Address objects found for the Person object with an id of 1. To relate these addresses to another Person object, we can do the following:
The ezcPersistentManyToManyRelation class works slightly different than the other ezcPersistentRelation classes. For this kind of relation, you need an extra table in your database to store the relation records. The next example shows the definition of an ezcPersistentManyToManyRelation relation, based on the Person and Address example classes. Each person can have several addresses and each address can be used by several persons. You need to use the original example classes for this, thus we do not need to extend them here. The definition of the relational mapping for the Person class must be extended as follows:
A similar exception applies to columnMap of this relation definition. It consists of ezcPersistentDoubleTableMap instances, which carry four column names each. The first column is the column to choose from the source table (usually its primary key). The second column defines the column in your relation table that maps to the first column. In our example, the column "id" from the "persons" table maps to the column "person_id" from the relation table "persons_addresses". The same applies to the third and fourth columns. The third column defines the column of the relation table that maps to the fourth column given. The fourth column specifies the column of your destination table to use for mapping. In our example, the relation table "persons_addresses" has a column "address_id", which is a foreign key referring to the column "id" in the table "addresses".
As stated earlier, the usage methods behave slightly differently when dealing with n:m relations. If you use ezcPersistentSession->addRelatedObject(), the desired relation record is inserted into the relation table. The same applies to the removeRelatedObject() method of ezcPersistentSession, which deletes the specific record. This also means that you do not need to store the affected objects explicitly after altering the relations between them. If you have made other changes to the objects, they must be stored to save the changes.
Aside from that, the ezcPersistentSession->delete() method keeps track of the relation records. If you delete a record, all of its relation records are automatically deleted.
If you want to use 1:1 relations, where two tables share a common primary key, you need to define a table to generate the key and the other table to use ezcPersistentManualGenerator. For our example, if one person may only have one address, the definition would be as follows:
This is the relation (defined in the definition file of the Person):
If you let both tables use ezcPersistentSequenceGenerator for the same key, ezcPersistentSession will fail to save a related object, since the id will already be set by the ezcPersistentSession::addRelatedObject() method.
Another way to make this work is to not use the same primary key for both tables, but to make the Address object have its own id and only use the Person id as a foreign key.
Since you can always look at a relation from two sides, ezcPersistentRelation implementations can be configured to be "reverse". A reverse relation indicates that the relation is already defined in the opposite direction and that the original direction is the main used one. The one marked as "reverse" is a secondary one, for consistency reasons. For a relation that is marked as reverse, it is not possible to use ezcPersistentSession->addRelatedObject() and ezcPersistentSession->removeRelatedObject(). You can still use ezcPersistentSession->getRelatedObjects() for relations that are flagged "reverse".
For most relation types, the reverse attribute of the relation definition object is set to false by default. You can manually set it. Exceptions are ezcPersistentManyToOneRelation relations. This relation type only makes sense as a reverse relation for ezcPersistentOneToManyRelation. Therefore, the reverse attribute is set to true for ezcPersistentManyToOneRelation and is not publicly accessible for writing.
The following example shows the reverse relation definition for the n:m relations example:
With the relation definition shown above, you would still be able to relate the Persons object to an Address object, but not to add or remove related Person objects to/from an Address. In other words, the following code still works:
While the following would not work:
Instead, only the other direction works, because this one is the main direction.
Cascading relations are done through the flag "cascade" of a ezcPersistentRelation implementation. All implementations except the ezcPersistentManyToManyRelation class support this flag. It allows you to automatically delete all related objects for a source object when the source object is deleted.
The following example shows how to add a cascading relation to a relation definition, based on the example from Defining a simple relation:
If you now use the following, the Person object and all related Address objects are deleted:
Beware that this does not work with ezcPersistentManyToManyRelation instances, because it could cause serious inconsistencies in your data.
In some cases it might be necessary to define multiple relations to the same PHP class. An example for this can be seen when enhancing the Person example from The persistence mapping as follows:
CREATE TABLE persons ( id integer unsigned not null auto_increment, full_name varchar(255), age integer, mother integer, father integer, PRIMARY KEY (id) )
Here each person is connected to 2 objects of the same table: The mother and the father. Since only 1 PHP class per table is desired, the class needs to be referenced by the Person class twice.
Assuming that the 2 new properties of the Person class have been defined correctly in the persistence mapping, the following code can be used to achieve the desired relations:
2 relations need to be defined to reflect the relation between a mother an her children. "mother" defines the relation from a child to its mother and "mothers_children" defines the opposite direction, from a child to its mother. Both relations operate on the Person class itself, therefore an ezcPersistentRelationCollection is used to carry the relation definitions.
Relations are defined as shown earlier in this section. The only difference here is, that the relation definitions themselves are not added directly to the $relations property of the ezcPersistentObjectDefinition instance. Instead they are assigned to unique names on a relation collection, which is then added to the $relations property of the object definition.
The comment indicating further code (...) in the example above indicates that further relations are missing in the collection: The relations "father" and "fathers_children" are excluded here, since they work exactly like the corresponding mother relations, above. A fifth relation is shown below:
As can be seen above, a relation collection can carry an arbitrary number of relations, which are of arbitrary type.
To make use of the relations to the Person class defined in the last section, the name of the desired relation has to be submitted to all relation operations:
The code above fetches all children of a mother, as defined by the relation "mothers_children". The third parameter to ezcPersistentSession->getRelatedObject() is mandatory in this case. If you leave it out, a ezcPersistentUndeterministicRelationException will be thrown. The parameter is ignored if you submit it when not working with a relation collection.
In the same manor a new related object can only be added if the affected relation is submitted to ezcPersistentSession->addRelatedObject() as shown below:
The identity map pattern, as described by Martin Fowler, ensures that only 1 PHP object with the same identity exists. That means, if you load an object from the database a second time, the originally created instance is re-used instead of creating a new instance. In addition to that, identity mapping can potentially save SQL queries and therefore reduce database load.
Note that with identity mapping, the methods updateFromQuery() and deleteFromQuery() will result in a complete reset of the identity map. Further details on that can be found under Effects of identity mapping.
Identity mapping avoids that you have 2 different PHP objects with the same database identity in your application. For example:
The variables $person and $samePerson are both references to the very same object. In fact, the second call to $identitySession->load() does not issue a database query at all, but just returns the existing instance of the desired Person object, since this has already been loaded before.
Identity mapping also affects finding of persistent objects:
The call to $identitySession->find() fetches all Person objects from the database. This includes the Person with ID 23, which has already been loaded before. Due to that, there is not a second instance of this object in $persons, but the existing instance is re-used. This does not save you an SQL query, but avoids inconsistencies. The change of the $name property of the Person object with ID 23 is reflected in the found objects, although the object has not been saved, yet.
Note: You should not use the methods updateFromQuery() and deleteFromQuery() with ezcPersistentSessionIdentityDecorator. If you use any of these methods, all cached objects will automatically be removed from the identity map, because the effects of these methods can not be traced by the mechanism. The reset of the identity map might result in unexpected inconsistencies, which should originally be avoided by the mechanism.