The PersistentObject eZ Component: Putting Relations Where Relations Belong ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :Author: Tobias Schlitt :Revision: $Rev$ :Date: $Date$ :Status: Draft .. contents:: Introduction ============ One of the cool new features of the eZ Components 2006.2 release is the support for relation mappings in the PersistentObject component. PersistentObject already allowed you to store objects in a database (and retrieve them from there). 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. This article will give you an introduction to the PersistentObject component in general, and explain the new feature of handling relations. To aid in the explanations, we will look at code from a simple example application, as is usually the case with my articles. 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 ======================= We will create a simple address book that allows you manage information about your friends, and their addresses and email addresses. 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". .. include:: trunk/docs/ezccontact.sql :literal: 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 and every person may 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 (so, 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: 1:n (one-to-many). - A person can have multiple 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/). .. _`application source`: 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. Getting things started ====================== 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 two previous points. Creating the first model (step 1) --------------------------------- 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 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: .. include:: trunk/models/person.php :literal: 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 the implementation of setState() is a bit sloppy (for simplicity reasons) as it would also set non-existent properties. Telling PersistentObject about it (step 2) ------------------------------------------ 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.:: 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; ?> For now, we will leave out the discussion on defining relations until later in this article. First, a definition of a persistent object is created using an instance of ezcPersistentObject definition. You need to specify two essential things to 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" 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 into the database for the first time. In our example, we are using the "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 believe I'd see some baffled faces now that you read this , because I must have looked the same, when I saw this first. If you want to know more about this, take a look at the `PHP manual about include`_. .. _`PHP manual about include`: http://www.php.net/manual/en/function.include.php#AEN5250 Using what we have (step 3) --------------------------- To perform any kind of action on a persistent object, we need an instance of ezcPersistentSession, which in turn needs a database connection: :: 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. .. _`Database component`: http://ez.no/doc/components/view/2006.2/(file)/classtrees_Database.html Using persistent objects ======================== 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 shortened source code: :: 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: :: 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: :: 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, while we want it to use the same one as the person object. Therefore, we need to use another generator here: :: 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, set the ID property, because the "id" and "person" columns were mapped to each other. .. _`UserInput component`: http://ez.no/doc/components/view/2006.2/(file)/classtrees_UserInput.html Advanced relations ------------------ 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. :: 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 in 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: :: 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 its 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 realize 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 realizes 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_addresss" 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: :: 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. That is actually not necessary here. 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: "removeaddress" action. :: 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. Again, you would have to manually store the object afterwards. As a final word about many-to-many relations, notice that the cascade option does not exist for them. PersistentObject does not know (or only with 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 it. .. _`PersistentObject tutorial`: http://ez.no/doc/components/view/2006.2/(file)/introduction_PersistentObject.html Final thoughts ============== 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 .. _`PersistentObjectDatabaseSchemaTiein component`: http://ez.no/doc/components/view/2006.2/(file)/classtrees_EventLogDatabaseTiein.html 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! .. Local Variables: mode: rst fill-column: 79 End: vim: et syn=rst tw=79