Learn / eZ Publish / The PersistentObject eZ Component: Putting Relations Where Relations Belong

The PersistentObject eZ Component: Putting Relations Where Relations Belong

Previous releases of PersistentObject already allowed you to store and retrieve objects in a database. With the latest 1.2 release of PersistentObject, you can configure the object relations in a central place and benefit from using PersistentObject throughout your application. To aid my explanations, we will look at code from a simple example application. The example application makes heavy use of other components, such as UserInput and Template, but this article will concentrate completely on the use of PersistentObject.

The example application source is available for those who want to play with the code.

We will create a simple address book that allows you to manage information about your friends. The application will have a web interface that facilitates the creation and deletion of objects. For simplicity, the manipulation of existing objects will be left out.

The database layout

The application relies on five database tables: The person table stores the first and last name of each person. A second table, called detail, contains additional details about people. The table email is used to store email addresses, and the table address stores physical addresses. Because a person can be assigned to several addresses and several people can live at one address, we have a relation table: person_address.

-- phpMyAdmin SQL Dump
-- version 2.6.3-rc1
-- http://www.phpmyadmin.net
-- 
-- Host: localhost
-- Generation Time: Jan 08, 2007 at 11:26 AM
-- Server version: 5.0.30
-- PHP Version: 5.1.6-pl10-gentoo
-- 
-- Database: `ezccontact`
-- 
 
-- --------------------------------------------------------
 
-- 
-- Table structure for table `address`
-- 
 
CREATE TABLE `address` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `street` varchar(255) NOT NULL,
  `zip` varchar(5) NOT NULL,
  `city` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 AUTO_INCREMENT=5 ;
 
-- --------------------------------------------------------
 
-- 
-- Table structure for table `detail`
-- 
 
CREATE TABLE `detail` (
  `person` int(11) NOT NULL,
  `birthday` int(11) DEFAULT NULL,
  `comment` varchar(255) DEFAULT NULL,
  PRIMARY KEY  (`person`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
-- --------------------------------------------------------
 
-- 
-- Table structure for table `email`
-- 
 
CREATE TABLE `email` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `person` int(11) NOT NULL,
  `email` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 AUTO_INCREMENT=11 ;
 
-- --------------------------------------------------------
 
-- 
-- Table structure for table `person`
-- 
 
CREATE TABLE `person` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `firstname` varchar(255) NOT NULL,
  `lastname` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;
 
-- --------------------------------------------------------
 
-- 
-- Table structure for table `person_address`
-- 
 
CREATE TABLE `person_address` (
  `person` int(11) NOT NULL,
  `address` int(11) NOT NULL,
  PRIMARY KEY  (`person`,`address`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

This database design contains several relations between objects. The person table (represented by a person object) is the central instance in the application and represents the starting point for all of our relations.

  • The detail table contains information describing exactly one person. Every person can only have one detail record. Here, we have the simplest kind of relation: a 1:1 (one-to-one) relation.
  • A person can have multiple email addresses (how many do you have?), but one email address can only belong to one person (at least in our example). Here, we have the most common relation type: a 1:n (one-to-many) relation.
  • A person can have multiple physical addresses (for example, a home address and an office address) and multiple persons can be assigned to the same address. This reflects the most complex relation type: a n:m (many-to-many) relation.

The application structure

The example application follows a very simple MVC (Model-View-Controller) pattern: The main file is index.php, which defines and runs the main controller. This controller responds to several "actions", and dispatches them to an action class, which will then handle the request. In addition, the controller handles the initialization and processing of a template (which is defined by the action object).

We will not discuss the controller and template mechanism in this article, but will concentrate on the configuration and usage of the PersistentObject component. If you download the application source, you can find the action classes in the directory actions/. We will also deal with the model classes used in the application (in the directory models/) and the definition files used by the PersistentObject component (in the directory persistent/).

The other directories are not discussed in this tutorial. Here is a brief summary of what they contain:

autoload/
This directory contains the autoload files for our application, which are used in combination with the eZ Components autoload mechanism.

docs/
You can find setup information for the application here. For example, there is the database structure and some example data as MySQL dumps.

templates/
The example application relies on the Template component, for which the HTML template files can be found here.

templatesc/
Because the Template component compiles template code into PHP source code, this directory is needed to cache the generated code.

We will see how to define, step by step, the persistent objects to reflect our database design and how to make use of them in the action classes.

As already mentioned, the person object is the central model of our application. There are three steps to take before you can make use of PersistentObject:

  1. Create the model class, which will be used in the application itself.
  2. Create a definition for the PersistentObject component to indicate in which way this model class reflects the database structure.
  3. Create an instance of ezcPersistentSession and configure it to make use of the model class and definition.

Step 1: Creating the first model

The PersistentObject component does not require you to inherit from a specific base class, nor to stick to a given naming scheme. The only requirement is to implement two methods in the object class:

getState()
This returns an array representing the current state of the object. The state is represented by the properties of the object; each property name is a key in the array, assigned to its corresponding value.

setState()
This is the corresponding method to set the state of an object. This method must accept the same format that getState() returns and must set the object properties accordingly.

Let's take a look at the model for the person object, which is called ezcappContactPerson:

<?php
 
class ezcappContactPerson
{
 
    public $id;
    public $firstname;
    public $lastname;
    public function getState()
    {
        return array( 
            "id"        => $this->id,
            "firstname" => $this->firstname,
            "lastname"  => $this->lastname,
        );
    }
    public function setState( array $state )
    {
        foreach ( $state as $propertyName => $propertyValue )
        {
            $this->$propertyName = $propertyValue;
        }
    }
 
}
 
?>

The class contains three public properties: $id, $firstname and $lastname, which reflect the fields of the person table in the database. (I'm quite sure that you would not want to have all of the fields to be public in a real application, but remember that this is just an example.)

The getState() and setState() methods perform the desired actions: getState() simply returns an array of all object properties and setState() sets the properties from a given array. You should note that our example implementation of setState() is a bit sloppy (for simplicity reasons) as it would also set non-existent properties.

Step 2: Telling PersistentObject about it

So far, so good. Now we need to tell PersistentObject that our new class ezcappContactPerson should reflect the database table person. PersistentObject is designed to understand different formats for storing information, but there is only one format manager available so far. ezcPersistentCodeManager can load a PHP data structure. Future versions of PersistentObject might include additional format managers that could, for example, load an XML structure. You could also write your own format managers.

ezcPersistentCodeManager requires us to provide a directory with PHP files that can be loaded when an action is requested on a certain persistent object. For this reason, the PHP files must be named after the class names, but in lowercase. The following definition is stored in a file called persistent/ezcappcontactperson.php:

<?php

$def = new ezcPersistentObjectDefinition();
$def->table = 'person';
$def->class = 'ezcappContactPerson';
 
$def->properties['firstname']               = new ezcPersistentObjectProperty();
$def->properties['firstname']->columnName   = 'firstname';
$def->properties['firstname']->propertyName = 'firstname';
$def->properties['firstname']->propertyType = ezcPersistentObjectProperty::PHP_TYPE_STRING;
 
$def->idProperty               = new ezcPersistentObjectIdProperty();
$def->idProperty->columnName   = 'id';
$def->idProperty->propertyName = 'id';
$def->idProperty->generator    = new ezcPersistentGeneratorDefinition( 'ezcPersistentSequenceGenerator' );
 
$def->properties['lastname']               = new ezcPersistentObjectProperty();
$def->properties['lastname']->columnName   = 'lastname';
$def->properties['lastname']->propertyName = 'lastname';
$def->properties['lastname']->propertyType = ezcPersistentObjectProperty::PHP_TYPE_STRING;
 
// ...
 
return $def;
?>

The discussion on defining relations is found later in this article.

First, a definition of a persistent object is created using an instance of ezcPersistentObject. You need to specify two essential things in the definition: the database table and the equivalent class name.

Next, we define the properties of our persistent object, which is done by defining an array called properties in the definition object. (Beware that the term "property" will be used quite often in different contexts.) This array has a key for every property of the persistent object. For example, the property ezcappContactPerson->firstname is defined in $def->properties['firstname']. We need to specify the corresponding column from the database, its datatype and the name of the property.

The ID property, which is commonly called the "primary key" in relational database terminology, is a special case. The ID property is used to uniquely identify a persistent object, in order to manipulate it. The ID property needs a definition object, because it needs a "generator" in addition to the usual information for a property. The generator takes care of creating a unique ID when a new persistent object is stored in the database for the first time. In our example, we are using ezcPersistentSequenceGenerator, which relies on the database to provide a new ID. For MySQL, this is auto_increment and for Oracle this is a sequence.

Finally, the definition we just created is returned from the file. I had a baffled look on my face when I first thought about this. Usually, the return statement is used at the end of a function, but in this case, our definition file is included elsewhere in the application. Therefore, it makes use of the return statement similar to a function. If you want to read more about this, see the PHP manual about include.

Step 3: Using what we have

To perform any kind of action on a persistent object, we need an instance of ezcPersistentSession, which in turn needs a database connection:

<?php
 
$db = ezcDbFactory::create( DSN );
ezcDbInstance::set( $db );
 
$session = new ezcPersistentSession(
    $db,
    new ezcPersistentCodeManager( dirname( __FILE__ ) . "/persistent" )
);
ezcPersistentSessionInstance::set( $session );
 
?>

The first two lines perform the database initialization and store the newly created database connection in a singleton-like mechanism (for more information, see the Database component). After that, we create the persistent session itself. It also needs to load the format manager, as discussed in step 2. Finally, we store the persistent session instance in a singleton container so that it can be accessed throughout our application.

In the introduction section we discussed the basics of how to configure PersistentObject. We created a simple model class, specified the database mapping for it and instantiated the persistent session. We will now demonstrate how to configure relations between persistent objects and how to work with them in an application. Other models are used, which are all similar to the person model that was just created.

The first steps

Let's examine how a new persistent object can be stored. We therefore take a look at the create action, but still leave out the relations it uses. The usage of the UserInput component, which is used to retrieve the POST variables in the action, is skipped as well. Here is the relevant section of the source code:

<?php
//...
 
$session = ezcPersistentSessionInstance::get();
 
$this->person = new ezcappContactPerson();
$this->person->firstname = $form->firstname;
$this->person->lastname  = $form->lastname;
 
$session->save( $this->person );
 
//...
?>

In the first line, the ezcPersistentSession instance is retrieved. Then, we create a new ezcappContactPerson object, assign its properties from some input form data and save the object. PersistentObject puts our newly created person object into the database and assigns the generated unique ID to the defined ID property. Note that the form object already ensured that our data is secure and that PersistentObject uses bind values to internally secure the SQL queries.

So, that was easy. During the create action, however, we also retrieve the details for a new person, which are stored in the table detail and therefore in another persistent object. Basically, we need to define our first relation: a one-to-one relation. As mentioned earlier, the class ezcappContactDetail looks very similar to the ezcappContactPerson class. The same applies to its definition file.

Relations are defined in the persistent object definitions themselves, using the relations property of the ezcPersistentObjectDefinition instance. The following code must be added to the definition of the person object, in order to relate it to the detail object:

<?php
// ...
 
// 1:1 relation to detail table
$def->relations['ezcappContactDetail'] = new ezcPersistentOneToOneRelation(
    "person",
    "detail"
);
$def->relations['ezcappContactDetail']->columnMap = array(
    new ezcPersistentSingleTableMap(
        "id",
        "person"
    ),
);
$def->relations['ezcappContactDetail']->cascade = true;
 
// ...
?>

We create a new key for the desired class in the relations array. The configuration class for a one-to-one relation is called ezcPersistentOneToOneRelation. Its constructor receives the names of two database tables: the first is the name of the source table (which is actually the current one, person); the second is the name of the destination table for the relation (detail in this case).

Furthermore, the relation needs a mapping of columns, which defines how the objects are related to each other. In the case of a one-to-one relation, this works through ezcPersistentSingleTableMap, because we only map one table on each end (in contrast to ezcDoubleTableMap, which will be discussed later). In our case, we map the id column of the table person to the person column of the table detail. As you might have noticed, the column map is an array, which means that you can also specify multiple column mappings, using one instance of ezcPersistentSingleTableMap for each mapping.

Finally, we specify that this relation should cascade delete actions. This means that whenever we delete a person object from the database, the corresponding detail object is deleted as well.

Now that we have the first relation defined, it can be used in our example from before:

<?php
// ...
 
$session = ezcPersistentSessionInstance::get();
 
$this->person = new ezcappContactPerson();
$this->person->firstname = $form->firstname;
$this->person->lastname  = $form->lastname;
 
$session->save( $this->person );
 
$this->detail = new ezcappContactDetail();
$this->detail->birthday = trim( $form->birthday ) !== "" ? strtotime( $form->birthday ) : null;
$this->detail->comment  = $form->comment;
 
$session->addRelatedObject( $this->person, $this->detail );
$session->save( $this->detail );
 
// ...
?>

The first part, for storing the person object, stays the same. After that, we create the detail object and assign the necessary properties from the form data. The $session->addRelatedObject() method assigns the relation. Finally, the detail object is saved to the database. Note that the addRelatedObject() method only sets the necessary properties (as you will see later, it does a bit more for other relations) but does not actually store the object.

Easy, wasn't it? But I have to admit, I purposely left out a small point about the detail model. As you can see from the database structure, the detail table uses the same ID as the person table. If we would define the ID property of the detail object using ezcPersistentSequenceGenerator, as we did for the person object, we would run into problems: the sequence generator would try to generate a new ID, but we want it to use the same one as the person object. Therefore, we need to use another generator here:

<?php
// ...
 
$def->idProperty               = new ezcPersistentObjectIdProperty();
$def->idProperty->columnName   = 'person';
$def->idProperty->propertyName = 'person';
$def->idProperty->generator    = new ezcPersistentGeneratorDefinition( 'ezcPersistentManualGenerator' );
 
// ...
?>

Instead of the sequence generator, we use the manual generator, which allows us to manually set the ID property for the detail object. Actually, PersistentObject takes care of this: the call to addRelatedObject(), in the previous piece of code, sets the ID property because the id and person columns were mapped to each other.

We just defined our first relation: the one-to-one relation between the person and detail objects. Additionally, we saw that in most one-to-one cases the same keys are used for related objects. As a result, one of them needs to use the manual generator. We also saw how to save newly created persistent objects and how to relate two persistent objects to each other.

Next, we will define some more relation types and see how to fetch objects that are related to each other.

In our example, the show action is responsible for displaying a person object, its details, its email addresses and its physical addresses. The actions template also shows a list of all addresses so that the user can assign new ones to a person. Therefore, we need to fetch all address objects as well.

<?php
// ...
 
$session = ezcPersistentSessionInstance::get();
$this->person       = $session->load( "ezcappContactPerson", $this->id );
$this->detail       = $session->getRelatedObject( $this->person, "ezcappContactDetail" );
$this->emails       = $session->getRelatedObjects( $this->person, "ezcappContactEmail" );
$this->addresses    = $session->getRelatedObjects( $this->person, "ezcappContactAddress" );
 
$addressQuery = $session->createFindQuery( "ezcappContactAddress" );
$addressQuery->orderBy( "Zip" )->orderBy( "City" )->orderBy( "Street" );
$this->allAddresses = $session->find( $addressQuery, "ezcappContactAddress" );
 
// ...
?>

As usual, we need an instance of ezcPersistentSession for any kind of action on a persistent object. Next, we fetch the desired person from the database using $session->load(). If you are using this method, you should be aware that it will throw an exception if the desired object does not exist. To fetch an object only if it exists, use the method loadIfExists().

Then, we need to fetch the three related objects: the detail object, the email objects and the address objects. Note that two different methods are used for this: getRelatedObject() (used for fetching the detail object) will fetch exactly one object and will throw an exception if no object is found. In contrast, getRelatedObjects() will fetch all related objects from the database (0 or more) and will always return an array of objects (even if it is empty), no matter how many were found.

Finally, we need the list of all address objects. This has actually nothing to do with the topic of relations. The ezcPersistentSession instance can create a query object for us, which is already prepared to find objects of a given class ($session->createFindQuery()). The query object we retrieve is an instance of ezcQuerySelect, which is part of the SQL abstraction system of the Database component. Using this object as the basis, we can restrict the performed search and add SQL parts to it. As you can see, the query class uses a so-called "fluent interface", where each method call returns the object itself, so that one can call methods in a chain. We are adding three ORDER BY clauses to our query here. Finally, we instruct the persistent session to find all objects of the class ezcappContactAddress that match the given query and return an array of them.

Let us now take a look at the definitions for the relations we just used. Again, we are enhancing the definition file of the person object:

<?php
// ...
 
// 1:n relation to email table
$def->relations['ezcappContactEmail'] = new ezcPersistentOneToManyRelation(
    "person",
    "email"
);
$def->relations['ezcappContactEmail']->columnMap = array(
    new ezcPersistentSingleTableMap(
        "id",
        "person"
    ),
);
$def->relations['ezcappContactEmail']->cascade = true;
 
 
// n:m relation to address table
$def->relations['ezcappContactAddress'] = new ezcPersistentManyToManyRelation(
    "person",
    "address",
    "person_address"
);
$def->relations['ezcappContactAddress']->columnMap = array(
    new ezcPersistentDoubleTableMap(
        "id",
        "person",
        "address",
        "id"
    ),
);
 
// ...
?>

The relation between the person and email objects is the most common relation type, the one-to-many relation. It is defined completely analagous to the one-to-one relation, using the specific relation class and the two tables we want to connect to each other. Since we are only using two tables, we need a single table map, just as for ezcPersistentOneToOneRelation. The cascade option again has the same effect: if a person is deleted, all of the associated email addresses are deleted too.

The second relation we define here is the most complex type: the many-to-many relation. It is more complex than the other two types, because we need a relation table to implement it. Therefore, the constructor of ezcPersistentManyToManyRelation requires three table names instead of two: the source table, the destination table and the relation table.

We are connecting the person table to the address table using the person_address table. Therefore, the column map of a n:m relation is an array of ezcPersistentDoubleTableMap, which connects the three named tables to two table connections. First, the field from the source table maps to the first field of the relation table. The third item is the second field of the relation table, which maps to the field from the destination table. In other words, we are mapping the column id of the table person to the column person of the table person_address and the column address of the table person_address to the column id of the table address.

More about relations

As already mentioned, the many-to-many relation is the most complex relation type. Earlier, we loaded all addresses so that the user can assign them to a person. To analyze this a bit deeper, we will take a look at the address action:

<?php
// ...
 
$session = ezcPersistentSessionInstance::get();
$this->person  = $session->load( "ezcappContactPerson", $form->person );
$this->address = $session->load( "ezcappContactAddress", $form->address );
// ...
$session->addRelatedObject( $this->person, $this->address );
 
// ...
?>

We load both desired objects from the database, using the well-known load() method. Again, we are using the addRelatedObject() method, but this time without saving any of the objects afterwards. For all other relation types, the addRelatedObject() method simply sets the desired object properties to the correct values. This manipulates the objects themselves, so we need to store them. In the case of a many-to-many relation, the original objects are not modified in any way. Instead, a new record is inserted into the relation table, which occurs without any manual saving.

So, let us also examine the counterpart: the removeaddress action.

<?php
// ...
 
$session = ezcPersistentSessionInstance::get();
$this->person  = $session->load( "ezcappContactPerson", $form->person );
$this->address = $session->load( "ezcappContactAddress", $form->address );
// ...
$session->removeRelatedObject( $this->person, $this->address );
 
// ...
?>

The same procedure occurs here: both objects are loaded, then a call to removeRelatedObject() removes the relation. As you might guess, you can also use this method with all other types of relations, where it will simply unset the relation properties to remove the relation. If desired, you would have to manually remove the objects afterwards.

As a final word about many-to-many relations, notice that the cascade option does not exist for them. PersistentObject does not know (without more SQL query overhead) if related objects can be deleted, as it needs to find out whether they are referenced by other objects.

The final relation type is the many-to-one relation. With all of the relations we configured so far, we can fetch objects from the database that are related to a person object. What happens if we want to fetch a person object related to one of those other objects? We need to configure the corresponding relation.

If you want to learn more about this relation type (or so-called "reverse" relations), you should take a look at the definition files for the email and address objects. The PersistentObject tutorial also contains some information about this.

The example application in this article gives a nice overview on the usage of PersistentObject and if you dig a bit deeper into the code, you can also learn a lot about some other components.

You should also take a look at the PersistentObjectDatabaseSchemaTiein component, which can help with generating PersistentObject definitions.

For future releases, I expect that PersistentObject will become more and more powerful. We will definitely enhance the usability with some API candy and a primary goal is to reduce the generated SQL overhead as much as possible.

I hope you enjoyed reading this article and that you try out the PersistentObject component. I would be very happy to receive some feedback!