++ Introduction
Many times you may find classes having similar things within your models. These things may contain anything related to the schema of the component itself (relations, column definitions, index definitions etc.). One obvious way of refactoring the code is having a base class with some classes extending it.
However inheritance solves only a fraction of things. The following subchapters show how many times using Doctrine_Template is much more powerful and flexible than using inheritance.
Doctrine_Template is a class templating system. Templates are basically ready-to-use little components that your Record classes can load. When a template is being loaded its setTableDefinition() and setUp() methods are being invoked and the method calls inside them are being directed into the class in question.
++ Simple templates
In the following example we define a template called TimestampTemplate. Basically the purpose of this template is to add date columns 'created' and 'updated' to the record class that loads this template. Additionally this template uses a listener called Timestamp listener which updates these fields based on record actions.
class TimestampListener extends Doctrine_Record_Listener
{
public function preInsert(Doctrine_Event $event)
{
$event->getInvoker()->created = date('Y-m-d', time());
$event->getInvoker()->updated = date('Y-m-d', time());
}
public function preUpdate(Doctrine_Event $event)
{
$event->getInvoker()->created = date('Y-m-d', time());
$event->getInvoker()->updated = date('Y-m-d', time());
}
}
class TimestampTemplate extends Doctrine_Template
{
public function setTableDefinition()
{
$this->hasColumn('created', 'date');
$this->hasColumn('updated', 'date');
$this->setListener(new TimestampListener());
}
}
Lets say we have a class called Blog that needs the timestamp functionality. All we need to do is to add loadTemplate() call in the class definition.
class Blog extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('title', 'string', 200);
$this->hasColumn('content', 'string');
}
public function setUp()
{
$this->loadTemplate('TimestampTemplate');
}
}
++ Templates with relations
Many times the situations tend to be much more complex than the situation in the previous chapter. You may have model classes with relations to other model classes and you may want to replace given class with some extended class.
Consider we have two classes, User and Email, with the following definitions:
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
}
public function setUp()
{
$this->hasMany('Email', array('local' => 'id', 'foreign' => 'user_id'));
}
}
class Email extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('address', 'string');
$this->hasColumn('user_id', 'integer');
}
public function setUp()
{
$this->hasOne('User', array('local' => 'user_id', 'foreign' => 'id'));
}
}
Now if we extend the User and Email classes and create, for example, classes ExtendedUser and ExtendedEmail, the ExtendedUser will still have a relation to the Email class - not the ExtendedEmail class. We could of course override the setUp() method of the User class and define relation to the ExtendedEmail class, but then we lose the whole point of inheritance. Doctrine_Template can solve this problem elegantly with its dependency injection solution.
In the following example we'll define two templates, UserTemplate and EmailTemplate, with almost identical definitions as the User and Email class had.
class UserTemplate extends Doctrine_Template
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
}
public function setUp()
{
$this->hasMany('EmailTemplate as Email', array('local' => 'id', 'foreign' => 'user_id'));
}
}
class EmailTemplate extends Doctrine_Template
{
public function setTableDefinition()
{
$this->hasColumn('address', 'string');
$this->hasColumn('user_id', 'integer');
}
public function setUp()
{
$this->hasOne('UserTemplate as User', array('local' => 'user_id', 'foreign' => 'id'));
}
}
Notice how we set the relations. We are not pointing to concrete Record classes, rather we are setting the relations to templates. This tells Doctrine that it should try to find concrete Record classes for those templates. If Doctrine can't find these concrete implementations the relation parser will throw an exception, but before we go ahead of things, here are the actual record classes:
class User extends Doctrine_Record
{
public function setUp()
{
$this->loadTemplate('UserTemplate');
}
}
class Email extends Doctrine_Record
{
public function setUp()
{
$this->loadTemplate('EmailTemplate');
}
}
Now consider the following code snippet. This does NOT work since we haven't yet set any concrete implementations for the templates.
$user = new User();
$user->Email; // throws an exception
The following version works. Notice how we set the concrete implementations for the templates globally using Doctrine_Manager.
$manager = Doctrine_Manager::getInstance();
$manager->setImpl('UserTemplate', 'User')
->setImpl('EmailTemplate', 'Email');
$user = new User();
$user->Email;
The implementations for the templates can be set at manager, connection and even at the table level.
++ Delegate methods
Besides from acting as a full table definition delegate system, Doctrine_Template allows the delegation of method calls. This means that every method within the loaded templates is available in the record that loaded the templates. Internally the implementation uses magic method called __call() to achieve this functionality.
Lets take an example: we have a User class that loads authentication functionality through a template.
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('fullname', 'string', 30);
}
public function setUp()
{
$this->loadTemplate('AuthTemplate');
}
}
class AuthTemplate extends Doctrine_Template
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 16);
$this->hasColumn('password', 'string', 16);
}
public function login($username, $password)
{
// some login functionality here
}
}
Now you can simply use the methods found in AuthTemplate within the User class as shown above.
$user = new User();
$user->login($username, $password);
You can get the record that invoked the delegate method by using the getInvoker() method of Doctrine_Template. Consider the AuthTemplate example. If we want to have access to the User object we just need to do the following:
class AuthTemplate extends Doctrine_Template
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 16);
$this->hasColumn('password', 'string', 16);
}
public function login($username, $password)
{
// do something with the Invoker object here
$object = $this->getInvoker();
}
}
++ Working with multiple templates
Each class can consists of multiple templates. If the templates contain similar definitions the most recently loaded template always overrides the former.
++ Core Templates
Doctrine comes bundled with some templates that offer out of the box functionality for your models. You can enable these templates in your models very easily. You can do it directly in your Doctrine_Records or you can specify them in your yaml schema if you are managing your models with a yaml schema file.
+++ Versionable
PHP Example
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 125);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->actAs('Versionable', array('versionColumn' => 'version', 'className' => '%CLASS%Version'));
}
}
YAML Example
---
User:
actAs:
Versionable:
versionColumn: version
className: %CLASS%Version
columns:
username:
type: string(125)
password:
type: string(255)
+++ Timestampable
The 2nd argument array is not required. It defaults to all the values that are present in the example below.
PHP Example
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 125);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->actAs('Timestampable', array('created' => array('name' => 'created_at',
'type' => 'timestamp',
'format' => 'Y-m-d H:i:s',
'disabled' => false,
'options' => array()),
'updated' => array('name' => 'updated_at',
'type' => 'timestamp',
'format' => 'Y-m-d H:i:s',
'disabled' => false,
'options' => array())));
}
}
YAML Example
---
User:
actAs:
Timestampable:
created:
name: created_at
type: timestamp
format:Y-m-d H:i:s
options: []
updated:
name: updated_at
type: timestamp
format: Y-m-d H:i:s
options: []
columns:
username:
type: string(125)
password:
type: string(255)
If you are only interested in using only one of the columns, such as a created_at timestamp, but not a an updated_at field, set the flag disabled=>true for either of the fields as in the example below.
YAML Example
---
User:
actAs:
Timestampable:
created:
name: created_at
type: timestamp
format:Y-m-d H:i:s
options: []
updated:
disabled: true
columns:
username:
type: string(125)
password:
type: string(255)
+++ Sluggable
If you do not specify the columns to create the slug from, it will default to just using the toString() method on the model.
PHP Example
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 125);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->actAs('Sluggable', array('fields' => array('username')));
}
}
YAML Example
---
User:
actAs:
Sluggable:
columns: [username]
fields:
username:
type: string(125)
password:
type: string(255)
+++ I18n
PHP Example
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 125);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->actAs('I18n', array('fields' => array('title')));
}
}
YAML Example
---
User:
actAs:
I18n:
fields: [title]
columns:
username:
type: string(125)
password:
type: string(255)
+++ NestedSet
PHP Example
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 125);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->actAs('NestedSet', array('hasManyRoots' => true, 'rootColumnName' => 'root_id'));
}
}
YAML Example
---
User:
actAs:
NestedSet:
hasManyRoots: true
rootColumnName: root_id
columns:
username:
type: string(125)
password:
type: string(255)
+++ Searchable
PHP Example
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 125);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->actAs('Searchable', array('fields' => array('title', 'content')));
}
}
YAML Example
---
User:
actAs:
Searchable:
fields: [title, content]
columns:
username:
type: string(125)
password:
type: string(255)