0.10 User Manual
0.10 User Manual
Chapter 17 Class templates
17.1 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.
17.2 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.
Listing 17.1
<?php
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.
Listing 17.2
<?php
class Blog extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('title', 'string', 200);
$this->hasColumn('content', 'string');
}
public function setUp()
{
$this->loadTemplate('TimestampTemplate');
}
}
?>
17.3 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:
Listing 17.3
<?php
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.
Listing 17.4
<?php
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:
Listing 17.5
<?php
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.
Listing 17.6
<?php
$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.
Listing 17.7
<?php
$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.
17.4 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.
Listing 17.8
<?php
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.
Listing 17.9
<?php
$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:
Listing 17.10
<?php
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();
}
}
?>
17.5 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.
17.6 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.
17.6.1 Versionable
Listing 17.11
<?php
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'));
}
}
?>
Listing 17.12
---
User:
actAs:
Versionable:
versionColumn: version
className: %CLASS%Version
columns:
username:
type: string(125)
password:
type: string(255)
17.6.2 Timestampable
The 2nd argument array is not required. It defaults to all the values that are present in the example below.
Listing 17.13
<?php
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())));
}
}
?>
Listing 17.14
---
User:
actAs:
Timestampable:
created:
name: created_at
type: timestamp
format:Y-m-d H:i:s
updated:
name: updated_at
type: timestamp
format: Y-m-d H:i:s
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.
Listing 17.15
---
User:
actAs:
Timestampable:
created:
name: created_at
type: timestamp
format:Y-m-d H:i:s
updated:
disabled: true
columns:
username:
type: string(125)
password:
type: string(255)
17.6.3 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.
Listing 17.16
<?php
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')));
}
}
?>
Listing 17.17
---
User:
actAs:
Sluggable:
fields: [username]
columns:
username:
type: string(125)
password:
type: string(255)
17.6.4 I18n
Listing 17.18
<?php
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')));
}
}
?>
Listing 17.19
---
User:
actAs:
I18n:
fields: [title]
columns:
username:
type: string(125)
password:
type: string(255)
17.6.5 NestedSet
Listing 17.20
<?php
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'));
}
}
?>
Listing 17.21
---
User:
actAs:
NestedSet:
hasManyRoots: true
rootColumnName: root_id
columns:
username:
type: string(125)
password:
type: string(255)
17.6.6 Searchable
Listing 17.22
<?php
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')));
}
}
?>
Listing 17.23
---
User:
actAs:
Searchable:
fields: [title, content]
columns:
username:
type: string(125)
password:
type: string(255)
17.6.7 Geographical
The below is only a demo. The geographical behavior can be used with any data record for determining the number of miles
or kilometers between 2 records.
Listing 17.24
<?php
class Zipcode extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('zipcode', 'string', 255);
$this->hasColumn('city', 'string', 255);
$this->hasColumn('state', 'string', 2);
$this->hasColumn('county', 'string', 255);
$this->hasColumn('zip_class', 'string', 255);
}
public function setUp()
{
parent::setUp();
$this->actAs('Geographical');
}
}
Listing 17.25
Zipcode:
actAs: [Geographical]
columns:
zipcode: string(255)
city: string(255)
state: string(2)
county: string(255)
zip_class: string(255)
The geographical plugin automatically adds the latitude and longitude columns to the records used for calculating distance
between 2 records.
Usage
Listing 17.26
<?php
$zipcode1 = Doctrine::getTable('Zipcode')->findOneByZipcode('37209');
$zipcode2 = Doctrine::getTable('Zipcode')->findOneByZipcode('37388');
// get distance between to zipcodes
echo $zipcode1->getDistance($zipcode2, $kilometers = false);
// Get the 50 closest zipcodes that are not in the same city
$query = $zipcode1->getDistanceQuery();
$query->orderby('miles asc');
$query->addWhere($query->getRootAlias() . '.city != ?', $zipcode1->city);
$query->limit(50);
$result = $query->execute();
foreach ($result as $zipcode) {
echo $zipcode->city . " - " . $zipcode->miles . "<br/>"; // $zipcode->kilometers
}
?>
Get some sample zip code data to test this
http://www.populardata.com/zip_codes.zip
Download and import the csv file with the following code
Listing 17.27
<?php
function parseCsvFile($file, $columnheadings = false, $delimiter = ',', $enclosure = "\"")
{
$row = 1;
$rows = array();
$handle = fopen($file, 'r');
while (($data = fgetcsv($handle, 1000, $delimiter, $enclosure)) !== FALSE) {
if (!($columnheadings == false) && ($row == 1)) {
$headingTexts = $data;
} elseif (!($columnheadings == false)) {
foreach ($data as $key => $value) {
unset($data[$key]);
$data[$headingTexts[$key]] = $value;
}
$rows[] = $data;
} else {
$rows[] = $data;
}
$row++;
}
fclose($handle);
return $rows;
}
$array = parseCsvFile('zipcodes.csv', false);
foreach ($array as $key => $value) {