Invenzzia » Resources / Articles / A photo gallery tutorial / Extending models

5. Extending models

By default, Doctrine generates two files for each model: ModelName and BaseModelName in /models/generated. The second one is rebuilt every time we run build-all-reload so we should not modify it manually. On the other hand, we have the first class that extends the base model. Here we can add some extra code that cannot be directly specified in YAML format.

The models should prepare the data to be displayed by the presentation layer (in our case – Open Power Template) so that it does not have to process them on its own. To achieve this, we need to extend the default models to provide some new information generated from the raw data from the database. In our case the critical field is the field with the photo/comment creation date. We know the following facts about it:

  1. In the database, we store the data as simple Unix timestamp (the number of seconds since 1 January, 1970). Although Doctrine supports the timestamp type, we do not use its features and moreover it would force us to perform even more complex conversions on the script side.

  2. The date field must be initialized with the current time while adding a new row.

  3. The script should display the date in the human-readable format, for example May 14, 2009, 11:15 AM.

The first goal is done, there are two to go and we will use two new Doctrine features for it:

  • Event listeners.
  • Automatic accessor overriding.

Doctrine allows the model to respond on various events and perform extra user-defined operations then. For example, before we insert a new row, we would like to initialize some fields automatically so that the model user does not have to remember about them. Some of the events can be handled simply by overriding the appropriate method in the model, and some other – with an event listener class. The listeners section in the YAML file specified the event listener classes used by the model and we just need to write them. Let's open the /models/Photo.php file and add a new method to the empty Photo class:

    public function preInsert($event)
        $this->date = time();
    } // end preInsert();

This is it: before a new row is inserted to the database, Doctrine runs the very method and sets the date field to the current time. Besides preInsert(), the library offers several other methods, such as postInsert() (executed after the row insertion) and similar operations for updating and deleting. They are especially useful, if our rows represent some files or directories in the file system. With event listeners, the models can handle them transparently to the user. As an exercise, you might want to add postDelete() method that removes the photo from the disk together with the row.

The date formatting must be performed in two ways. The first one is the use of the automatic accessor overriding allowing us to create fictional fields in our models. From the user's point of view, there is no difference between the true and fictional fields – we may read and save to them. However, Doctrine notices the difference and in case of auto accessor overriding, it redirects the call to such "false field" to one of our methods where we can do whatever we want to. In our case, the original date field contains the date in the system format: the number of seconds since 1.1.1970. A corresponding false field, date_text will run the accessor method that executes the date() function on the original date and returns the formatted text. Below, we can see the accessor method:

    public function getDateText()
        return date('F j, Y, g:i a', $this->date);
    } // end getDateText();

The formatted date is created on-the-fly on the user demand from the value of the date field. To process the value setting, we need to create setDateText($value) method, however in our case we do not need it. Please note that Doctrine automatically converts the naming style – the fields use the foo_bar convention, whereas their accessor methods – getFooBar and setFooBar.

The automatic accessor overriding should be used very carefully. They have a higher priority over the ordinary fields and the functionality is not too well-designed, because... it was not supposed to appear in Doctrine. It was forced by the library users, but the developers decided to make it disabled by default. The problem is that Doctrine models have some internal methods like getSomething that have nothing to do with the automatic accessors. If we create a field with one of these "reserved" names by accident, our script will surely crash. If you are going to use this feature, please take a look at the API documentation to see, what field names must not be used then.

However, the accessor method solves the problem only partially. It works only, when we access the model object, whereas in most cases we would like the SELECT statements to return arrays because of performance reasons. Here, we may find the event listener class very helpful where we plug into the hydration procedure (the conversion of the result set to the requested format) and add the required extra fields to the output array. This is a complete class that can be pasted to the same file, as the model:

class PhotoListener extends Doctrine_Record_Listener
    public function preHydrate(Doctrine_Event $event)
        $data = $event->data;
            $data['date_text'] = date('F j, Y, g:i a', $data['date']);
        $event->data = $data;
    } // end preHydrate();
} // end PhotoListener;

The preHydrate() method is executed when Doctrine attempts to build an array from the query result. We check whether it contains the date field and optionally generate its formatted version. You must pay attention to two issues here:

  1. You cannot access the $event->data field directly. Its value must be rewritten to a temporary variable and at the end – back to the field, as in the example.

  2. The result set does not have to contain the date field – before we perform any operation on it, we must check if it actually exists.

The Comment model contains the date field to that should be processed in the same way. As an exercise, try to extend the comment model using the code we have already written for Photo as a sample.