This project is no longer maintained and has been archived. |
Defining Models
As we mentioned before, at the lowest level in Doctrine your schema is represented by a set of php classes that map the schema meta data for your database tables.
In this chapter we will explain in detail how you can map your schema information using php code.
Columns
One problem with database compatibility is that many databases differ in their
behavior of how the result set of a query is returned. MySQL leaves the field
names unchanged, which means if you issue a query of the form `"SELECT myField
FROM ..."`` then the result set will contain the field ``myField
`.
Unfortunately, this is just the way MySQL and some other databases do it. Postgres for example returns all field names in lowercase whilst Oracle returns all field names in uppercase. "So what? In what way does this influence me when using Doctrine?", you may ask. Fortunately, you don't have to bother about that issue at all.
Doctrine takes care of this problem transparently. That means if you define a
derived Record class and define a field called myField
you will always
access it through $record->myField
(or $record['myField']
, whatever
you prefer) no matter whether you're using MySQL or Postgres or Oracle etc.
In short: You can name your fields however you want, using under_scores, camelCase or whatever you prefer.
In Doctrine columns and column aliases are case sensitive. So when you are using columns in your DQL queries, the column/field names must match the case in your model definition. |
Column Lengths
In Doctrine column length is an integer that specifies the column length. Some
column types depend not only the given portable type but also on the given
length. For example type string
with length 1000 will be translated into native
type TEXT
on mysql.
The length is different depending on the type of column you are using:
integer
- Length is the the number of bytes the integer occupies.
string
- Number of the characters allowed in the string.
float/decimal
- Total number of characters allowed excluding the decimal.
enum
- If using native enum length does not apply but if using emulated enums then it is just the string length of the column value.
Column Aliases
Doctrine offers a way of setting column aliases. This can be very useful when you want to keep the application logic separate from the database logic. For example if you want to change the name of the database field all you need to change at your application is the column definition.
// models/Book.php
class Book extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('bookTitle as title', 'string');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Now the column in the database is named bookTitle but you can access the property on your objects using title.
Default values
Doctrine supports default values for all data types. When default value is attached to a record column this means two things. First this value is attached to every newly created Record and when Doctrine creates your database tables it includes the default value in the create table statement.
// models/generated/BaseUser.php
class User extends BaseUser
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 255,
array('default' => 'default username'));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Now when you print the name on a brand new User record it will print the default value:
// test.php
$ user = new User();
echo $user->username; // default username
Data types
Introduction
All DBMS provide multiple choice of data types for the information that can be stored in their database table fields. However, the set of data types made available varies from DBMS to DBMS.
To simplify the interface with the DBMS supported by Doctrine, a base set of data types was defined. Applications may access them independently of the underlying DBMS.
The Doctrine applications programming interface takes care of mapping data types when managing database options. It is also able to convert that is sent to and received from the underlying DBMS using the respective driver.
The following data type examples should be used with Doctrine's createTable method. The example array at the end of the data types section may be used with createTable to create a portable table on the DBMS of choice (please refer to the main Doctrine documentation to find out what DBMS back ends are properly supported). It should also be noted that the following examples do not cover the creation and maintenance of indices, this chapter is only concerned with data types and the proper usage thereof.
It should be noted that the length of the column affects in database level type as well as application level validated length (the length that is validated with Doctrine validators).
- Example: Column named
content
with typestring
and length 3000 results in database typeTEXT
of which has database level length of 4000. However when the record is validated it is only allowed to have 'content' -column with maximum length of 3000. - Example: Column with type
integer
and length 1 results inTINYINT
on many databases.
In general Doctrine is smart enough to know which integer/string type to use depending on the specified length.
Type modifiers
Within the Doctrine API there are a few modifiers that have been designed to aid in optimal table design. These are:
- The notnull modifiers
- The length modifiers
- The default modifiers
- unsigned modifiers for some field definitions, although not all DBMS's support this modifier for integer field types.
- collation modifiers (not supported by all drivers)
- fixed length modifiers for some field definitions.
Building upon the above, we can say that the modifiers alter the field definition to create more specific field types for specific usage scenarios. The notnull modifier will be used in the following way to set the default DBMS NOT NULL Flag on the field to true or false, depending on the DBMS's definition of the field value: In PostgreSQL the "NOT NULL" definition will be set to "NOT NULL", whilst in MySQL (for example) the "NULL" option will be set to "NO". In order to define a "NOT NULL" field type, we simply add an extra parameter to our definition array (See the examples in the following section)
Using the above example, we can also explore the default field operator. Default is set in the same way as the notnull operator to set a default value for the field. This value may be set in any character set that the DBMS supports for text fields, and any other valid data for the field's data type. In the above example, we have specified a valid time for the "Time" data type, '12:34:05'. Remember that when setting default dates and times, as well as datetimes, you should research and stay within the epoch of your chosen DBMS, otherwise you will encounter difficult to diagnose errors!
The above example will create a character varying field of length 12 characters in the database table. If the length definition is left out, Doctrine will create a length of the maximum allowable length for the data type specified, which may create a problem with some field types and indexing. Best practice is to define lengths for all or most of your fields.
Boolean
The boolean data type represents only two values that can be either 1 or 0. Do not assume that these data types are stored as integers because some DBMS drivers may implement this type with single character text fields for a matter of efficiency. Ternary logic is possible by using null as the third possible value that may be assigned to fields of this type.
The next several examples are not meant for you to use and give them a try. They are simply for demonstrating purposes to show you how to use the different Doctrine data types using PHP code or YAML schema files. |
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('booltest', 'boolean');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Integer
The integer type is the same as integer type in PHP. It may store integer values as large as each DBMS may handle.
Fields of this type may be created optionally as unsigned integers but not all DBMS support it. Therefore, such option may be ignored. Truly portable applications should not rely on the availability of this option.
The integer type maps to different database type depending on the column length.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('integertest', 'integer', 4, array(
'unsigned' => true
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Float
The float data type may store floating point decimal numbers. This data type is suitable for representing numbers withina large scale range that do not require high accuracy. The scale and the precision limits of the values that may be stored in a database depends on the DBMS that it is used.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('floattest', 'float');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Decimal
The decimal data type may store fixed precision decimal numbers. This data type is suitable for representing numbers that require high precision and accuracy.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('decimaltest', 'decimal');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
You can specify the length of the decimal just like you would set the
length
of any other column and you can specify the scale
as an
option in the third argument:
class Test extends Doctrine_Record
{
public function setTableDefinition() {
$this->hasColumn('decimaltest', 'decimal', 18,
array('scale' => 2)
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
String
The text data type is available with two options for the length: one that is explicitly length limited and another of undefined length that should be as large as the database allows.
The length limited option is the most recommended for efficiency reasons. The undefined length option allows very large fields but may prevent the use of indexes, nullability and may not allow sorting on fields of its type.
The fields of this type should be able to handle 8 bit characters. Drivers take care of DBMS specific escaping of characters of special meaning with the values of the strings to be converted to this type.
By default Doctrine will use variable length character types. If fixed length types should be used can be controlled via the fixed modifier.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('stringtest', 'string', 200, array(
'fixed' => true
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Array
This is the same as the 'array' type in PHP:
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('arraytest', 'array', 10000);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Object
Doctrine supports objects as column types. Basically you can set an object to a field and Doctrine handles automatically the serialization / unserialization of that object.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('objecttest', 'object');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
The array and object types simply serialize the data when persisting to the database and unserialize the data when pulling from the database. |
Blob
Blob (Binary Large OBject) data type is meant to store data of undefined length that may be too large to store in text fields, like data that is usually stored in files.
Blob fields are usually not meant to be used as parameters of query
search clause (WHERE
) unless the underlying DBMS supports a feature
usually known as "full text search".
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('blobtest', 'blob');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Clob
Clob (Character Large OBject) data type is meant to store data of undefined length that may be too large to store in text fields, like data that is usually stored in files.
Clob fields are meant to store only data made of printable ASCII characters whereas blob fields are meant to store all types of data.
Clob fields are usually not meant to be used as parameters of query
search clause (WHERE
) unless the underlying DBMS supports a feature
usually known as "full text search".
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('clobtest', 'clob');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Timestamp
The timestamp data type is a mere combination of the date and the time
of the day data types. The representation of values of the time stamp
type is accomplished by joining the date and time string values in a
single string joined by a space. Therefore, the format template is
YYYY-MM-DD HH:MI:SS
. The represented values obey the same rules and
ranges described for the date and time data types.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('timestamptest', 'timestamp');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Time
The time data type may represent the time of a given moment of the day. DBMS independent representation of the time of the day is also accomplished by using text strings formatted according to the ISO-8601 standard.
The format defined by the ISO-8601 standard for the time of the day is HH:MI:SS where HH is the number of hour the day from 00 to 23 and MI and SS are respectively the number of the minute and of the second from 00 to 59. Hours, minutes and seconds numbered below 10 should be padded on the left with 0.
Some DBMS have native support for time of the day formats, but for others the DBMS driver may have to represent them as integers or text values. In any case, it is always possible to make comparisons between time values as well sort query results by fields of this type.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('timetest', 'time');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Date
The date data type may represent dates with year, month and day. DBMS independent representation of dates is accomplished by using text strings formatted according to the IS0-8601 standard.
The format defined by the ISO-8601 standard for dates is YYYY-MM-DD where YYYY is the number of the year (Gregorian calendar), MM is the number of the month from 01 to 12 and DD is the number of the day from 01 to 31. Months or days numbered below 10 should be padded on the left with 0.
Some DBMS have native support for date formats, but for others the DBMS driver may have to represent them as integers or text values. In any case, it is always possible to make comparisons between date values as well sort query results by fields of this type.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('datetest', 'date');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Enum
Doctrine has a unified enum type. The possible values for the column can be specified on the column definition with Doctrine_Record::hasColumn
If you wish to use native enum types for your DBMS if it supports it then you must set the following attribute:
|
Here is an example of how to specify the enum values:
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('enumtest', 'enum', null,
array('values' => array('php', 'java', 'python'))
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Gzip
Gzip datatype is the same as string except that its automatically compressed when persisted and uncompressed when fetched. This datatype can be useful when storing data with a large compressibility ratio, such as bitmap images.
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('gziptest', 'gzip');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
The family of php functions for compressing are used internally for compressing and uncompressing the contents of the gzip column type. |
Examples
Consider the following definition:
class Example extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('id', 'string', 32, array(
'type' => 'string',
'fixed' => 1,
'primary' => true,
'length' => '32' )
);
$this->hasColumn('someint', 'integer', 10, array(
'type' => 'integer',
'unsigned' => true,
'length' => '10'
));
$this->hasColumn('sometime', 'time', 25, array(
'type' => 'time',
'default' => '12:34:05',
'notnull' => true,
'length' => '25'
));
$this->hasColumn('sometext', 'string', 12, array(
'type' => 'string',
'length' => '12'
));
$this->hasColumn('somedate', 'date', 25, array(
'type' => 'date',
'length' => '25'
));
$this->hasColumn('sometimestamp', 'timestamp', 25, array(
'type' => 'timestamp',
'length' => '25'
));
$this->hasColumn('someboolean', 'boolean', 25, array(
'type' => 'boolean',
'length' => '25'
));
$this->hasColumn('somedecimal', 'decimal', 18, array(
'type' => 'decimal',
'length' => '18'
));
$this->hasColumn('somefloat', 'float', 2147483647, array(
'type' => 'float',
'length' => '2147483647'
));
$this->hasColumn('someclob', 'clob', 2147483647, array(
'type' => 'clob',
'length' => '2147483647'
));
$this->hasColumn('someblob', 'blob', 2147483647, array(
'type' => 'blob',
'length' => '2147483647'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
1 Example:
tableName: example
columns:
id:
type: string(32)
fixed: true
primary: true
someint:
type: integer(10)
unsigned: true
sometime:
type: time(25)
default: '12:34:05'
notnull: true
sometext: string(12)
somedate: date(25)
sometimestamp: timestamp(25)
someboolean: boolean(25)
somedecimal: decimal(18)
somefloat: float(2147483647)
someclob: clob(2147483647)
someblob: blob(2147483647)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
The above example will create the following database table in Pgsql:
Column | Type |
---|---|
id |
character(32) |
someint |
integer |
sometime |
time without time zone |
sometext |
character or varying(12) |
somedate |
date |
sometimestamp |
timestamp without time zone |
someboolean |
boolean |
somedecimal |
numeric(18,2) |
somefloat |
double precision |
someclob |
text |
someblob |
bytea |
The schema will create the following database table in Mysql:
Field | Type |
---|---|
id |
char(32) |
someint |
integer |
sometime |
time |
sometext |
varchar(12) |
somedate |
date |
sometimestamp |
timestamp |
someboolean |
tinyint(1) |
somedecimal |
decimal(18,2) |
somefloat |
double |
someclob |
longtext |
someblob |
longblob |
Relationships
Introduction
In Doctrine all record relations are being set with Doctrine_Record::hasOne methods. Doctrine supports almost all kinds of database relations from simple one-to-one foreign key relations to join table self-referencing relations.
Unlike the column definitions the Doctrine_Record::hasMany and Doctrine_Record::hasOne methods are placed within a method called setUp(). Both methods take two arguments: the first argument is a string containing the name of the class and optional alias, the second argument is an array consisting of relation options. The option array contains the following keys:
Name | Optional | Description |
---|---|---|
local |
No | The local field of the relation. Local field is the linked field inthe defining class. |
foreign |
No | The foreign fieldof the relation. Foreign field is the linked field in the linked class. |
refClass |
Yes | The name of the association class.This is only needed for many-to-many associations. |
owningSide |
Yes | Set to boolean true to indicate the owningside of the relation. The owning side is the side that owns the foreignkey. There can only be one owning side in an association between twoclasses. Note that this option is required if Doctrine can't guess theowning side or it's guess is wrong. An example where this is the case iswhen both 'local' and 'foreign' are part of the identifier (primarykey). It never hurts to specify the owning side in this way. |
onDelete |
Yes | The onDelete integrity action that is applied on
the foreign key constraint when the tables are
created byDoctrine. |
onUpdate |
Yes | The onUpdate integrity action that is applied on
the foreign key constraint when thetables are
created by Doctrine. |
cascade |
Yes | Specify application level cascading operations. Currently only delete issupported |
So lets take our first example, say we have two classes Forum_Board
and Forum_Thread
. Here Forum_Board
has many
Forum_Threads
, hence their relation is one-to-many. We don't want
to write Forum_
when accessing relations, so we use relation
aliases and use the alias Threads.
First lets take a look at the Forum_Board
class. It has three
columns: name, description and since we didn't specify any primary key,
Doctrine auto-creates an id column for it.
We define the relation to the Forum_Thread
class by using the
hasMany method. Here the local field is the primary key of the
board class whereas the foreign field is the board_id
field of the
Forum_Thread
class.
// models/Forum_Board.php
class Forum_Board extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 100);
$this->hasColumn('description', 'string', 5000);
}
public function setUp()
{
$this->hasMany('Forum_Thread as Threads', array(
'local' => 'id',
'foreign' => 'board_id'
));
}
}
Notice the as keyword being used above. This means that the |
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Then lets have a peek at the Forum_Thread
class. The columns here
are irrelevant, but pay attention to how we define the relation. Since
each Thread can have only one Board we are using the hasOne
method. Also notice how we once again use aliases and how the local
column here is board_id
while the foreign column is the id
column.
// models/Forum_Thread.php
class Forum_Thread extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('board_id', 'integer');
$this->hasColumn('title', 'string', 200);
$this->hasColumn('updated', 'integer', 10);
$this->hasColumn('closed', 'integer', 1);
}
public function setUp()
{
$this->hasOne('Forum_Board as Board', array(
'local' => 'board_id',
'foreign' => 'id'
));
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
1 # schema.yml
Forum_Thread:
columns:
user_id: integer
board_id: integer
title: string(200)
updated: integer(10)
closed: integer(1)
relations:
User:
local: user_id
foreign: id
foreignAlias: Threads
Board:
class: Forum_Board
local: board_id
foreign: id
foreignAlias: Threads
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Now we can start using these classes. The same accessors that you've already used for properties are all available for relations.
First lets create a new board:
// test.php
$ board = new Forum_Board();
$ board->name = 'Some board';
Now lets create a new thread under the board:
// ...
$ board->Threads[0]->title = 'new thread 1';
$ board->Threads[1]->title = 'new thread 2';
Each Thread
needs to be associated to a user so lets create a new
User
and associate it to each Thread
:
// ...
$ user = new User();
$ user->username = 'jwage';
$ board->Threads[0]->User= $user;
$ board->Threads[1]->User = $user;
Now we can save all the changes with one call. It will save the new board as well as its threads:
// ...
$ board->save();
Lets do a little inspecting and see the data structure that is created when you use the code from above. Add some code to test.php to output an array of the object graph we've just populated:
print_r($board->toArray(true));
The Doctrine_Record::toArray takes all the data of a
Doctrine_Record instance and converts it to an array so you
can easily inspect the data of a record. It accepts an argument named
|
Now when you execute test.php with PHP from your terminal you should see the following
1 $ php test.php
Array (
[id] => 2
[name] => Some board
[description] =>
[Threads] => Array
(
[0] => Array
(
[id] => 3
[user_id] => 1
[board_id] => 2
[title] => new thread 1
[updated] =>
[closed] =>
[User] => Array
(
[id] => 1
[is_active] => 1
[is_super_admin] => 0
[first_name] =>
[last_name] =>
[username] => jwage
[password] =>
[type] =>
[created_at] => 2009-01-20 16:41:57
[updated_at] => 2009-01-20 16:41:57
)
)
[1] => Array
(
[id] => 4
[user_id] => 1
[board_id] => 2
[title] => new thread 2
[updated] =>
[closed] =>
[User] => Array
(
[id] => 1
[is_active] => 1
[is_super_admin] => 0
[first_name] =>
[last_name] =>
[username] => jwage
[password] =>
[type] =>
[created_at] => 2009-01-20 16:41:57
[updated_at] => 2009-01-20 16:41:57
)
)
)
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Notice how the auto increment primary key and foreign keys are automatically set by Doctrine internally. You don't have to worry about the setting of primary keys and foreign keys at all! |
Foreign Key Associations
One to One
One-to-one relations are probably the most basic relations. In the
following example we have two classes, User
and Email
with their
relation being one-to-one.
First lets take a look at the Email
class. Since we are binding a
one-to-one relationship we are using the hasOne method. Notice how
we define the foreign key column (user_id
) in the Email
class.
This is due to a fact that Email
is owned by the User
class and
not the other way around. In fact you should always follow this
convention - always place the foreign key in the owned class.
The recommended naming convention for foreign key columns is:
[tableName]_[primaryKey]
. As here the foreign table is 'user' and
its primary key is 'id' we have named the foreign key column as
'user_id'.
// models/Email.php
class Email extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('address', 'string', 150);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
When using YAML schema files it is not required to specify the relationship
on the opposite end( |
The Email
class is very similar to the User
class. Notice how the local
and foreign columns are switched in the hasOne definition compared
to the definition of the Email
class.
// models/User.php
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
$this->hasOne('Email', array(
'local' => 'id',
'foreign' => 'user_id'
));
}
}
Notice how we override the setUp method and call
parent |
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
One to Many and Many to One
One-to-Many and Many-to-One relations are very similar to One-to-One relations. The recommended conventions you came in terms with in the previous chapter also apply to one-to-many and many-to-one relations.
In the following example we have two classes: User
and
Phonenumber
. We define their relation as one-to-many (a user can
have many phonenumbers). Here once again the Phonenumber
is clearly
owned by the User
so we place the foreign key in the Phonenumber
class.
// models/User.php
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
// ...
$this->hasMany('Phonenumber as Phonenumbers', array(
'local' => 'id',
'foreign' => 'user_id'
));
}
}
// models/Phonenumber.php
class Phonenumber extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('phonenumber', 'string', 50);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Tree Structure
A tree structure is a self-referencing foreign key relation. The following definition is also called Adjacency List implementation in terms of hierarchical data concepts.
// models/Task.php
class Task extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 100);
$this->hasColumn('parent_id', 'integer');
}
public function setUp()
{
$this->hasOne('Task as Parent', array(
'local' => 'parent_id',
'foreign' => 'id'
));
$this->hasMany('Task as Subtasks', array(
'local' => 'id',
'foreign' => 'parent_id'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
The above implementation is purely an example and is not the most efficient
way to store and retrieve hierarchical data. Check the |
Join Table Associations
Many to Many
If you are coming from relational database background it may be familiar to you how many-to-many associations are handled: an additional association table is needed.
In many-to-many relations the relation between the two components is always an aggregate relation and the association table is owned by both ends. For example in the case of users and groups: when a user is being deleted, the groups he/she belongs to are not being deleted. However, the associations between this user and the groups he/she belongs to are instead being deleted. This removes the relation between the user and the groups he/she belonged to, but does not remove the user nor the groups.
Sometimes you may not want that association table rows are being deleted when
user / group is being deleted. You can override this behavior by setting the
relations to association component (in this case Groupuser
) explicitly.
In the following example we have Groups and Users of which relation is defined
as many-to-many. In this case we also need to define an additional class called
Groupuser
.
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
// ...
$this->hasMany('Group as Groups', array(
'local' => 'user_id',
'foreign' => 'group_id',
'refClass' => 'UserGroup'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
The above |
// models/Group.php
class Group extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('groups');
$this->hasColumn('name', 'string', 30);
}
public function setUp()
{
$this->hasMany('User as Users', array(
'local' => 'group_id',
'foreign' => 'user_id',
'refClass' => 'UserGroup'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Please note that
|
// models/UserGroup.php
class UserGroup extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer', null, array(
'primary' => true
));
$this->hasColumn('group_id', 'integer', null, array(
'primary' => true
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Notice how the relationship is bi-directional. Both User
has many Group
and Group
has many User
. This is required by Doctrine in order for
many-to-many relationships to fully work.
Now lets play around with the new models and create a user and assign it some
groups. First create a new User
instance:
// test.php
$ user = new User();
Now add two new groups to the User
:
// ...
$ user->Groups[0]->name = 'First Group';
$ user->Groups[1]->name = 'Second Group';
Now you can save the groups to the database:
// ...
$ user->save();
Now you can delete the associations between user and groups it belongs to:
// ...
$ user->UserGroup->delete();
$ groups = new Doctrine_Collection(Doctrine_Core::getTable('Group'));
$ groups[0]->name = 'Third Group';
$ groups[1]->name = 'Fourth Group';
$ user->Groups[2] = $groups[0]; // $user will now have 3 groups
$ user->Groups = $groups; // $user will now have two groups 'Third Group' and 'Fourth Group'
$ user->save();
Now if we inspect the $user
object data with the
Doctrine_Record::toArray:
// ...
print_r($user->toArray(true));
The above example would produce the following output
1 $ php test.php
Array
(
[id] => 1
[is_active] => 1
[is_super_admin] => 0
[first_name] =>
[last_name] =>
[username] => default username
[password] =>
[type] =>
[created_at] => 2009-01-20 16:48:57
[updated_at] => 2009-01-20 16:48:57
[Groups] => Array
(
[0] => Array
(
[id] => 3
[name] => Third Group
)
[1] => Array
(
[id] => 4
[name] => Fourth Group
)
)
[UserGroup] => Array
(
)
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Self Referencing (Nest Relations)
Non-Equal Nest Relations
// models/User.php
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
// ...
$this->hasMany('User as Parents', array(
'local' => 'child_id',
'foreign' => 'parent_id',
'refClass' => 'UserReference'
));
$this->hasMany('User as Children', array(
'local' => 'parent_id',
'foreign' => 'child_id',
'refClass' => 'UserReference'
));
}
}
// models/UserReference.php
class UserReference extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('parent_id', 'integer', null, array(
'primary' => true
));
$this->hasColumn('child_id', 'integer', null, array(
'primary' => true
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Equal Nest Relations
Equal nest relations are perfectly suitable for expressing relations where a class references to itself and the columns within the reference class are equal.
This means that when fetching related records it doesn't matter which column in the reference class has the primary key value of the main class.
The previous clause may be hard to understand so lets take an example. We define a class called User which can have many friends. Notice here how we use the 'equal' option.
// models/User.php
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
// ...
$this->hasMany('User as Friends', array(
'local' => 'user1',
'foreign' => 'user2',
'refClass' => 'FriendReference',
'equal' => true,
));
}
}
// models/FriendReference.php
class FriendReference extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user1', 'integer', null, array(
'primary' => true
));
$this->hasColumn('user2', 'integer', null, array(
'primary' => true
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Now lets define 4 users: Jack Daniels, John Brandy, Mikko Koskenkorva and Stefan Beer with Jack Daniels and John Brandy being buddies and Mikko Koskenkorva being the friend of all of them.
// test.php
$ daniels = new User();
$ daniels->username = 'Jack Daniels';
$ brandy = new User();
$ brandy->username = 'John Brandy';
$ koskenkorva = new User();
$ koskenkorva->username = 'Mikko Koskenkorva';
$ beer = new User();
$ beer->username = 'Stefan Beer';
$ daniels->Friends[0] = $brandy;
$ koskenkorva->Friends[0] = $daniels;
$ koskenkorva->Friends[1] = $brandy;
$ koskenkorva->Friends[2] = $beer;
$ conn->flush();
Calling Doctrine_Connection::flush will trigger an operation that saves all unsaved objects and wraps it in a single transaction. |
Now if we access for example the friends of Stefan Beer it would return one user 'Mikko Koskenkorva':
// ...
$ beer->free();
unset($beer);
$ user = Doctrine_Core::getTable('User')->findOneByUsername('Stefan Beer');
print_r($user->Friends->toArray());
Now when you execute test.php you will see the following:
Foreign Key Constraints
Introduction
A foreign key constraint specifies that the values in a column (or a group of columns) must match the values appearing in some row of another table. In other words foreign key constraints maintain the referential integrity between two related tables.
Say you have the product table with the following definition:
// models/Product.php
class Product extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('price', 'decimal', 18);
$this->hasColumn('discounted_price', 'decimal', 18);
}
public function setUp()
{
$this->hasMany('Order as Orders', array(
'local' => 'id',
'foreign' => 'product_id'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Let's also assume you have a table storing orders of those products. We want to ensure that the order table only contains orders of products that actually exist. So we define a foreign key constraint in the orders table that references the products table:
// models/Order.php
class Order extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('orders');
$this->hasColumn('product_id', 'integer');
$this->hasColumn('quantity', 'integer');
}
public function setUp()
{
$this->hasOne('Product', array(
'local' => 'product_id',
'foreign' => 'id'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Foreign key columns are automatically indexed by Doctrine to ensure optimal performance when issuing queries involving the foreign key. |
When exported the class Order
would execute the following SQL:
Now it is impossible to create orders
with a product_id
that
does not appear in the product
table.
We say that in this situation the orders table is the referencing table and the products table is the referenced table. Similarly, there are referencing and referenced columns.
Foreign Key Names
When you define a relationship in Doctrine, when the foreign key is
created in the database for you Doctrine will try to create a foreign
key name for you. Sometimes though, this name may not be something you
want so you can customize the name to use with the foreignKeyName
option to your relationship setup.
// models/Order.php
class Order extends Doctrine_Record
{
// ...
public function setUp()
{
$this->hasOne('Product', array(
'local' => 'product_id',
'foreign' => 'id',
'foreignKeyName' => 'product_id_fk'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Integrity Actions
CASCADE
Delete or update the row from the parent table and automatically delete
or update the matching rows in the child table. Both `ON DELETE
CASCADE`` and ``ON UPDATE CASCADE
` are supported. Between two tables,
you should not define several ON UPDATE CASCADE
clauses that act on
the same column in the parent table or in the child table.
SET NULL
Delete or update the row from the parent table and set the foreign key
column or columns in the child table to NULL
. This is valid only if
the foreign key columns do not have the NOT NULL
qualifier
specified. Both ON DELETE SET NULL
and ON UPDATE SET NULL
clauses are supported.
NO ACTION
In standard SQL, NO ACTION
means no action in the sense that an
attempt to delete or update a primary key value is not allowed to
proceed if there is a related foreign key value in the referenced table.
RESTRICT
Rejects the delete or update operation for the parent table. `NO
ACTION`` and ``RESTRICT`` are the same as omitting the ``ON DELETE
` or
ON UPDATE
clause.
SET DEFAULT
In the following example we define two classes, User
and
Phonenumber
with their relation being one-to-many. We also add a
foreign key constraint with onDelete cascade action. This means that
every time a user
is being deleted its associated phonenumbers
will also be deleted.
The integrity constraints listed above are case sensitive and must be in upper case when being defined in your schema. |
Below is an example where the database delete cascading is used.
class Phonenumber extends Doctrine_Record
{
// ...
public function setUp()
{
parent::setUp();
// ...
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id',
'onDelete' => 'CASCADE'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Notice how the integrity constraints are placed on the side where the foreign key exists. This is required in order for the integrity constraints to be exported to your database properly. |
Indexes
Introduction
Indexes are used to find rows with specific column values quickly. Without an index, the database must begin with the first row and then read through the entire table to find the relevant rows.
The larger the table, the more this consumes time. If the table has an index for the columns in question, the database can quickly determine the position to seek to in the middle of the data file without having to look at all the data. If a table has 1,000 rows, this is at least 100 times faster than reading rows one-by-one.
Indexes come with a cost as they slow down the inserts and updates. However, in general you should always use indexes for the fields that are used in SQL where conditions.
Adding indexes
You can add indexes by using Doctrine_Record::index
. An example of
adding a simple index to field called name:
The following index examples are not meant for you to actually add to your test Doctrine environment. They are only meant to demonstrate the API for adding indexes. |
class IndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->index('myindex', array(
'fields' => array('name')
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
An example of adding a multi-column index to field called name
:
class MultiColumnIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string');
$this->index('myindex', array(
'fields' => array('name', 'code')
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
An example of adding multiple indexes on same table:
class MultipleIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string'); $this->hasColumn('age', 'integer');
$this->index('myindex', array(
'fields' => array('name', 'code')
));
$this->index('ageindex', array(
'fields' => array('age')
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Index options
Doctrine offers many index options, some of them being database specific. Here is a full list of available options:
Name | Description |
---|---|
sorting |
A string valuethat can be either 'ASC' or 'DESC'. |
length |
Index length (only some drivers support this). |
primary |
Whether or not the index is a primary index. |
type |
A string value that can be unique, 'fulltext', 'gist' or 'gin'. |
Here is an example of how to create a unique index on the name column.
class MultipleIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string');
$this->hasColumn('age', 'integer');
$this->index('myindex', array(
'fields' => array(
'name' => array(
'sorting' => 'ASC',
'length' => 10),
'code'
),
'type' => 'unique',
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Special indexes
Doctrine supports many special indexes. These include Mysql FULLTEXT and Pgsql GiST indexes. In the following example we define a Mysql FULLTEXT index for the field 'content'.
// models/Article.php
class Article extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 255);
$this->hasColumn('content', 'string');
$this->option('type', 'MyISAM');
$this->index('content', array(
'fields' => array('content'),
'type' => 'fulltext'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Notice how we set the table type to |
Checks
You can create any kind of CHECK
constraints by using the check
method of the Doctrine_Record. In the last example we add
constraint to ensure that price is always higher than the discounted price.
// models/Product.php
class Product extends Doctrine_Record
{
public function setTableDefinition()
{
// ...
$this->check('price > discounted_price');
}
// ...
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
Generates (in pgsql):
Some databases don't support |
If the Doctrine validators are turned on the given definition would also ensure that when a record is being saved its price is always greater than zero.
If some of the prices of the saved products within a transaction is
below zero, Doctrine throws Doctrine_Validator_Exception
and
automatically rolls back the transaction.
Table Options
Doctrine offers various table options. All table options can be set via
the Doctrine_Record::option
function.
For example if you are using MySQL and want to use INNODB tables it can be done as follows:
class MyInnoDbRecord extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->option('type', 'INNODB');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
In the following example we set the collate and character set options:
class MyCustomOptionRecord extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->option('collate', 'utf8_unicode_ci');
$this->option('charset', 'utf8');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
It is worth noting that for certain databases (Firebird, MySql and
PostgreSQL) setting the charset option might not be enough for Doctrine
to return data properly. For those databases, users are advised to also
use the setCharset
function of the database connection:
$ conn = Doctrine_Manager::connection();
$ conn->setCharset('utf8');
Record Filters
Doctrine offers the ability to attach record filters when defining your models. A record filter is invoked whenever you access a property on a model that is invalid. So it allows you to essentially add properties dynamically to a model through the use of one of these filters.
To attach a filter you just need to add it in the setUp method of your model definition:
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 255);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->hasOne('Profile', array(
'local' => 'id',
'foreign' => 'user_id'
));
$this->unshiftFilter(new Doctrine_Record_Filter_Compound(array('Profile')));
}
}
class Profile extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('first_name', 'string', 255);
$this->hasColumn('last_name', 'string', 255);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
));
}
}
Now with the above example we can easily access the properties of the
Profile
relationship when using an instance of User
. Here is an
example:
$ user = Doctrine_Core::getTable('User')
->createQuery('u')
->innerJoin('u.Profile p')
->where('p.username = ?', 'jwage')
->fetchOne();
echo $user->first_name . ' ' . $user->last_name;
When we ask for the first_name
and last_name
properties they
do not exist on the $user
instance so they are forwarded to the
Profile
relationship. It is the same as if you were to do the
following:
echo $user->Profile->first_name . ' ' . $user->Profile->last_name;
You can write your own record filters pretty easily too. All that is
required is you create a class which extends
Doctrine_Record_Filter
and implements the filterSet and
filterGet methods. Here is an example:
class MyRecordFilter extends Doctrine_Record_Filter
{
public function filterSet(Doctrine_Record $record, $name, $value)
{
// try and set the property
throw new Doctrine_Record_UnknownPropertyException(sprintf(
'Unknown record property / related component "%s" on "%s"',
$name,
get_class($record)
));
}
public function filterGet(Doctrine_Record, $name)
{
// try and get the property
throw new Doctrine_Record_UnknownPropertyException(sprintf(
'Unknown record property / related component "%s" on "%s"',
$name,
get_class($record)
));
}
}
Now you can add the filter to your models:
class MyModel extends Doctrine_Record
{
// ...
public function setUp()
{
// ...
$this->unshiftFilter(new MyRecordFilter());
}
}
Remember to be sure to throw an instance of the
|
Transitive Persistence
Doctrine offers both database and application level cascading operations. This section will explain in detail how to setup both application and database level cascades.
Application-Level Cascades
Since it can be quite cumbersome to save and delete individual objects, especially if you deal with an object graph, Doctrine provides application-level cascading of operations.
Save Cascades
You may already have noticed that save operations are already cascaded to associated objects by default.
Delete Cascades
Doctrine provides a second application-level cascade style: delete. Unlike the save cascade, the delete cascade needs to be turned on explicitly as can be seen in the following code snippet:
// models/User.php
class User extends BaseUser
{
// ...
public function setUp()
{
parent::setup();
// ...
$this->hasMany('Address as Addresses', array(
'local' => 'id',
'foreign' => 'user_id',
'cascade' => array('delete')
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter
The cascade
option is used to specify the operations that are
cascaded to the related objects on the application-level.
Please note that the only currently supported value is |
In the example above, Doctrine would cascade the deletion of a User
to it's associated Addresses
. The following describes the generic
procedure when you delete a record through $record->delete:
- Doctrine looks at the relations to see if there are any deletion cascades it needs to apply. If there are no deletion cascades, go to 3).
- For each relation that has a delete cascade specified, Doctrine verifies that the objects that are the target of the cascade are loaded. That usually means that Doctrine fetches the related objects from the database if they're not yet loaded.(Exception: many-valued associations are always re-fetched from the database, to make sure all objects are loaded). For each associated object, proceed with step 1).
- Doctrine orders all deletions and executes them in the most efficient way, maintaining referential integrity.
From this description one thing should be instantly clear: Application-level cascades happen on the object-level, meaning operations are cascaded from one object to another and in order to do that the participating objects need to be available.
This has some important implications:
- Application-level delete cascades don't perform well on many-valued associations when there are a lot of objects in the related collection (that is because they need to be fetched from the database, the actual deletion is pretty efficient).
- Application-level delete cascades do not skip the object lifecycle as database-level cascades do (see next chapter). Therefore all registered event listeners and other callback methods are properly executed in an application-level cascade.
Database-Level Cascades
Some cascading operations can be done much more efficiently at the database level. The best example is the delete cascade.
Database-level delete cascades are generally preferrable over application-level delete cascades except:
- Your database does not support database-level cascades (i.e. when using MySql with MYISAM tables).
- You have listeners that listen on the object lifecycle and you want them to get invoked.
Database-level delete cascades are applied on the foreign key constraint. Therefore they're specified on that side of the relation that owns the foreign key. Picking up the example from above, the definition of a database-level cascade would look as follows:
// models/Address.php
class Address extends Doctrine_Record
{
public function setTableDefinition() {
$this->hasColumn('user_id', 'integer');
$this->hasColumn('address', 'string', 255);
$this->hasColumn('country', 'string', 255);
$this->hasColumn('city', 'string', 255);
$this->hasColumn('state', 'string', 2);
$this->hasColumn('postal_code', 'string', 25);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id',
'onDelete' => 'CASCADE'
));
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:
The onDelete
option is translated to proper DDL/DML statements when
Doctrine creates your tables.
Note that |
Currently, the only two supported database-level cascade styles are for
onDelete
and onUpdate
. Both are specified on the side that owns
the foreign key and applied to your database schema when Doctrine
creates your tables.
Conclusion
Now that we know everything about how to define our Doctrine models, I think we are ready to move on to learning about how to work with models in your application.
This is a very large topic as well so take a break, grab a mountain dew and hurry back for the next chapter.