Doctrine ORM For PHP
Doctrine ORM For PHP
Doctrine ORM For PHP
Doctrine 1.1
License: Creative Commons Attribution-Share Alike 3.0 Unported License
Version: manual-1.1-en-2009-05-20
Table of Contents ii
Table of Contents
Introduction .................................................................................................... 13
Code Examples ........................................................................................................ 13
What is Doctrine? .................................................................................................... 13
What is an ORM? ..................................................................................................... 13
What is the Problem?............................................................................................... 13
Minimum Requirements .......................................................................................... 14
Basic Overview ........................................................................................................ 14
Doctrine Explained .................................................................................................. 15
Key Concepts ........................................................................................................... 16
Further Reading ...................................................................................................... 17
Conclusion ............................................................................................................... 17
Getting Started ............................................................................................... 18
Checking Requirements........................................................................................... 18
Installing .................................................................................................................. 19
Sandbox .............................................................................................................................. 19
SVN..................................................................................................................................... 19
Installing ......................................................................................................................................... 19
Updating ......................................................................................................................................... 20
SVN Externals .................................................................................................................... 20
PEAR Installer .................................................................................................................... 20
Download Pear Package ..................................................................................................... 21
Implementing........................................................................................................... 21
Including Doctrine Libraries .............................................................................................. 21
Require Doctrine Base Class .............................................................................................. 21
Register Autoloader............................................................................................................ 22
Autoloading Explained.................................................................................................................... 22
Bootstrap File ..................................................................................................................... 23
Test Script .......................................................................................................................... 23
Conclusion ............................................................................................................... 24
Introduction to Connections........................................................................... 25
DSN, the Data Source Name ................................................................................... 25
Examples ............................................................................................................................ 27
Opening New Connections ...................................................................................... 27
Lazy Database Connecting ...................................................................................... 28
Testing your Connection.......................................................................................... 28
Conclusion ............................................................................................................... 29
Configuration .................................................................................................. 30
Levels of Configuration............................................................................................ 30
Defaults Attributes................................................................................................... 31
Default Column Options ..................................................................................................... 31
Decimal ........................................................................................................................................... 56
String .............................................................................................................................................. 57
Array ............................................................................................................................................... 57
Object ............................................................................................................................................. 58
Blob................................................................................................................................................. 58
Clob................................................................................................................................................. 58
Timestamp ...................................................................................................................................... 59
Time ................................................................................................................................................ 59
Date ................................................................................................................................................ 60
Enum............................................................................................................................................... 60
Gzip................................................................................................................................................. 61
Examples ............................................................................................................................ 61
Relationships ........................................................................................................... 64
Introduction........................................................................................................................ 64
Foreign Key Associations ................................................................................................... 68
One to One ...................................................................................................................................... 68
One to Many and Many to One ....................................................................................................... 70
Tree Structure ................................................................................................................................ 71
Join Table Associations....................................................................................................... 72
Many to Many ................................................................................................................................. 72
Self Referencing (Nest Relations) .................................................................................................. 76
Non-Equal Nest Relations ............................................................................................................................ 76
Equal Nest Relations .................................................................................................................................... 78
Foreign Key Constraints..................................................................................................... 80
Introduction .................................................................................................................................... 80
Integrity Actions ............................................................................................................................. 82
Indexes..................................................................................................................... 83
Introduction........................................................................................................................ 83
Adding indexes ................................................................................................................... 84
Index options ...................................................................................................................... 85
Special indexes ................................................................................................................... 86
Checks ..................................................................................................................... 87
Table Options........................................................................................................... 88
Transitive Persistence ............................................................................................. 90
Application-Level Cascades ................................................................................................ 90
Save Cascades ................................................................................................................................ 90
Delete Cascades.............................................................................................................................. 90
Database-Level Cascades ................................................................................................... 91
Conclusion ............................................................................................................... 93
Working with Models ...................................................................................... 94
Define Test Schema ................................................................................................. 94
Dealing with Relations............................................................................................. 98
Creating Related Records .................................................................................................. 98
Retrieving Related Records.............................................................................................. 100
Updating Related Records................................................................................................ 101
Deleting Related Records ................................................................................................. 101
Working with Related Records ......................................................................................... 102
Testing the Existence of a Relation .............................................................................................. 102
Many-to-Many Relations ........................................................................................ 103
Creating a New Link......................................................................................................... 103
Deleting a Link ................................................................................................................. 103
Fetching Data ........................................................................................................ 104
Sample Queries ................................................................................................................ 107
Field Lazy Loading ........................................................................................................... 113
Arrays and Objects ................................................................................................ 113
To Array............................................................................................................................ 113
Connection............................................................................................................. 160
Available Drivers .............................................................................................................. 160
Creating Connections ....................................................................................................... 160
Flushing the Connection .................................................................................................. 160
Table ...................................................................................................................... 161
Getting a Table Object...................................................................................................... 161
Getting Column Information............................................................................................. 161
Getting Relation Information............................................................................................ 162
Finder Methods ................................................................................................................ 164
Custom Table Classes ................................................................................................................... 165
Custom Finders ................................................................................................................ 165
Record.................................................................................................................... 166
Properties ......................................................................................................................... 166
Updating Records............................................................................................................. 170
Replacing Records............................................................................................................ 171
Refreshing Records .......................................................................................................... 171
Refreshing relationships................................................................................................... 172
Deleting Records .............................................................................................................. 173
Using Expression Values .................................................................................................. 174
Getting Record State ........................................................................................................ 174
Getting Object Copy ......................................................................................................... 175
Saving a Blank Record ..................................................................................................... 176
Mapping Custom Values................................................................................................... 176
Serializing......................................................................................................................... 176
Checking Existence .......................................................................................................... 177
Function Callbacks for Columns ...................................................................................... 177
Collection ............................................................................................................... 177
Accessing Elements .......................................................................................................... 178
Adding new Elements ....................................................................................................... 178
Getting Collection Count .................................................................................................. 179
Saving the Collection........................................................................................................ 179
Deleting the Collection ..................................................................................................... 180
Key Mapping..................................................................................................................... 180
Loading Related Records ................................................................................................. 181
Validator ................................................................................................................ 182
More Validation ................................................................................................................ 183
Valid or Not Valid ............................................................................................................. 184
Implicit Validation ........................................................................................................................ 184
Explicit Validation......................................................................................................................... 184
Profiler ................................................................................................................... 185
Basic Usage ...................................................................................................................... 186
Locking Manager ................................................................................................... 186
Optimistic Locking ........................................................................................................... 186
Pessimistic Locking .......................................................................................................... 186
Examples .......................................................................................................................... 187
Technical Details .............................................................................................................. 188
Views...................................................................................................................... 188
Using Views ...................................................................................................................... 188
Conclusion ............................................................................................................. 189
Native SQL .................................................................................................... 190
Introduction ........................................................................................................... 190
Component Queries ............................................................................................... 190
Fetching from Multiple Components ..................................................................... 191
Conclusion ............................................................................................................. 192
Chapter 1
Introduction
Code Examples
The text in this book contains lots of PHP code examples. All starting and ending PHP tags
have been removed to reduce the length of the book. Be sure to include the PHP tags when
you copy and paste the examples.
What is Doctrine?
Doctrine is an object relational mapper (ORM) for PHP 5.2.3+ that sits on top of a powerful
database abstraction layer (DBAL). One of its key features is the option to write database
queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL),
inspired by Hibernates HQL. This provides developers with a powerful alternative to SQL that
maintains flexibility without requiring unnecessary code duplication.
What is an ORM?
Object relational mapping is a technique used in programming languages when dealing with
databases for translating incompatible data types in relational databases. This essentially
allows for us to have a "virtual object database," that can be used from the programming
language. Lots of free and commercial packages exist that allow this but sometimes
developers chose to create their own ORM.
1. http://en.wikipedia.org/wiki/Object-relational_mapping
"person object" with "slots" to hold the data that comprise the entry: the person's name, a list
(or array) of phone numbers, and a list of addresses. The list of phone numbers would itself
contain "phone number objects" and so on. The address book entry is treated as a single value
by the programming language (it can be referenced by a single variable, for instance).
Various methods can be associated with the object, such as a method to return the preferred
phone number, the home address, and so on.
However, many popular database products such as SQL DBMS can only store and manipulate
scalar values such as integers and strings organized within tables.
The programmer must either convert the object values into groups of simpler values for
storage in the database (and convert them back upon retrieval), or only use simple scalar
values within the program. Object-relational mapping is used to implement the first approach.
The height of the problem is translating those objects to forms that can be stored in the
database for easy retrieval, while preserving the properties of the objects and their
relationships; these objects are then said to be persistent.
Minimum Requirements
Doctrine requires PHP >= 5.2.3+, although it doesn't require any external libraries. For
database function call abstraction Doctrine uses PDO which comes bundled with the PHP
official release that you get from www.php.net.
If you use a 3 in 1 package under windows like Uniform Server, MAMP or any other non-
official package, you may be required to perform additional configurations.
Basic Overview
Doctrine is a tool for object-relational mapping in PHP. It sits on top of PDO and is itself
divided into two main layers, the DBAL and the ORM. The picture below shows how the layers
of Doctrine work together.
The DBAL(Database Abstraction Layer) completes and extends the basic database
abstraction/independence that is already provided by PDO. The DBAL library can be used
standalone, if all you want is a powerful database abstraction layer on top of PDO. The ORM
layer depends on the DBAL and therefore, when you load the ORM package the DBAL is
already included.
Doctrine Explained
The following section tries to explain where Doctrine stands in the world of ORM tools. The
Doctrine ORM is mainly built around the Active Record2, Data Mapper3 and Meta Data
Mapping4 patterns.
Through extending a specific base class named Doctrine_Record, all the child classes get
the typical ActiveRecord interface (save/delete/etc.) and it allows Doctrine to easily
participate in and monitor the lifecycles of your records. The real work, however, is mostly
forwarded to other components, like the Doctrine_Table class. This class has the typical
Data Mapper interface, createQuery(), find(id), findAll(), findBy*(),
findOneBy*() etc. So the ActiveRecord base class enables Doctrine to manage your records
and provides them with the typical ActiveRecord interface whilst the mapping footwork is
done elsewhere.
The ActiveRecord approach comes with its typical limitations. The most obvious is the
enforcement for a class to extend a specific base class in order to be persistent (a
Doctrine_Record). In general, the design of your domain model is pretty much restricted
by the design of your relational model. There is an exception though. When dealing with
inheritance structures, Doctrine provides some sophisticated mapping strategies which allow
your domain model to diverge a bit from the relational model and therefore give you a bit
more freedom.
Doctrine is in a continuous development process and we always try to add new features that
provide more freedom in the modeling of the domain. However, as long as Doctrine remains
mainly an ActiveRecord approach, there will always be a pretty large, (forced) similarity of
these two models.
The current situation is depicted in the following picture.
As you see in the picture, the domain model can't drift far away from the bounds of the
relational model.
After mentioning these drawbacks, it's time to mention some advantages of the ActiveRecord
approach. Apart from the (arguably slightly) simpler programming model, it turns out that the
strong similarity of the relational model and the Object Oriented (OO) domain model also has
an advantage: It makes it relatively easy to provide powerful generation tools, that can create
2. http://www.martinfowler.com/eaaCatalog/activeRecord.html
3. http://www.martinfowler.com/eaaCatalog/dataMapper.html
4. http://www.martinfowler.com/eaaCatalog/metadataMapping.html
a basic domain model out of an existing relational schema. Further, as the domain model
can't drift far from the relational model due to the reasons above, such generation and
synchronization tools can easily be used throughout the development process. Such tools are
one of Doctrine's strengths.
We think that these limitations of the ActiveRecord approach are not that much of a problem
for the majority of web applications because the complexity of the business domains is often
moderate, but we also admit that the ActiveRecord approach is certainly not suited for
complex business logic (which is often approached using Domain-Driven Design) as it simply
puts too many restrictions and has too much influence on your domain model.
Doctrine is a great tool to drive the persistence of simple or moderately complex domain
models(1) and you may even find that it's a good choice for complex domain models if you
consider the trade-off between making your domain model more database-centric and
implementing all the mapping on your own (because at the time of this writing we are not
aware of any powerful ORM tools for PHP that are not based on an ActiveRecord approach).
(1) Note that complexity != size. A domain model can be pretty large without being
complex and vice versa. Obviously, larger domain models have a greater probability of
being complex.
Now you already know a lot about what Doctrine is and what it is not. If you would like to dive
in now and get started right away, jump straight to the next chapter "Getting Started".
Key Concepts
The Doctrine Query Language (DQL) is an object query language. It let's you express queries
for single objects or full object graphs, using the terminology of your domain model: class
names, field names, relations between classes, etc. This is a powerful tool for retrieving or
even manipulating objects without breaking the separation of the domain model (field names,
class names, etc) from the relational model (table names, column names, etc). DQL looks very
much like SQL and this is intended because it makes it relatively easy to grasp for people
knowing SQL. There are, however, a few very important differences you should always keep
in mind:
Take this example DQL query:
• We select from classes and not tables. We are selecting from the User class/model.
• We join along associations (u.Phonenumbers)
• We can reference fields (u.level)
• There is no join condition (ON x.y = y.x). The associations between your classes and
how these are expressed in the database are known to Doctrine (You need to make
this mapping known to Doctrine, of course. How to do that is explained later in the
Defining Models (page 51) chapter.).
DQL expresses a query in the terms of your domain model (your classes, the attributes they
have, the relations they have to other classes, etc.).
It's very important that we speak about classes, fields and associations between classes here.
User is not a table / table name . It may be that the name of the database table that the User
class is mapped to is indeed named User but you should nevertheless adhere to this
differentiation of terminology. This may sound nit picky since, due to the ActiveRecord
approach, your relational model is often very similar to your domain model but it's really
important. The column names are rarely the same as the field names and as soon as
inheritance is involved, the relational model starts to diverge from the domain model. You can
have a class User that is in fact mapped to several tables in the database. At this point it
should be clear that talking about "selecting from the User table" is simply wrong then. And
as Doctrine development continues there will be more features available that allow the two
models to diverge even more.
Further Reading
For people new to object-relational mapping and (object-oriented) domain models we
recommend the following literature:
The books by Martin Fowler5 cover a lot of the basic ORM terminology, the different
approaches of modeling business logic and the patterns involved.
Another good read is about Domain Driven Design6. Though serious Domain-Driven Design is
currently not possible with Doctrine, this is an excellent resource for good domain modeling,
especially in complex business domains, and the terminology around domain models that is
pretty widespread nowadays is explained in depth (Entities, Value Objects, Repositories, etc).
Conclusion
Well, now that we have given a little educational reading about the methodologies and
principals behind Doctrine we are pretty much ready to dive in to everything that is Doctrine.
Lets dive in to setting up Doctrine in the Getting Started (page 18) chapter.
5. http://www.martinfowler.com/books.html
6. http://domaindrivendesign.org/books/#DDD
Chapter 2
Getting Started
Checking Requirements
First we need to make sure that you can run Doctrine on your server. We can do this one of
two ways:
First create a small PHP script named phpinfo.php and upload it somewhere on your web
server that is accessible to the web:
Listing phpinfo();
2-1
Now execute it from your browser by going to http://localhost/phpinfo.php7. You will see a list
of information detailing your PHP configuration. Check that your PHP version is >= 5.2.3 and
that you have PDO and the desired drivers installed.
You can also check your PHP installation has the necessary requirements by running some
commands from the terminal. We will demonstrate in the next example.
Check that your PHP version is >= 5.2.3 with the following command:
Listing $ php -v
2-2
Now check that you have PDO and the desired drivers installed with the following command:
Listing $ php -i
2-3
You could also execute the phpinfo.php from the command line and get the same result as
the above example:
Checking the requirements are required in order to run the examples used throughout this
documentation.
7. http://localhost/phpinfo.php
Installing
Currently it is possible to install Doctrine four different ways that are listed below:
• SVN (subversion)
• SVN externals
• PEAR Installer
• Download PEAR Package
It is recommended to download Doctrine via SVN (subversion), because in this case updating
is easy. If your project is already under version control with SVN, you should choose SVN
externals.
If you wish to just try out Doctrine in under 5 minutes, the sandbox package is
recommended. We will discuss the sandbox package in the next section.
Sandbox
Doctrine also provides a special package which is a zero configuration Doctrine
implementation for you to test Doctrine without writing one line of code. You can download it
from the download page8.
SVN
It is highly recommended that you use Doctrine via SVN and the externals option. This option
is the best as you will receive the latest bug fixes from SVN to ensure the best experience
using Doctrine.
Installing
To install Doctrine via SVN is very easy. You can download any version of Doctrine from the
SVN server: http://svn.doctrine-project.org9
To check out a specific version you can use the following command from your terminal:
If you do not have a SVN client, chose one from the list below. Find the Checkout option and
enter http://svn.doctrine-project.org/1.010 in the path or repository url parameter. There is no
need for a username or password to check out Doctrine.
8. http://www.doctrine-project.org/download
9. http://svn.doctrine-project.org
10. http://svn.doctrine-project.org/1.0
11. http://tortoisesvn.tigris.org/
12. http://www.apple.com/downloads/macosx/development_tools/svnx.html
13. http://www.eclipse.org/
14. http://subclipse.tigris.org/
Updating
Updating Doctrine with SVN is just as easy as installing. Simply execute the following
command from your terminal:
SVN Externals
If your project is already under version control with SVN, then it is recommended that you
use SVN externals to install Doctrine.
You can start by navigating to your checked out project in your terminal:
Listing $ cd /var/www/my_project
2-7
Now that you are under your checked out project, you can execute the following command
from your terminal and setup Doctrine as an SVN external:
The above command will open your editor and you need to place the following text inside and
save:
PEAR Installer
Doctrine also provides a PEAR server for installing and updating Doctrine on your servers.
You can easily install Doctrine with the following command:
Replace the above 1.0.x with the version you wish to install. For example "1.1.0".
15. http://versionsapp.com/
Implementing
Now that you have Doctrine in your hands, we are ready to implement Doctrine in to our
application. This is the first step towards getting started with Doctrine.
First create a directory named doctrine_test. This is where we will place all our test code:
It will open up your editor and place the following inside and save:
Now when you do SVN update you will get the Doctrine libraries updated:
16. http://www.doctrine-project.org/download
Listing // bootstrap.php
2-20
/**
* Bootstrap Doctrine.php, register autoloader specify
* configuration attributes and load models.
*/
require_once(dirname(__FILE__) . '/lib/vendor/doctrine/Doctrine.php');
Register Autoloader
Now that we have the Doctrine class present, we need to register the class autoloader
function in the bootstrap file:
Listing // bootstrap.php
2-21
// ...
spl_autoload_register(array('Doctrine', 'autoload'));
Lets also create the singleton Doctrine_Manager instance and assign it to a variable named
$manager:
Listing // bootstrap.php
2-22
// ...
$manager = Doctrine_Manager::getInstance();
Autoloading Explained
You can read about the PHP autoloading on the php website here17. Using the autoloader
allows us to lazily load classes as they are requested instead of pre-loading all classes. This
is a huge benefit to performance.
The way the Doctrine autoloader works is simple. Because our class names and paths are
related, we can determine the path to a Doctrine class based on its name.
Imagine we have a class named Doctrine_Some_Class and we instantiate an instance of it:
The above code will trigger a call to the Doctrine::autoload() function and pass it the
name of the class instantiated. The class name string is manipulated and transformed in to a
path and required. Below is some pseudo code that shows how the class is found and
required:
17. http://www.php.net/spl_autoload_register
Obviously the real Doctrine::autoload() function is a bit more complex and has some
error checking to ensure the file exists but the above code demonstrates how it works.
Bootstrap File
We will use this bootstrap class in later chapters and sections so be sure to create it!
The bootstrap file we have created should now look like the following:
// bootstrap.php Listing
2-25
/**
* Bootstrap Doctrine.php, register autoloader specify
* configuration attributes and load models.
*/
require_once(dirname(__FILE__) . '/lib/vendor/doctrine/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();
This new bootstrapping file will be referenced several times in this book as it is where we will
make changes to our implementation as we learn how to use Doctrine step by step.
The configuration attributes mentioned above are a feature in Doctrine used for
configuring and controlling functionality. You will learn more about attributes and how to
get/set them in the Configuration (page 30) chapter.
Test Script
Now lets create a simple test script that we can use to run various tests as we learn about the
features of Doctrine.
Create a new file in the doctrine_test directory named test.php and place the following
code inside:
// test.php Listing
2-26
require_once('bootstrap.php');
echo Doctrine::getPath();
Now you can execute the test script from your command line. This is how we will perform
tests with Doctrine throughout the chapters so make sure it is working for you! It should
output the path to your Doctrine installation.
Conclusion
Phew! This was our first chapter where we actually got into some code. As you saw, first we
were able to check that our server can actually run Doctrine. Then we learned all the
different ways we can download and install Doctrine. Lastly we learned how to implement
Doctrine by setting up a small test environment that we will use to perform some exercises in
the remaining chapters of the book.
Now lets move on and get our first taste of Doctrine connections in the Introduction to
Connections (page 25) chapter.
Chapter 3
Introduction to Connections
List of options
Name Description
charset Some backends support setting the client charset.
new_link Some RDBMS do not create new connections when connecting to the same host
multiple times. This option will attempt to force a new connection.
The DSN can either be provided as an associative array or as a string. The string format of
the supplied DSN is in its fullest form:
Listing
3-1
18. http://www.php.net/pdo
phptype(dbsyntax)://username:password@protocol+hostspec/
database?option=value
Listing phptype://username:password@protocol+hostspec:110//usr/db_file.db
3-2
phptype://username:password@hostspec/database
phptype://username:password@hostspec
phptype://username@hostspec
phptype://hostspec/database
phptype://hostspec
phptype:///database
phptype:///database?option=value&anotheroption=anothervalue
phptype(dbsyntax)
phptype
Listing phptype(syntax)://user:pass@protocol(proto_opts)/database
3-3
If your database, option values, username or password contain characters used to delineate
DSN parts, you can escape them via URI hex encodings:
Please note, that some features may be not supported by all database drivers.
Examples
Example 1. Connect to database through a socket
mysql://user@unix(/path/to/socket)/pear Listing
3-4
pgsql://user:pass@tcp(localhost:5555)/pear Listing
3-5
If you use, the ip address {127.0.0.1}, the port parameter is ignored (default: 3306).
sqlite:////full/unix/path/to/file.db?mode=0666 Listing
3-6
sqlite:///c:/full/windows/path/to/file.db?mode=0666 Listing
3-7
mysqli://user:pass@localhost/pear?key=client-key.pem&cert=client-cert.pem Listing
3-8
// bootstrap.php Listing
3-9
// ...
$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';
// bootstrap.php Listing
3-10
19. http://www.php.net/PDO
// ...
$conn->setOption('username', $user);
$conn->setOption('password', $password);
Listing // bootstrap.php
3-11
// ...
Listing /**
3-12
* Bootstrap Doctrine.php, register autoloader and specify
* configuration attributes
*/
require_once('../doctrine/branches/1.0/lib/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();
To test the connection lets modify our test.php script and perform a small test. Since we
create a variable name $conn, that variable is available to the test script so lets setup a small
test to make sure our connection is working:
First lets create a test table and insert a record:
Listing
3-13
// test.php
// ...
$conn->export->createTable('test', array('name' => array('type' => 'string')));
$conn->execute('INSERT INTO test (name) VALUES (?)', array('jwage'));
Now lets execute a simple SELECT query from the test table we just created to make sure
the data was inserted and that we can retrieve it:
// test.php Listing
3-14
// ...
$stmt = $conn->prepare('SELECT * FROM test');
$stmt->execute();
$results = $stmt->fetchAll();
print_r($results);
Conclusion
Great! Now we learned some basic operations of Doctrine connections. We have modified our
Doctrine test environment to have a new connection. This is required because the examples
in the coming chapters will require a connection.
Lets move on to the Configuration (page 30) chapter and learn how you can control
functionality and configurations using the Doctrine attribute system.
Chapter 4
Configuration
Doctrine controls configuration of features and functionality using attributes. In this section
we will discuss how to set and get attributes as well as an overview of what attributes exist
for you to use to control Doctrine functionality.
Levels of Configuration
Doctrine has a three-level configuration structure. You can set configuration attributes at a
global, connection and table level. If the same attribute is set on both lower level and upper
level, the uppermost attribute will always be used. So for example if a user first sets default
fetchmode in global level to Doctrine::FETCH_BATCH and then sets a table fetchmode to
Doctrine::FETCH_LAZY, the lazy fetching strategy will be used whenever the records of
that table are being fetched.
• Global level - The attributes set in global level will affect every connection and
every table in each connection.
• Connection level - The attributes set in connection level will take effect on each
table in that connection.
• Table level - The attributes set in table level will take effect only on that table.
Listing // bootstrap.php
4-1
// ...
$manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
In the next example above we override the global attribute on given connection:
Listing // bootstrap.php
4-2
// ...
$conn->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_NONE);
In the last example we override once again the connection level attribute in the table level:
Listing // bootstrap.php
4-3
// ...
$table = Doctrine::getTable('User');
$table->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
We haven't introduced the above used Doctrine::getTable() method. You will learn
more about the table objects used in Doctrine in the Table (page 161) section of the next
chapter.
Defaults Attributes
Doctrine has a few specific attributes available that allow you to specify the default values of
things that in the past were hardcoded values. Such as default column length, default column
type, etc.
// bootstrap.php Listing
4-4
// ...
$manager->setAttribute(Doctrine::ATTR_DEFAULT_COLUMN_OPTIONS,
array('type' => 'string', 'length' => 255, 'notnull' => true));
$manager->setAttribute(Doctrine::ATTR_DEFAULT_IDENTIFIER_OPTIONS, Listing
4-5
array('name' => '%s_id', 'type' => 'string', 'length' => 16));
Portability
Each database management system (DBMS) has it's own behaviors. For example, some
databases capitalize field names in their output, some lowercase them, while others leave
them alone. These quirks make it difficult to port your applications over to another database
type. Doctrine strives to overcome these differences so your applications can switch between
DBMS's without any changes. For example switching from sqlite to mysql.
The portability modes are bitwised, so they can be combined using | and removed using ^.
See the examples section below on how to do this.
You can read more about the bitwise operators on the PHP website: http://www.php.net/
language.operators.bitwise20
20. http://www.php.net/language.operators.bitwise
Name Description
PORTABILITY_ALL Turn on all portability features. This is the
default setting.
PORTABILITY_DELETE_COUNT Force reporting the number of rows deleted.
Some DBMS's don't count the number of rows
deleted when performingsimple DELETE FROM
tablename queries. This mode tricks such
DBMS's into telling the count by adding WHERE
1=1 to the end of DELETE queries.
PORTABILITY_EMPTY_TO_NULL Convert empty strings values to null in data in
and output. Needed because Oracle considers
empty strings to be null, while most other
DBMS's know the difference between empty
and null.
PORTABILITY_ERRORS Makes certain error messages in certain
drivers compatible with those from other
DBMS's
PORTABILITY_FIX_ASSOC_FIELD_NAMES This removes any qualifiers from keys in
associative fetches. Some RDBMS, like for
example SQLite, will by default use the fully
qualified name for a column in assoc fetches if
it is qualified in a query.
PORTABILITY_FIX_CASE Convert names of tables and fields to lower or
upper case in all methods. The case depends
on the field_case option that may be set to
either CASE_LOWER (default) or
CASE_UPPER
PORTABILITY_NONE Turn off all portability features.
PORTABILITY_NUMROWS Enable hack that makes numRows() work in
Oracle.
PORTABILITY_EXPR Makes DQL API throw exceptions when non-
portable expressions are being used.
PORTABILITY_RTRIM Right trim the data output for all data fetches.
This does not applied in drivers for RDBMS
that automatically right trim values of fixed
length character values, even if they do not
right trim value of variable length character
values.
Examples
Now we can use the setAttribute() method to enable portability for lowercasing and
trimming with the following code:
Listing // bootstrap.php
4-6
// ...
$conn->setAttribute('portability',
Doctrine::PORTABILITY_FIX_CASE | Doctrine::PORTABILITY_RTRIM);
// bootstrap.php Listing
4-7
// ...
$conn->setAttribute('portability',
Doctrine::PORTABILITY_ALL ^ Doctrine::PORTABILITY_RTRIM);
Identifier quoting
You can quote the db identifiers (table and field names) with quoteIdentifier(). The
delimiting style depends on which database driver is being used.
Just because you CAN use delimited identifiers, it doesn't mean you SHOULD use them. In
general, they end up causing way more problems than they solve. Anyway, it may be
necessary when you have a reserved word as a field name (in this case, we suggest you to
change it, if you can).
Some of the internal Doctrine methods generate queries. Enabling the quote_identifier
attribute of Doctrine you can tell Doctrine to quote the identifiers in these generated queries.
For all user supplied queries this option is irrelevant.
Portability is broken by using the following characters inside delimited identifiers:
Delimited identifiers are known to generally work correctly under the following drivers:
Mssql, Mysql, Oracle, Pgsql, Sqlite and Firebird.
When using the Doctrine::ATTR_QUOTE_IDENTIFIER option, all of the field identifiers will
be automatically quoted in the resulting SQL statements:
// bootstrap.php Listing
4-8
// ...
$conn->setAttribute(Doctrine::ATTR_QUOTE_IDENTIFIER, true);
Will result in a SQL statement that all the field names are quoted with the backtick '`'
operator (in MySQL).
SELECT Listing
4-9
*
FROM sometable
WHERE `id` = '123'
As opposed to:
Listing
4-10
SELECT
*
FROM sometable
WHERE id = '123'
Exporting
The export attribute is used for telling Doctrine what it should export when exporting classes
to your database for creating your tables.
If you don't want to export anything when exporting you can use:
Listing // bootstrap.php
4-11
// ...
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_NONE);
For exporting tables only (but not constraints) you can use on of the following:
Listing // bootstrap.php
4-12
// ...
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_TABLES);
You can also use the following syntax as it is the same as the above:
Listing // bootstrap.php
4-13
// ...
$manager->setAttribute(Doctrine::ATTR_EXPORT,
Doctrine::EXPORT_ALL ^ Doctrine::EXPORT_CONSTRAINTS);
Listing // bootstrap.php
4-14
// ...
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL);
You can change the index naming convention with the following code:
// bootstrap.php Listing
4-15
// ...
$manager->setAttribute(Doctrine::ATTR_IDXNAME_FORMAT, '%s_index');
// bootstrap.php Listing
4-16
// ...
$manager->setAttribute(Doctrine::ATTR_SEQNAME_FORMAT, '%s_sequence');
// bootstrap.php Listing
4-17
// ...
$manager->setAttribute(Doctrine::ATTR_TBLNAME_FORMAT, '%s_table');
// bootstrap.php Listing
4-18
// ...
$manager->setAttribute(Doctrine::ATTR_DBNAME_FORMAT, 'myframework_%s');
Validation attributes
Doctrine provides complete control over what it validates. The validation procedure can be
controlled with Doctrine::ATTR_VALIDATE.
The validation modes are bitwised, so they can be combined using | and removed using ^.
See the examples section below on how to do this.
VALIDATE_TYPES Makes Doctrine validate all field types. Doctrine does loose type
validation. This means that for example string with value '13.3'
will not pass as an integer but '13' will.
VALIDATE_CONSTRAINTS Makes Doctrine validate all field constraints such as notnull,
email etc.
VALIDATE_ALL Turns on all validations.
Validation by default is turned off so if you wish for your data to be validated you will need
to enable it. Some examples of how to change this configuration are provided below.
Examples
You can turn on all validations by using the Doctrine::VALIDATE_ALL attribute with the
following code:
Listing // bootstrap.php
4-19
// ...
$manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
You can also configure Doctrine to validate lengths and types, but not constraints with the
following code:
Listing // bootstrap.php
4-20
// ...
$manager->setAttribute(Doctrine::ATTR_VALIDATE,
Doctrine::VALIDATE_LENGTHS | Doctrine::VALIDATE_TYPES);
Listing // bootstrap.php
4-21
// ...
$conn->setAttribute('validate', 'none');
Internally when strings are used they are converted to the constants and used.
Conclusion
Now we have gone over some of the most common attributes used to configure Doctrine.
Some of these attributes may not apply to you ever or you may not understand what you could
use them for now. As you read the next chapters you will see which attributes you do and
don't need to use and things will begin to make more sense.
If you saw some attributes you wanted to change the value above, then you should have
added it to your bootstrap.php file and it should look something like the following now:
/** Listing
4-22
* Bootstrap Doctrine.php, register autoloader and specify
* configuration attributes
*/
require_once('../doctrine/branches/1.0/lib/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL);
$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING,
Doctrine::MODEL_LOADING_CONSERVATIVE);
Now we are ready to move on to the next chapter where we will learn everything their is to
know about Doctrine Connections (page 38).
Chapter 5
Connections
Introduction
From the start Doctrine has been designed to work with multiple connections. Unless
separately specified Doctrine always uses the current connection for executing the queries.
In this chapter we will demonstrate how to create and work with Doctrine connections.
Opening Connections
Doctrine_Manager provides the static method Doctrine_Manager::connection()
which opens new connections.
In this example we will show you to open a new connection:
Listing // test.php
5-1
// ...
$conn = Doctrine_Manager::connection('mysql://username:password@localhost/test',
'connection 1');
Retrieve Connections
If you use the Doctrine_Manager::connection() method and don't pass any arguments
it will return the current connection:
Listing // test.php
5-2
// ...
$conn2 = Doctrine_Manager::connection();
Current Connection
The current connection is the last opened connection. In the next example we will show how
you can get the current connection from the Doctrine_Manager instance:
// test.php Listing
5-3
// ...
$conn2 = Doctrine_Manager::connection('mysql://username2:password2@localhost/
test2', 'connection 2');
// test.php Listing
5-4
// ...
$manager->setCurrentConnection('connection 1');
Iterating Connections
You can iterate over the opened connections by simply passing the manager object to a
foreach clause. This is possible since Doctrine_Manager implements special
IteratorAggregate interface.
// test.php Listing
5-5
// ...
foreach($manager as $conn) {
echo $conn->getName() . "\n";
}
// test.php Listing
5-6
// ...
$conn = Doctrine_Manager::connection();
$name = $manager->getConnectionName($conn);
Close Connection
You can easily close a connection and remove it from the Doctrine connection registry with
the following code:
Listing // test.php
5-7
// ...
$conn = Doctrine_Manager::connection();
$manager->closeConnection($conn);
If you wish to close the connection but not remove it from the Doctrine connection registry
you can use the following code instead:
Listing // test.php
5-8
// ...
$conn = Doctrine_Manager::connection();
$conn->close();
Listing // test.php
5-9
// ...
$conns = $manager->getConnections();
foreach ($conns as $conn) {
echo $conn->getName() . "\n";
}
The above is essentially the same as iterating over the Doctrine_Manager object like we did
earlier. Here it is again:
Listing // test.php
5-10
// ...
foreach ($manager as $conn) {
echo $conn->getName() . "\n";
}
Count Connections
You can easily get the number of connections from a Doctrine_Manager object since it
implements the Countable interface.
// test.php Listing
5-11
// ...
$num = count($manager);
echo $num;
// test.php Listing
5-12
// ...
$num = $manager->count();
// test.php Listing
5-13
// ...
$manager->createDatabases();
$manager->dropDatabases();
// test.php Listing
5-14
// ...
$conn->createDatabase();
$conn->dropDatabase();
Conclusion
Now that we have learned all about Doctrine connections we should be ready to dive right in
to models in the Introduction to Models (page 42) chapter. We will learn a little bit about
Doctrine models first. Then we will start to have some fun and create our first test models
and see what kind of magic Doctrine can provide for you.
Chapter 6
Introduction to Models
Introduction
At the lowest level, Doctrine represents your database schema with a set of PHP classes.
These classes define the schema and behavior of your model.
A basic model that represents a user in a web application might look something like this.
We aren't actually going to use the above class definition, it is only meant to be an
example. We will generate our first class definition from an existing database table later in
this chapter.
Generating Models
Doctrine offers ways to generate these classes to make it easier to get started using Doctrine.
Generating from existing databases is only meant to be a convenience for getting started.
After you generate from the database you will have to tweak it and clean things up as
needed.
Existing Databases
A common case when looking for ORM tools like Doctrine is that the database and the code
that access it is growing large/complex. A more substantial tool is needed than manual SQL
code.
Doctrine has support for generating Doctrine_Record classes from your existing database.
There is no need for you to manually write all the Doctrine_Record classes for your domain
model.
Now we would like to convert it into Doctrine_Record class. With Doctrine this is easy!
Remember our test script we created in the Getting Started (page 18) chapter? We're going to
use that generate our models.
First we need to modify our bootstrap.php to use the MySQL database instead of sqlite
memory:
// bootstrap.php Listing
6-3
// ...
$conn = Doctrine_Manager::connection('mysql://root:mys3cr3et@localhost/
doctrine_test', 'doctrine');
// ...
You can use the $conn->createDatabase() method to create the database if it does not
already exist and the connected user has permission to create databases. Then use the
above provided CREATE TABLE statement to create the table.
Now we need a place to store our generated classes so lets create a directory named models
in the doctrine_test directory:
Now we just need to add the code to our test.php script to generate the model classes:
Listing // test.php
6-5
// ...
Doctrine::generateModelsFromDb('models', array('doctrine'),
array('generateTableClasses' => true));
The generateModelsFromDb method only requires one parameter and it is the import
directory (the directory where the generated record files will be written to). The second
argument is an array of database connection names to generate models for, and the third is
the array of options to use for the model building.
That's it! Now there should be a file called BaseUser.php in your doctrine_test/
models/generated directory. The file should look like the following:
Listing // models/generated/BaseUser.php
6-6
/**
* This class has been auto-generated by the Doctrine ORM Framework
*/
abstract class BaseUser extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('user');
$this->hasColumn('id', 'integer', 8, array('type' => 'integer', 'length' => 8,
'primary' => true, 'autoincrement' => true));
$this->hasColumn('first_name', 'string', 255, array('type' => 'string',
'length' => 255));
$this->hasColumn('last_name', 'string', 255, array('type' => 'string',
'length' => 255));
$this->hasColumn('username', 'string', 255, array('type' => 'string', 'length'
=> 255));
$this->hasColumn('password', 'string', 255, array('type' => 'string', 'length'
=> 255));
$this->hasColumn('type', 'string', 255, array('type' => 'string', 'length' =>
255));
$this->hasColumn('is_active', 'integer', 1, array('type' => 'integer',
'length' => 1, 'default' => '1'));
$this->hasColumn('is_super_admin', 'integer', 1, array('type' => 'integer',
'length' => 1, 'default' => '0'));
$this->hasColumn('created_at', 'timestamp', null, array('type' => 'timestamp',
'notnull' => true));
$this->hasColumn('updated_at', 'timestamp', null, array('type' => 'timestamp',
'notnull' => true));
}
}
You should also have a file called User.php in your doctrine_test/models directory. The
file should look like the following:
Listing // models/User.php
6-7
/**
* This class has been auto-generated by the Doctrine ORM Framework
*/
Doctrine will automatically generate a skeleton Doctrine_Table class for the model at
doctrine_test/models/UserTable.php because we passed the option
generateTableClasses with a value of true. The file should look like the following:
// models/UserTable.php Listing
6-8
/**
* This class has been auto-generated by the Doctrine ORM Framework
*/
class UserTable extends Doctrine_Table
{
You can place custom functions inside the User and UserTable classes to customize the
functionality of your models. Below are some examples:
// models/User.php Listing
6-9
// ...
class User extends BaseUser
{
public function setPassword($password)
{
return $this->_set('password', md5($password));
}
}
In order for the above password accessor overriding to work properly you must enabled
the auto_accessor_override attribute in your bootstrap.php file like done below.
// bootstrap.php Listing
6-10
// ...
$manager->setAttribute(Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
Now when you try and set a users password it will be md5 encrypted. First we need to modify
our bootstrap.php file to include some code for autoloading our models from the models
directory:
// bootstrap.php Listing
6-11
// ...
Doctrine::loadModels('models');
The model loading is fully explained later in the Autoloading Models (page 48) section of
this chapter.
Now we can modify test.php to include some code which will test the changes we made to
the File model:
Listing // test.php
6-12
// ...
Now when you execute test.php from your terminal you should see the following:
Here is an example of some custom functions you might add to the UserTable class:
Listing // models/UserTable.php
6-14
// ...
class UserTable extends Doctrine_Table
{
public function getCreatedToday()
{
$today = date('Y-m-d h:i:s', strtotime(date('Y-m-d')));
return $this->createQuery('u')
->where('u.created_at > ?', $today)
->execute();
}
}
In order for custom Doctrine_Table classes to be loaded you must enable the
autoload_table_classes attribute in your bootstrap.php file like done below.
Listing // boostrap.php
6-15
// ...
$manager->setAttribute(Doctrine::ATTR_AUTOLOAD_TABLE_CLASSES, true);
Now you have access to this function when you are working with the UserTable instance:
Listing // test.php
6-16
// ...
$usersCreatedToday = Doctrine::getTable('User')->getCreatedToday();
Schema Files
You can alternatively manage your models with YAML schema files and generate PHP classes
from them. First lets generate a YAML schema file from the existing models we already have
to make things easier. Change test.php to have the following code inside:
Listing // test.php
6-17
// ...
Doctrine::generateYamlFromModels('schema.yml', 'models');
Now you should see a file named schema.yml created in the root of the doctrine_test
directory. It should look like the following:
--- Listing
6-19
User:
tableName: user
columns:
id:
type: integer(8)
primary: true
autoincrement: true
is_active:
type: integer(1)
default: '1'
is_super_admin:
type: integer(1)
default: '0'
created_at:
type: timestamp(25)
notnull: true
updated_at:
type: timestamp(25)
notnull: true
first_name: string(255)
last_name: string(255)
username: string(255)
password: string(255)
type: string(255)
So now that we have a valid YAML schema file, we can now maintain our schema from here
and generate the PHP classes from here. Lets create a new php script called generate.php.
This script will re-generate everything and make sure the database is reinstantiated each
time the script is called:
// generate.php Listing
6-20
require_once('bootstrap.php');
Doctrine::dropDatabases();
Doctrine::createDatabases();
Doctrine::generateModelsFromYaml('schema.yml', 'models');
Doctrine::createTablesFromModels('models');
Now you can alter your schema.yml and re-generate your models by running the following
command from your terminal:
Now that we have our YAML schema file setup and we can re-generate our models from the
schema files lets cleanup the file a little and take advantage of some of the power of Doctrine:
Listing ---
6-22
User:
actAs: [Timestampable]
columns:
is_active:
type: integer(1)
default: '1'
is_super_admin:
type: integer(1)
default: '0'
first_name: string(255)
last_name: string(255)
username: string(255)
password: string(255)
type: string(255)
You can learn more about YAML Schema Files in its dedicated chapter (page 193).
Autoloading Models
Doctrine offers two ways of loading models. We have conservative(lazy) loading, and
aggressive loading. Conservative loading will not require the PHP file initially, instead it will
cache the path to the class name and this path is then used in the Doctrine::autoload()
we registered earlier with spl_autoload_register(). Below are some examples using the both
types of model loading.
Conservative
Conservative model loading is going to be the ideal model loading method for a production
environment. This method will lazy load all of the models instead of loading them all when
model loading is executed.
Conservative model loading requires that each file contain only one class, and the file must be
named after the class. For example, if you have a class named User, it must be contained in a
file named User.php.
To use conservative model loading we need to set the model loading attribute to be
conservative:
$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Listing
6-24
Doctrine::MODEL_LOADING_CONSERVATIVE);
We already made this change in an earlier step in the bootstrap.php file so you don't
need to make this change again.
When we use the Doctrine::loadModels() functionality all found classes will be cached
internally so the autoloader can require them later.
Doctrine::loadModels('models'); Listing
6-25
Now when we instantiate a new class, for example a File class, the autoloader will be
triggered and the class is required.
Instantiating the class above triggers a call to Doctrine::autoload() and the class that
was found in the call to Doctrine::loadModels() will be required and made available.
Aggressive
Aggressive model loading is the default model loading method and is very simple, it will look
for all files with a .php extension and will include it. Doctrine can not satisfy any inheritance
and if your models extend another model, it cannot include them in the correct order so it is
up to you to make sure all dependencies are satisfied in each class.
With aggressive model loading you can have multiple classes per file and the file name is not
required to be related to the name of the class inside of the file.
The downside of aggressive model loading is that every php file is included in every request,
so if you have lots of models it is recommended you use conservative model loading.
To use aggressive model loading we need to set the model loading attribute to be aggressive:
$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Listing
6-27
Doctrine::MODEL_LOADING_AGGRESSIVE);
Aggressive is the default of the model loading attribute so explicitly setting it is not
necessary if you wish to use it.
When we use the Doctrine::loadModels() functionality all the classes found will be
included right away:
Doctrine::loadModels('/path/to/models'); Listing
6-28
Custom Accessors/Mutators
With Doctrine it is possible to define custom accessors and mutators to use in your Doctrine
models. This is possible with the hasAccessor() and hasMutator() mapping methods. Or,
you can define both the mutator and accessor in one call using the hasAccessorMutator()
method.
Imagine a user model where you want to define a custom password mutator that encrypts the
password using the PHP md5() method:
Now when you try and set the password the custom mutator will be invoked instead of the
normal mutating procedure.
Conclusion
This chapter is probably the most intense chapter so far but it is a good one. We learned a
little about how to use models, how to generate models from existing databases, how to write
our own models, and how to maintain our models as YAML schema files. We also modified our
Doctrine test environment to implement some functionality for loading models from our
models directory.
This topic of Doctrine models is so large that it warranted the chapters being split in to three
pieces to make it easier on the developer to absorb all the information. In the next chapter
(page 51) we will really get in to the API we use to define our models.
Chapter 7
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:
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.
Listing // models/Book.php
7-1
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-2
# schema.yml
# ...
Book:
columns:
bookTitle:
name: bookTitle as title
type: string
Now the column in the database is named bookTitle but you can access the property on your
objects using title.
Listing // test.php
7-3
// ...
$book = new Book();
$book->title = 'Some book';
$book->save();
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.
Listing // models/generated/BaseUser.php
7-4
// ...
// ...
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-5
# schema.yml
# ...
User:
# ...
columns:
username:
type: string(255)
default: default username
# ...
Now when you print the name on a brand new User record it will print the default value:
// test.php Listing
7-6
// ...
$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 1. Column named 'content' with type 'string' and length 3000 results in database
type 'TEXT' 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 2. Column with type 'integer' and length 1 results in 'TINYINT' 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:
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-10
Test:
columns:
booltest: boolean
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-12
Test:
columns:
integertest:
type: integer(4)
unsigned: true
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.
$this->hasColumn('floattest', 'float');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-14
Test:
columns:
floattest: float
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-16
Test:
columns:
decimaltest: decimal
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:
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-18
Test:
columns:
decimaltest:
type: decimal(18)
scale: 2
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-20
Test:
columns:
stringtest:
type: string(200)
fixed: true
Array
This is the same as the 'array' type in PHP.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-22
Test:
columns:
arraytest: array(10000)
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-24
Test:
columns:
objecttest: object
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"
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-26
Test:
columns:
blobtest: blob
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"
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-28
Test:
columns:
clobtest: clob
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
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-30
Test:
columns:
timestamptest: timestamp
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 from00 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.
{
$this->hasColumn('timetest', 'time');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-32
Test:
columns:
timetest: time
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-34
Test:
columns:
datetest: date
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:
Listing
7-36
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-37
Test:
columns:
enumtest:
type: enum
values: [php, java, python]
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.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-39
Test:
columns:
gziptest: gzip
The family of php functions for compressing21 are used internally for compressing and
uncompressing the contents of the gzip column type.
Examples
Consider the following definition:
21. http://www.php.net/gzcompress
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-41
Example:
tableName: example
columns:
id:
type: string(32)
fixed: true
primary: true
someint:
type: integer(10)
unsigned: true
sometime:
type: time(25)
default: '12
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)
The above example will create the following database table in Pgsql:
Column Type
id character(32)
somename character varying(12)
somedate date
sometimestamp timestamp without time zone
someboolean boolean
somedecimal numeric(18,2)
somefloat double precision
sometime time without time zone
someclob text
someblob bytea
The schema schema will create the following database table in Mysql:
Field Type
id char(32)
somename varchar(12)
somedate date
sometimestamp timestamp
someboolean tinyint(1)
somedecimal decimal(18,2)
somefloat double
sometime time
someclob longtext
someblob longblob
Relationships
Introduction
In Doctrine all record relations are being set with Doctrine_Record::hasMany,
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:
cascade Yes Specify application level cascading operations. Currently only delete
is supported
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 Listing
7-42
Notice the as keyword being used above. This means that the Forum_Board has a many
relationship defined to Forum_Thread but is aliased as Threads.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-43
# schema.yml
# ...
Forum_Board:
columns:
name: string(100)
description: string(5000)
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 Listing
7-44
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-45
# 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
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:
Listing // test.php
7-46
// ...
$board = new Forum_Board();
$board->name = 'Some board';
// test.php Listing
7-47
// ...
$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:
Now we can save all the changes with one call. It will save the new board as well as its
threads:
// test.php Listing
7-49
// ...
$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)); Listing
7-50
Now when you execute test.php with PHP from your terminal you should see the following:
[last_name] =>
[username] => jwage
[password] =>
[type] =>
[created_at] => 2009-01-20 16:41:57
[updated_at] => 2009-01-20 16:41:57
)
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!
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 Listing
7-52
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-53
# schema.yml
# ...
Email:
columns:
user_id: integer
address: string(150)
relations:
User:
local: user_id
foreign: id
When using YAML schema files it is not required to specify the relationship on the opposite
end(User) because the relationship is automatically flipped and added for you. The
relationship will be named the name of the class. So in this case the relationship on the
User side will be called Email and will be many. If you wish to customize this you can use
the foreignAlias and foreignType options.
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 Listing
7-54
$this->hasOne('Email', array(
'local' => 'id',
'foreign' => 'user_id'
)
);
}
}
Notice how we override the setUp() method and call parent::setUp(). This is because
the BaseUser class which is generated from YAML or from an existing database contains
the main setUp() method and we override it in the User class to add an additional
relationship.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-55
# schema.yml
# ...
User:
# ...
relations:
# ...
Email:
local: id
foreign: user_id
Listing // models/User.php
7-56
// ...
// models/Phonenumber.php
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-57
# schema.yml
# ...
User:
# ...
relations:
# ...
Phonenumbers:
type: many
class: Phonenumber
local: id
foreign: user_id
Phonenumber:
columns:
user_id: integer
phonenumber: string(50)
relations:
User:
local: user_id
foreign: id
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 Listing
7-58
$this->hasColumn('parent_id', 'integer');
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-59
# schema.yml
# ...
Task:
columns:
name: string(100)
parent_id: integer
relations:
Parent:
class: Task
local: parent_id
foreign: id
foreignAlias: Subtasks
The above implementation is purely an example and is not the most efficient way to store
and retrieve hierarchical data. Check the NestedSet behavior included in Doctrine for the
recommended way to deal with hierarchical data.
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.
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-61
User:
# ...
relations:
# ...
Groups:
class: Group
local: user_id
foreign: group_id
refClass: UserGroup
// models/Group.php Listing
7-62
)
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-63
# schema.yml
# ...
Group:
tableName: groups
columns:
name: string(30)
relations:
Users:
class: User
local: group_id
foreign: user_id
refClass: UserGroup
Please note that group is a reserved keyword so that is why we renamed the table to
groups using the setTableName method. The other option is to turn on identifier quoting
using the Doctrine::ATTR_QUOTE_IDENTIFIERS attribute so that the reserved word is
escaped with quotes.
Listing // models/UserGroup.php
7-65
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-66
# schema.yml
# ...
UserGroup:
columns:
user_id:
type: integer
primary: true
group_id:
type: integer
primary: true
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 Listing
7-67
// ...
$user = new User();
// test.php Listing
7-68
// ...
$user->Groups[0]->name = 'First Group';
// test.php Listing
7-69
// ...
$user->save();
Now you can delete the associations between user and groups it belongs to:
// test.php Listing
7-70
// ...
$user->UserGroup->delete();
$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();
Listing // test.php
7-71
// ...
print_r($user->toArray(true));
Listing // models/User.php
7-73
// ...
// models/UserReference.php
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-74
# schema.yml
# ...
User:
# ...
relations:
# ...
Parents:
class: User
local: child_id
foreign: parent_id
refClass: UserReference
foreignAlias: Children
UserReference:
columns:
parent_id:
type: integer
primary: true
child_id:
type: integer
primary: true
Listing // models/User.php
7-75
// ...
// models/FriendReference.php
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-76
# schema.yml
# ...
User:
# ...
relations:
# ...
Friends:
class: User
local: user1
foreign: user2
refClass: FriendReference
equal: true
FriendReference:
columns:
user1:
type: integer
primary: true
user2:
type: integer
primary: true
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 Listing
7-77
// ...
$daniels = new User();
$daniels->username = 'Jack Daniels';
$daniels->Friends[0] = $brandy;
$koskenkorva->Friends[0] = $daniels;
$koskenkorva->Friends[1] = $brandy;
$koskenkorva->Friends[2] = $beer;
$conn->flush();
Now if we access for example the friends of Stefan Beer it would return one user 'Mikko
Koskenkorva':
// test.php Listing
7-78
// ...
$beer->free();
unset($beer);
$user = Doctrine::getTable('User')->findOneByUsername('Stefan Beer');
print_r($user->Friends->toArray());
Now when you execute test.php you will see the following:
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:
Listing // models/Product.php
7-80
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-81
# schema.yml
# ...
Product:
columns:
name:
type: string
price:
type: decimal(18)
discounted_price:
type: decimal(18)
relations:
Orders:
class: Order
local: id
foreign: product_id
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 Listing
7-82
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-83
# schema.yml
# ...
Order:
tableName: orders
columns:
product_id: integer
quantity: integer
relations:
Product:
local: product_id
foreign: id
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.
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.
// ...
// ...
$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 (page 193) chapter:
--- Listing
7-86
# schema.yml
# ...
Phonenumber:
# ...
relations:
# ...
User:
local: user_id
foreign: id
onDelete: CASCADE
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.
$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 (page 193) chapter:
Listing ---
7-88
IndexTest:
columns:
name: string
indexes:
myindex:
fields: [name]
$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 (page 193) chapter:
Listing ---
7-90
MultiColumnIndexTest:
columns:
name: string
code: string
indexes:
myindex:
fields: [name, code]
$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 (page 193) chapter:
--- Listing
7-92
MultipleIndexTest:
columns:
name: string
code: string
age: integer
indexes:
myindex:
fields: [name, code]
ageindex:
fields: [age]
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 value that 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.
$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 (page 193) chapter:
Listing ---
7-94
MultipleIndexTest:
columns:
name: string
code: string
age: integer
indexes:
myindex:
fields:
name:
sorting: ASC
length: 10
code: -
type: unique
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'.
Listing // models/Article.php
7-95
$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 (page 193) chapter:
--- Listing
7-96
# schema.yml
# ...
Article:
options:
type: MyISAM
columns:
name: string(255)
content: string
indexes:
content:
fields: [content]
type: fulltext
Notice how we set the table type to MyISAM. This is because the fulltext index type is
only supported in MyISAM so you will receive an error if you use something like InnoDB.
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 Listing
7-97
// ...
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
7-98
# schema.yml
# ...
Product:
# ...
checks:
price_check: price > discounted_price
Some databases don't support CHECK constraints. When this is the case Doctrine simply
skips the creation of check constraints.
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:
$this->option('type', 'INNODB');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-101
MyInnoDbRecord:
columns:
name: string
options:
type: INNODB
In the following example we set the collate and character set options:
{
$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 (page 193) chapter:
--- Listing
7-103
MyCustomOptionRecord:
columns:
name: string
options:
collate: utf8_unicode_ci
charset: utf8
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:
You can set the default charset and collate at the manager, connection or table level just like
all the other configurations in Doctrine.
Set globally on a manager instance.
// bootstrap.php Listing
7-105
// ...
$manager->setCollate('utf8_unicode_ci');
$manager->setCharset('utf8');
// bootstrap.php Listing
7-106
// ...
$connection->setCollate('utf8_unicode_ci');
$connection->setCharset('utf8');
You can also set this at the table level like always:
$this->setCollate('utf8_unicode_ci');
$this->setCharset('utf8');
}
}
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:
Listing // models/User.php
7-108
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-109
# schema.yml
# ...
User:
# ...
relations:
# ...
Addresses:
class: Address
local: id
foreign: user_id
cascade: [delete]
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 delete, more options will be added
in future releases of Doctrine.
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():
1. 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).
2. 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).
3. 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:
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 Listing
7-110
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
7-111
# schema.yml
# ...
Address:
columns:
user_id: integer
address: string(255)
country: string(255)
city: string(255)
state: string(2)
postal_code: string(25)
relations:
User:
local: user_id
foreign: id
onDelete: CASCADE
The onDelete option is translated to proper DDL/DML statements when Doctrine creates
your tables.
Note that 'onDelete' => 'CASCADE' is specified on the Address class, since the Address
owns the foreign key (user_id) and database-level cascades are applied on the foreign key.
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 (page 94) 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 (page 94).
Chapter 8
Listing $ rm schema.yml
8-1
$ touch schema.yml
$ rm -rf models/*
For the next several examples we will use the following schema:
Listing // models/User.php
8-2
$this->hasOne('Email', array(
'local' => 'id',
'foreign' => 'user_id'
)
);
// models/Email.php
// models/Phonenumber.php
$this->hasColumn('primary_num', 'boolean');
}
// models/Group.php
// models/UserGroup.php
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
8-3
# schema.yml
User:
columns:
username: string(255)
password: string(255)
relations:
Groups:
class: Group
local: user_id
foreign: group_id
refClass: UserGroup
foreignAlias: Users
Email:
columns:
user_id: integer
address: string(255)
relations:
User:
foreignType: one
Phonenumber:
columns:
user_id: integer
phonenumber: string(255)
primary_num: boolean
relations:
User:
foreignAlias: Phonenumbers
Group:
tableName: groups
columns:
name: string(255)
UserGroup:
columns:
user_id:
type: integer
primary: true
group_id:
type: integer
primary: true
Now that you have your schema defined you can instantiate the database by simply running
the generate.php script we so conveniently created in the previous chapter.
Listing // test.php
8-5
// ...
$user = new User();
$user['username'] = 'jwage';
$user['password'] = 'changeme';
$email = $user->Email;
$email = $user->get('Email');
$email = $user['Email'];
When accessing a one-to-one related record that doesn't exist, Doctrine automatically creates
the object. That is why the above code is possible.
Listing // test.php
8-6
// ...
$user->Email->address = '[email protected]';
$user->save();
Listing // test.php
8-7
// ...
$user->Phonenumbers[]->phonenumber = '123 123';
$user->Phonenumbers[]->phonenumber = '456 123';
$user->Phonenumbers[]->phonenumber = '123 777';
Now we can easily save the user and the associated phonenumbers:
Listing // test.php
8-8
// ...
$user->save();
Another way to easily create a link between two related components is by using
Doctrine_Record::link(). It often happens that you have two existing records that you would
like to relate (or link) to one another. In this case, if there is a relation defined between the
involved record classes, you only need the identifiers of the related record(s):
Lets create a few new Phonenumber objects and keep track of the new phone number
identifiers::
// test.php Listing
8-9
// ...
$phoneIds = array();
$phoneIds[] = $phone1['id'];
$phoneIds[] = $phone2['id'];
Let's link the phone numbers to the user, since the relation to Phonenumbers exists for the
User record
// test.php Listing
8-10
Remember to save the record after calling link(). By default the links are not persisted
until you save the record. If you wish to save the links to the database instantly then you
can pass the third argument $now with a value of true.
Now the following will issue query to database creating links because of the third argument
being true. Without it you would have to call save() to persist the links.
If a relation to the User record class is defined for the Phonenumber record class, you may
even do this:
First create a user to work with:
// test.php Listing
8-13
// ...
$user = new User();
$user['username'] = 'jwage';
$user['password'] = 'changeme';
$user->save();
Listing // test.php
8-14
// ...
$phone1 = new Phonenumber();
$phone1['phonenumber'] = '555 202 7890';
$phone1->save();
Listing // test.php
8-15
// ...
$phone1->link('User', array($user['id']));
$phone1->save();
Listing // test.php
8-16
// ...
$phone2 = new Phonenumber();
$phone2['phonenumber'] = '555 100 7890';
$phone2->save();
Listing // test.php
8-17
// ...
$phone2->link('User', array($user['id']));
$phone2->save();
Listing // test.php
8-18
// ...
$user = Doctrine::getTable('User')->find(1);
echo $user->Email['address'];
echo $user->Phonenumber[0]->phonenumber;
Much more efficient way of doing this is using DQL. The following example uses only one SQL
query for the retrieval of related components.
Listing // test.php
8-19
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Email e')
->leftJoin('u.Phonenumber p')
->where('u.id = ?', 1);
$user = $q->fetchOne();
echo $user->Email['address'];
echo $user->Phonenumber[0]['phonenumber'];
// test.php Listing
8-20
// ...
$user->Email['address'] = '[email protected]';
$user->Phonenumber[0]['phonenumber'] = '123123';
$user->save();
In the above example calling $user->save() saves the email and phonenumber.
// test.php Listing
8-21
// ...
$user->Email->delete();
// test.php Listing
8-22
// ...
$user->Phonenumber[3]->delete();
// test.php Listing
8-23
// ...
$user->Phonenumbers->delete();
Or can just delete the entire user and all related objects:
Listing // test.php
8-24
// ...
$user->delete();
Usually in a typical web application the primary keys of the related objects that are to be
deleted come from a form. In this case the most efficient way of deleting the related records
is using DQL DELETE statement. Lets say we have once again Users and Phonenmbers with
their relation being one-to-many. Deleting the given Phonenumbers for given user id can be
achieved as follows:
Listing // test.php
8-25
// ...
$q = Doctrine_Query::create()
->delete('Phonenumber')
->addWhere('user_id = ?', 5)
->whereIn('id', array(1, 2, 3));
$numDeleted = $q->execute();
Sometimes you may not want to delete the Phonenumber records but to simply unlink the
relations by setting the foreign key fields to null. This can of course be achieved with DQL but
perhaps to most elegant way of doing this is by using Doctrine_Record::unlink().
Please note that the unlink() method is very smart. It not only sets the foreign fields for
related Phonenumbers to null but it also removes all given Phonenumber references from
the User object.
Lets say we have a User who has three Phonenumbers (with identifiers 1, 2 and 3). Now
unlinking the Phonenumbers 1 and 3 can be achieved as easily as:
Listing // test.php
8-26
// ...
$user->unlink('Phonenumber', array(1, 3));
$user->save();
echo $user->Phonenumber->count(); // 1
Listing // test.php
8-27
// ...
$user = new User();
if (isset($user->Email)) {
// ...
}
Now the next example will return true because we instantiated the Email relationship:
// test.php Listing
8-28
// ...
$obj->Email = new Email();
if(isset($obj->Email)) {
// ...
}
Many-to-Many Relations
Doctrine requires that Many-to-Many relationships be bi-directional. For example: both
User must have many Groups and Group must have many User.
// test.php Listing
8-29
// ...
$user = new User();
$user->username = 'Some User';
$user->Groups[0]->username = 'Some Group';
$user->Groups[1]->username = 'Some Other Group';
$user->save();
However in real world scenarios you often already have existing groups, where you want to
add a given user. The most efficient way of doing this is:
// test.php Listing
8-30
// ...
$groupUser = new GroupUser();
$groupUser->user_id = $userId;
$groupUser->group_id = $groupId;
$groupUser->save();
Deleting a Link
The right way to delete links between many-to-many associated records is by using the DQL
DELETE statement. Convenient and recommended way of using DQL DELETE is through the
Query API.
// test.php Listing
8-31
// ...
$q = Doctrine_Query::create()
->delete('GroupUser')
->addWhere('user_id = ?', 5)
->whereIn('group_id', array(1, 2));
$deleted = $q->execute();
Another way to unlink the relationships between related objects is through the
Doctrine_Record::unlink method. However, you should avoid using this method unless
you already have the parent model, since it involves querying the database first.
Listing // test.php
8-32
// ...
$user = Doctrine::getTable('User')->find(5);
$user->unlink('Group', array(1, 2));
$user->save();
You can also unlink ALL relationships to Group by omitting the second argument:
Listing // test.php
8-33
// ...
$user->unlink('Group');
$user->save();
While the obvious and convenient way of deleting a link between User and Group would be
the following, you still should *NOT* do this:
Listing // test.php
8-34
// ...
$user = Doctrine::getTable('User')->find(5);
$user->GroupUser->remove(0)->remove(1);
$user->save();
This is due to a fact that the call to $user->GroupUser loads all Group links for given User.
This can be time-consuming task if the User belongs to many Groups. Even if the user
belongs to few groups this will still execute an unnecessary SELECT statement.
Fetching Data
Doctrine provides several different "hydration modes". These are different ways in which data
can be retrieved. Understanding all these modes is important to always make the right
choice.
There is one major distinction that divides all the hydration modes into 2 groups: The ones
that are based on identity and the ones that are not.
The hydration modes that are based on (object) identity are: HYDRATE_RECORD,
HYDRATE_ARRAY. The former generates an object graph while the latter generates a nested
array structure that is in many cases very similar to the object graph (Not though that
HYDRATE_RECORD/HYDRATE_ARRAY can potentially produce differing results in more
"complex" query scenarios due to the natural difference of objects and arrays, their
comparison, equality and identity semantics!).
The fact that these two hydration modes are based on identity becomes evident through the
behavior of Doctrine to auto-add PK/ID fields of classes used in a DQL query if they are not
already present. This means: These two hydration modes *require* the primary keys/
identifiers of all those classes that are participating in a DQL query in such a way that they
are "fetched" (eg at least one field appears in the SELECT clause).
HYDRATE_RECORD/HYDRATE_ARRAY
Lets consider we have users and phonenumbers with their relation being one-to-many. Now
consider the following plain sql query:
If you are familiar with these kind of one-to-many joins it may be familiar to you how the basic
result set is constructed. Whenever the user has more than one phonenumbers there will be
duplicated data in the result set. The result set might look something like:
Here Jack Daniels has 2 phonenumbers, John Beer has one whereas John Smith has 3
phonenumbers. You may notice how clumsy this result set is. Its hard to iterate over it as you
would need some duplicate data checkings here and there.
Doctrine identity hydration removes all duplicated data. It also performs many other things
such as:
1. Custom indexing of result set elements (only with HYDRATE_ARRAY/
HYDRATE_RECORD)
2. Value casting and preparation (with all hydration modes except HYDRATE_NONE)
3. Value assignment listening (only with HYDRATE_ARRAY/HYDRATE_RECORD)
4. Makes multi-dimensional array out of the two-dimensional result set array, the
number of dimensions is equal to the number of nested joins (only with
HYDRATE_ARRAY)
Now consider the DQL equivalent of the SQL query we used:
$q = Doctrine_Query::create() Listing
8-37
->select('u.id, u.name, p.phonenumber')
->from('User u')
->leftJoin('u.Phonenumber p');
This structure also applies to the hydration of objects(records) which is the default hydration
mode of Doctrine. The only differences are that the individual elements are represented as
Doctrine_Record objects and the arrays converted into Doctrine_Collection objects. Whether
dealing with arrays or objects you can:
1. Iterate over the results using foreach
2. Access individual elements using array access brackets
3. Get the number of elements using count() function
4. Check if given element exists using isset()
5. Unset given element using unset()
The other group of hydration modes are not based on identity. These are: HYDRATE_NONE,
HYDRATE_SCALAR and HYDRATE_SINGLE_SCALAR.
HYDRATE_NONE
This is the fastest but least useful hydration mode. It is equal to a $pdoStmt-
>fetchAll(PDO::FETCH_NUM); The reason Doctrine uses FETCH_NUM is that the column
aliases in the SQL query are generated by Doctrine, therefore FETCH_ASSOC would be of no
use because you don't know what the names of the columns in the result set would be. This
hydration mode is mainly useful for debugging purposes or for some other simple scenarios.
HYDRATE_SCALAR
This hydration mode creates a flat/rectangular result set that can contain duplicate data. It's
best to think of this as a normal SQL result set with a few subtle but important differences:
1. column names are converted to field names (column aliases)
2. data type conversions are applied
Let's look at an example DQL query: SELECT u.*, p.* FROM User u LEFT JOIN
u.phonenumbers p The result with HYDRATE_SCALAR could look like this:
Listing array(
8-39
0 => array(
'u_id' => '1',
'u_name' => 'roman',
'p_number' => '1234',
'p_id' => '42'
),
1 => array(
'u_id' => '1',
'u_name' => 'roman',
'p_number' => '1111',
'p_id' => '43'
),
...
)
As you can see, it looks like a regular SQL result set of a JOINed SQL query. However, in
order to avoid ambiguities between field names all field names in the result set are prefixed
with the DQL alias that you specified in the query. This makes this hydration mode robust
even for complex queries, yet the result is very predictable because the DQL aliases as well as
the field names stem from your DQL query and your object model, respectively. In addition,
as noted earlier, data type conversions take place where necessary.
HYDRATE_SINGLE_SCALAR
This is basically a sub-type of HYDRATE_SCALAR. This hydration mode turns out to be very
useful. Without further explanation, let's look at some examples:
$q = Doctrine_Query::create(); Listing
8-40
$q->select("u.name")->from("User u");
$res = $q->execute(array(), Doctrine::HYDRATE_SINGLE_SCALAR);
echo $res; // prints 'romanb'
$q = Doctrine_Query::create(); Listing
8-41
$q->select("COUNT(u.id) num_ids")->from("User u");
$res = $q->execute(array(), Doctrine::HYDRATE_SINGLE_SCALAR);
echo $res; // echos '1' or whatever the count is..
As you can see this hydration mode is self-explanatory. Gone are the times of having to grab
such a result from $result[0][0] or similar.
You should use array or scalar hydration when you only need data for read-only purposes,
whereas you should use the record(object) hydration when you need/want to operate on the
data and/or use the business logic that is coded in your entities (records).
Sample Queries
Count number of records for a relationship:
// test.php Listing
8-42
// ...
$q = Doctrine_Query::create()
->select('u.*, COUNT(DISTINCT p.id) AS num_phonenumbers')
->from('User u')
->leftJoin('u.Phonenumbers p')
->groupBy('u.id');
$users = $q->fetchArray();
echo $users[0]['Phonenumbers'][0]['num_phonenumbers'];
// test.php Listing
8-43
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Groups g');
$users = $q->fetchArray();
// test.php Listing
8-44
// ...
$q = Doctrine_Query::create()
->from('User u')
->where('u.username = ?', 'jwage');
$users = $q->fetchArray();
Listing // test.php
8-45
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumbers p')
->where('u.username = ? AND p.id = ?', array(1, 1));
$users = $q->fetchArray();
You can also optionally use the andWhere() method to add to the existing where parts.
Listing // test.php
8-46
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumbers p')
->where('u.username = ?', 1)
->andWhere('p.id = ?', 1);
$users = $q->fetchArray();
Listing // test.php
8-47
// ...
$q = Doctrine_Query::create()
->from('User u')
->whereIn('u.id', array(1, 2, 3));
$users = $q->fetchArray();
Listing // test.php
8-48
// ...
$q = Doctrine_Query::create()
->from('User u')
->where('u.id IN (1, 2, 3)');
$users = $q->fetchArray();
Listing // test.php
8-49
// ...
$userEncryptedKey = 'a157a558ac00449c92294c7fab684ae0';
$q = Doctrine_Query::create()
->from('User u')
->where("MD5(CONCAT(u.username, 'secret_key')) = ?", $userEncryptedKey);
$user = $q->fetchOne();
$q = Doctrine_Query::create()
->from('User u')
->where('LOWER(u.username) = LOWER(?)', 'jwage');
$user = $q->fetchOne();
Limiting result sets using aggregate functions. Limit to users with more than one
phonenumber:
// test.php Listing
8-50
// ...
$q = Doctrine_Query::create()
->select('u.*, COUNT(DISTINCT p.id) AS num_phonenumbers')
->from('User u')
->leftJoin('u.Phonenumbers p')
->having('num_phonenumbers > 1')
->groupBy('u.id');
$users = $q->fetchArray();
// test.php Listing
8-51
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumbers p WITH p.primary_num = ?', true);
$users = $q->fetchArray();
// test.php Listing
8-52
// ...
$q = Doctrine_Query::create()
->select('u.username, p.phone')
->from('User u')
->leftJoin('u.Phonenumbers p');
$users = $q->fetchArray();
Using a wildcard to select all User columns but only one Phonenumber column:
// test.php Listing
8-53
// ...
$q = Doctrine_Query::create()
->select('u.*, p.phonenumber')
->from('User u')
->leftJoin('u.Phonenumbers p');
$users = $q->fetchArray();
Listing // test.php
8-54
// ...
$q = Doctrine_Query::create()
->delete('Phonenumber')
->addWhere('user_id = 5');
$deleted = $q->execute();
Listing // test.php
8-55
// ...
$q = Doctrine_Query::create()
->update('User u')
->set('u.is_active', '?', true)
->where('u.id = ?', 1);
$updated = $q->execute();
Perform DQL update with DBMS function. Make all usernames lowercase:
Listing // test.php
8-56
// ...
$q = Doctrine_Query::create()
->update('User u')
->set('u.username', 'LOWER(u.username)');
$updated = $q->execute();
Listing // test.php
8-57
// ...
$q = Doctrine_Query::create()
->from('User u')
->where('u.username LIKE ?', '%jwage%');
$users = $q->fetchArray();
Use the INDEXBY keyword to hydrate the data where the key of record entry is the
name of the column you assign:
Listing // test.php
8-58
// ...
$q = Doctrine_Query::create()
->from('User u INDEXBY u.username');
$users = $q->fetchArray();
// test.php Listing
8-59
// ...
print_r($users['jwage']);
$q = Doctrine_Query::create() Listing
8-60
->from('User u')
->where('u.username = ?', array('Arnold'));
$users = $q->fetchArray();
$q = Doctrine_Query::create() Listing
8-61
->from('User u')
->where('u.username = :username', array(':username' => 'Arnold'));
$users = $q->fetchArray();
Using subqueries in your WHERE. Find users not in group named Group 2:
// test.php Listing
8-62
// ...
$q = Doctrine_Query::create()
->from('User u')
->where('u.id NOT IN (SELECT u.id FROM User u2 INNER JOIN u2.Groups g WHERE
g.name = ?)', 'Group 2');
$users = $q->fetchArray();
You can accomplish this without using subqueries. The two examples below would have the
same result as the example above.
// test.php Listing
8-63
// ...
$q = Doctrine_Query::create()
->from('User u')
->innerJoin('u.Groups g WITH g.name != ?', 'Group 2')
$users = $q->fetchArray();
// test.php Listing
8-64
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Groups g')
->where('g.name != ?', 'Group 2');
$users = $q->fetchArray();
Doctrine has many different ways you can execute queries and retrieve the data. Below are
examples of all the different ways you can execute a query:
First lets create a sample query to test with:
Listing // test.php
8-65
// ...
$q = Doctrine_Query::create()
->from('User u');
You can also use array hydration by specifying the hydration method to the second
argument of the execute() method:
Listing // test.php
8-67
// ...
$users = $q->execute(array(), Doctrine::HYDRATE_ARRAY)
You can also specify the hydration method by using the setHydrationMethod()
method:
Sometimes you may want to totally bypass hydration and return the raw data that
PDO returns:
Listing // test.php
8-69
// ...
$users = $q->execute(array(), Doctrine::HYDRATE_NONE);
More can be read about skipping hydration in the improving performance (page 354)
chapter.
Listing // test.php
8-70
// ...
$user = $q->fetchOne();
// test.php Listing
8-71
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->where('u.id = ?', 1)
$user = $q->fetchOne();
The following lazy-loads the password field and executes one additional database query to
retrieve the value:
// test.php Listing
8-72
// ...
$user->description;
Doctrine does the proxy evaluation based on loaded field count. It does not evaluate which
fields are loaded on field-by-field basis. The reason for this is simple: performance. Field lazy-
loading is very rarely needed in PHP world, hence introducing some kind of variable to check
which fields are loaded would introduce unnecessary overhead to basic fetching.
To Array
The toArray() method returns an array representation of your records or collections. It also
accesses the relationships the objects may have. If you need to print a record for debugging
purposes you can get an array representation of the object and print that.
// test.php Listing
8-73
// ...
print_r($user->toArray());
If you want include the relationships in the array then you need to pass the $deep argument
with a value of true:
// test.php Listing
8-74
// ...
print_r($user->toArray(true));
From Array
If you have an array of values you want to use to fill a record or even a collection, the
fromArray() method simplifies this common task.
Listing // test.php
8-75
// ...
$data = array(
'name' => 'John',
'age' => '25',
'Emails' => array(
array('address' => '[email protected]'),
array('address' => '[email protected]')),
'Groups' => array(1, 2, 3)
);
It is possible to use fromArray() with custom model mutators like the following:
Listing // models/User.php
8-76
Listing // test.php
8-77
// ...
$user->fromArray(array('encrypted_password' => 'changeme'));
Listing // test.php
8-78
// ...
$q = Doctrine_Query::create()
->select('u.*, g.*')
->from('User u')
->leftJoin('u.Groups g')
->where('id = ?', 1);
$user = $q->fetchOne();
// test.php Listing
8-79
// ...
$arrayUser = $user->toArray(true);
Now use the same query to retrieve the record and synchronize the record with the
$arrayUser variable:
// test.php Listing
8-80
// ...
$user = Doctrine_Query::create()
->select('u.*, g.*')
->from('User u')
->leftJoin('u.Groups g')
->where('id = ?', 1)
->fetchOne();
$user->synchronizeWithArray($arrayUser);
$user->save();
You can also synchronize relationships by specifying an array of ids to link the record to.
The above code will remove any existing groups and link the user to the group id 1, 2, and 3.
// ...
}
The only drawback is that it doesn't provide a way to pass parameters to the constructor.
Conclusion
By now we should know absolutely everything their is to know about models. We know how to
create them, load them and most importantly we know how to use them and work with
columns and relationships. Now we are ready to move on to learn about how to use the DQL
(Doctrine Query Language) (page 117).
Chapter 9
Introduction
Doctrine Query Language (DQL) is an Object Query Language created for helping users in
complex object retrieval. You should always consider using DQL (or raw SQL) when retrieving
relational data efficiently (eg. when fetching users and their phonenumbers).
In this chapter we will execute dozens of examples of how to use the Doctrine Query
Language. All of these examples assume you are using the schemas defined in the previous
chapters, primarily the Defining Models (page 51) chapter. We will define one additional
model for our testing purposes which can be found right below this note.
// models/Account.php Listing
9-1
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
9-2
# schema.yml
# ...
Account:
columns:
name: string(255)
amount: decimal
• From the start it has been designed to retrieve records(objects) not result set rows
• DQL understands relations so you don't have to type manually sql joins and join
conditions
• DQL is portable on different databases
• DQL has some very complex built-in algorithms like (the record limit algorithm)
which can help developer to efficiently retrieve objects
• It supports some functions that can save time when dealing with one-to-many, many-
to-many relational data with conditional fetching.
If the power of DQL isn't enough, you should consider using the RawSql API (page 190) for
object population.
You may already be familiar with the following syntax:
DO NOT USE THE FOLLOWING CODE. It uses many sql queries for object population.
Listing // test.php
9-3
// ...
$users = Doctrine::getTable('User')->findAll();
foreach($users as $user) {
echo $user->username . " has phonenumbers: \n";
foreach($user->Phonenumbers as $phonenumber) {
echo $phonenumber->phonenumber . "\n";
}
}
Here is the same code but implemented more efficiently using only one SQL query for
object population.
Listing // test.php
9-4
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumbers p');
echo $q->getSql();
Lets take a look at the SQL that would be generated by the above query:
Listing SELECT
9-5
u.id AS u__id,
u.is_active AS u__is_active,
u.is_super_admin AS u__is_super_admin,
u.first_name AS u__first_name,
u.last_name AS u__last_name,
u.username AS u__username,
u.password AS u__password,
u.type AS u__type,
u.created_at AS u__created_at,
u.updated_at AS u__updated_at,
p.id AS p__id,
p.user_id AS p__user_id,
p.phonenumber AS p__phonenumber
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
Now lets execute the query and play with the data:
// test.php Listing
9-6
// ...
$users = $q->execute();
foreach($users as $user) {
echo $user->username . " has phonenumbers: \n";
foreach($user->Phonenumbers as $phonenumber) {
echo $phonenumber->phonenumber . "\n";
}
}
Using double quotes (") in DQL strings is discouraged. This is sensible in MySQL standard
but in DQL it can be confused as an identifier. Instead it is recommended to use prepared
statements for your values and it will be escaped properly.
SELECT queries
SELECT statement syntax:
SELECT Listing
9-7
[ALL | DISTINCT]
<select_expr>,
...
[
FROM <components>
[
WHERE <where_condition>]
[
GROUP BY <groupby_expr>
[ASC | DESC],
... ]
[
HAVING <where_condition>]
[
ORDER BY <orderby_expr>
[ASC | DESC],
...]
[
LIMIT <row_count>
OFF
SET <offset>}]
The SELECT statement is used for the retrieval of data from one or more components.
Each select_expr indicates a column or an aggregate function value that you want to
retrieve. There must be at least one select_expr in every SELECT statement.
First insert a few sample Account records:
// test.php Listing
9-8
// ...
Now you can test the selecting of the data with these next few sample queries:
Listing // test.php
9-10
// ...
$q = Doctrine_Query::create()
->select('a.name')
->from('Account a');
echo $q->getSql();
Lets take a look at the SQL that would be generated by the above query:
Listing SELECT
9-11
a.id AS a__id,
a.name AS a__name
FROM account a
Listing // test.php
9-12
// ...
$accounts = $q->execute();
print_r($accounts->toArray());
An asterisk can be used for selecting all columns from given component. Even when using an
asterisk the executed sql queries never actually use it (Doctrine converts asterisk to
appropriate column names, hence leading to better performance on some databases).
// test.php Listing
9-14
// ...
$q = Doctrine_Query::create()
->select('a.*')
->from('Account a');
echo $q->getSql();
Compare the generated SQL from the last query example to the SQL generated by the query
right above:
SELECT Listing
9-15
a.id AS a__id,
a.name AS a__name,
a.amount AS a__amount
FROM account a
Notice how the asterisk is replace by all the real column names that exist in the Account
model.
// test.php Listing
9-16
// ...
$accounts = $q->execute();
print_r($accounts->toArray());
FROM clause components indicates the component or components from which to retrieve
records.
Listing // test.php
9-18
// ...
$q = Doctrine_Query::create()
->select('u.username, p.*')
->from('User u')
->leftJoin('u.Phonenumbers p')
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-19
u.id AS u__id,
u.username AS u__username,
p.id AS p__id,
p.user_id AS p__user_id,
p.phonenumber AS p__phonenumber
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
The WHERE clause, if given, indicates the condition or conditions that the records must satisfy
to be selected. where_condition is an expression that evaluates to true for each row to be
selected. The statement selects all rows if there is no WHERE clause.
Listing // test.php
9-20
// ...
$q = Doctrine_Query::create()
->select('a.name')
->from('Account a')
->where('a.amount > 2000');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-21
a.id AS a__id,
a.name AS a__name
FROM account a
WHERE a.amount > 2000
In the WHERE clause, you can use any of the functions and operators that DQL supports,
except for aggregate (summary) functions. The HAVING clause can be used for narrowing the
results with aggregate functions:
Listing // test.php
9-22
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->leftJoin('u.Phonenumbers p')
->having('COUNT(p.id) > 3');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-23
u.id AS u__id,
u.username AS u__username
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
HAVING COUNT(p.id) > 3
// test.php Listing
9-24
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->orderBy('u.username');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-25
u.id AS u__id,
u.username AS u__username
FROM user u
ORDER BY u.username
The LIMIT and OFFSET clauses can be used for efficiently limiting the number of records to a
given row_count
// test.php Listing
9-26
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->limit(20);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-27
u.id AS u__id,
u.username AS u__username
FROM user u
LIMIT 20
DISTINCT keyword
Aggregate values
Aggregate value SELECT syntax:
Listing // test.php
9-28
// ...
$q = Doctrine_Query::create()
->select('u.id, COUNT(t.id) AS num_threads')
->from('User u, u.Threads t')
->where('u.id = ?', 1)
->groupBy('u.id');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-29
u.id AS u__id,
COUNT(f.id) AS f__0
FROM user u
LEFT JOIN forum__thread f ON u.id = f.user_id
WHERE u.id = ?
GROUP BY u.id
Listing // test.php
9-30
// ...
$users = $q->execute();
You can easily access the num_threads data with the following code:
Listing // test.php
9-31
// ...
echo $users->num_threads . ' threads found';
UPDATE queries
UPDATE statement syntax:
// test.php Listing
9-33
// ...
$q = Doctrine_Query::create()
->update('Account')
->set('amount', 'amount + 200')
->where('id > 200');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
// test.php Listing
9-35
// ...
$rows = $q->execute();
echo $rows;
DELETE Queries
DELETE Listing
9-36
FROM <component_name>
WHERE <where_condition>
ORDER BY <order_by>
LIMIT <record_count>
• The DELETE statement deletes records from component_name and returns the
number of records deleted.
• The optional WHERE clause specifies the conditions that identify which records to
delete. Without WHERE clause, all records are deleted.
• If the ORDER BY clause is specified, the records are deleted in the order that is
specified.
• The LIMIT clause places a limit on the number of rows that can be deleted. The
statement will stop as soon as it has deleted record_count records.
// test.php Listing
9-37
// ...
$q = Doctrine_Query::create()
->delete('Account a')
->where('a.id > 3');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing DELETE
9-38
FROM account
WHERE id > 3
Listing // test.php
9-39
// ...
$rows = $q->execute();
echo $rows;
When executing DQL UPDATE and DELETE queries the executing of a query returns the
number of affected rows.
FROM clause
Syntax:
The FROM clause indicates the component or components from which to retrieve records. If
you name more than one component, you are performing a join. For each table specified, you
can optionally specify an alias.
Consider the following DQL query:
Listing // test.php
9-41
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-42
u.id AS u__id
FROM user u
Here User is the name of the class (component) and u is the alias. You should always use
short aliases, since most of the time those make the query much shorther and also because
when using for example caching the cached form of the query takes less space when short
aliases are being used.
JOIN syntax
DQL JOIN Syntax:
DQL supports two kinds of joins INNER JOINs and LEFT JOINs. For each joined component,
you can optionally specify an alias.
The default join type is LEFT JOIN. This join can be indicated by the use of either LEFT
JOIN clause or simply ',', hence the following queries are equal:
// test.php Listing
9-43
// ...
$q = Doctrine_Query::create()
->select('u.id, p.id')
->from('User u')
->leftJoin('u.Phonenumbers p');
$q = Doctrine_Query::create()
->select('u.id, p.id')
->from('User u, u.Phonenumbers p');
echo $q->getSql();
The recommended form is the first because it is more verbose and easier to read and
understand what is being done.
The above call to getSql() would output the following SQL query:
SELECT Listing
9-44
u.id AS u__id,
p.id AS p__id
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
Notice how the JOIN condition is automatically added for you. This is because Doctrine
knows how User and Phonenumber are related so it is able to add it for you.
INNER JOIN produces an intersection between two specified components (that is, each and
every record in the first component is joined to each and every record in the second
component). So basically INNER JOIN can be used when you want to efficiently fetch for
example all users which have one or more phonenumbers.
By default DQL auto-adds the primary key join condition:
// test.php Listing
9-45
// ...
$q = Doctrine_Query::create()
->select('u.id, p.id')
->from('User u')
->leftJoin('u.Phonenumbers p');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-46
u.id AS u__id,
p.id AS p__id
FROM User u
LEFT JOIN Phonenumbers p ON u.id = p.user_id
ON keyword
If you want to override this behavior and add your own custom join condition you can do it
with the ON keyword. Consider the following DQL query:
Listing // test.php
9-47
// ...
$q = Doctrine_Query::create()
->select('u.id, p.id')
->from('User u')
->leftJoin('u.Phonenumbers p ON u.id = 2');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-48
u.id AS u__id,
p.id AS p__id
FROM User u
LEFT JOIN Phonenumbers p ON u.id = 2
Notice how the ON condition that would be normally automatically added is not present and
the user specified condition is used instead.
WITH keyword
Most of the time you don't need to override the primary join condition, rather you may want
to add some custom conditions. This can be achieved with the WITH keyword.
Listing // test.php
9-49
// ...
$q = Doctrine_Query::create()
->select('u.id, p.id')
->from('User u')
->leftJoin('u.Phonenumbers p WITH u.id = 2');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-50
u.id AS u__id,
p.id AS p__id
FROM User u
LEFT JOIN Phonenumbers p ON u.id = p.user_id
AND u.id = 2
Notice how the ON condition isn't completely replaced. Instead the conditions you specify
are appended on to the automatic condition that is added for you.
The Doctrine_Query API offers two convenience methods for adding JOINS. These are called
innerJoin() and leftJoin(), which usage should be quite intuitive as shown below:
// test.php Listing
9-51
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->leftJoin('u.Groups g')
->innerJoin('u.Phonenumbers p WITH u.id > 3')
->leftJoin('u.Email e');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-52
u.id AS u__id
FROM user u
LEFT JOIN user_group u2 ON u.id = u2.user_id
LEFT JOIN groups g ON g.id = u2.group_id
INNER JOIN phonenumber p ON u.id = p.user_id
AND u.id > 3
LEFT JOIN email e ON u.id = e.user_id
INDEXBY keyword
The INDEXBY keyword offers a way of mapping certain columns as collection / array keys. By
default Doctrine indexes multiple elements to numerically indexed arrays / collections. The
mapping starts from zero. In order to override this behavior you need to use INDEXBY
keyword as shown above:
// test.php Listing
9-53
// ...
$q = Doctrine_Query::create()
->from('User u INDEXBY u.username');
$users = $q->execute();
The INDEXBY keyword does not alter the generated SQL. It is simply used internally by
Doctrine_Query to hydrate the data with the specified column as the key of each record
in the collection.
Now the users in $users collection are accessible through their names:
Listing // test.php
9-54
// ...
echo $user['jack daniels']->id;
The INDEXBY keyword can be applied to any given JOIN. This means that any given
component can have each own indexing behavior. In the following we use distinct indexing
for both Users and Groups.
Listing // test.php
9-55
// ...
$q = Doctrine_Query::create()
->from('User u INDEXBY u.username')
->innerJoin('u.Groups g INDEXBY g.name');
$users = $q->execute();
Listing // test.php
9-56
// ...
echo $users['jack daniels']->Groups['drinkers club']->createdAt;
WHERE clause
Syntax:
• The WHERE clause, if given, indicates the condition or conditions that the records
must satisfy to be selected.
• where_condition is an expression that evaluates to true for each row to be
selected.
• The statement selects all rows if there is no WHERE clause.
• When narrowing results with aggregate function values HAVING clause should be
used instead of WHERE clause
Listing // test.php
9-58
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.type = ?', 'registered')
->andWhere('u.is_active = ?', 1)
->orWhere('u.is_super_admin = ?', 1);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-59
u.id AS u__id
FROM user u
WHERE u.type = ?
AND u.is_active = ?
OR u.is_super_admin = ?
Conditional expressions
Literals
Strings
A string literal that includes a single quote is represented by two single quotes; for example:
´´literal´s´´.
// test.php Listing
9-60
// ...
$q = Doctrine_Query::create()
->select('u.id, u.username')
->from('User u')
->where('u.username = ?', 'Vincent');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-61
u.id AS u__id,
u.username AS u__username
FROM user u
WHERE u.username = ?
Because we passed the value of the username as a parameter to the where() method it is
not included in the generated SQL. PDO handles the replacement when you execute the
query. To check the parameters that exist on a Doctrine_Query instance you can use the
getParams() method.
Integers
Integer literals support the use of PHP integer literal syntax.
// test.php Listing
9-62
// ...
$q = Doctrine_Query::create()
->select('a.id')
->from('User u')
->where('u.id = 4');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-63
u.id AS u__id
FROM user u
WHERE u.id = 4
Floats
Float literals support the use of PHP float literal syntax.
Listing // test.php
9-64
// ...
$q = Doctrine_Query::create()
->select('a.id')
->from('Account a')
->where('a.amount = 432.123');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-65
a.id AS a__id
FROM account a
WHERE a.amount = 432.123
Booleans
The boolean literals are true and false.
Listing // test.php
9-66
// ...
$q = Doctrine_Query::create()
->select('a.id')
->from('User u')
->where('u.is_super_admin = true');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-67
u.id AS u__id
FROM user u
WHERE u.is_super_admin = 1
Enums
The enumerated values work in the same way as string literals.
// test.php Listing
9-68
// ...
$q = Doctrine_Query::create()
->select('a.id')
->from('User u')
->where("u.type = 'admin'");
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-69
u.id AS u__id
FROM user u
WHERE u.type = 'admin'
Predefined reserved literals are case insensitive, although its a good standard to write them
in uppercase.
Input parameters
Here are some examples of using positional parameters:
Single positional parameter:
// test.php Listing
9-70
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.username = ?', array('Arnold'));
echo $q->getSql();
When the passed parameter for a positional parameter contains only one value you can
simply pass a single scalar value instead of an array containing one value.
The above call to getSql() would output the following SQL query:
SELECT Listing
9-71
u.id AS u__id
FROM user u
WHERE u.username = ?
// test.php Listing
9-72
// ...
$q = Doctrine_Query::create()
->from('User u')
->where('u.id > ? AND u.username LIKE ?', array(50, 'A%'));
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-73
u.id AS u__id
FROM user u
WHERE (u.id > ?
AND u.username LIKE ?)
Listing // test.php
9-74
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.username = :name', array(':name' => 'Arnold'));
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-75
u.id AS u__id
FROM user u
WHERE u.username = :name
Listing // test.php
9-76
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.id > :id', array(':id' => 50))
->andWhere('u.username LIKE :name', array(':name' => 'A%'));
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-77
u.id AS u__id
FROM user u
WHERE u.id > :id
AND u.username LIKE :name
Operator Description
. Navigation operator
Arithmetic operators:
+, - unary
*, / multiplication and division
+, - addition and subtraction
=, >, >=, <, <=, <> (not equal), Comparison operators
[NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY
Logical operators:
NOT
AND
OR
In expressions
Syntax:
An IN conditional expression returns true if the operand is found from result of the subquery
or if its in the specificied comma separated value list, hence the IN expression is always false
if the result of the subquery is empty.
When value list is being used there must be at least one element in that list.
Here is an example where we use a subquery for the IN:
// test.php Listing
9-79
// ...
$q = Doctrine_Query::create()
->from('User u')
->where('u.id IN (SELECT u.id FROM User u INNER JOIN u.Groups g WHERE g.id =
?)', 1);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-80
u.id AS u__id
FROM user u
WHERE u.id IN (SELECT
u2.id AS u2__id
FROM user u2
INNER JOIN user_group u3 ON u2.id = u3.user_id
INNER JOIN groups g ON g.id = u3.group_id
WHERE g.id = ?)
// test.php Listing
9-81
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->whereIn('u.id', array(1, 3, 4, 5));
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-82
u.id AS u__id
FROM user u
WHERE u.id IN (?,
?,
?,
?)
You can also optionally use the following DQL syntax when working with IN conditions.
Listing // test.php
9-83
// ...
$q = Doctrine_Query::create()
->from('User u')
->andWhere('u.id IN ?', array(array(1, 2, 3, 4, 5)));
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-84
u.id AS u__id,
u.username AS u__username,
u.password AS u__password
FROM user u
WHERE u.id IN (?,
?,
?,
?,
?)j
Notice how the placeholders were added automatically for you. You will see that their is
one ? for every parameter you passed.
Like Expressions
Syntax:
The string_expression must have a string value. The pattern_value is a string literal or a
string-valued input parameter in which an underscore (_) stands for any single character, a
percent (%) character stands for any sequence of characters (including the empty sequence),
and all other characters stand for themselves. The optional escape_character is a single-
character string literal or a character-valued input parameter (i.e., char or Character) and is
used to escape the special meaning of the underscore and percent characters in
pattern_value.
Examples:
• address.phone LIKE '12%3' is true for '123' '12993' and false for '1234'
• asentence.word LIKE 'l_se' is true for 'lose' and false for 'loose'
• aword.underscored LIKE '\_%' ESCAPE '\' is true for '_foo' and false for 'bar'
• address.phone NOT LIKE '12%3' is false for '123' and '12993' and true for '1234'
If the value of the string_expression or pattern_value is NULL or unknown, the value of the
LIKE expression is unknown. If the escape_characteris specified and is NULL, the value of the
LIKE expression is unknown.
Find all users whose email ends with '@gmail.com':
// test.php Listing
9-86
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->leftJoin('u.Email e')
->where('e.address LIKE ?', '%@gmail.com');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-87
u.id AS u__id
FROM user u
LEFT JOIN email e ON u.id = e.user_id
WHERE e.address LIKE ?
// test.php Listing
9-88
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.username LIKE ?', 'A%');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-89
u.id AS u__id
FROM user u
WHERE u.username LIKE ?
Exists Expressions
Syntax:
The EXISTS operator returns TRUE if the subquery returns one or more rows and FALSE
otherwise.
The NOT EXISTS operator returns TRUE if the subquery returns 0 rows and FALSE
otherwise.
For the next few examples we need to add the ReaderLog model.
Listing // models/ReaderLog.php
9-91
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
9-92
# schema.yml
# ...
ReaderLog:
columns:
article_id:
type: integer
primary: true
user_id:
type: integer
primary: true
After adding the ReaderLog model don't forget to run the generate.php script!
Now we can run some tests! First, finding all articles which have readers:
Listing // test.php
9-94
// ...
$q = Doctrine_Query::create()
->select('a.id')
->from('Article a')
->where('EXISTS (SELECT r.id FROM ReaderLog r WHERE r.article_id = a.id)');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-95
a.id AS a__id
FROM article a
WHERE EXISTS (SELECT
r.id AS r__id
FROM reader_log r
WHERE r.article_id = a.id)
// test.php Listing
9-96
// ...
$q = Doctrine_Query::create()
->select('a.id')
->from('Article a')
->where(NOT EXISTS (SELECT r.id FROM ReaderLog r WHERE r.article_id = a.id));
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-97
a.id AS a__id
FROM article a
WHERE NOT EXISTS (SELECT
r.id AS r__id
FROM reader_log r
WHERE r.article_id = a.id)
An ALL conditional expression returns true if the comparison operation is true for all values
in the result of the subquery or the result of the subquery is empty. An ALL conditional
expression is false if the result of the comparison is false for at least one row, and is unknown
if neither true nor false.
$q = Doctrine_Query::create() Listing
9-99
->from('C')
->where('C.col1 < ALL (FROM C2(col1))');
An ANY conditional expression returns true if the comparison operation is true for some value
in the result of the subquery. An ANY conditional expression is false if the result of the
subquery is empty or if the comparison operation is false for every value in the result of the
subquery, and is unknown if neither true nor false.
Listing $q = Doctrine_Query::create()
9-100
->from('C')
->where('C.col1 > ANY (FROM C2(col1))');
Listing $q = Doctrine_Query::create()
9-101
->from('C')
->where('C.col1 > SOME (FROM C2(col1))');
The comparison operators that can be used with ALL or ANY conditional expressions are =,
<, <=, >, >=, <>. The result of the subquery must be same type with the conditional
expression.
NOT IN is an alias for <> ALL. Thus, these two statements are equal:
Listing FROM C
9-102
WHERE C.col1 <> ALL (
FROM C2(col1));
FROM C
WHERE C.col1 NOT IN (
FROM C2(col1));
Listing $q = Doctrine_Query::create()
9-103
->from('C')
->where('C.col1 <> ALL (FROM C2(col1))');
$q = Doctrine_Query::create()
->from('C')
->where('C.col1 NOT IN (FROM C2(col1))');
Subqueries
A subquery can contain any of the keywords or clauses that an ordinary SELECT query can
contain.
Some advantages of the subqueries:
• They allow queries that are structured so that it is possible to isolate each part of a
statement.
• They provide alternative ways to perform operations that would otherwise require
complex joins and unions.
• They are, in many people's opinion, readable. Indeed, it was the innovation of
subqueries that gave people the original idea of calling the early SQL "Structured
Query Language."
Here is an example where we find all users which don't belong to the group id 1:
Listing // test.php
9-104
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.id NOT IN (SELECT u2.id FROM User u2 INNER JOIN u2.Groups g WHERE
g.id = ?)', 1);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-105
u.id AS u__id
FROM user u
WHERE u.id NOT IN (SELECT
u2.id AS u2__id
FROM user u2
INNER JOIN user_group u3 ON u2.id = u3.user_id
INNER JOIN groups g ON g.id = u3.group_id
WHERE g.id = ?)
Here is an example where we find all users which don't belong to any groups
// test.php Listing
9-106
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.id NOT IN (SELECT u2.id FROM User u2 INNER JOIN u2.Groups g)');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-107
u.id AS u__id
FROM user u
WHERE u.id NOT IN (SELECT
u2.id AS u2__id
FROM user u2
INNER JOIN user_group u3 ON u2.id = u3.user_id
INNER JOIN groups g ON g.id = u3.group_id)
Functional Expressions
String functions
The CONCAT function returns a string that is a concatenation of its arguments. In the
example above we map the concatenation of users first_name and last_name to a value called
name
// test.php Listing
9-108
// ...
$q = Doctrine_Query::create()
->select('CONCAT(u.first_name, u.last_name) AS name')
->from('User u');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-109
u.id AS u__id,
CONCAT(u.first_name,
u.last_name) AS u__0
FROM user u
Now we can execute the query and get the mapped function value:
foreach($users as $user) {
// here 'name' is not a property of $user,
// its a mapped function value
echo $user->name;
}
The second and third arguments of the SUBSTRING function denote the starting position and
length of the substring to be returned. These arguments are integers. The first position of a
string is denoted by 1. The SUBSTRING function returns a string.
Listing // test.php
9-111
// ...
$q = Doctrine_Query::create();
->select('u.username')
->from('User u')
->where("SUBSTRING(u.username, 0, 1) = 'z'");
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-112
u.id AS u__id,
u.username AS u__username
FROM user u
WHERE SUBSTRING(u.username
FROM 0 FOR 1) = 'z'
Notice how the SQL is generated with the proper SUBSTRING syntax for the DBMS you are
using!
The TRIM function trims the specified character from a string. If the character to be trimmed
is not specified, it is assumed to be space (or blank). The optional trim_character is a single-
character string literal or a character-valued input parameter (i.e., char or Character)[30]. If
a trim specification is not provided, BOTH is assumed. The TRIM function returns the
trimmed string.
Listing // test.php
9-113
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->where('TRIM(u.username) = ?', 'Someone');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-114
u.id AS u__id,
u.username AS u__username
FROM user u
WHERE TRIM(u.username) = ?
The LOWER and UPPER functions convert a string to lower and upper case, respectively.
They return a string.
// test.php Listing
9-115
// ...
$q = Doctrine_Query::create();
->select('u.username')
->from('User u')
->where("LOWER(u.username) = 'jon wage'");
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-116
u.id AS u__id,
u.username AS u__username
FROM user u
WHERE LOWER(u.username) = 'someone'
The LOCATE function returns the position of a given string within a string, starting the
search at a specified position. It returns the first position at which the string was found as an
integer. The first argument is the string to be located; the second argument is the string to be
searched; the optional third argument is an integer that represents the string position at
which the search is started (by default, the beginning of the string to be searched). The first
position in a string is denoted by 1. If the string is not found, 0 is returned.
The LENGTH function returns the length of the string in characters as an integer.
Arithmetic functions
Availible DQL arithmetic functions:
ABS(simple_arithmetic_expression) Listing
9-117
SQRT(simple_arithmetic_expression)
MOD(simple_arithmetic_expression, simple_arithmetic_expression)
• The ABS function returns the absolute value for given number.
• The SQRT function returns the square root for given number.
• The MOD function returns the modulus of first argument using the second
argument.
Subqueries
Introduction
Doctrine allows you to use sub-dql queries in the FROM, SELECT and WHERE statements.
Below you will find examples for all the different types of subqueries Doctrine supports.
Listing // test.php
9-118
// ...
$q = Doctrine_Query::create()
->select('u.id')
->from('User u')
->where('u.id NOT IN (SELECT u.id FROM User u INNER JOIN u.Groups g WHERE g.id
= ?)', 1);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-119
u.id AS u__id
FROM user u
WHERE u.id NOT IN (SELECT
u2.id AS u2__id
FROM user u2
INNER JOIN user_group u3 ON u2.id = u3.user_id
INNER JOIN groups g ON g.id = u3.group_id
WHERE g.id = ?)
Retrieve the users phonenumber in a subquery and include it in the resultset of user
information.
Listing // test.php
9-120
// ...
$q = Doctrine_Query::create()
->select('u.id')
->addSelect('(SELECT p.phonenumber FROM Phonenumber p WHERE p.user_id = u.id
LIMIT 1) as phonenumber')
->from('User u');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-121
u.id AS u__id,
(SELECT
p.phonenumber AS p__phonenumber
FROM phonenumber p
WHERE p.user_id = u.id
LIMIT 1) AS u__0
FROM user u
GROUP BY and HAVING clauses can be used for dealing with aggregate functions. The
Following aggregate functions are available on DQL: COUNT, MAX, MIN, AVG, SUM
Selecting alphabetically first user by name.
// test.php Listing
9-124
// ...
$q = Doctrine_Query::create()
->select('MIN(a.amount)')
->from('Account a');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-125
MIN(a.amount) AS a__0
FROM account a
// test.php Listing
9-126
// ...
$q = Doctrine_Query::create()
->select('SUM(a.amount)')
->from('Account a');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-127
SUM(a.amount) AS a__0
FROM account a
Listing
9-128
// test.php
// ...
$q = Doctrine_Query::create()
->select('u.username')
->addSelect('COUNT(p.id) as num_phonenumbers')
->from('User u')
->leftJoin('u.Phonenumbers p')
->groupBy('u.id');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-129
u.id AS u__id,
u.username AS u__username,
COUNT(p.id) AS p__0
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
GROUP BY u.id
The HAVING clause can be used for narrowing the results using aggregate values. In the
following example we fetch all users which have atleast 2 phonenumbers
Listing // test.php
9-130
// ...
$q = Doctrine_Query::create()
->select('u.username')
->addSelect('COUNT(p.id) as num_phonenumbers')
->from('User u')
->leftJoin('u.Phonenumbers p')
->groupBy('u.id')
->having('num_phonenumbers >= 2');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-131
u.id AS u__id,
u.username AS u__username,
COUNT(p.id) AS p__0
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
GROUP BY u.id
HAVING p__0 >= 2
You can access the number of phonenumbers with the following code:
Listing // test.php
9-132
// ...
$users = $q->execute();
foreach($users as $user) {
ORDER BY clause
Introduction
Record collections can be sorted efficiently at the database level using the ORDER BY clause.
Syntax:
, ...]
Examples:
// test.php Listing
9-133
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->leftJoin('u.Phonenumbers p')
->orderBy('u.username, p.phonenumber');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-134
u.id AS u__id,
u.username AS u__username
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
ORDER BY u.username,
p.phonenumber
In order to sort in reverse order you can add the DESC (descending) keyword to the name of
the column in the ORDER BY clause that you are sorting by. The default is ascending order;
this can be specified explicitly using the ASC keyword.
// test.php Listing
9-135
// ...
$q = Doctrine_Query::create()
->select('u.username')
->from('User u')
->leftJoin('u.Email e')
->orderBy('e.address DESC, u.id ASC');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-136
u.id AS u__id,
u.username AS u__username
FROM user u
LEFT JOIN email e ON u.id = e.user_id
ORDER BY e.address DESC,
u.id ASC
Listing // test.php
9-137
// ...
$q = Doctrine_Query::create()
->select('u.username, COUNT(p.id) count')
->from('User u')
->innerJoin('u.Phonenumbers p')
->orderby('count');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-138
u.id AS u__id,
u.username AS u__username,
COUNT(p.id) AS p__0
FROM user u
INNER JOIN phonenumber p ON u.id = p.user_id
ORDER BY p__0
Listing // test.php
9-139
// ...
$q = Doctrine_Query::create()
->select('t.id, RANDOM() AS rand')
->from('Forum_Thread t')
->orderby('rand')
->limit(1);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-140
f.id AS f__id,
RAND() AS f__0
FROM forum__thread f
ORDER BY f__0
LIMIT 1
// test.php Listing
9-141
// ...
$q = Doctrine_Query::create()
->select('u.username, p.phonenumber')
->from('User u')
->leftJoin('u.Phonenumbers p')
->limit(20);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-142
u.id AS u__id,
u.username AS u__username,
p.id AS p__id,
p.phonenumber AS p__phonenumber
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
LIMIT 20
Driver Portability
DQL LIMIT clause is portable on all supported databases. Special attention have been paid to
following facts:
• Only Mysql, Pgsql and Sqlite implement LIMIT / OFFSET clauses natively
• In Oracle / Mssql / Firebird LIMIT / OFFSET clauses need to be emulated in driver
specific way
• The limit-subquery-algorithm needs to execute to subquery separately in mysql,
since mysql doesn't yet support LIMIT clause in subqueries
• Pgsql needs the order by fields to be preserved in SELECT clause, hence limit-
subquery-algorithm needs to take this into consideration when pgsql driver is used
• Oracle only allows < 30 object identifiers (= table/column names/aliases), hence the
limit subquery must use as short aliases as possible and it must avoid alias collisions
with the main query.
The limit-subquery-algorithm
The limit-subquery-algorithm is an algorithm that DQL parser uses internally when one-to-
many / many-to-many relational data is being fetched simultaneously. This kind of special
algorithm is needed for the LIMIT clause to limit the number of records instead of sql result
set rows.
This behavior can be overwritten using the configuration system (at global, connection or
table level) using:
In the following example we have users and phonenumbers with their relation being one-to-
many. Now lets say we want fetch the first 20 users and all their related phonenumbers.
Now one might consider that adding a simple driver specific LIMIT 20 at the end of query
would return the correct results. Thats wrong, since we you might get anything between 1-20
users as the first user might have 20 phonenumbers and then record set would consist of 20
rows.
DQL overcomes this problem with subqueries and with complex but efficient subquery
analysis. In the next example we are going to fetch first 20 users and all their phonenumbers
with single efficient query. Notice how the DQL parser is smart enough to use column
aggregation inheritance even in the subquery and how it's smart enough to use different
aliases for the tables in the subquery to avoid alias collisions.
Listing // test.php
9-144
// ...
$q = Doctrine_Query::create()
->select('u.id, u.username, p.*')
->from('User u')
->leftJoin('u.Phonenumbers p')
->limit(20);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-145
u.id AS u__id,
u.username AS u__username,
p.id AS p__id,
p.phonenumber AS p__phonenumber,
p.user_id AS p__user_id
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
WHERE u.id IN (SELECT
DISTINCT u2.id
FROM user u2
LIMIT 20)
In the next example we are going to fetch first 20 users and all their phonenumbers and only
those users that actually have phonenumbers with single efficient query, hence we use an
INNER JOIN. Notice how the DQL parser is smart enough to use the INNER JOIN in the
subquery:
Listing // test.php
9-146
// ...
$q = Doctrine_Query::create()
->select('u.id, u.username, p.*')
->from('User u')
->innerJoin('u.Phonenumbers p')
->limit(20);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
9-147
u.id AS u__id,
u.username AS u__username,
p.id AS p__id,
p.phonenumber AS p__phonenumber,
p.user_id AS p__user_id
FROM user u
INNER JOIN phonenumber p ON u.id = p.user_id
WHERE u.id IN (SELECT
DISTINCT u2.id
FROM user u2
INNER JOIN phonenumber p2 ON u2.id = p2.user_id
LIMIT 20)
Named Queries
When you are dealing with a model that may change, but you need to keep your queries easily
updated, you need to find an easy way to define queries. Imagine for example that you change
one field and you need to follow all queries in your application to make sure it'll not break
anything.
Named Queries is a nice and effective way to solve this situation, allowing you to create
Doctrine_Queries and reuse them without the need to keep rewritting them.
The Named Query support is built at the top of Doctrine_Query_Registry support.
Doctrine_Query_Registry is a class for registering and naming queries. It helps with the
organization of your applications queries and along with that it offers some very nice
convenience stuff.
The queries are added using the add() method of the registry object. It takes two parameters,
the query name and the actual DQL query.
// test.php Listing
9-148
// ...
$r = Doctrine_Manager::getInstance()->getQueryRegistry();
$userTable = Doctrine::getTable('User');
To access the Named Query (will return you a Doctrine_Query instance, always):
Listing $q = $userTable->createNamedQuery('get.by.id');
9-151
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-152
u.id AS u__id,
u.username AS u__username
FROM user u
WHERE u.id = ?
Listing // test.php
9-153
// ...
$users = Doctrine::getTable('User')
->createNamedQuery('get.by.similar.usernames')
->execute(array('%jon%wage%'));
Listing // test.php
9-154
// ...
$users = Doctrine::getTable('User')
->find('get.by.similar.usernames', array('%jon%wage%'));
The method find() also accepts a third parameter, which is the hydration mode.
// test.php Listing
9-155
// ...
$articleTable = Doctrine::getTable('Article');
BNF
QL_statement ::= select_statement | update_statement | delete_statement Listing
9-156
select_statement ::= select_clause from_clause [where_clause]
[groupby_clause]
[having_clause] [orderby_clause]
update_statement ::= update_clause [where_clause]
delete_statement ::= delete_clause [where_clause]
from_clause ::=
FROM identification_variable_declaration
{, {identification_variable_declaration | collection_member_declaration}}*
identification_variable_declaration ::= range_variable_declaration { join
| fetch_join }*
range_variable_declaration ::= abstract_schema_name [AS ]
identification_variable
join ::= join_spec join_association_path_expression [AS ]
identification_variable
fetch_join ::= join_specFETCH join_association_path_expression
association_path_expression ::=
collection_valued_path_expression |
single_valued_association_path_expression
join_spec::= [LEFT [OUTER ] |INNER ]JOIN
join_association_path_expression ::=
join_collection_valued_path_expression |
join_single_valued_association_path_expression
join_collection_valued_path_expression::=
identification_variable.collection_valued_association_field
join_single_valued_association_path_expression::=
identification_variable.single_valued_association_field
collection_member_declaration ::=
IN ( collection_valued_path_expression) [AS ] identification_variable
single_valued_path_expression ::=
state_field_path_expression | single_valued_association_path_expression
state_field_path_expression ::=
{identification_variable |
single_valued_association_path_expression}.state_field
single_valued_association_path_expression ::=
identification_variable.{single_valued_association_field.}*
single_valued_association_field
collection_valued_path_expression ::=
identification_variable.{single_valued_association_field.}*collection_valued_association
state_field ::= {embedded_class_state_field.}*simple_state_field
update_clause ::=UPDATE abstract_schema_name [[AS ]
identification_variable]
SET update_item {, update_item}*
update_item ::= [identification_variable.]{state_field |
single_valued_association_field} =
new_value
new_value ::=
simple_arithmetic_expression |
string_primary |
datetime_primary |
boolean_primary |
enum_primary
simple_entity_expression |
NULL
delete_clause ::=DELETE FROM abstract_schema_name [[AS ]
identification_variable]
select_clause ::=SELECT [DISTINCT ] select_expression {,
select_expression}*
select_expression ::=
single_valued_path_expression |
aggregate_expression |
identification_variable |
OBJECT( identification_variable) |
constructor_expression
constructor_expression ::=
NEW constructor_name( constructor_item {, constructor_item}*)
constructor_item ::= single_valued_path_expression | aggregate_expression
aggregate_expression ::=
{AVG |MAX |MIN |SUM }( [DISTINCT ] state_field_path_expression) |
COUNT ( [DISTINCT ] identification_variable | state_field_path_expression |
single_valued_association_path_expression)
where_clause ::=WHERE conditional_expression
groupby_clause ::=GROUP BY groupby_item {, groupby_item}*
groupby_item ::= single_valued_path_expression | identification_variable
having_clause ::=HAVING conditional_expression
orderby_clause ::=ORDER BY orderby_item {, orderby_item}*
orderby_item ::= state_field_path_expression [ASC |DESC ]
subquery ::= simple_select_clause subquery_from_clause [where_clause]
[groupby_clause] [having_clause]
subquery_from_clause ::=
FROM subselect_identification_variable_declaration
{, subselect_identification_variable_declaration}*
subselect_identification_variable_declaration ::=
identification_variable_declaration |
association_path_expression [AS ] identification_variable |
collection_member_declaration
simple_select_clause ::=SELECT [DISTINCT ] simple_select_expression
simple_select_expression::=
single_valued_path_expression |
aggregate_expression |
identification_variable
conditional_expression ::= conditional_term | conditional_expressionOR
conditional_term
conditional_term ::= conditional_factor | conditional_termAND
conditional_factor
conditional_factor ::= [NOT ] conditional_primary
conditional_primary ::= simple_cond_expression |( conditional_expression)
simple_cond_expression ::=
comparison_expression |
between_expression |
like_expression |
in_expression |
null_comparison_expression |
empty_collection_comparison_expression |
collection_member_expression |
exists_expression
between_expression ::=
arithmetic_expression [NOT ]BETWEEN
arithmetic_expressionAND arithmetic_expression |
string_expression [NOT ]BETWEEN string_expressionAND string_expression |
datetime_expression [NOT ]BETWEEN
datetime_expressionAND datetime_expression
in_expression ::=
state_field_path_expression [NOT ]IN ( in_item {, in_item}* | subquery)
in_item ::= literal | input_parameter
like_expression ::=
string_expression [NOT ]LIKE pattern_value [ESCAPE escape_character]
null_comparison_expression ::=
{single_valued_path_expression | input_parameter}IS [NOT ] NULL
empty_collection_comparison_expression ::=
collection_valued_path_expressionIS [NOT] EMPTY
collection_member_expression ::= entity_expression
[NOT ]MEMBER [OF ] collection_valued_path_expression
exists_expression::= [NOT ]EXISTS (subquery)
all_or_any_expression ::= {ALL |ANY |SOME } (subquery)
comparison_expression ::=
string_expression comparison_operator {string_expression |
all_or_any_expression} |
boolean_expression {= |<> } {boolean_expression | all_or_any_expression} |
enum_expression {= |<> } {enum_expression | all_or_any_expression} |
datetime_expression comparison_operator
{datetime_expression | all_or_any_expression} |
entity_expression {= |<> } {entity_expression | all_or_any_expression} |
arithmetic_expression comparison_operator
{arithmetic_expression | all_or_any_expression}
comparison_operator ::== |> |>= |< |<= |<>
arithmetic_expression ::= simple_arithmetic_expression | (subquery)
simple_arithmetic_expression ::=
arithmetic_term | simple_arithmetic_expression {+ |- } arithmetic_term
arithmetic_term ::= arithmetic_factor | arithmetic_term {* |/ }
arithmetic_factor
arithmetic_factor ::= [{+ |- }] arithmetic_primary
arithmetic_primary ::=
state_field_path_expression |
numeric_literal |
(simple_arithmetic_expression) |
input_parameter |
functions_returning_numerics |
aggregate_expression
Magic Finders
Doctrine offers some magic finders for your Doctrine models that allow you to find a record
by any column that is present in the model. This is helpful for simply finding a user by their
username, or finding a group by the name of it. Normally this would require writing a
Doctrine_Query instance and storing this somewhere so it can be reused. That is no longer
needed for simple situations like that.
The basic pattern for the finder methods are as follows: findBy%s($value) or
findOneBy%s($value). The %s can be a column name or a relation alias. If you give a column
name you must give the value you are looking for. If you specify a relationship alias, you can
either pass an instance of the relation class to find, or give the actual primary key value.
First lets retrieve the UserTable instance to work with:
// test.php Listing
9-157
// ...
$userTable = Doctrine::getTable('User');
Now we can easily find a User record by its primary key by using the find() method:
// test.php Listing
9-158
// ...
$user = $userTable->find(1);
Now if you want to find a single user by their username you can use the following magic
finder:
// test.php Listing
9-159
// ...
$user = $userTable->findOneByUsername('jonwage');
You can also easily find records by using the relationships between records. User has many
Phonenumbers we can find those Phonenumbers by passing the findBy**() method a
User instance:
// test.php Listing
9-160
// ...
$phonenumberTable = Doctrine::getTable('Phonenumber');
$phonenumbers = $phonenumberTable->findByUser($user);
The documented magic finders above are made possibly by using PHP's __call()22
overloading functionality. The undefined functions are forwarded to
Doctrine_Table::__call() where the Doctrine_Query objects are built, executed
and returned to the user.
Debugging Queries
The Doctrine_Query object has a few functions that can be used to help debug problems
with the query:
Sometimes you may want to see the complete DQL string of your Doctrine_Query object:
// test.php Listing
9-161
// ...
$q = Doctrine_Query::create()
22. http://us3.php.net/__call
->select('u.id')
->from('User u')
->orderBy('u.username');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
9-162
u.id AS u__id
FROM user u
ORDER BY u.username
The SQL returned above by the Doctrine_Query::getSql() function does not replace
the tokens with the parameters. This is the job of PDO and when we execute the query we
pass the parameters to PDO where the replacement is executed. You can retrieve the array
of parameters with the Doctrine_Query::getParams() method.
Listing // test.php
9-163
// ...
print_r($q->getParams());
The above call to getParams() returns the unmodified array of parameters that are used
to build the final params to be passed to the query. If you wish to get the flattened array of
params passed to PDO then use the getFlattenedParams() method.
Listing // test.php
9-164
// ...
print_r($q->getFlattenedParams());
Conclusion
The Doctrine Query Language is by far one of the most advanced and helpful feature of
Doctrine. It allows you to easily select very complex data from RDBMS relationships
efficiently!
Now that we have gone over most of the major components of Doctrine and how to use them
we are going to take a step back in the next chapter and look at everything from a birds eye
view in the Component Overview (page 159) chapter.
Chapter 10
Component Overview
This chapter is intended to give you a birds eye view of all the main components that make up
Doctrine and how they work together. We've discussed most of the components in the
previous chapters but after this chapter you will have a better idea of all the components and
what their jobs are.
Manager
The Doctrine_Manager class is a singleton and is the root of the configuration hierarchy
and is used as a facade for controlling several aspects of Doctrine. You can retrieve the
singleton instance with the following code.
// test.php Listing
10-1
// ...
$manager = Doctrine_Manager::getInstance();
Retrieving Connections
// test.php Listing
10-2
// ...
$connections = $manager->getConnections();
foreach ($connections as $connection) {
echo $connection->getName() . "\n";
}
The Doctrine_Manager implements an iterator so you can simple loop over the $manager
variable to loop over the connections.
// test.php Listing
10-3
// ...
foreach ($manager as $connection) {
echo $connection->getName() . "\n";
}
Connection
Doctrine_Connection is a wrapper for database connection. The connection is typically an
instance of PDO but because of how Doctrine is designed, it is possible to design your own
adapters that mimic the functionality that PDO provides.
The Doctrine_Connection class handles several things:
• Handles database portability things missing from PDO (eg. LIMIT / OFFSET
emulation)
• Keeps track of Doctrine_Table objects
• Keeps track of records
• Keeps track of records that need to be updated / inserted / deleted
• Handles transactions and transaction nesting
• Handles the actual querying of the database in the case of INSERT / UPDATE /
DELETE operations
• Can query the database using DQL. You will learn more about DQL in the DQL
(Doctrine Query Language) (page 117) chapter.
• Optionally validates transactions using Doctrine_Validator and gives full information
of possible errors.
Available Drivers
Doctrine has drivers for every PDO-supported database. The supported databases are:
Creating Connections
Listing // bootstrap.php
10-4
// ...
$conn = Doctrine_Manager::connection('mysql://username:password@localhost/test',
'connection 1');
We have already created a new connection in the previous chapters. You can skip the
above step and use the connection we've already created. You can retrieve it by using the
Doctrine_Manager::connection() method.
Listing // test.php
10-5
// ...
$conn = Doctrine_Manager::connection();
$conn->flush();
Calling Doctrine_Connection::flush() will save all unsaved record instances for that
connection. You could of course optionally call save() on each record instance and it would
be the same thing.
// test.php Listing
10-6
// ...
$user1->save();
$user2->save();
Table
Doctrine_Table holds the schema information specified by the given component (record).
For example if you have a User class that extends Doctrine_Record, each schema
definition call gets delegated to a unique table object that holds the information for later use.
Each Doctrine_Table is registered by Doctrine_Connection. You can retrieve the table
object for each component easily which is demonstrated right below.
For example, lets say we want to retrieve the table object for the User class. We can do this
by simply giving User as the first argument for the Doctrine::getTable() method.
// test.php Listing
10-7
// ...
$accountTable = Doctrine::getTable('Account');
// test.php Listing
10-8
// ...
$columns = $accountTable->getColumns();
$columns = $accountTable->getColumns();
foreach ($columns as $column)
{
print_r($column);
}
Sometimes this can be an overkill. The following example shows how to retrieve the column
names as an array:
Listing // test.php
10-10
// ...
$names = $accountTable->getColumnNames();
print_r($names);
Listing // test.php
10-12
// ...
$userTable = Doctrine::getTable('User');
$relations = $userTable->getRelations();
Phonenumbers:
Local - id
Foreign - user_id
Groups:
Local - user_id
Foreign - group_id
Friends:
Local - user1
Foreign - user2
Addresses:
Local - id
Foreign - user_id
Threads:
Local - id
Foreign - user_id
You can get the Doctrine_Relation object for an individual relationship by using the
Doctrine_Table::getRelation() method.
// test.php Listing
10-14
// ...
$relation = $userTable->getRelation('Phonenumbers');
Notice how in the above examples the $relation variable holds an instance of
Doctrine_Relation_ForeignKey yet we can access it like an array. This is because,
like many Doctrine classes, it implements ArrayAccess.
You can debug all the information of a relationship by using the toArray() method and
using print_r() to inspect it.
// test.php Listing
10-16
// ...
$array = $relation->toArray();
print_r($array);
Finder Methods
Doctrine_Table provides basic finder methods. These finder methods are very fast to write
and should be used if you only need to fetch data from one database table. If you need queries
that use several components (database tables) use Doctrine_Connection::query().
You can easily find an individual user by its primary key by using the find() method:
Listing // test.php
10-17
// ...
$user = $userTable->find(2);
print_r($user->toArray());
You can also use the findAll() method to retrieve a collection of all User records in the
database:
Listing // test.php
10-19
// ...
foreach ($userTable->findAll() as $user) {
echo $user->username . "\n";
}
The findAll() method is not recommended as it will return all records in the database
and if you need to retrieve information from relationships it will lazily load that data
causing high query counts. You can learn how to retrieve records and their related records
efficiently by reading the DQL (Doctrine Query Language) (page 117) chapter.
You can also retrieve a set of records with a DQL where condition by using the findByDql()
method:
// test.php Listing
10-21
// ...
$users = $userTable->findByDql('username LIKE ?', '%jw%');
foreach($users as $user) {
echo $user->username . "\n";
}
Doctrine also offers some additional magic finder methods that can be read about in the
Magic Finders (page 156) section of the DQL chapter.
// models/UserTable.php Listing
10-23
Custom Finders
You can add custom finder methods to your custom table object. These finder methods may
use fast Doctrine_Table finder methods or DQL API (page 117)
(Doctrine_Query::create()).
// models/UserTable.php Listing
10-24
Doctrine will check if a child Doctrine_Table class called UserTable exists when calling
getTable() and if it does, it will return an instance of that instead of the default
Doctrine_Table.
In order for custom Doctrine_Table classes to be loaded you must enable the
autoload_table_classes attribute in your bootstrap.php file like done below.
Listing // boostrap.php
10-25
// ...
$manager->setAttribute(Doctrine::ATTR_AUTOLOAD_TABLE_CLASSES, true);
Now when we ask for the User table object we will get the following:
$users = $userTable->findByName("Jack");
The above example where we add a findByName() method is made possible automatically
by the magic finder methods. You can read about them in the Magic Finders (page 156)
section of the DQL chapter.
Record
Doctrine represents tables in your RDBMS with child Doctrine_Record classes. These
classes are where you define your schema information, options, attributes, etc. Instances of
these child classes represents records in the database and you can get and set properties on
these objects.
Properties
Each assigned column property of Doctrine_Record represents a database table column.
You will learn more about how to define your models in the Defining Models (page 51) chapter.
Now accessing the columns is easy:
Listing // test.php
10-27
// ...
$userTable = Doctrine::getTable('User');
$user = $userTable->find(1);
Listing // test.php
10-28
// ...
echo $user->username;
// test.php Listing
10-29
// ...
echo $user->get(username);
// test.php Listing
10-30
// ...
echo $user['username'];
The recommended way to access column values is by using the ArrayAccess as it makes it
easy to switch between record and array fetching when needed.
Iterating through the properties of a record can be done in similar way as iterating through
an array - by using the foreach construct. This is possible since Doctrine_Record
implements a magic IteratorAggregate interface.
// test.php Listing
10-31
// ...
foreach ($user as $field => $value) {
echo $field . ': ' . $value . "\n";
}
As with arrays you can use the isset() for checking if given property exists and unset() for
setting given property to null.
We can easily check if a property named 'name' exists in a if conditional:
// test.php Listing
10-32
// ...
if (isset($user['username'])) {
If we want to unset the name property we can do it using the unset() function in php:
// test.php Listing
10-33
// ...
unset($user['username']);
When you have set values for record properties you can get an array of the modified fields
and values using Doctrine_Record::getModified()
// test.php Listing
10-34
// ...
$user['username'] = 'Jack Daniels';
print_r($user->getModified());
Listing // test.php
10-36
// ...
echo $user->isModified() ? 'Modified':'Not Modified';
If you want to also check if any of the referenced relationships are modified you can pass a
$deep argument to isModified() with a value of true. Below you will find an example:
Now if we deep check if the user is modified it will return true because the Email address is
modified.
Sometimes when a record has been updated you may want to inspect the old values and not
just the new values. This is possible in Doctrine with the getModified() method by passing
an argument of true. This will return the old values instead of the new ones.
Listing // test.php
10-39
// ...
$user = Doctrine::getTable('User')->findOneByName('zYne-);
$user->name = 'zYne-';
$oldValues = $user->getModified(true);
/*
array(
'name' => 'zYne',
)
*/
$newValues = $user->getModified(false);
/*
array(
'name' => 'zYne-,
)
After saving a record in Doctrine, the modified properties are cleared out and the state of the
object is returned to un-modified. Sometimes you may want to retrieve the values that were
last modified and this is possible by using the getLastModified() method.
// test.php Listing
10-40
// ...
// you can retrieve the last modified properties like the following
print_r($user->getLastModified()); // array('username' => 'jwage')
Sometimes you may want to retrieve the column count of given record. In order to do this you
can simply pass the record as an argument for the count() function. This is possible since
Doctrine_Record implements a magic Countable interface. The other way would be calling
the count() method.
// test.php Listing
10-41
// ...
echo $record->count();
echo count($record);
Doctrine_Record offers a special method for accessing the identifier of given record. This
method is called identifier() and it returns an array with identifier field names as keys
and values as the associated property values.
// test.php Listing
10-42
// ...
$user['username'] = 'Jack Daniels';
$user->save();
A common case is that you have an array of values which you need to assign to a given
record. It may feel awkward and clumsy to set these values separately. No need to worry
though, Doctrine_Record offers a way for merging a given array or record to another
The merge() method iterates through the properties of the given record or array and assigns
the values to the object
// test.php Listing
10-43
// ...
$values = array(
'username' => 'someone',
'age' => 11,
);
$user->merge($values);
You can also merge a one records values in to another like the following:
Listing // test.php
10-44
// ...
$user1 = new User();
$user1->username = 'jwage';
Updating Records
Updating objects is very easy, you just call the Doctrine_Record::save() method. The
other way is to call Doctrine_Connection::flush() which saves all objects. It should be
noted though that flushing is a much heavier operation than just calling save method.
Listing // test.php
10-45
// ...
$userTable = Doctrine::getTable('User');
$user = $userTable->find(2);
$user->save();
}
Sometimes you may want to do a direct update. In direct update the objects aren't loaded
from database, rather the state of the database is directly updated. In the following example
we use DQL UPDATE statement to update all users.
Run a query to make all user names lowercase:
Listing // test.php
10-46
// ...
$q = Doctrine_Query::create()
->update('User u')
->set('u.username', 'LOWER(u.name)');
$q->execute();
You can also run an update using objects if you already know the identifier of the record.
When you use the Doctrine_Record::assignIdentifier() method it sets the record
identifier and changes the state so that calling Doctrine_Record::save() performs an
update instead of insert.
Listing
10-47
// test.php
// ...
$user = new User();
$user->assignIdentifier(1);
$user->username = 'jwage';
$user->save();
Replacing Records
Replacing records is simple. If you instantiate a new object and save it and then late
instantiate another new object with the same primary key or unique index value which
already exists in the database, then it will replace/update that row in the database instead of
inserting a new one. Below is an example.
First, imagine a User model where username is a unique index.
// test.php Listing
10-48
// ...
$user = new User();
$user->username = 'jwage';
$user->password = 'changeme';
$user->save();
INSERT INTO user (username, password) VALUES (?,?) ('jwage', 'changeme') Listing
10-49
Now lets create another new object and set the same username but a different password.
// test.php Listing
10-50
// ...
$user = new User();
$user->username = 'jwage';
$user->password = 'newpassword';
$user->replace();
Refreshing Records
Sometimes you may want to refresh your record with data from the database, use
Doctrine_Record::refresh().
// test.php Listing
10-52
// ...
$user = Doctrine::getTable('User')->find(2);
$user->username = 'New name';
Now if you use the Doctrine_Record::refresh() method it will select the data from the
database again and update the properties of the instance.
Listing // test.php
10-53
// ...
$user->refresh();
Refreshing relationships
The Doctrine_Record::refresh() method can also refresh the already loaded record
relationships, but you need to specify them on the original query.
First lets retrieve a User with its associated Groups:
Listing // test.php
10-54
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Groups')
->where('id = ?');
$user = $q->fetchOne(array(1));
Listing // test.php
10-55
// ...
$q = Doctrine_Query::create()
->from('Group g')
->leftJoin('g.Users')
->where('id = ?');
$group = $q->fetchOne(array(1));
Now lets link the retrieved User and Group through a UserGroup instance:
Listing // test.php
10-56
// ...
$userGroup = new UserGroup();
$userGroup->user_id = $user->id;
$userGroup->group_id = $group->id;
$userGroup->save();
You can also link a User to a Group in a much simpler way, by simply adding the Group to
the User. Doctrine will take care of creating the UserGroup instance for you automatically:
Listing // test.php
10-57
// ...
$user->Groups[] = $group;
$user->save()
// test.php Listing
10-58
// ...
$user->refresh(true);
$group->refresh(true);
You can also lazily refresh all defined relationships of a model using
Doctrine_Record::refreshRelated():
// test.php Listing
10-59
// ...
$user = Doctrine::getTable('User')->findOneByName('jon');
$user->refreshRelated();
If you want to refresh an individual specified relationship just pass the name of a relationship
to the refreshRelated() function and it will lazily load the relationship:
// test.php Listing
10-60
// ...
$user->refreshRelated('Phonenumber');
Deleting Records
Deleting records in Doctrine is handled by Doctrine_Record::delete(),
Doctrine_Collection::delete() and Doctrine_Connection::delete() methods.
// test.php Listing
10-61
// ...
$userTable = Doctrine::getTable("User");
$user = $userTable->find(2);
If you have a Doctrine_Collection of User records you can call delete() and it will loop
over all records calling Doctrine_Record::delete() for you.
// test.php Listing
10-62
// ...
$users = $userTable->findAll();
Now you can delete all users and their related composite objects by calling
Doctrine_Collection::delete(). It will loop over all Users in the collection calling
delete one each one:
// test.php Listing
10-63
// ...
$users->delete();
Listing // test.php
10-64
// ...
$user = new User();
$user->username = 'jwage';
$user->updated_at = new Doctrine_Expression('NOW()');
$user->save();
When you use Doctrine_Expression with your objects in order to get the updated value
you will have to manually call refresh() to get the updated value from the database.
Listing // test.php
10-66
// ...
$user->refresh();
Name Description
Doctrine_Record::STATE_PROXY Record is in proxy state meaning its persistent but
not all of its properties are loaded from the database.
Doctrine_Record::STATE_TCLEAN Record is transient clean, meaning its transient and
none of its properties are changed.
Doctrine_Record::STATE_TDIRTY Record is transient dirty, meaning its transient and
some of its properties are changed.
Doctrine_Record::STATE_DIRTY Record is dirty, meaning its persistent and some of
its properties are changed.
You can easily get the state of a record by using the Doctrine_Record::state() method:
// test.php Listing
10-67
// ...
$user = new User();
if ($user->state() == Doctrine_Record::STATE_TDIRTY) {
echo 'Record is transient dirty';
}
The above object is TDIRTY because it has some default values specified in the schema. If
we use an object that has no default values and instantiate a new instance it will return
TCLEAN.
// test.php Listing
10-68
// ...
$account = new Account();
if ($account->state() == Doctrine_Record::STATE_TCLEAN) {
echo 'Record is transient clean';
}
// test.php Listing
10-69
// ...
$copy = $user->copy();
Notice that copying the record with copy() returns a new record (state TDIRTY) with the
values of the old record, and it copies the relations of that record. If you do not want to copy
the relations too, you need to use copy(false).
Get a copy of user without the relations
// test.php Listing
10-70
// ...
$copy = $user->copy(false);
Using the PHP clone functionality simply uses this copy() functionality internally:
// test.php Listing
10-71
// ...
$copy = clone $user;
Listing // test.php
10-72
// ...
$user = new User();
$user->state('TDIRTY');
$user->save();
When setting the state you can optionally pass a string for the state and it will be converted
to the appropriate state constant. In the example above, `TDIRTY` is actually converted to
`Doctrine_Record::STATE_TDIRTY`.
Listing // test.php
10-73
// ...
class User extends Doctrine_Record
{
public function setTableDefinition()
{
// ...
$this->mapValue('name');
}
// ...
}
$user->name = 'jwage';
echo $user->name; // jwage
Serializing
Sometimes you may want to serialize your record objects (possibly for caching purposes):
Listing // test.php
10-74
// ...
$string = serialize($user);
$user = unserialize($string);
Checking Existence
Very commonly you'll need to know if given record exists in the database. You can use the
exists() method for checking if given record has a database row equivalent:
// test.php Listing
10-75
// ...
$record = new User();
$record->username = 'someone';
$record->save();
// test.php Listing
10-76
// ...
$record->call('trim', 'username');
Collection
Doctrine_Collection is a collection of records (see Doctrine_Record). As with records the
collections can be deleted and saved using Doctrine_Collection::delete() and
Doctrine_Collection::save() accordingly.
When fetching data from database with either DQL API (see Doctrine_Query) or rawSql
API (see Doctrine_RawSql) the methods return an instance of Doctrine_Collection by
default.
The following example shows how to initialize a new collection:
// test.php Listing
10-77
// ...
$users = new Doctrine_Collection('User');
// test.php Listing
10-78
// ...
$users[0]->username = 'Arnold';
$users[1]->username = 'Somebody';
$users->save(); Listing
10-79
Splitting a collection in to a simple key => value pair array is simple. It can be accomplished
with the toKeyValueArray() method.
Listing // test.php
10-80
// ...
$q = Doctrine_Query::create()
->from('User u');
$users = $q->execute();
Listing array(
10-81
4 => 'zYne',
5 => 'Arnold Schwarzenegger',
6 => 'Michael Caine',
7 => 'Takeshi Kitano',
8 => 'Sylvester Stallone',
9 => 'Kurt Russell',
10 => 'Jean Reno',
11 => 'Edward Furlong',
)
Accessing Elements
You can access the elements of Doctrine_Collection with set() and get() methods or
with ArrayAccess interface.
Listing // test.php
10-82
// ...
$userTable = Doctrine::getTable('User');
$users = $userTable->findAll();
Listing // test.php
10-83
// ...
$users[0]->username = "Jack Daniels";
$users[1]->username = "John Locke";
In the following example we fetch all users from database (there are 5) and then add couple
of users in the collection.
As with PHP arrays the indexes start from zero.
// test.php Listing
10-85
// ...
$users = $userTable->findAll();
echo count($users); // 5
You could also optionally omit the 5 and 6 from the array index and it will automatically
increment just as a PHP array would:
// test.php Listing
10-86
// ...
$users[]->username = 'new user 3'; // key is 7
$users[]->username = 'new user 4'; // key is 8
// test.php Listing
10-87
// ...
$users = $userTable->findAll();
echo $users->count();
// test.php Listing
10-88
// ...
echo count($users);
// test.php Listing
10-89
// ...
$users = $userTable->findAll();
$users->save();
Listing DELETE
10-90
FROM user
WHERE id IN (1,2,3,
... ,N)
Key Mapping
Sometimes you may not want to use normal indexing for collection elements. For example in
some cases mapping primary keys as collection keys might be useful. The following example
demonstrates how this can be achieved.
Map the id column
Listing // test.php
10-91
// ....
$userTable = Doctrine::getTable('User');
$userTable->setAttribute(Doctrine::ATTR_COLL_KEY, 'id');
Now user collections will use the values of id column as element indexes:
Listing // test.php
10-92
// ...
$users = $userTable->findAll();
Listing // test.php
10-93
// ...
$userTable = Doctrine::getTable('User');
$userTable->setAttribute(Doctrine::ATTR_COLL_KEY, 'username');
Now user collections will use the values of name column as element indexes:
Listing // test.php
10-94
// ...
$users = $userTable->findAll();
Note this would only be advisable if the username column is specified as unique in your
schema otherwise you will have cases where data cannot be hydrated properly due to
duplicate collection keys.
// test.php Listing
10-95
// ...
$q = Doctrine_Query::create()
->from('User u');
$users = $q->execute();
// test.php Listing
10-96
// ...
$users->loadRelated('Phonenumbers');
foreach($users as $user) {
echo $user->Phonenumbers[0]->phonenumber;
// no additional db queries needed here
}
// test.php Listing
10-97
// ...
$users->loadRelated('Groups');
foreach($users as $user) {
echo $user->Groups[0]->name;
}
The example below shows how to do this more efficiently by using the DQL API.
Write a Doctrine_Query that loads everything in one query:
Listing // test.php
10-98
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumbers p')
->leftJoin('u.Groups g');
$users = $q->execute();
Now when we use the Phonenumbers and Groups no additional database queries are
needed:
Listing // test.php
10-99
// ...
foreach($users as $user) {
echo $user->Phonenumbers[0]->phonenumber;
echo $user->Groups[0]->name;
}
Validator
Validation in Doctrine is a way to enforce your business rules in the model part of the MVC
architecture. You can think of this validation as a gateway that needs to be passed right
before data gets into the persistent data store. The definition of these business rules takes
place at the record level, that means in your active record model classes (classes derived
from Doctrine_Record). The first thing you need to do to be able to use this kind of
validation is to enable it globally. This is done through the Doctrine_Manager.
Listing // bootstrap.php
10-100
// ...
$manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
• Data type validations: All values assigned to columns are checked for the right type.
That means if you specified a column of your record as type 'integer', Doctrine will
validate that any values assigned to that column are of this type. This kind of type
validation tries to be as smart as possible since PHP is a loosely typed language. For
example 2 as well as "7" are both valid integers whilst "3f" is not. Type validations
occur on every column (since every column definition needs a type).
• Length validation: As the name implies, all values assigned to columns are validated
to make sure that the value does not exceed the maximum length.
You can combine the following constants by using bitwise operations: VALIDATE_ALL,
VALIDATE_TYPES, VALIDATE_LENGTHS, VALIDATE_CONSTRAINTS, VALIDATE_NONE.
For example to enable all validations except length validations you would use:
Listing // bootstrap.php
10-101
// ...
$manager->setAttribute(Doctrine::ATTR_VALIDATE, VALIDATE_ALL &
~VALIDATE_LENGTHS);
You can read more about this topic in the Data Validation (page 207) chapter.
More Validation
The type and length validations are handy but most of the time they're not enough. Therefore
Doctrine provides some mechanisms that can be used to validate your data in more detail.
Validators are an easy way to specify further validations. Doctrine has a lot of predefined
validators that are frequently needed such as email, country, ip, range and regexp
validators. You find a full list of available validators in the Data Validation (page 207) chapter.
You can specify which validators apply to which column through the 4th argument of the
hasColumn() method. If that is still not enough and you need some specialized validation
that is not yet available as a predefined validator you have three options:
The first two options are advisable if it is likely that the validation is of general use and is
potentially applicable in many situations. In that case it is a good idea to implement a new
validator. However if the validation is special it is better to use hooks provided by Doctrine:
If you need a special validation in your active record you can simply override one of these
methods in your active record class (a descendant of Doctrine_Record). Within these
methods you can use all the power of PHP to validate your fields. When a field does not pass
your validation you can then add errors to the record's error stack. The following code
snippet shows an example of how to define validators together with custom validation:
// models/User.php Listing
10-102
// models/Email.php
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
10-103
# schema.yml
# ...
Email:
columns:
address:
type: string(150)
email: true
unique: true
Implicit Validation
Whenever a record is going to be saved to the persistent data store (i.e. through calling
$record->save()) the full validation procedure is executed. If errors occur during that
process an exception of the type Doctrine_Validator_Exception will be thrown. You can
catch that exception and analyze the errors by using the instance method
Doctrine_Validator_Exception::getInvalidRecords(). This method returns an
ordinary array with references to all records that did not pass validation. You can then further
explore the errors of each record by analyzing the error stack of each record. The error stack
of a record can be obtained with the instance method
Doctrine_Record::getErrorStack(). Each error stack is an instance of the class
Doctrine_Validator_ErrorStack. The error stack provides an easy to use interface to
inspect the errors.
Explicit Validation
You can explicitly trigger the validation for any record at any time. For this purpose
Doctrine_Record provides the instance method Doctrine_Record::isValid(). This
method returns a boolean value indicating the result of the validation. If the method returns
false, you can inspect the error stack in the same way as seen above except that no exception
is thrown, so you simply obtain the error stack of the record that didnt pass validation
through Doctrine_Record::getErrorStack().
The following code snippet shows an example of handling implicit validation which caused a
Doctrine_Validator_Exception.
Listing // test.php
10-104
// ...
$user = new User();
try {
You could also use $e->getInvalidRecords(). The direct way used above is just more simple
when you know the records you're dealing with.
You can also retrieve the error stack as a nicely formatted string for easy use in your
applications:
// test.php Listing
10-105
// ...
echo $user->getErrorStackAsString();
It would output an error string that looks something like the following:
Profiler
Doctrine_Connection_Profiler is an event listener for Doctrine_Connection. It
provides flexible query profiling. Besides the SQL strings the query profiles include elapsed
time to run the queries. This allows inspection of the queries that have been performed
without the need for adding extra debugging code to model classes.
Doctrine_Connection_Profiler can be enabled by adding it as an event listener for
Doctrine_Connection.
// test.php Listing
10-107
// ...
$profiler = new Doctrine_Connection_Profiler();
$conn = Doctrine_Manager::connection();
$conn->setListener($profiler);
Basic Usage
Perhaps some of your pages is loading slowly. The following shows how to build a complete
profiler report from the connection:
Listing // test.php
10-108
// ...
$time = 0;
foreach ($profiler as $event) {
$time += $event->getElapsedSecs();
echo $event->getName() . " " . sprintf("%f", $event->getElapsedSecs()) . "\n";
echo $event->getQuery() . "\n";
$params = $event->getParams();
if( ! empty($params)) {
print_r($params);
}
}
echo "Total time: " . $time . "\n";
Frameworks like symfony23, Zend24, etc. offer web debug toolbars that use this
functionality provided by Doctrine for reporting the number of queries executed on every
page as well as the time it takes for each query.
Locking Manager
The term 'Transaction' does not refer to database transactions here but to the general
meaning of this term.
Locking is a mechanism to control concurrency. The two most well known locking strategies
are optimistic and pessimistic locking. The following is a short description of these two
strategies from which only pessimistic locking is currently supported by Doctrine.
Optimistic Locking
The state/version of the object(s) is noted when the transaction begins. When the transaction
finishes the noted state/version of the participating objects is compared to the current state/
version. When the states/versions differ the objects have been modified by another
transaction and the current transaction should fail. This approach is called 'optimistic'
because it is assumed that it is unlikely that several users will participate in transactions on
the same objects at the same time.
Pessimistic Locking
The objects that need to participate in the transaction are locked at the moment the user
starts the transaction. No other user can start a transaction that operates on these objects
while the locks are active. This ensures that the user who starts the transaction can be sure
that no one else modifies the same objects until he has finished his work.
23. http://www.symfony-project.com
24. http://framework.zend.com
Doctrine's pessimistic offline locking capabilities can be used to control concurrency during
actions or procedures that take several HTTP request and response cycles and/or a lot of time
to complete.
Examples
The following code snippet demonstrates the use of Doctrine's pessimistic offline locking
capabilities.
At the page where the lock is requested get a locking manager instance:
// test.php Listing
10-109
// ...
$lockingManager = new Doctrine_Locking_Manager_Pessimistic();
Ensure that old locks which timed out are released before we try to acquire our lock 300
seconds = 5 minutes timeout. This can be done by using the releaseAgedLocks()
method.
// test.php Listing
10-110
// ...
$user = Doctrine::getTable('User')->find(1);
try
{
$lockingManager->releaseAgedLocks(300);
if ($gotLock)
{
echo "Got lock!";
}
else
{
echo "Sorry, someone else is currently working on this record";
}
} catch(Doctrine_Locking_Exception $dle) {
echo $dle->getMessage();
// handle the error
}
At the page where the transaction finishes get a locking manager instance:
// test.php Listing
10-111
// ...
$user = Doctrine::getTable('User')->find(1);
try
{
if ($lockingManager->releaseLock($user, 'jwage'))
{
Technical Details
The pessimistic offline locking manager stores the locks in the database (therefore 'offline').
The required locking table is automatically created when you try to instantiate an instance of
the manager and the ATTR_CREATE_TABLES is set to TRUE. This behavior may change in the
future to provide a centralized and consistent table creation procedure for installation
purposes.
Views
Database views can greatly increase the performance of complex queries. You can think of
them as cached queries. Doctrine_View provides integration between database views and
DQL queries.
Using Views
Using views on your database using Doctrine is easy. We provide a nice Doctrine_View
class which provides functionality for creating, dropping and executing views.
The Doctrine_View class integrates with the Doctrine_Query class by saving the SQL
that would be executed by Doctrine_Query.
First lets create a new Doctrine_Query instance to work with:
Listing // test.php
10-112
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumber p')
->limit(20);
Now lets create the Doctrine_View instance and pass it the Doctrine_Query instance as
well as a name for identifying that database view:
Listing // test.php
10-113
// ...
$view = new Doctrine_View($q, 'RetrieveUsersAndPhonenumbers');
Now we can easily create the view by using the Doctrine_View::create() method:
Listing
10-114
// test.php
// ...
try {
$view->create();
} catch (Exception $e) {}
Alternatively if you want to drop the database view you use the Doctrine_View::drop()
method:
// test.php Listing
10-115
// ...
try {
$view->drop();
} catch (Exception $e) {}
Using views are extremely easy. Just use the Doctrine_View::execute() for executing
the view and returning the results just as a normal Doctrine_Query object would:
// test.php Listing
10-116
// ...
$users = $view->execute();
Conclusion
We now have been exposed to a very large percentage of the core functionality provided by
Doctrine. The next chapters of this book are documentation that cover some of the optional
functionality that can help make your life easier on a day to basis.
Lets move on to the next chapter (page 190) where we can learn about how to use native SQL
to hydrate our data in to arrays and objects instead of the Doctrine Query Language.
Chapter 11
Native SQL
Introduction
Doctrine_RawSql provides a convenient interface for building raw sql queries. Similar to
Doctrine_Query, Doctrine_RawSql provides means for fetching arrays and objects.
Whichever way you prefer.
Using raw sql for fetching might be useful when you want to utilize database specific features
such as query hints or the CONNECT keyword in Oracle.
Creating a Doctrine_RawSql object is easy:
Listing // test.php
11-1
// ...
$q = new Doctrine_RawSql();
Listing // test.php
11-2
// ...
$conn = Doctrine_Manager::connection();
$q = new Doctrine_RawSql($conn);
Component Queries
The first thing to notice when using Doctrine_RawSql is that you always have to place the
fields you are selecting in curly brackets {}. Also for every selected component you have to
call addComponent().
The following example should clarify the usage of these:
Listing // test.php
11-3
// ...
$q->select('{u.*}')
->from('user u')
->addComponent('u', 'User');
$users = $q->execute();
print_r($users->toArray());
Note above that we tell that user table is bound to class called User by using the
addComponent() method.
// test.php Listing
11-4
// ...
$q = new Doctrine_RawSql();
$q->select('{u.*}, {p.*}');
Now we need to add the FROM part to the query with the join to the phonenumber table from
the user table and map everything together:
// test.php Listing
11-5
// ...
$q->from('user u LEFT JOIN phonenumber p ON u.id = p.user_id')
Now we tell that user table is bound to class called User we also add an alias for User class
called u. This alias will be used when referencing the User class.
// test.php Listing
11-6
// ...
$q->addComponent('u', 'User u');
// test.php Listing
11-7
// ...
$q->addComponent('p', 'u.Phonenumbers p');
Notice how we reference that the Phonenumber class is the User's phonenumber.
Now we can execute the Doctrine_RawSql query just like if you were executing a
Doctrine_Query object:
Listing // test.php
11-8
// ...
$users = $q->execute();
echo get_class($users) . "\n";
echo get_class($users[0]) . "\n";
echo get_class($users[0]['Phonenumbers'][0]) . "\n";
Conclusion
This chapter may or may not be useful for you right now. In most cases the Doctrine Query
Language is plenty sufficient for retrieving the complex data sets you require. But if you
require something outside the scope of what Doctrine_Query is capable of then
Doctrine_RawSql can help you.
In the previous chapters you've seen a lot of mention about YAML schema files and have been
given examples of schema files but haven't really been trained on how to write your own. The
next chapter explains in great detail how to maintain your models as YAML Schema Files
(page 193).
Chapter 12
Introduction
The purpose of schema files is to allow you to manage your model definitions directly from a
YAML file rather then editing php code. The YAML schema file is parsed and used to generate
all your model definitions/classes. This makes Doctrine model definitions much more portable.
Schema files support all the normal things you would write with manual php code.
Component to connection binding, relationships, attributes, templates/behaviors, indexes, etc.
Abbreviated Syntax
Doctrine offers the ability to specify schema in an abbreviated syntax. A lot of the schema
parameters have values they default to, this allows us to abbreviate the syntax and let
Doctrine just use its defaults. Below is an example of schema taking advantage of all the
abbreviations.
--- Listing
12-1
detect_relations: true
User:
columns:
username: string
password: string
contact_id: integer
Contact:
columns:
first_name: string
last_name: string
phone: string
email: string
address: string
Verbose Syntax
Here is the 100% verbose form of the above schema:
Listing ---
12-2
User:
columns:
username:
type: string(255)
password:
type: string(255)
contact_id:
type: integer
relations:
Contact:
class: Contact
local: contact_id
foreign: id
foreignAlias: User
foreignType: one
type: one
Contact:
columns:
first_name:
type: string(255)
last_name:
type: string(255)
phone:
type: string(255)
email:
type: string(255)
address:
type: string(255)
relations:
User:
class: User
local: id
foreign: contact_id
foreignAlias: Contact
foreignType: one
type: one
In the above example we do not define the detect_relations option, instead we manually
define the relationships so we have complete control over the configuration of the local/
foreign key, type and alias of the relationship on each side.
Relationships
When specifying relationships it is only necessary to specify the relationship on the end
where the foreign key exists. When the schema file is parsed, it reflects the relationship and
builds the opposite end automatically. If you specify the other end of the relationship
manually, the auto generation will have no effect.
Detect Relations
Doctrine offers the ability to specify a detect_relations option as you saw earlier. This
feature provides automatic relationship building based on column names. If you have a User
model with a contact_id and a class with the name Contact exists, it will automatically
create the relationships between the two.
Customizing Relationships
Doctrine only requires that you specify the relationship on the end where the foreign key
exists. The opposite end of the relationship will be reflected and built on the opposite end.
The schema syntax offers the ability to customize the relationship alias and type of the
opposite end. This is good news because it means you can maintain all the relevant
relationship information in one place. Below is an example of how to customize the alias and
type of the opposite end of the relationship. It demonstrates the relationships User has one
Contact and Contact has one User as UserModel. Normally it would have automatically
generated User has one Contact and Contact has many User. The foreignType and
foreignAlias options allow you to customize the opposite end of the relationship.
--- Listing
12-3
User:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
relations:
Contact:
foreignType: one
foreignAlias: UserModel
Contact:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
You can quickly detect and create the relationships between two models with the
detect_relations option like below.
--- Listing
12-4
detect_relations: true
User:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
avatar_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
Avatar:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
image_file:
type: string(255)
The resulting relationships would be User has one Avatar and Avatar has many User.
One to One
Listing ---
12-5
User:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
relations:
Contact:
foreignType: one
Contact:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
One to Many
Listing ---
12-6
User:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
Phonenumber:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
user_id:
type: integer(4)
relations:
User:
foreignAlias: Phonenumbers
Many to Many
--- Listing
12-7
User:
columns:
id:
type: integer(4)
autoincrement: true
primary: true
username:
type: string(255)
password:
type: string(255)
attributes:
export: all
validate: true
Group:
tableName: group_table
columns:
id:
type: integer(4)
autoincrement: true
primary: true
name:
type: string(255)
relations:
Users:
foreignAlias: Groups
class: User
refClass: GroupUser
GroupUser:
columns:
group_id:
type: integer(4)
primary: true
user_id:
type: integer(4)
primary: true
relations:
Group:
foreignAlias: GroupUsers
User:
foreignAlias: GroupUsers
This creates a set of models where User has many Groups, Group has many Users,
GroupUser has one User and GroupUser has one Group.
Listing Doctrine_Manager::connection('mysql://jwage:pass@localhost/connection1',
12-8
'connection1');
Now somewhere in your Doctrine bootstrapping of Doctrine you would bind the model to that
connection:
Schema files offer the ability to bind it to a specific connection by specifying the connection
parameter. If you do not specify the connection the model will just use the current connection
set on the Doctrine_Manager instance.
Listing ---
12-10
User:
connection: connection1
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
Attributes
Doctrine offers the ability to set attributes for your generated models directly in your schema
files similar to how you would if you were manually writing your Doctrine_Record child
classes.
--- Listing
12-11
User:
connection: connection1
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
attributes:
export: none
validate: false
Enums
To use enum columns in your schema file you must specify the type as enum and specify an
array of values for the possible enum values.
--- Listing
12-12
TvListing:
tableName: tv_listing
actAs: [Timestampable]
columns:
notes:
type: string
taping:
type: enum
length: 4
values: ['live', 'tape']
region:
type: enum
length: 4
values: ['US', 'CA']
ActAs Behaviors
You can attach behaviors to your models with the actAs option. You can specify something
like the following:
--- Listing
12-13
User:
connection: connection1
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
actAs:
Timestampable:
Sluggable:
fields: [username]
name: slug # defaults to 'slug'
type: string # defaults to 'clob'
length: 255 # defaults to null. clob doesn't require a length
The options specified on the Sluggable behavior above are optional as they will use defaults
values if you do not specify anything. Since they are defaults it is not necessary to type it
out all the time.
Listing ---
12-14
User:
connection: connection1
columns:
# ...
actAs: [Timestampable, Sluggable]
Listeners
If you have a listener you'd like attached to a model, you can specify them directly in the yml
as well.
Listing ---
12-15
User:
listeners: [ MyCustomListener ]
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
The above syntax will generated a base class that looks something like the following:
public setUp()
{
// ...
$this->addListener(new MyCustomListener());
}
}
Options
Specify options for your tables and when Doctrine creates your tables from your models the
options will be set on the create table statement.
--- Listing
12-17
User:
connection: connection1
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
options:
type: INNODB
collate: utf8_unicode_ci
charset: utf8
Indexes
Please see the Indexes (page 83) section of the chapter (page 51) for more information about
indexes and their options.
--- Listing
12-18
UserProfile:
columns:
user_id:
type: integer
length: 4
primary: true
autoincrement: true
first_name:
type: string
length: 20
last_name:
type: string
length: 20
indexes:
name_index:
fields:
first_name:
sorting: ASC
length: 10
primary: true
last_name: []
type: unique
This is the PHP line of code that is auto-generated inside setTableDefinition() inside
your base model class for the index definition used above:
Listing
12-19
$this->index('name_index', array(
'fields' => array(
'first_name' => array(
'sorting' => 'ASC',
'length' => '10',
'primary' => true
),
'last_name' => array()),
'type' => 'unique'
)
);
Inheritance
Below we will demonstrate how you can setup the different types of inheritance using YAML
schema files.
Simple Inheritance
Listing ---
12-20
Entity:
columns:
name: string(255)
username: string(255)
password: string(255)
User:
inheritance:
extends: Entity
type: simple
Group:
inheritance:
extends: Entity
type: simple
Any columns or relationships defined in models that extend another in simple inheritance
will be moved to the parent when the PHP classes are built.
You can read more about this topic in the Simple (page 227) chapter.
Concrete Inheritance
Listing ---
12-21
TextItem:
columns:
topic: string(255)
Comment:
inheritance:
extends: TextItem
type: concrete
columns:
content: string(300)
You can read more about this topic in the Concrete (page 229) chapter.
Like simple inheritance, any columns or relationships added to the children will be
automatically removed and moved to the parent when the PHP classes are built.
First lets defined a model named Entity that our other models will extend from:
--- Listing
12-22
Entity:
columns:
name: string(255)
type: string(255)
The type column above is optional. It will be automatically added when it is specified in the
child class.
Now lets create a User model that extends the Entity model:
--- Listing
12-23
User:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: User
columns:
username: string(255)
password: string(255)
The type option under the inheritance definition is optional as it is implied if you
specify a keyField or keyValue. If the keyField is not specified it will default to add a
column named type. The keyValue will default to the name of the model if you do not
specify anything.
Again lets create another model that extends Entity named Group:
--- Listing
12-24
Group:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: Group
columns:
description: string(255)
The User username and password and the Group description columns will be
automatically moved to the parent Entity.
You can read more about this topic in the Column Aggregation (page 232).
Column Aliases
If you want the ability alias a column name as something other than the column name in the
database this is easy to accomplish with Doctrine. We simple use the syntax "column_name
as field_name" in the name of our column:
Listing ---
12-25
User:
columns:
login:
name: login as username
type: string(255)
password:
type: string(255)
The above example would allow you to access the column named login from the alias
username.
Packages
Doctrine offers the "package" parameter which will generate the models in to sub folders.
With large schema files this will allow you to better organize your schemas in to folders.
Listing ---
12-26
User:
package: User
columns:
username: string(255)
The model files from this schema file would be put in a folder named User. You can specify
more sub folders by doing "package: User.Models" and the models would be in User/Models
Listing ---
12-27
User:
package: User
package_custom_path: /path/to/generate/package
columns:
username: string(255)
Name Description
connection Name of connection to bind the models to.
attributes Array of attributes for models.
actAs Array of behaviors for the models to act as.
Now here is an example schema where we use some of the above global parameters:
--- Listing
12-28
connection: conn_name1
actAs: [Timestampable]
options:
type: INNODB
package: User
detect_relations: true
User:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
contact_id:
type: integer(4)
username:
type: string(255)
password:
type: string(255)
Contact:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
All of the settings at the top will be applied to every model which is defined in that YAML file.
--- Listing
12-29
User:
columns:
username:
type: string(255)
extra:
test: 123
password:
type: string(255)
Now after you build your models from yaml you can now access the data with the following
php code.
The above code will generate the models for schema.yml at /path/to/generate/models.
Below is a table containing the different options you can use to customize the building of
models. Notice we use the packagesPrefix, baseClassName and suffix options above.
Conclusion
Now that we have learned all about YAML Schema files we are ready to move on to a great
topic regarding Data Validation (page 207). This is an important topic because if you are not
validating user inputted data yourself then we want Doctrine to validate data before being
persisted to the database.
Chapter 13
Data Validation
Introduction
Data types are a way to limit the kind of data that can be stored in a table. For many
applications, however, the constraint they provide is too coarse. For example, a column
containing a product price should probably only accept positive values. But there is no
standard data type that accepts only positive numbers. Another issue is that you might want
to constrain column data with respect to other columns or rows. For example, in a table
containing product information, there should be only one row for each product number.
Doctrine allows you to define *portable* constraints on columns and tables. Constraints give
you as much control over the data in your tables as you wish. If a user attempts to store data
in a column that would violate a constraint, an error is raised. This applies even if the value
came from the default value definition.
Doctrine constraints act as database level constraints as well as application level validators.
This means double security: the database doesn't allow wrong kind of values and neither does
the application.
Here is a full list of available validators within Doctrine:
25. http://www.postgresql.org/docs/8.2/static/ddl-constraints.html
Below is an example of how you use the validator and how to specify the arguments for the
validators on a column.
In our example we will use the minlength validator.
Listing // models/User.php
13-1
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-2
# schema.yml
# ...
User:
columns:
username:
type: string(255)
minlength: 12
# ...
Examples
Not Null
A not-null constraint simply specifies that a column must not assume the null value. A not-null
constraint is always written as a column constraint.
The following definition uses a notnull constraint for column name. This means that the
specified column doesn't accept null values.
// models/User.php Listing
13-3
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-4
# schema.yml
# ...
User:
columns:
username:
type: string(255)
notnull: true
primary: true
# ...
When this class gets exported to database the following SQL statement would get executed
(in MySQL):
The notnull constraint also acts as an application level validator. This means that if Doctrine
validators are turned on, Doctrine will automatically check that specified columns do not
contain null values when saved.
If those columns happen to contain null values Doctrine_Validator_Exception is raised.
Email
The e-mail validator simply validates that the inputted value is indeed a valid e-mail address
and that the MX records for the address domain resolve as a valid e-mail address.
Listing // models/User.php
13-6
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-7
# schema.yml
# ...
User:
columns:
# ...
email:
type: string(255)
email: true
# ...
Now when we try and create a user with an invalid email address it will not validate:
Listing // test.php
13-8
// ...
$user = new User();
$user->username = 'jwage';
$user->email = 'jonwage';
if ( ! $user->isValid()) {
echo 'User is invalid!';
}
The above code will throw an exception because jonwage is not a valid e-mail address. Now
we can take this even further and give a valid e-mail address format but an invalid domain
name:
// test.php Listing
13-9
// ...
$user = new User();
$user->username = 'jwage';
$user->email = '[email protected]';
if ( ! $user->isValid()) {
echo 'User is invalid!';
}
Now the above code will still fail because the domain
somefakedomainiknowdoesntexist.com does not exist and the php function
checkdnsrr()26 returned false.
You may not always want to validate the mx record of an e-mail address. If this is the case
then you can disable it by using the check_mx option and setting it to false.
// models/User.php Listing
13-10
Not Blank
The not blank validator is similar to the not null validator except that it will fail on empty
strings or strings with white space.
// models/User.php Listing
13-11
// ...
26. http://www.php.net/checkdnsrr
)
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-12
# schema.yml
# ...
User:
columns:
username:
type: string(255)
notblank: true
# ...
Now if we try and save a User record with a username that is a single blank white space,
validation will fail:
Listing // test.php
13-13
// ...
$user = new User();
$user->username = ' ';
if ( ! $user->isValid()) {
echo 'User is invalid!';
}
No Space
The no space validator is simple. It checks that the value doesn't contain any spaces.
Listing // models/User.php
13-14
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-15
# schema.yml
# ...
User:
columns:
username:
type: string(255)
nospace: true
# ...
Now if we try and save a User with a username that has a space in it, the validation will fail:
if ( ! $user->isValid()) {
echo 'User is invalid!';
}
Past
The past validator checks if the given value is a valid date in the past. In this example we'll
have a User model with a birthday column and we want to validate that the date is in the
past.
// models/User.php Listing
13-17
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-18
# schema.yml
# ...
User:
columns:
# ...
birthday:
type: timestamp
past: true
# ...
Now if we try and set a birthday that is not in the past we will get a validation error.
Future
The future validator is the opposite of the past validator and checks if the given value is a
valid date in the future. In this example we'll have a User model with a
next_appointment_date column and we want to validate that the date is in the future.
Listing // models/User.php
13-19
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-20
# schema.yml
# ...
User:
columns:
# ...
next_appointment_date:
type: timestamp
future: true
# ...
Now if we try and set an appointment date that is not in the future we will get a validation
error.
Min Length
The min length does exactly what it says. It checks that the value string length is greater than
the specified minimum length. In this example we will have a User model with a password
column where we want to make sure the length of the password is at least 5 characters long.
Listing // models/User.php
13-21
{
public function setTableDefinition()
{
parent::setTableDefinition();
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-22
# schema.yml
# ...
User:
columns:
# ...
password:
type: timestamp
minlength: 5
# ...
Now if we try and save a User with a password that is shorter than 5 characters, the
validation will fail.
// test.php Listing
13-23
// ...
$user = new User();
$user->username = 'jwage';
$user->password = 'test';
if ( ! $user->isValid()) {
echo 'User is invalid because "test" is only 4 characters long!';
}
Country
The country validator checks if the given value is a valid country code.
// models/User.php Listing
13-24
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-25
# schema.yml
# ...
User:
columns:
# ...
country:
type: string(2)
country: true
# ...
Now if you try and save a User with an invalid country code the validation will fail.
Listing // test.php
13-26
// ...
$user = new User();
$user->username = 'jwage';
$user->country_code = 'zz';
if ( ! $user->isValid()) {
echo 'User is invalid because "zz" is not a valid country code!';
}
IP Address
The ip address validator checks if the given value is a valid ip address.
Listing // models/User.php
13-27
// ...
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-28
# schema.yml
# ...
User:
columns:
# ...
ip_address:
type: string(15)
ip: true
# ...
Now if you try and save a User with an invalid ip address the validation will fail.
if ( ! $user->isValid()) {
echo User is invalid because "123.123" is not a valid ip address
}
HTML Color
The html color validator checks that the given value is a valid html hex color.
// models/User.php Listing
13-30
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-31
# schema.yml
# ...
User:
columns:
# ...
favorite_color:
type: string(7)
htmlcolor: true
# ...
Now if you try and save a User with an invalid html color value for the favorite_color
column the validation will fail.
Listing // test.php
13-32
// ...
$user = new User();
$user->username = 'jwage';
$user->favorite_color = 'red';
if ( ! $user->isValid()) {
echo 'User is invalid because "red" is not a valid hex color';
}
Range
The range validator checks if value is within given range of numbers.
Listing // models/User.php
13-33
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-34
# schema.yml
# ...
User:
columns:
# ...
age:
type: integer(3)
Now if you try and save a User with an age that is less than 10 or greater than 100, the
validation will fail.
// test.php Listing
13-35
// ...
$user = new User();
$user->username = 'jwage';
$user->age = '3';
if ( ! $user->isValid()) {
echo 'User is invalid because "3" is less than the minimum of "10"';
}
You can use the range validator to validate max and min values by omitting either one of the
0 or 1 keys of the range array. Below is an example:
// models/User.php Listing
13-36
// ...
The above would make it so that age has a max of 100. To have a minimum value simple
specify 0 instead of 1 in the range array.
The YAML syntax for this would look like the following:
--- Listing
13-37
# schema.yml
# ...
User:
columns:
# ...
age:
type: integer(3)
range:
1: 100
# ...
Unique
Unique constraints ensure that the data contained in a column or a group of columns is
unique with respect to all the rows in the table.
In general, a unique constraint is violated when there are two or more rows in the table
where the values of all of the columns included in the constraint are equal. However, two null
values are not considered equal in this comparison. That means even in the presence of a
unique constraint it is possible to store duplicate rows that contain a null value in at least one
of the constrained columns. This behavior conforms to the SQL standard, but some databases
do not follow this rule. So be careful when developing applications that are intended to be
portable.
The following definition uses a unique constraint for column name.
Listing // models/User.php
13-38
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-39
# schema.yml
# ...
User:
columns:
username:
type: string(255)
unique: true
# ....
You should only use unique constraints for columns other than the primary key because
they are always unique already.
If you wish to add a unique constraint to more than one column then you can use the
unique() mapping method.
{
$this->hasColumn('username', 'string', 255);
$this->hasColumn('email_address', 'string', 255);
$this->unique('username', 'email_address');
}
}
Using the unique() method is a convenience method for adding a unique index for the
specified fields. You can accomplish the same thing by manually adding the unique index
for the specified fields.
Regular Expression
The regular expression validator is a simple way to validate column values against your own
provided regular expression. In this example we will make sure the username contains only
valid letters or numbers.
// models/User.php Listing
13-41
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-42
# schema.yml
# ...
User:
columns:
username:
type: string(255)
regexp: '/[a-zA-Z0-9]/'
# ...
Now if we were to try and save a User with a username that has any other character than a
letter or number in it, the validation will fail:
// test.php Listing
13-43
// ...
$user = new User();
$user->username = '[jwage';
if ( ! $user->isValid()) {
echo 'User is invalid because the username contains a [ character';
}
Credit Card
The credit card validator simply checks that the given value is indeed a valid credit card
number.
Listing // models/User.php
13-44
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-45
# schema.yml
# ...
User:
columns:
# ...
cc_number:
type: integer(16)
creditcard: true
# ...
Read Only
The read only validator will fail validation if you modify a column that has the readonly
validator enabled on it.
Listing // models/User.php
13-46
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
13-47
# schema.yml
# ...
User:
columns:
# ...
readonly_value:
type: integer(16)
readonly: true
# ...
Now if I ever try and modify the column named readonly_value from a User object
instance, validation will fail.
Unsigned
The unsigned validator checks that the given integer value is unsigned.
// models/User.php Listing
13-48
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-49
# schema.yml
# ...
User:
columns:
# ...
age:
type: integer(3)
unsigned: true
# ...
Now if I try and save a User with a negative age the validation will fail:
Listing // test.php
13-50
// ...
$user = new User();
$user->username = 'jwage';
$user->age = '-100';
if ( ! $user->isValid()) {
echo 'User is invalid because -100 is signed';
}
US State
The us state validator checks that the given string is a valid US state code.
Listing // models/State.php
13-51
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
13-52
# schema.yml
# ...
State:
columns:
name: string(255)
code:
type: string(2)
usstate: true
Now if I try and save a State with an invalid state code then the validation will fail.
if ( ! $state->isValid()) {
echo 'State is invalid because "ZZ" is not a valid state code';
}
Validating Relationships
Sometimes you may want to run validation deep on any referenced relationships. This can be
accomplished by passing a $deep argument to isValid() with a value of true. Below you
can find an example:
$user->name = 'jwage';
$user->Emails[] = $mail;
Now if we check validation it will return false because the referenced Email object does not
have an address set.
Custom Validators
You can register custom validators for Doctrine to be aware of by using the
Doctrine_Manager::registerValidator() method.
// bootstrap.php Listing
13-56
// ...
$manager->registerValidators('MyValidator');
You can also get the possible validators by using the getValidators() method.
// test.php Listing
13-57
// ...
$validators = $manager->getValidators();
Conclusion
If we want Doctrine to validate our data before being persisted to the database, now we have
the knowledge on how to do it. We can use the validators that are provided with the Doctrine
core to perform common validations of our data.
The next chapter (page 227) is an important one as we will discuss a great feature of Doctrine,
Inheritance (page 227)! Inheritance is a great way accomplish complex functionality with
minimal code. After we discuss inheritance we will move on to a custom strategy that
provides even better functionality than inheritance, called Behaviors (page 235).
Chapter 14
Inheritance
Doctrine supports three types of inheritance strategies which can be mixed together. The
three types are simple, concrete and column aggregation. You will learn about these three
different types of inheritance and how to use them in this chapter.
For this chapter lets delete all our existing schemas and models from our test environment
we created and have been using in the earlier chapters.
$ rm schema.yml Listing
14-1
$ touch schema.yml
$ rm -rf models/*
Simple
Simple inheritance is the easiest and simplest inheritance to use. In simple inheritance all the
child classes share the same columns as the parent and all information is stored in the parent
table.
// models/Entity.php Listing
14-2
// models/User.php Listing
14-3
Listing // models/Group.php
14-4
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
14-5
# schema.yml
# ...
Entity:
columns:
name: string(30)
username: string(20)
password: string(16)
created_at: timestamp
updated_at: timestamp
User:
inheritance:
extends: Entity
type: simple
Group:
inheritance:
extends: Entity
type: simple
Listing // test.php
14-6
// ...
$sql = Doctrine::generateSqlFromArray(array('Entity', 'User', 'Group'));
echo $sql[0];
When using YAML schema files you are able to define columns in the child classes but
when the YAML is parsed the columns are moved to the parent for you automatically. This
is only a convenience to you so that you can organize your columns easier.
Concrete
Concrete inheritance creates separate tables for child classes. However in concrete
inheritance each class generates a table which contains all columns (including inherited
columns). In order to use concrete inheritance you'll need to add explicit
parent::setTableDefinition() calls to child classes as shown below.
// models/TextItem.php Listing
14-8
Now lets create a model named Comment that extends TextItem and add an extra column
named content:
// models/Comment.php Listing
14-9
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
14-10
# schema.yml
# ...
TextItem:
columns:
topic: string(100)
Comment:
inheritance:
extends: TextItem
type: concrete
columns:
content: string(300)
// test.php Listing
14-11
// ...
$sql = Doctrine::generateSqlFromArray(array('TextItem', 'Comment'));
In concrete inheritance you don't necessarily have to define additional columns, but in order
to make Doctrine create separate tables for each class you'll have to make iterative
setTableDefinition() calls.
In the following example we have three database tables called entity, user and group.
Users and groups are both entities. The only thing we have to do is write 3 classes
(Entity, Group and User) and make iterative setTableDefinition() method calls.
Listing // models/Entity.php
14-13
// models/User.php
// models/Group.php
class Group extends Entity
{
public function setTableDefinition()
{
// the following method call is needed in
// concrete inheritance
parent::setTableDefinition();
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
14-14
Entity:
columns:
name: string(30)
username: string(20)
password: string(16)
created: integer(11)
User:
inheritance:
extends: Entity
type: concrete
Group:
tableName: groups
inheritance:
extends: Entity
type: concrete
// test.php Listing
14-15
// ...
$sql = Doctrine::generateSqlFromArray(array('Entity', 'User', 'Group'));
echo $sql[0] . "\n";
echo $sql[1] . "\n";
echo $sql[2] . "\n";
Column Aggregation
In the following example we have one database table called entity. Users and groups are
both entities and they share the same database table.
The entity table has a column called type which tells whether an entity is a group or a
user. Then we decide that users are type 1 and groups type 2.
The only thing we have to do is to create 3 records (the same as before) and add the call to
the Doctrine_Table::setSubclasses() method from the parent class.
Listing // models/Entity.php
14-17
$this->setSubclasses(array(
'User' => array('type' => 1),
'Group' => array('type' => 2)
)
);
}
}
// models/User.php
class User extends Entity
{ }
// models/Group.php
class Group extends Entity
{ }
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
14-18
Entity:
columns:
username: string(20)
password: string(16)
created_at: timestamp
updated_at: timestamp
User:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: 1
Group:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: 2
// test.php Listing
14-19
// ...
$sql = Doctrine::generateSqlFromArray(array('Entity', 'User', 'Group'));
echo $sql[0];
Notice how the type column was automatically added. This is how column aggregation
inheritance knows which model each record in the database belongs to.
This feature also enable us to query the Entity table and get a User or Group object back if
the returned object matches the constraints set in the parent class.
See the code example below for an example of this. First lets save a new User object:
// test.php Listing
14-21
// ...
$user = new User();
$user->name = 'Bjarte S. Karlsen';
$user->username = 'meus';
$user->password = 'rat';
$user->save();
// test.php Listing
14-22
// ...
$group = new Group();
$group->name = 'Users';
$group->username = 'users';
$group->password = 'password';
$group->save();
Now if we query the Entity model for the id of the User we created, the Doctrine_Query
will return an instance of User.
// test.php Listing
14-23
// ...
$q = Doctrine_Query::create()
->from('Entity e')
->where('e.id = ?');
$user = $q->fetchOne(array($user->id));
If we do the same thing as above but for the Group record, it will return an instance of
Group.
Listing // test.php
14-24
// ...
$q = Doctrine_Query::create()
->from('Entity e')
->where('e.id = ?');
$group = $q->fetchOne(array($group->id));
The above is possible because of the type column. Doctrine knows which class each record
was created by, so when data is being hydrated it can be hydrated in to the appropriate
sub-class.
Listing $q = Doctrine_Query::create()
14-25
->select('u.id')
->from('User u');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
14-26
e.id AS e__id
FROM entity e
WHERE (e.type = '1')
Notice how the type condition was automatically added to the query so that it will only
return records that are of type User.
Conclusion
Now that we've learned about how to take advantage of PHPs inheritance features with our
models we can move on to learning about Doctrine Behaviors (page 235). This is one of the
most sophisticated and useful features in Doctrine for accomplishing complex models with
small and easy to maintain code.
Chapter 15
Behaviors
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 re-factoring the code is having a base class with
some classes extending it.
However inheritance solves only a fraction of things. The following sections show how using
Doctrine_Template is much more powerful and flexible than using inheritance.
Doctrine_Template is a class template 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.
This chapter describes the usage of various behaviors available for Doctrine. You'll also learn
how to create your own behaviors. In order to grasp the concepts of this chapter you should
be familiar with the theory behind Doctrine_Template and
Doctrine_Record_Generator. We will explain what these classes are shortly.
When referring to behaviors we refer to class packages that use templates, generators and
listeners extensively. All the introduced components in this chapter can be considered core
behaviors, that means they reside at the Doctrine main repository.
Usually behaviors use generators side-to-side with template classes (classes that extend
Doctrine_Template). The common workflow is:
As you may already know templates are used for adding common definitions and options to
record classes. The purpose of generators is much more complex. Usually they are being used
for creating generic record classes dynamically. The definitions of these generic classes
usually depend on the owner class. For example the columns of the AuditLog versioning
class are the columns of the parent class with all the sequence and autoincrement definitions
removed.
Simple Templates
In the following example we define a template called TimestampBehavior. 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 // models/TimestampListener.php
15-1
Listing // models/TimestampBehavior.php
15-2
$this->addListener(new TimestampListener());
}
}
Lets say we have a class called BlogPost that needs the timestamp functionality. All we need
to do is to add actAs() call in the class definition.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
15-4
BlogPost:
actAs: [TimestampBehavior]
columns:
title: string(200)
body: clob
Now when we try and utilize the BlogPost model you will notice that the created and
updated columns were added for you and automatically set when saved:
print_r($blogPost->toArray());
The above described functionality is available via the Timestampable behavior that we
have already talked about. You can go back and read more about it in the Timestampable
(page 245) section of this chapter.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-8
User:
columns:
username: string(255)
password: string(255)
Email:
columns:
address: string
user_id: integer
relations:
User:
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 and 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 // models/UserTemplate.php
15-9
// models/EmailTemplate.php Listing
15-10
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:
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-12
User:
actAs: [UserTemplate]
Email:
actAs: [EmailTemplate]
Now consider the following code snippet. This does NOT work since we haven't yet set any
concrete implementations for the templates.
Listing // test.php
15-13
// ...
$user = new User();
$user->Emails; // throws an exception
The following version works. Notice how we set the concrete implementations for the
templates globally using Doctrine_Manager:
Listing // bootstrap.php
15-14
// ...
$manager->setImpl('UserTemplate', 'User')
->setImpl('EmailTemplate', 'Email');
Now this code will work and won't throw an exception like it did before:
print_r($user->toArray(true));
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 add to our previous example and add some custom methods to the UserTemplate:
// models/UserTemplate.php Listing
15-17
Now take a look at the following code and how we can use it:
if ($user->authenticate('jwage', 'changemte')) {
echo 'Authenticated successfully!';
} else {
echo 'Could not authenticate user!';
}
You can also delegate methods to Doctrine_Table classes just as easily. But, to avoid
naming collisions the methods for table classes must have the string TableProxy appended
to the end of the method name.
Here is an example where we add a new finder method:
// models/UserTemplate.php Listing
15-19
->select('u.username')
->from('User u')
->innerJoin('u.Emails e')
->execute();
}
}
Now we can access that function from the Doctrine_Table object for the User model:
$users = $userTable->findUsersWithEmail();
Each class can consists of multiple templates. If the templates contain similar definitions
the most recently loaded template always overrides the former.
Creating Behaviors
This subchapter provides you the means for creating your own behaviors. Lets say we have
various different Record classes that need to have one-to-many emails. We achieve this
functionality by creating a generic behavior which creates Email classes on the fly.
We start this task by creating a behavior called EmailBehavior with a
setTableDefinition() method. Inside the setTableDefinition() method various
helper methods can be used for easily creating the dynamic record definition. Commonly the
following methods are being used:
}
}
Core Behaviors
For the next several examples using the core behaviors lets delete all our existing schemas
and models from our test environment we created and have been using in the earlier
chapters.
$ rm schema.yml Listing
15-23
$ touch schema.yml
$ rm -rf models/*
Introduction
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 YAML.
In the next several examples we will demonstrate some of the behaviors that come bundled
with Doctrine.
Versionable
Lets create a BlogPost model that we want to have the ability to have versions:
// models/BlogPost.php Listing
15-24
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
15-25
BlogPost:
actAs:
Versionable:
versionColumn: version
className: %CLASS%Version
auditLog: true
deleteVersions: true
columns:
title: string(255)
body: clob
The auditLog option can be used to turn off the audit log history. This is when you want
to maintain a version number but not maintain the data at each version.
Listing // test.php
15-26
// ...
$sql = Doctrine::generateSqlFromArray(array('BlogPost'));
echo $sql[0] . "\n";
echo $sql[1];
Notice how we have 2 additional statements we probably didn't expect to see. The behavior
automatically created a blog_post_version table and related it to blog_post.
Now when we insert or update a BlogPost the version table will store all the old versions of
the record and allow you to revert back at anytime. When you instantiate a BlogPost for the
first time this is what is happening internally:
print_r($blogPost->toArray());
Notice how the value of the version column is 2. This is because we have saved 2 versions
of the BlogPost model. We can easily revert to another version by using the revert()
method that the behavior includes.
$blogPost->revert(1); Listing
15-30
print_r($blogPost->toArray());
Notice how the value of the version column is set to 1 and the title is back to the
original value was set it to when creating the BlogPost.
Timestampable
The Timestampable behavior will automatically add a created_at and updated_at column
and automatically set the values when a record is inserted and updated.
Since it is common to want to know the date a post is made ets expand our BlogPost model
and add the Timestampable behavior to automatically set these dates for us.
// models/BlogPost.php Listing
15-32
{
$this->actAs('Timestampable');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-33
# schema.yml
# ...
BlogPost:
actAs:
# ...
Timestampable:
# ...
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 disabled to true for either of the fields as in the
example below.
Listing ---
15-34
BlogPost:
actAs:
# ...
Timestampable:
created:
name: created_at
type: timestamp
format: Y-m-d H
updated:
disabled: true
# ...
print_r($blogPost->toArray());
Look how the created_at and updated_at values were automatically set for you!
Here is a list of all the options you can use with the Timestampable behavior on the created
side of the behavior:
Here is a list of all the options you can use with the Timestampable behavior on the updated
side of the behavior that are not possible on the created side:
Sluggable
The Sluggable behavior is a nice piece of functionality that will automatically add a column
to your model for storing a unique human readable identifier that can be created from
columns like title, subject, etc. These values can be used for search engine friendly urls.
Lets expand our BlogPost model to use the Sluggable behavior because we will want to
have nice URLs for our posts:
// models/BlogPost.php Listing
15-37
$this->actAs('Sluggable', array(
'unique' => true,
'fields' => array('username'),
'canUpdate' => true
)
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
15-38
# schema.yml
# ...
BlogPost:
actAs:
# ...
Sluggable:
unique: true
fields: [title]
canUpdate: true
# ...
Now look what happens when we create a new post. The slug column will automatically be
set for us:
print_r($blogPost->toArray());
Notice how the value of the slug column was automatically set based on the value of the
title column. When a slug is created, by default it is urlized which means all non-url-
friendly characters are removed and white space is replaced with hyphens(-).
The unique flag will enforce that the slug created is unique. If it is not unique an auto
incremented integer will be appended to the slug before saving to database.
The canUpdate flag will allow the users to manually set the slug value to be used when
building the url friendly slug.
Here is a list of all the options you can use on the Sluggable behavior:
fields array() The fields that are used to build slug value.
uniqueBy array() The fields that make determine a unique
slug.
uniqueIndex true Whether or not to create a unique index.
canUpdate false Whether or not the slug can be updated.
builder array('Doctrine_Inflector', The Class::method() used to build the slug.
'urlize')
indexName sluggable The name of the index to create.
I18n
Doctrine_I18n package is a behavior for Doctrine that provides internationalization
support for record classes. In the following example we have a NewsItem class with two
fields title and content. We want to have the field title with different languages
support. This can be achieved as follows:
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
15-42
NewsItem:
actAs:
I18n:
fields: [title, body]
columns:
title: string(255)
body: clob
Below is a list of all the options you can use with the I18n behavior:
Listing // test.php
15-43
// ...
$sql = Doctrine::generateSqlFromArray(array('NewsItem'));
echo $sql[0] . "\n";
echo $sql[1];
Notice how the field title is not present in the news_item table. Since its present in the
translation table it would be a waste of resources to have that same field in the main table.
Basically Doctrine always automatically removes all translated fields from the main table.
Now the first time you initialize a new NewsItem record Doctrine initializes the behavior that
builds the followings things:
1. Record class called NewsItemTranslation
2. Bi-directional relations between NewsItemTranslation and NewsItem
Lets take a look at how we can manipulate the translations of the NewsItem:
Listing // test.php
15-45
// ...
$newsItem = new NewsItem();
$newsItem->Translation['en']->title = 'some title';
$newsItem->Translation['en']->body = 'test';
$newsItem->Translation['fi']->title = 'joku otsikko';
$newsItem->Translation['fi']->body = 'test';
$newsItem->save();
print_r($newsItem->toArray());
How do we retrieve the translated data now? This is easy! Lets find all items and their finnish
translations:
// test.php Listing
15-47
// ...
$newsItems = Doctrine_Query::create()
->from('NewsItem n')
->leftJoin('n.Translation t')
->where('t.lang = ?')
->execute(array('fi'));
echo $newsItems[0]->Translation['fi']->title;
NestedSet
The NestedSet behavior allows you to turn your models in to a nested set tree structure
where the entire tree structure can be retrieved in one efficient query. It also provided a nice
interface for manipulating the data in your trees.
Lets take a Category model for example where the categories need to be organized in a
hierarchical tree structure:
// models/Category.php Listing
15-49
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-50
# schema.yml
# ...
Category:
actAs:
NestedSet:
hasManyRoots: true
rootColumnName: root_id
columns:
name: string(255)
Listing // test.php
15-51
// ...
$sql = Doctrine::generateSqlFromArray(array('Category'));
echo $sql[0];
Notice how the root_id, lft, rgt and level columns are automatically added. These
columns are used to organize the tree structure and are handled automatically for you
internally.
We won't discuss the NestedSet behavior in 100% detail here. It is a very large behavior so
it has its own dedicated chapter (page 271).
Searchable
The Searchable behavior is a fulltext indexing and searching tool. It can be used for
indexing and searching both database and files.
Imagine we have a Job model for job postings and we want it to be easily searchable:
Listing // models/Job.php
15-53
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
15-54
Job:
actAs:
Searchable:
fields: [title, description]
columns:
title: string(255)
description: clob
// test.php Listing
15-55
// ...
$sql = Doctrine::generateSqlFromArray(array('Job'));
echo $sql[0] . "\n";
echo $sql[1] . "\n";
echo $sql[2];
Notice how the job_index table is automatically created for you and a foreign key
between job and job_index was automatically created.
Because the Searchable behavior is such a large topic, we have more information on this
that can be found in the Searching (page 262) chapter.
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 // models/Zipcode.php
15-57
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-58
# schema.yml
# ...
Zipcode:
actAs: [Geographical]
columns:
zipcode: string(255)
city: string(255)
state: string(2)
county: string(255)
zip_class: string(255)
Listing // test.php
15-59
// ...
$sql = Doctrine::generateSqlFromArray(array('Zipcode'));
echo $sql[0];
longitude DOUBLE,
PRIMARY KEY(id)) ENGINE = INNODB
Notice how the geographical behavior automatically adds the latitude and longitude
columns to the records used for calculating distance between two records. Below you will
find some example usage.
// test.php Listing
15-61
// ...
$zipcode1 = Doctrine::getTable('Zipcode')->findOneByZipcode('37209');
$zipcode2 = Doctrine::getTable('Zipcode')->findOneByZipcode('37388');
Now we can get the distance between those two records by using the getDistance()
method that the behavior provides:
// test.php Listing
15-62
// ...
echo $zipcode1->getDistance($zipcode2, $kilometers = false);
The 2nd argument of the getDistance() method is whether or not to return the distance
in kilometers. The default is false.
Now lets get the 50 closest zipcodes that are not in the same city:
// test.php Listing
15-63
// ...
$q = $zipcode1->getDistanceQuery();
$q->orderby('miles asc')
->addWhere($q->getRootAlias() . '.city != ?', $zipcode1->city)
->limit(50);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
15-64
z.id AS z__id,
z.zipcode AS z__zipcode,
z.city AS z__city,
z.state AS z__state,
z.county AS z__county,
z.zip_class AS z__zip_class,
z.latitude AS z__latitude,
z.longitude AS z__longitude,
((ACOS(SIN(* PI() / 180) * SIN(z.latitude * PI() / 180) + COS(* PI() /
180) * COS(z.latitude * PI() / 180) * COS((- z.longitude) * PI() / 180)) *
180 / PI()) * 60 * 1.1515) AS z__0,
((ACOS(SIN(* PI() / 180) * SIN(z.latitude * PI() / 180) + COS(* PI() /
180) * COS(z.latitude * PI() / 180) * COS((- z.longitude) * PI() / 180)) *
180 / PI()) * 60 * 1.1515 * 1.609344) AS z__1
FROM zipcode z
WHERE z.city != ?
ORDER BY z__0 asc
LIMIT 50
Notice how the above SQL query includes a bunch of SQL that we did not write. This was
automatically added by the behavior to calculate the number of miles between records.
Now we can execute the query and use the calculated number of miles values:
Listing // test.php
15-65
// ...
$result = $q->execute();
Listing // test.php
15-66
// ...
function parseCsvFile($file, $columnheadings = false, $delimiter = ',', $enclosure
= "\"")
{
$row = 1;
$rows = array();
$handle = fopen($file, 'r');
fclose($handle);
return $rows;
}
27. http://www.populardata.com/zip_codes.zip
SoftDelete
The SoftDelete behavior is a very simple yet highly desired model behavior which overrides
the delete() functionality and adds a deleted_at column. When delete() is called,
instead of deleting the record from the database, deleted_at column is set to the current
date and time. Below is an example of how to create a model with the SoftDelete behavior
being used.
// models/SoftDeleteTest.php Listing
15-67
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
15-68
# schema.yml
# ...
SoftDeleteTest:
actAs: [SoftDelete]
columns:
name:
type: string(255)
primary: true
// test.php Listing
15-69
// ...
$sql = Doctrine::generateSqlFromArray(array('SoftDeleteTest'));
echo $sql[0];
You are required to enable DQL callbacks in order for all executed queries to have the dql
callbacks executed on them. In the SoftDelete behavior they are used to filter the select
statements to exclude all records where the deleted_at flag is set with an additional
WHERE condition.
Listing // bootstrap.php
15-71
// ...
$manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
Listing // test.php
15-72
// ...
$record = new SoftDeleteTest();
$record->name = 'new record';
$record->save();
Listing // test.php
15-73
// ...
$record->delete();
print_r($record->toArray());
Also, when we query, any records where deleted_at is not null are excluded from the results:
Listing // test.php
15-75
// ...
$q = Doctrine_Query::create()
->from('SoftDeleteTest t');
echo $q->getSql();
The above call to getSql() would output the following SQL query:
SELECT Listing
15-76
s.name AS s__name,
s.deleted_at AS s__deleted_at
FROM soft_delete_test s
WHERE (s.deleted_at IS NULL)
Notice how the where condition is automatically added to only return the records that have
not been deleted.
// test.php Listing
15-77
// ...
$count = $q->count();
echo $count;
The above would be echo 0 because it would exclude the record saved above because the
delete flag was set.
Nesting Behaviors
Below is an example of several behaviors to give a complete wiki database that is versionable,
searchable, sluggable, and full I18n.
$i18n->addChild($auditLog)
->addChild($search)
->addChild($slug);
$this->actAs($i18n);
$this->actAs('Timestampable');
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-79
WikiTest:
actAs:
I18n:
fields: [title, content]
actAs:
Versionable:
fields: [title, content]
Searchable:
fields: [title, content]
Sluggable:
fields: [title]
columns:
title: string(255)
content: string
The above example of nesting behaviors is currently broken in Doctrine. We are working
furiously to come up with a backwards compatible fix. We will announce when the fix is
ready and update the documentation accordingly.
Generating Files
By default with behaviors the classes which are generated are evaluated at run-time and no
files containing the classes are ever written to disk. This can be changed with a configuration
option. Below is an example of how to configure the I18n behavior to generate the classes and
write them to files instead of evaluating them at run-time.
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
15-81
NewsArticle:
actAs:
I18n:
fields: [title, body]
generateFiles: true
generatePath: /path/to/generate
columns:
title: string(255)
body: string(255)
author: string(255)
Now the behavior will generate a file instead of generating the code and using eval()28 to
evaluate it at runtime.
Doctrine::initializeModels(array('BlogPost')); Listing
15-82
$q = Doctrine_Query::create()
->from('BlogPostTranslation t')
->where('t.id = ? AND t.lang = ?', array(1, 'en'));
$translations = $q->execute();
This is required because the behaviors are not instantiated until the model is instantiated
for the first time. The above initializeModels() method instantiates the passed models
and makes sure the information is properly loaded in to the array of loaded models.
Conclusion
By now we should know a lot about Doctrine behaviors. We should know how to write our
own for our models as well as how to use all the great behaviors that come bundled with
Doctrine.
Now we are ready to move on to discuss the Searchable (page 262) behavior in more detail in
the Searching (page 262) chapter. As it is a large topic we have devoted an entire chapter to it.
28. http://www.php.net/eval
Chapter 16
Searching
Introduction
Searching is a huge topic, hence an entire chapter has been devoted to a behavior called
Searchable. It is a fulltext indexing and searching tool. It can be used for indexing and
searching both database and files.
Consider we have a class called NewsItem with the following definition:
Listing // models/NewsItem.php
16-1
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
16-2
# schema.yml
# ...
NewsItem:
columns:
title: string(255)
body: clob
Now lets say we have an application where users are allowed to search for different news
items, an obvious way to implement this would be building a form and based on that forms
submitted values build DQL queries such as:
Listing // test.php
16-3
// ...
$q = Doctrine_Query::create()
->from('NewsItem i')
->where('n.title LIKE ? OR n.content LIKE ?');
As the application grows these kind of queries become very slow. For example when using the
previous query with parameters '%framework%' and '%framework%' (this would be
equivalent of 'find all news items whose title or content contains word 'framework') the
database would have to traverse through each row in the table, which would naturally be very
very slow.
Doctrine solves this with its search component and inverse indexes. First lets alter our
definition a bit:
// models/NewsItem.php Listing
16-4
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
16-5
# schema.yml
# ...
NewsItem:
actAs:
Searchable:
fields: [title, content]
# ...
// test.php Listing
16-6
// ...
$sql = Doctrine::generateSqlFromArray(array('NewsItem'));
echo $sql[0] . "\n";
echo $sql[1] . "\n";
echo $sql[2];
title VARCHAR(255),
body LONGTEXT,
PRIMARY KEY(id)) ENGINE = INNODB
ALTER TABLE news_item_index ADD FOREIGN KEY (id) REFERENCES news_item(id) ON
UPDATE CASCADE ON DELETE CASCADE
Here we tell Doctrine that NewsItem class acts as searchable (internally Doctrine loads
Doctrine_Template_Searchable) and fields title and content are marked as fulltext
indexed fields. This means that every time a NewsItem is added or updated Doctrine will:
1. Update the inverse search index or
2. Add new pending entry to the inverse search index (sometimes it can be efficient to update
the inverse search index in batches)
Index structure
The structure of the inverse index Doctrine uses is the following:
[ (string) keyword] [ (string) field ] [ (integer) position ] [ (mixed) [foreign_keys] ]
Column Description
keyword The keyword in the text that can be searched for.
field The field where the keyword was found.
position The position where the keyword was found.
[foreign_keys] The foreign keys of the record being indexed.
In the NewsItem example the [foreign_keys] would simply contain one field named id with
foreign key references to NewsItem(id) and with onDelete => CASCADE constraint.
An example row in this table might look something like:
In this example the word database is the third word of the title field of NewsItem with id
of 1.
Index Building
Whenever a searchable record is being inserted into database Doctrine executes the index
building procedure. This happens in the background as the procedure is being invoked by the
search listener. The phases of this procedure are:
1. Analyze the text using a Doctrine_Search_Analyzer based class
2. Insert new rows into index table for all analyzed keywords
Sometimes you may not want to update the index table directly when new searchable entries
are added. Rather you may want to batch update the index table in certain intervals. For
disabling the direct update functionality you'll need to set the batchUpdates option to true
when you attach the behavior:
Listing // models/NewsItem.php
16-8
{
// ...
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
16-9
# schema.yml
# ...
NewsItem:
actAs:
Searchable:
fields: [title, content]
batchUpdates: true
# ...
The actual batch updating procedure can be invoked with the batchUpdateIndex()
method. It takes two optional arguments: limit and offset. Limit can be used for limiting
the number of batch indexed entries while the offset can be used for setting the first entry to
start the indexing from.
First lets insert a new NewsItem records:
// test.php Listing
16-10
// ...
$newsItem = new NewsItem();
$newsItem->title = 'Test';
$newsItem->body = 'test';
$newsItem->save();
If you don't have batch updates enabled then the index will be automatically updated for
you when you insert or update NewsItem records. If you do have batch updates enabled
then you can perform the batch updates by using the following code:
// test.php Listing
16-11
// ...
$newsItemTable = Doctrine::getTable('NewsItem');
$newsItemTable->batchUpdateIndex();
Text Analyzers
By default Doctrine uses Doctrine_Search_Analyzer_Standard for analyzing the text.
This class performs the following things:
• Strips out stop-keywords (such as 'and', 'if' etc.) As many commonly used words
such as 'and', 'if' etc. have no relevance for the search, they are being stripped out
in order to keep the index size reasonable.
• Makes all keywords lowercased. When searching words 'database' and 'DataBase'
are considered equal by the standard analyzer, hence the standard analyzer
lowercases all keywords.
• Replaces all non alpha-numeric marks with whitespace. In normal text many
keywords might contain non alpha-numeric chars after them, for example
'database.'. The standard analyzer strips these out so that 'database' matches
'database.'.
• Replaces all quotation marks with empty strings so that "O'Connor" matches
"oconnor"
You can write your own analyzer class by making a class that implements
Doctrine_Search_Analyzer_Interface. Here is an example where we create an
analyzer named MyAnalyzer:
Listing // models/MyAnalyzer.php
16-12
The search analyzers must only contain one method named analyze() and it should
return the modified inputted text to be indexed.
Listing // test.php
16-13
// ...
$newsItemTable = Doctrine::getTable('NewsItem');
$search = $newsItemTable
->getTemplate('Doctrine_Template_Searchable')
->getPlugin();
Query language
Doctrine_Search provides a query language similar to Apache Lucene. The
Doctrine_Search_Query converts human readable, easy-to-construct search queries to
their complex DQL equivalents which are then converted to SQL like normal.
Performing Searches
Here is a simple example to retrieve the record ids and relevance data.
// test.php Listing
16-14
// ...
$newsItemTable = Doctrine::getTable('NewsItem');
$results = $newsItemTable->search('test');
print_r($results);
SELECT Listing
16-15
COUNT(keyword) AS relevance,
id
FROM article_index
WHERE id IN (SELECT
id
FROM article_index
WHERE keyword = ?)
AND id IN (SELECT
id
FROM article_index
WHERE keyword = ?)
GROUP BY id
ORDER BY relevance DESC
Now you can use those results in another query to retrieve the actual NewsItem objects:
// test.php Listing
16-17
// ...
$ids = array();
foreach ($results as $result) {
$ids[] = $result['id'];
}
$q = Doctrine_Query::create()
->from('NewsItem i')
->whereIn('i.id', $ids);
$newsItems = $q->execute();
print_r($newsItems->toArray());
You can optionally pass the search() function a query object to modify with a where
condition subquery to limit the results using the search index.
Listing // test.php
16-19
// ...
$q = Doctrine_Query::create()
->from('NewsItem i');
$q = Doctrine::getTable('Article')
->search('test', $q);
echo $q->getSql();
The above call to getSql() would output the following SQL query:
Listing SELECT
16-20
n.id AS n__id,
n.title AS n__title,
n.body AS n__body
FROM news_item n
WHERE n.id IN (SELECT
id
FROM news_item_index
WHERE keyword = ?
GROUP BY id)
Now we can execute the query and get the NewsItem objects:
Listing // test.php
16-21
// ...
$newsItems = $q->execute();
print_r($newsItems->toArray());
File searches
As stated before Doctrine_Search can also be used for searching files. Lets say we have a
directory which we want to be searchable. First we need to create an instance of
Doctrine_Search_File which is a child of Doctrine_Search providing some extra
functionality needed for the file searches.
// test.php Listing
16-23
// ...
$search = new Doctrine_Search_File();
Second thing to do is to generate the index table. By default Doctrine names the database
index class as FileIndex.
Lets check the SQL that is generated by the above models created:
// test.php Listing
16-24
// ...
$sql = Doctrine::generateSqlFromArray(array('FileIndex'));
You can create the actual table in the database by using the
Doctrine::createTablesFromArray() method:
// test.php Listing
16-26
// ...
Doctrine::createTablesFromArray(array('FileIndex'));
Now we can start using the file searcher. In this example lets just index the models
directory:
// test.php Listing
16-27
// ...
$search->indexDirectory('models');
The indexDirectory() iterates recursively through given directory and analyzes all files
within it updating the index table as necessary.
Finally we can start searching for pieces of text within the indexed files:
Listing // test.php
16-28
// ...
$results = $search->search('hasColumn');
print_r($results);
Conclusion
Now that we have learned all about the Searchable behavior we are ready to learn in more
detail about the NestedSet behavior in the Hierarchical Data (page 271) chapter. The
NestedSet is a large topic like the Searchable behavior so it got its own dedicated chapter
as well.
Chapter 17
Hierarchical Data
Introduction
Most users at one time or another have dealt with hierarchical data in a SQL database and no
doubt learned that the management of hierarchical data is not what a relational database is
intended for. The tables of a relational database are not hierarchical (like XML), but are
simply a flat list. Hierarchical data has a parent-child relationship that is not naturally
represented in a relational database table.
For our purposes, hierarchical data is a collection of data where each item has a single parent
and zero or more children (with the exception of the root item, which has no parent).
Hierarchical data can be found in a variety of database applications, including forum and
mailing list threads, business organization charts, content management categories, and
product categories.
In a hierarchical data model, data is organized into a tree-like structure. The tree structure
allows repeating information using parent/child relationships. For an explanation of the tree
data structure, see here29.
There are three major approaches to managing tree structures in relational databases, these
are:
• http://www.dbazine.com/oracle/or-articles/tropashko430
• http://dev.mysql.com/tech-resources/articles/hierarchical-data.html31
29. http://en.wikipedia.org/wiki/Tree_data_structure
30. http://www.dbazine.com/oracle/or-articles/tropashko4
31. http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
Nested Set
Introduction
Nested Set is a solution for storing hierarchical data that provides very fast read access.
However, updating nested set trees is more costly. Therefore this solution is best suited for
hierarchies that are much more frequently read than written to. And because of the nature of
the web, this is the case for most web applications.
For more detailed information on the Nested Set, read here:
• http://www.sitepoint.com/article/hierarchical-data-database/232
• http://dev.mysql.com/tech-resources/articles/hierarchical-data.html33
Setting Up
To set up your model as Nested Set, you must add some code to the setUp() method of your
model. Take this Category model below for example:
Listing // models/Category.php
17-1
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
17-2
# schema.yml
# ...
Category:
actAs: [NestedSet]
columns:
name: string(255)
32. http://www.sitepoint.com/article/hierarchical-data-database/2
33. http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
You must never assign values to lft, rgt, level. These are managed transparently by the
nested set implementation.
Multiple Trees
The nested set implementation can be configured to allow your table to have multiple root
nodes, and therefore multiple trees within the same table.
The example below shows how to setup and use multiple roots with the Category model:
// models/Category.php Listing
17-3
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
17-4
# schema.yml
# ...
Category:
actAs:
NestedSet:
hasManyRoots: true
rootColumnName: root_id
columns:
name: string(255)
The rootColumnName is the column used to differentiate between trees. When you create a
new root node you have the option to set the root_id manually, otherwise Doctrine will
assign a value for you.
In general use you do not need to deal with the root_id directly. For example, when you
insert a new node into an existing tree or move a node between trees Doctrine transparently
handles the associated root_id changes for you.
Doctrine_Node_Interface. Tree objects are bound to your table objects and node objects
are bound to your record objects. This looks as follows:
The full tree interface is available by using the following code:
Listing // test.php
17-5
// ...
$treeObject = Doctrine::getTable('Category')->getTree();
Listing // test.php
17-6
// ...
$nodeObject = $category->getNode();
With the above code the full node interface is available on $nodeObject.
In the following sub-chapters you'll see code snippets that demonstrate the most frequently
used operations with the node and tree classes.
// ...
$category = new Category();
$category->name = 'Root Category 1';
$category->save();
$treeObject = Doctrine::getTable('Category')->getTree();
$treeObject->createRoot($category);
Inserting a Node
In the next example we're going to add a new Category instance as a child of the root
Category we created above:
Listing // test.php
17-8
// ...
$child1 = new Category();
$child1->name = 'Child Category 1';
$child1->getNode()->insertAsLastChildOf($category);
$child2->getNode()->insertAsLastChildOf($category);
Deleting a Node
Deleting a node from a tree is as simple as calling the delete() method on the node object:
Listing // test.php
17-9
// ...
The above code calls $category->delete() internally. It's important to delete on the
node and not on the record. Otherwise you may corrupt the tree.
Deleting a node will also delete all descendants of that node. So make sure you move them
elsewhere before you delete the node if you don't want to delete them.
Moving a Node
Moving a node is simple. Doctrine offers several methods for moving nodes around between
trees:
// test.php Listing
17-10
// ...
$category = new Category();
$category->name = 'Root Category 2';
$category->save();
$categoryTable = Doctrine::getTable('Category');
$treeObject = $categoryTable->getTree();
$treeObject->createRoot($category);
• moveAsLastChildOf($other)
• moveAsFirstChildOf($other)
• moveAsPrevSiblingOf($other)
• moveAsNextSiblingOf($other).
Examining a Node
You can examine the nodes and what type of node they are by using some of the following
functions:
// test.php Listing
17-11
// ...
$isLeaf = $category->getNode()->isLeaf();
$isRoot = $category->getNode()->isRoot();
The above used functions return true/false depending on whether or not they are a leaf or
root node.
Listing // test.php
17-12
// ...
$hasNextSib = $category->getNode()->hasNextSibling();
$hasPrevSib = $category->getNode()->hasPrevSibling();
You can also retrieve the next or previous siblings if they exist with the following methods:
Listing // test.php
17-13
// ...
$nextSib = $category->getNode()->getNextSibling();
$prevSib = $category->getNode()->getPrevSibling();
If you want to retrieve an array of all the siblings you can simply use the getSiblings()
method:
Listing // test.php
17-14
// ...
$siblings = $category->getNode()->getSiblings();
Listing // test.php
17-15
// ...
$hasChildren = $category->getNode()->hasChildren();
$hasParent = $category->getNode()->hasParent();
You can retrieve a nodes first and last child by using the following methods:
Listing // test.php
17-16
// ...
$firstChild = $category->getNode()->getFirstChild();
$lastChild = $category->getNode()->getLastChild();
Listing // test.php
17-17
// ...
$parent = $category->getNode()->getParent();
You can get the children of a node by using the following method:
// test.php Listing
17-18
// ...
$children = $category->getNode()->getChildren();
The getChildren() method returns only the direct descendants. If you want all
descendants, use the getDescendants() method.
You can get the descendants or ancestors of a node by using the following methods:
// test.php Listing
17-19
// ...
$descendants = $category->getNode()->getDescendants();
$ancestors = $category->getNode()->getAncestors();
Sometimes you may just want to get the number of children or descendants. You can use the
following methods to accomplish this:
// test.php Listing
17-20
// ...
$numChildren = $category->getNode()->getNumberChildren();
$numDescendants = $category->getNode()->getNumberDescendants();
The getDescendants() and getAncestors() both accept a parameter that you can use to
specify the depth of the resulting branch. For example getDescendants(1) retrieves only
the direct descendants (the descendants that are 1 level below, that's the same as
getChildren()). In the same fashion getAncestors(1) would only retrieve the direct
ancestor (the parent), etc. getAncestors() can be very useful to efficiently determine the
path of this node up to the root node or up to some specific ancestor (i.e. to construct a
breadcrumb navigation).
The next example assumes you have hasManyRoots set to false so in order for the below
example to work properly you will have to set that option to false. We set the value to true
in a earlier section.
// test.php Listing
17-21
// ...
$treeObject = Doctrine::getTable('Category')->getTree();
$tree = $treeObject->fetchTree();
Advanced Usage
The previous sections have explained the basic usage of Doctrine's nested set
implementation. This section will go one step further.
Listing // test.php
17-22
// ...
$q = Doctrine_Query::create();
->select('c.name, p.name, m.name')
->from('Category c')
->leftJoin('c.HottestProduct p')
->leftJoin('p.Manufacturer m');
Now we need to set the above query as the base query for the tree:
There it is, the tree with all the related data you need, all in one query.
If you don't set your own base query then one will be automatically created for you
internally.
When you are done it is a good idea to reset the base query back to normal:
Listing // test.php
17-24
// ...
$treeObject->resetBaseQuery();
You can take it even further. As mentioned in the chapter Improving Performance (page 354)
you should only fetch objects when you need them. So, if we need the tree only for display
purposes (read-only) we can use the array hydration to speed things up a bit:
Listing // test.php
17-25
// ...
$q = Doctrine_Query::create();
->select('c.name, p.name, m.name')
->from('Category c')
->leftJoin('c.HottestProduct p')
->leftJoin('p.Manufacturer m')
->setHydrationMode(Doctrine::HYDRATE_ARRAY);
$treeObject = Doctrine::getTable('Category')->getTree();
$treeObject->setBaseQuery($q);
$tree = $treeObject->fetchTree();
$treeObject->resetBaseQuery();
Now you got a nicely structured array in $tree and if you use array access on your records
anyway, such a change will not even effect any other part of your code. This method of
modifying the query can be used for all node and tree methods (getAncestors(),
getDescendants(), getChildren(), getParent(), ...). Simply create your query, set it
as the base query on the tree object and then invoke the appropriate method.
// test.php Listing
17-26
// ...
$treeObject = Doctrine::getTable('Category')->getTree();
$rootColumnName = $treeObject->getAttribute('rootColumnName');
After doing all the examples above the code above should render as follows:
Conclusion
Now that we have learned all about the NestedSet behavior and how to manage our
hierarchical data using Doctrine we are ready to learn about Data Fixtures (page 280). Data
fixtures are a great tool for loading small sets of test data in to your applications to be used
for unit and functional tests or to populate your application with its initial data.
Chapter 18
Data Fixtures
Data fixtures are meant for loading small sets of test data through your models to populate
your database with data to test against. The data fixtures are often used side by side with
some kind of unit/functional testing suite.
Importing
Importing data fixtures is just as easy as dumping. You can use the loadData() function:
Listing Doctrine::loadData('/path/to/data.yml');
18-1
You can either specify an individual yml file like we have done above, or you can specify an
entire directory:
Listing Doctrine::loadData('/path/to/directory');
18-2
If you want to append the imported data to the already existing data then you need to use the
second argument of the loadData() function. If you don't specify the second argument as
true then the data will be purged before importing.
Here is how you can append instead of purging:
Dumping
You can dump data to fixtures file in many different formats to help you get started with
writing your data fixtures. You can dump your data fixtures to one big YAML file like the
following:
Listing Doctrine::dumpData('/path/to/data.yml');
18-4
Or you can optionally dump all data to individual files. One YAML file per model like the
following:
Implement
Now that we know a little about data fixtures lets implement them in to our test environment
we created and have been using through the previous chapters so that we can test the
example fixtures used in the next sections.
First create a directory in your doctrine_test directory named fixtures and create a file
named data.yml inside:
Now we need to just modify our generate.php script to include the code for loading the
data fixtures. Add the following code to the bottom of generate.php:
// generate.php Listing
18-7
// ...
Doctrine::loadData('fixtures');
Writing
You can write your fixtures files manually and load them in to your applications. Below is a
sample data.yml fixtures file. You can also split your data fixtures file up in to multiple files.
Doctrine will read all fixtures files and parse them, then load all data.
For the next several examples we will use the following models:
// models/Resouce.php Listing
18-8
// models/ResourceType.php
// models/Tag.php
// models/ResourceTag.php
// models/Category.php
)
);
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
18-9
# schema.yml
Resource:
columns:
name: string(255)
resource_type_id: integer
relations:
Type:
class: ResourceType
foreignAlias: Resources
Tags:
class: Tag
refClass: ResourceTag
foreignAlias: Resources
ResourceType:
columns:
name: string(255)
Tag:
columns:
name: string(255)
ResourceTag:
columns:
resource_id: integer
tag_id: integer
Category:
actAs: [NestedSet]
columns:
name: string(255)
Article:
actAs:
I18n:
fields: [title, body]
columns:
title: string(255)
body: clob
All row keys across all YAML data fixtures must be unique. For example below tutorial,
doctrine, help, cheat are all unique.
Listing ---
18-10
# fixtures/data.yml
Resource:
Resource_1:
name: Doctrine Video Tutorial
Type: Video
Tags: [tutorial, doctrine, help]
Resource_2:
name: Doctrine Cheat Sheet
Type: Image
Tags: [tutorial, cheat, help]
ResourceType:
Video:
name: Video
Image:
name: Image
Tag:
tutorial:
name: tutorial
doctrine:
name: doctrine
help:
name: help
cheat:
name: cheat
You could optionally specify the Resources each tag is related to instead of specifying the
Tags a Resource has.
Listing ---
18-11
# fixtures/data.yml
# ...
Tag:
tutorial:
name: tutorial
Resources: [Resource_1, Resource_2]
doctrine:
name: doctrine
Resources: [Resource_1]
help:
name: help
Resources: [Resource_1, Resource_2]
cheat:
name: cheat
Resources: [Resource_1]
--- Listing
18-12
# fixtures/data.yml
# ...
Category:
Category_1:
name: Categories # the root node
children:
Category_2:
name: Category 1
Category_3:
name: Category 2
children:
Category_4:
name: Subcategory of Category 2
When writing data fixtures for the NestedSet you must either specify at least a children
element of the first data block or specify NestedSet: true under the model which is a
NestedSet in order for the data fixtures to be imported using the NestedSet api.
--- Listing
18-13
# fixtures/data.yml
# ...
Category:
NestedSet: true
Category_1:
name: Categories
# ...
Or simply specifying the children keyword will make the data fixtures importing using the
NestedSet api.
Listing ---
18-14
# fixtures/data.yml
# ...
Category:
Category_1:
name: Categories
children: []
# ...
If you don't use one of the above methods then it is up to you to manually specify the lft, rgt
and level values for your nested set records.
Listing ---
18-15
# fixtures/data.yml
# ...
Article:
Article_1:
Translation:
en:
title: Title of article
body: Body of article
fr:
title: French title of article
body: French body of article
Conclusion
By now we should be able to write and load our own data fixtures in our application. So, now
we will move on to learning about the underlying Database Abstraction Layer (page 287) in
Doctrine. This layer is what makes all the previously discussed functionality possible. You can
use this layer standalone apart from the ORM. In the next chapter we'll explain how you can
use the DBAL by itself.
Chapter 19
The Doctrine Database Abstraction Layer is the underlying framework that the ORM uses to
communicate with the database and send the appropriate SQL depending on which database
type you are using. It also has the ability to query the database for information like what table
a database has or what fields a table has. This is how Doctrine is able to generate your
models from existing databases so easily.
This layer can be used independently of the ORM. This might be of use for example if you
have an existing application that uses PDO directly and you want to port it to use the Doctrine
Connections and DBAL. At a later phase you could begin to use the ORM for new things and
rewrite old pieces to use the ORM.
The DBAL is composed of a few different modules. In this chapter we will discuss the
different modules and what their jobs are.
Export
The Export module provides methods for managing database structure. The methods can be
grouped based on their responsibility: create, edit (alter or update), list or delete (drop)
database elements. The following document lists the available methods, providing examples
of their use.
Introduction
Every schema altering method in the Export module has an equivalent which returns the SQL
that is used for the altering operation. For example createTable() executes the query /
queries returned by createTableSql().
In this chapter the following tables will be created, altered and finally dropped, in a database
named events_db:
events
people
event_participants
Creating Databases
It is simple to create new databases with Doctrine. It is only a matter of calling the
createDatabase() function with an argument that is the name of the database to create.
Listing // test.php
19-1
// ...
$conn->export->createDatabase('events_db');
Now lets change the connection in our bootstrap.php file to connect to the new
events_db:
Listing // bootstrap.php
19-2
/**
* Bootstrap Doctrine.php, register autoloader and specify
* configuration attributes
*/
// ...
$conn = Doctrine_Manager::connection('mysql://root:@localhost/events_db',
'doctrine');
// ...
Creating Tables
Now that the database is created and we've re-configured our connection, we can proceed
with adding some tables. The method createTable() takes three parameters: the table
name, an array of field definition and some extra options (optional and RDBMS-specific).
Now lets create the events table:
Listing // test.php
19-3
//
$definition = array(
'id' => array(
'type' => 'integer',
'primary' => true,
'autoincrement' => true
),
'name' => array(
'type' => 'string',
'length' => 255
),
'datetime' => array(
'type' => 'timestamp'
)
);
$conn->export->createTable('events', $definition);
The keys of the definition array are the names of the fields in the table. The values are arrays
containing the required key type as well as other keys, depending on the value of type. The
values for the type key are the same as the possible Doctrine datatypes. Depending on the
datatype, the other options may vary.
// test.php Listing
19-4
// ...
$definition = array(
'id' => array(
'type' => 'integer',
'primary' => true,
'autoincrement' => true
),
'name' => array(
'type' => 'string',
'length' => 255
)
);
$conn->export->createTable('people', $definition);
You can also specify an array of options as the third argument to the createTable()
method:
// test.php Listing
19-5
// ...
$options = array(
'comment' => 'Repository of people',
'character_set' => 'utf8',
// ...
Listing // test.php
19-6
// ...
$options = array(
'foreignKeys' => array(
'events_id_fk' => array(
'local' => 'event_id',
'foreign' => 'id',
'foreignTable' => 'events',
'onDelete' => 'CASCADE',
)
),
'primary' => array('event_id', 'person_id'),
);
$definition = array(
'event_id' => array(
'type' => 'integer',
'primary' => true
),
'person_id' => array(
'type' => 'integer',
'primary' => true
),
);
In the above example notice how we omit a foreign key for the person_id. In that example
we omit it so we can show you how to add an individual foreign key to a table in the next
example. Normally it would be best to have both foreign keys defined on the in the
foreignKeys.
Now lets add the missing foreign key in the event_participants table the on person_id
column:
Listing // test.php
19-7
// ...
$definition = array('local' => 'person_id',
'foreign' => 'id',
'foreignTable' => 'people',
'onDelete' => 'CASCADE');
$conn->export->createForeignKey('event_participants', $definition);
Altering table
Doctrine_Export drivers provide an easy database portable way of altering existing database
tables.
// test.php Listing
19-8
// ...
$alter = array(
'add' => array(
'new_column' => array(
'type' => 'string',
'length' => 255
),
'new_column2' => array(
'type' => 'string',
'length' => 255
)
)
);
The above call to alterTableSql() would output the following SQL query:
If you only want execute generated sql and not return it, use the alterTable() method.
// test.php Listing
19-10
// ...
$conn->export->alterTable('events', $alter);
The alterTable() method requires two parameters and has an optional third:
The types of changes that are currently supported are defined as follows:
Change Description
name New name for the table.
add Associative array with the names of fields to be added as indexes of the array. The
value of each entry of the array should be set to another associative array with the
properties of the fields to be added. The properties of the fields should be the same
as defined by the Doctrine parser.
remove Associative array with the names of fields to be removed as indexes of the array.
Currently the values assigned to each entry are ignored. An empty array should be
used for future compatibility.
rename Associative array with the names of fields to be renamed as indexes of the array.
The value of each entry of the array should be set to another associative array with
the entry named name with the new field name and the entry named Declaration
that is expected to contain the portion of the field declaration already in DBMS
specific SQL code as it is used in the CREATE TABLE statement.
change Associative array with the names of the fields to be changed as indexes of the
array. Keep in mind that if it is intended to change either the name of a field and
any other properties, the change array entries should have the new names of the
fields as array indexes.
The value of each entry of the array should be set to another associative array with the
properties of the fields to that are meant to be changed as array entries. These entries should
be assigned to the new values of the respective properties. The properties of the fields should
be the same as defined by the Doctrine parser.
Listing // test.php
19-11
// ...
$alter = array('name' => 'event',
'add' => array(
'quota' => array(
'type' => 'integer',
'unsigned' => 1
)
),
'remove' => array(
'new_column2' => array()
),
'change' => array(
'name' => array(
'length' => '20',
'definition' => array(
'type' => 'string',
'length' => 20
)
)
),
'rename' => array(
'new_column' => array(
'name' => 'gender',
'definition' => array(
'type' => 'string',
'length' => 1,
'default' => 'M'
)
)
)
);
$conn->export->alterTable('events', $alter);
Notice how we renamed the table to event, lets rename it back to events. We only
renamed it to demonstrate the functionality and we will need the table to be named
events for the next examples.
// test.php Listing
19-12
// ...
$alter = array(
'name' => 'events'
);
$conn->export->alterTable('event', $alter);
Creating Indexes
To create an index, the method createIndex() is used, which has similar signature as
createConstraint(), so it takes table name, index name and a definition array. The
definition array has one key named fields with a value which is another associative array
containing fields that will be a part of the index. The fields are defined as arrays with possible
keys: sorting, with values ascending and descending length, integer value
Not all RDBMS will support index sorting or length, in these cases the drivers will ignore
them. In the test events database, we can assume that our application will show events
occuring in a specific timeframe, so the selects will use the datetime field in WHERE
conditions. It will help if there is an index on this field.
// test.php Listing
19-13
// ...
$definition = array(
'fields' => array(
'datetime' => array()
)
);
// test.php Listing
19-14
// ...
try {
$conn->export->dropSequence('nonexisting');
} catch(Doctrine_Exception $e) {
Listing // test.php
19-15
// ...
$conn->export->dropConstraint('events', 'PRIMARY', true);
The third parameter gives a hint that this is a primary key constraint.
Listing // test.php
19-16
// ...
$conn->export->dropConstraint('event_participants', 'event_id');
It is recommended to not actually execute the next two examples. In the next section we
will need the events_db to be intact for our examples to work.
Listing // test.php
19-18
// ...
$conn->export->dropTable('events');
Listing // test.php
19-19
// ...
$conn->export->dropDatabase('events_db');
Import
The import module allows you to inspect a the contents of a database connection and learn
about the databases and schemas in each database.
Introduction
To see what's in the database, you can use the list*() family of functions in the Import
module.
Name Description
listDatabases() List the databases
listFunctions() List the available functions.
Below you will find examples on how to use the above listed functions:
Listing Databases
// test.php Listing
19-20
// ...
$databases = $conn->import->listDatabases();
print_r($databases);
Listing Sequences
// test.php Listing
19-21
// ...
$sequences = $conn->import->listSequences('events_db');
print_r($sequences);
Listing Constraints
// test.php Listing
19-22
// ...
$constraints = $conn->import->listTableConstraints('event_participants');
print_r($constraints);
// ...
$columns = $conn->import->listTableColumns('events');
print_r($columns);
// ...
$indexes = $conn->import->listTableIndexes('events');
print_r($indexes);
Listing Tables
Listing $tables = $conn->import->listTables();
19-25
print_r($tables);
Listing Views
$sql = "CREATE VIEW last_ten_events AS SELECT * FROM events ORDER BY id DESC LIMIT
0,10";
$conn->exec($sql);
DataDict
Introduction
Doctrine uses the DataDict module internally to convert native RDBMS types to Doctrine
types and the reverse. DataDict module uses two methods for the conversions:
// ...
$declaration = $conn->dataDict->getPortableDeclaration('VARCHAR(255)');
print_r($declaration);
// ...
$portableDeclaration = array(
'type' => 'string',
'length' => 20,
'fixed' => true
);
$nativeDeclaration = $conn->dataDict->getNativeDeclaration($portableDeclaration);
echo $nativeDeclaration;
Drivers
Mysql
// test.php Listing
19-32
// ...
$fields = array(
'id' => array(
'type' => 'integer',
'autoincrement' => true
),
'name' => array(
'type' => 'string',
'fixed' => true,
'length' => 8
)
);
Conclusion
This chapter is indeed a nice one. The Doctrine DBAL is a great tool all by itself. It is probably
one of the most fully featured and easy to use PHP database abstraction layers available
today.
Now we are ready to move on and learn about how to use Transactions (page 299).
Chapter 20
Transactions
Introduction
A database transaction is a unit of interaction with a database management system or similar
system that is treated in a coherent and reliable way independent of other transactions that
must be either entirely completed or aborted. Ideally, a database system will guarantee all of
the ACID (Atomicity, Consistency, Isolation, and Durability) properties for each transaction.
• Atomicity34 refers to the ability of the DBMS to guarantee that either all of the tasks
of a transaction are performed or none of them are. The transfer of funds can be
completed or it can fail for a multitude of reasons, but atomicity guarantees that one
account won't be debited if the other is not credited as well.
• Consistency35 refers to the database being in a legal state when the transaction
begins and when it ends. This means that a transaction can't break the rules, or
integrity constraints, of the database. If an integrity constraint states that all
accounts must have a positive balance, then any transaction violating this rule will
be aborted.
• Isolation36 refers to the ability of the application to make operations in a transaction
appear isolated from all other operations. This means that no operation outside the
transaction can ever see the data in an intermediate state; a bank manager can see
the transferred funds on one account or the other, but never on both - even if she
ran her query while the transfer was still being processed. More formally, isolation
means the transaction history (or schedule37) is serializable38. For performance
reasons, this ability is the most often relaxed constraint. See the isolation39 article
for more details.
• Durability40 refers to the guarantee that once the user has been notified of success,
the transaction will persist, and not be undone. This means it will survive system
failure, and that the database system41 has checked the integrity constraints and
won't need to abort the transaction. Typically, all transactions are written into a
log42 that can be played back to recreate the system to its state right before the
failure. A transaction can only be deemed committed after it is safely in the log.
34. http://en.wikipedia.org/wiki/Atomicity
35. http://en.wikipedia.org/wiki/Database_consistency
36. http://en.wikipedia.org/wiki/Isolation_%28computer_science%29
37. http://en.wikipedia.org/wiki/Schedule_%28computer_science%29
38. http://en.wikipedia.org/wiki/Serializability
39. http://en.wikipedia.org/wiki/Isolation_%28computer_science%29
40. http://en.wikipedia.org/wiki/Durability_%28computer_science%29
41. http://en.wikipedia.org/wiki/Database_system
42. http://en.wikipedia.org/wiki/Database_log
In Doctrine all operations are wrapped in transactions by default. There are some things that
should be noticed about how Doctrine works internally:
Listing $conn->beginTransaction();
20-1
$user = Doctrine::getTable('User')->find(5);
$user->name = 'Modified user';
$user->save();
Now we can commit all the queries by using the commit() method:
Listing $conn->commit();
20-3
Nesting
You can easily nest transactions with the Doctrine DBAL. Check the code below for a simple
example demonstrating nested transactions.
First lets create a standard PHP function named saveUserAndGroup():
$user->save();
$group->save();
$conn->commit();
}
Listing try {
20-5
$conn->beginTransaction();
saveUserAndGroup($conn,$user,$group);
saveUserAndGroup($conn,$user2,$group2);
saveUserAndGroup($conn,$user3,$group3);
$conn->commit();
} catch(Doctrine_Exception $e) {
$conn->rollback();
}
Notice how the three calls to saveUserAndGroup() are wrapped in a transaction, and
each call to the function starts its own nested transaction.
Savepoints
Doctrine supports transaction savepoints. This means you can set named transactions and
have them nested.
The Doctrine_Transaction::beginTransaction($savepoint) sets a named
transaction savepoint with a name of $savepoint. If the current transaction has a savepoint
with the same name, the old savepoint is deleted and a new one is set.
try { Listing
20-6
$conn->beginTransaction();
// do some operations here
$conn->commit('mysavepoint');
} catch(Exception $e) {
$conn->rollback('mysavepoint');
}
$conn->commit();
} catch(Exception $e) {
$conn->rollback();
}
Mysql, for example, does not release the row locks that were stored in memory after the
savepoint.
Savepoints that were set at a later time than the named savepoint are deleted.
The Doctrine_Transaction::commit($savepoint) removes the named savepoint from
the set of savepoints of the current transaction.
All savepoints of the current transaction are deleted if you execute a commit or if a rollback is
being called without savepoint name parameter.
try { Listing
20-7
$conn->beginTransaction();
// do some operations here
Isolation Levels
A transaction isolation level sets the default transactional behavior. As the name 'isolation
level' suggests, the setting determines how isolated each transation is, or what kind of locks
are associated with queries inside a transaction. The four available levels are (in ascending
order of strictness):
READ UNCOMMITTED
Barely transactional, this setting allows for so-called 'dirty reads', where queries inside
one transaction are affected by uncommitted changes in another transaction.
READ COMMITTED
Committed updates are visible within another transaction. This means identical queries
within a transaction can return differing results. This is the default in some DBMS's.
REPEATABLE READ
Within a transaction, all reads are consistent. This is the default of Mysql INNODB
engine.
SERIALIZABLE
Updates are not permitted in other transactions if a transaction has run an ordinary
SELECT query.
Listing $tx->setIsolation('SERIALIZABLE');
20-10
Some drivers (like Mysql) support the fetching of current transaction isolation level. It can
be done as follows:
Conclusion
Transactions are a great feature for ensuring the quality and consistency of your database.
Now that you know about transactions we are ready to move on and learn about the events
sub-framework.
The events sub-framework is a great feature that allows you to hook in to core methods of
Doctrine and alter the operations of internal functionality without modifying one line of core
code.
Chapter 21
Event Listeners
Introduction
Doctrine provides flexible event listener architecture that not only allows listening for
different events but also for altering the execution of the listened methods.
There are several different listeners and hooks for various Doctrine components. Listeners
are separate classes whereas hooks are empty template methods within the listened class.
Hooks are simpler than event listeners but they lack the separation of different aspects. An
example of using Doctrine_Record hooks:
Listing // models/BlogPost.php
21-1
By now we have defined lots of models so you should be able to define your own
setTableDefinition() for the BlogPost model or even create your own custom model!
Now you can use the above model with the following code assuming we added a title, body
and created column to the model:
Listing // test.php
21-2
// ...
$blog = new BlogPost();
$blog->title = 'New title';
$blog->body = 'Some content';
$blog->save();
echo $blog->created;
The above example will output the current date as PHP knows it.
Each listener and hook method takes one parameter Doctrine_Event object.
Doctrine_Event object holds information about the event in question and can alter the
execution of the listened method.
For the purposes of this documentation many method tables are provided with column named
params indicating names of the parameters that an event object holds on given event. For
example the preCreateSavepoint event has one parameter with the name of the created
savepoint, which is quite intuitively named as savepoint.
Connection Listeners
Connection listeners are used for listening the methods of Doctrine_Connection and its
modules (such as Doctrine_Transaction). All listener methods take one argument
Doctrine_Event which holds information about the listened event.
}
}
Note that by declaring a class that extends Doctrine_EventListener you don't have to
define all the methods within the Doctrine_EventListener_Interface. This is due to a
fact that Doctrine_EventListener already has empty skeletons for all these methods.
Sometimes it may not be possible to define a listener that extends
Doctrine_EventListener (you might have a listener that inherits some other base class).
In this case you can make it implement Doctrine_EventListener_Interface.
All listener methods must be defined here otherwise PHP throws fatal error.
The third way of creating a listener is a very elegant one. You can make a class that
implements Doctrine_Overloadable. This interface has only one method: __call(),
which can be used for catching *all* the events.
Attaching listeners
You can attach the listeners to a connection with setListener().
Aliasing Listeners
You can optionally alias a listener when adding it. This is useful if you register the same
listener multiple times and want to have a unique alias to reference it by. Below you will
examples of how to utilize this feature.
}
}
--- Listing
21-9
User:
#...
listeners:
FooListener:
class: My_Listener_FooListener
Enabling/Disabling Listeners
If you wish to enable and disable existing listeners this is possible by using the disabled
option of the listeners. Below you will find a simple example.
Disable all record listeners
Doctrine::getTable('Foo')->getRecordListener()->setOption('disabled', Listing
21-11
array('preSerialize', 'postHydrate'));
Doctrine::getTable('Foo')->getRecordListener()->get('FooListener')->setOption('disabled',
Listing
21-12
true);
Doctrine::getTable('Foo')->getRecordListener()->get('FooListener')->setOption('disabled',
Listing
21-13
array('preSave', 'postInsert'));
Transaction Listeners
All of the below listeners are invoked in the Doctrine_Transaction class. And they are all
passed an instance of Doctrine_Event.
postTransactionCommit() commit()
preCreateSavepoint() createSavepoint() savepoint
postCreateSavepoint() createSavepoint() savepoint
preRollbackSavepoint() rollbackSavepoint() savepoint
postRollbackSavepoint() rollbackSavepoint() savepoint
preReleaseSavepoint() releaseSavepoint() savepoint
postReleaseSavepoint() releaseSavepoint() savepoint
Hydration Listeners
The hydration listeners can be used for listening to resultset hydration procedures. Two
methods exist for listening to the hydration procedure: preHydrate() and postHydrate().
If you set the hydration listener on connection level the code within the preHydrate() and
postHydrate() blocks will be invoked by all components within a multi-component
resultset. However if you add a similar listener on table level it only gets invoked when the
data of that table is being hydrated.
Consider we have a class called User with the following fields: first_name, last_name and
age. In the following example we create a listener that always builds a generated field called
full_name based on first_name and last_name fields.
// test.php Listing
21-15
// ...
class HydrationListener extends Doctrine_Record_Listener
{
public function preHydrate(Doctrine_Event $event)
{
$data = $event->data;
Now all we need to do is attach this listener to the User record and fetch some users:
// test.php Listing
21-16
// ...
$userTable = Doctrine::getTable('User');
$userTable->addRecordListener(new HydrationListener());
$q = Doctrine_Query::create()
->from('User');
$users = $q->execute();
Record Listeners
Doctrine_Record provides listeners very similar to Doctrine_Connection. You can set
the listeners at global, connection and table level.
Here is a list of all available listener methods:
All of the below listeners are invoked in the Doctrine_Record and Doctrine_Validator
classes. And they are all passed an instance of Doctrine_Event.
Methods Listens
preSave() save()
postSave() save()
preUpdate() save() when the record state is DIRTY
postUpdate() save() when the record state is DIRTY
preInsert() save() when the record state is TDIRTY
postInsert() save() when the record state is TDIRTY
preDelete() delete()
postDelete() delete()
preValidate() validate()
postValidate() validate()
Just like with connection listeners there are three ways of defining a record listener: by
extending Doctrine_Record_Listener, by implementing
Doctrine_Record_Listener_Interface or by implementing Doctrine_Overloadable.
In the following we'll create a global level listener by implementing
Doctrine_Overloadable:
Note that by adding a manager level listener it affects on all connections and all tables /
records within these connections. In the following we create a connection level listener:
Many times you want the listeners to be table specific so that they only apply on the actions
on that given table.
Here is an example:
Record Hooks
All of the below listeners are invoked in the Doctrine_Record and Doctrine_Validator
classes. And they are all passed an instance of Doctrine_Event.
Methods Listens
preSave() save()
postSave() save()
preUpdate() save() when the record state is DIRTY
postUpdate() save() when the record state is DIRTY
preInsert() save() when the record state is TDIRTY
postInsert() save() when the record state is TDIRTY
preDelete() delete()
postDelete() delete()
preValidate() validate()
postValidate() validate()
Here is a simple example where we make use of the preInsert() and preUpdate()
methods:
DQL Hooks
Doctrine allows you to attach record listeners globally, on each connection, or on specific
record instances. Doctrine_Query implements preDql*() hooks which are checked for on
any attached record listeners and checked for on the model instance itself whenever a query
is executed. The query will check all models involved in the from part of the query for any
hooks which can alter the query that invoked the hook.
Here is a list of the hooks you can use with DQL:
Methods Listens
preDqlSelect() from()
preDqlUpdate() update()
preDqlDelete() delete()
Below is an example record listener attached directly to the model which will implement the
SoftDelete functionality for the User model.
/**
* Implement postDelete() hook and set the deleted flag to true
*
* @param Doctrine_Event $event
* @return void
*/
public function postDelete(Doctrine_Event $event)
{
$name = $this->_options['name'];
$event->getInvoker()->$name = true;
$event->getInvoker()->save();
}
/**
* Implement preDqlDelete() hook and modify a dql delete query so it updates
the deleted flag
* instead of deleting the record
*
* @param Doctrine_Event $event
* @return void
*/
public function preDqlDelete(Doctrine_Event $event)
{
$params = $event->getParams();
$field = $params['alias'] . '.deleted';
$q = $event->getQuery();
if ( ! $q->contains($field)) {
$q->from('')->update($params['component'] . ' ' . $params['alias']);
$q->set($field, '?', array(false));
$q->addWhere($field . ' = ?', array(true));
}
}
/**
* Implement preDqlDelete() hook and add the deleted flag to all queries for
which this model
* is being used in.
*
* @param Doctrine_Event $event
* @return void
*/
public function preDqlSelect(Doctrine_Event $event)
{
$params = $event->getParams();
$field = $params['alias'] . '.deleted';
$q = $event->getQuery();
if ( ! $q->contains($field)) {
$q->addWhere($field . ' = ?', array(false));
}
}
}
All of the above methods in the listener could optionally be placed in the user class below.
Doctrine will check there for the hooks as well:
In order for these dql callbacks to be checked, you must explicitly turn them on. Because this
adds a small amount of overhead for each query, we have it off by default. We already
enabled this attribute in an earlier chapter.
Here it is again to refresh your memory:
Listing // bootstrap.php
21-26
// ...
$manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
Now when you interact with the User model it will take in to account the deleted flag:
Delete user through record instance:
The above call to $user->delete() does not actually delete the record instead it sets the
deleted flag to true.
Listing $q = Doctrine_Query::create()
21-28
->from('User u');
echo $q->getSql();
Listing SELECT
21-29
u.id AS u__id,
u.username AS u__username,
u.password AS u__password,
u.deleted AS u__deleted
FROM user u
WHERE u.deleted = ?
Notice how the "u.deleted = ?" was automatically added to the where condition with a
parameter value of true.
Chaining Listeners
Doctrine allows chaining of different event listeners. This means that more than one listener
can be attached for listening the same events. The following example attaches two listeners
for given connection:
In this example Debugger and Logger both inherit Doctrine_EventListener:
Event Codes
Doctrine_Event uses constants as event codes. Below is the list of all available event
constants:
• Doctrine_Event::CONN_QUERY
• Doctrine_Event::CONN_EXEC
• Doctrine_Event::CONN_PREPARE
• Doctrine_Event::CONN_CONNECT
• Doctrine_Event::STMT_EXECUTE
• Doctrine_Event::STMT_FETCH
• Doctrine_Event::STMT_FETCHALL
• Doctrine_Event::TX_BEGIN
• Doctrine_Event::TX_COMMIT
• Doctrine_Event::TX_ROLLBACK
• Doctrine_Event::SAVEPOINT_CREATE
• Doctrine_Event::SAVEPOINT_ROLLBACK
• Doctrine_Event::SAVEPOINT_COMMIT
• Doctrine_Event::RECORD_DELETE
• Doctrine_Event::RECORD_SAVE
• Doctrine_Event::RECORD_UPDATE
• Doctrine_Event::RECORD_INSERT
• Doctrine_Event::RECORD_SERIALIZE
• Doctrine_Event::RECORD_UNSERIALIZE
• Doctrine_Event::RECORD_DQL_SELECT
• Doctrine_Event::RECORD_DQL_DELETE
• Doctrine_Event::RECORD_DQL_UPDATE
Here are some examples of hooks being used and the code that is returned:
$event->skipOperation();
}
}
Query
$event->skipNextListener();
}
}
Conclusion
Event listeners are a great feature in Doctrine and combined with Behaviors (page 235) they
can provide some very complex functionality with a minimal amount of code.
Now we are ready to move on to discuss the best feature in Doctrine for improving
performance, Caching (page 318).
Chapter 22
Caching
Introduction
Doctrine_Cache offers an intuitive and easy-to-use query caching solution. It provides the
following things:
• Multiple cache backends to choose from (including Memcached, APC and Sqlite)
• Advanced options for fine-tuning. Doctrine_Cache has many options for fine-
tuning performance.
Listing // bootstrap.php
22-1
// ...
$options = array();
$cacheDriver = new Doctrine_Cache_Memcache($options);
Each driver has its own possible values for the $options array.
Drivers
Memcache
Memcache driver stores cache records into a memcached server. Memcached is a high-
performance, distributed memory object caching system. In order to use this backend, you
need a memcached daemon and the memcache PECL extension.
You can instantiate the Memcache cache driver with the following code:
Listing // bootstrap.php
22-2
// ...
$servers = array(
'host' => 'localhost',
'port' => 11211,
'persistent' => true
);
APC
The Alternative PHP Cache (APC) is a free and open opcode cache for PHP. It was conceived
to provide a free, open, and robust framework for caching and optimizing PHP intermediate
code. The APC cache driver of Doctrine stores cache records in shared memory.
You can instantiate the APC cache driver with the following code:
// bootstrap.php Listing
22-3
// ...
$cacheDriver = new Doctrine_Cache_Apc();
Db
Db caching backend stores cache records into given database. Usually some fast flat-file
based database is used (such as sqlite).
You can instantiate the database cache driver with the following code:
// bootstrap.php Listing
22-4
// ...
$cacheConn = Doctrine_Manager::connection(new PDO('sqlite::memory:'));
$cacheDriver = new Doctrine_Cache_Db(array('connection' => $cacheConn));
You should always use query cache in a production environment. That said, you can easily
use it during development, too. Whenever you change a DQL query and execute it the first
time Doctrine sees that it has been modified and will therefore create a new cache entry,
so you don't even need to invalidate the cache.
It's worth noting that the effectiveness of the query cache greatly relies on the usage of
prepared statements (which are used by Doctrine by default anyway). You should not directly
embed dynamic query parts and always use placeholders instead.
When using a result cache things get even better. Then your query process looks as follows
(assuming a valid cache entry is found):
1. Init new DQL query
2. Return the result set
As you can see, the result cache implies the query cache shown previously. You should always
consider using a result cache if the data returned by the query does not need to be up-to-date
at any time.
Query Cache
Listing
22-5
// bootstrap.php
// ...
$manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
The value of $cacheDriver above could be any of the drivers we instantiated in the
previous section of this chapter.
// bootstrap.php Listing
22-6
// ...
$conn->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
Fine Tuning
In the previous chapter we used global caching attributes. These attributes can be overriden
at the query level. You can override the cache driver by calling useQueryCache() and pass
it an instance of a valid Doctrine cache driver. This rarely makes sense for the query cache
but is possible:
$q = Doctrine_Query::create() Listing
22-7
->useQueryCache(new Doctrine_Cache_Apc());
Result Cache
// bootstrap.php Listing
22-8
// ...
$manager->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
// bootstrap.php Listing
22-9
// ...
$conn->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
Usually the cache entries are valid for only some time. You can set global value for how long
the cache entries should be considered valid by using
Doctrine::ATTR_RESULT_CACHE_LIFESPAN.
Set the lifespan as one hour (60 seconds * 60 minutes = 1 hour = 3600 secs):
// bootstrap.php Listing
22-10
// ...
$manager->setAttribute(Doctrine::ATTR_RESULT_CACHE_LIFESPAN, 3600);
Now as we have set a cache driver for use we can make a DQL query use it by calling the
useResultCache() method:
Fetch blog post titles and the number of comments:
Listing $q = Doctrine_Query::create();
22-11
->select('b.title, COUNT(c.id) count')
->from('BlogPost b')
->leftJoin('b.Comments c')
->limit(10)
->useResultCache(true);
$blogPosts = $q->execute();
Fine Tuning
In the previous chapter we used global caching attributes. These attributes can be overriden
at the query level. You can override the cache driver by calling useCache() and pass it an
instance of a valid Doctrine cache driver.
Listing $q = Doctrine_Query::create()
22-12
->useResultCache(new Doctrine_Cache_Apc());
Listing $q = Doctrine_Query::create()
22-13
->setResultCacheLifeSpan(60 * 30);
Conclusion
Using the caching feature of Doctrine is highly recommended in both development and
production environments. Their are no adverse affects to using it and it will only help the
performance of your application.
The caching feature is the second to last feature we will discuss in this book before wrapping
things up by discussing things like the technologies used (page 360) in Doctrine, coding
standards (page 373) and unit testing (page 348). Lets move on to discuss the last feature of
Doctrine, Migrations (page 323).
Chapter 23
Migrations
The Doctrine migration package allows you to easily update your production databases
through a nice programmatic interface. The changes are done in a way so that your database
is versioned and you can walk backwards and forwards through the database versions.
Performing Migrations
Before we learn how to create the migration classes lets take a look at how we can run
migrations so that we can implement them in our Doctrine test environment in the next
section.
First lets create a new instance of Doctrine_Migration and pass it the path to our
migration classes:
Now we can migrate to the latest version by calling the migrate() method:
$migration->migrate(); Listing
23-2
If you want to migrate to a specific version you can pass an argument to migrate(). For
example we can migrate to version 3 from 0:
$migration->migrate(3); Listing
23-3
$migration->migrate(0); Listing
23-4
If you want to get the current version of the database you can use the
getCurrentVersion() method:
Omitting the version number argument for the migrate() method means that internally
Doctrine will try and migrate to the latest class version number that it could find in the
directory you passed.
Transactions in Migrations
Internally Doctrine does not wrap migration versions in transactions. It is up to you as the
Implement
Now that we know how to perform migrations lets implement a little script in our Doctrine
test environment named migrate.php.
First we need to create a place for our migration classes to be stored so lets create a
directory named migrations:
Now create the migrate.php script in your favorite editor and place the following code
inside:
Listing // migrate.php
23-7
require_once('bootstrap.php');
The name of the class can be whatever you want, but the name of the file which contains
the class must have a prefix containing a number that is used for loading the migrations in
the correct order.
Below are a few examples of some migration classes you can use to build your database
starting from version 1.
For the first version lets create a new table named migration_test:
Listing // migrations/1_add_table.php
23-8
}
}
Now lets create a second version where we add a new column to the table we added in the
previous version:
// migrations/2_add_column.php Listing
23-9
Finally, lets change the type of the field1 column in the table we created previously:
// migrations/3_change_column.php Listing
23-10
Now that we have created the three migration classes above we can run our migrate.php
script we implemented earlier:
If you look in the database you will see that we have the table named migrate_test created
and the version number in the migration_version is set to three.
If you want to migrate back to where we started you can pass a version number to the
migrate() method in the migrate.php script:
// migrate.php Listing
23-12
// ...
$migration = new Doctrine_Migration('migrations');
$migration->migrate(0);
If you look in the database now, everything we did in the up() methods has been reversed by
the contents of the down() method.
Available Operations
Here is a list of the available methods you can use to alter your database in your migration
classes.
Create Table
Listing // ...
23-14
public function up()
{
$columns = array(
'id' => array(
'type' => 'integer',
'unsigned' => 1
'notnull' => 1
'default' => 0
),
'name' => array(
'type' => 'string',
'length' => 12
),
'password' => array(
'type' => 'string',
'length' => 12
)
);
$options = array(
'type' => 'INNODB',
'charset' => 'utf8'
);
You might notice already that the data structures used to manipulate the your schema are
the same as the data structures used with the database abstraction layer. This is because
internally the migration package uses the database abstraction layer to perform the
operations specified in the migration classes.
Drop Table
Listing // ...
23-15
public function down()
{
$this->dropTable('table_name');
}
// ...
Rename Table
// ... Listing
23-16
public function up()
{
$this->renameTable('old_table_name', 'new_table_name');
}
// ...
Create Constraint
// ... Listing
23-17
public function up()
{
$definition = array(
'fields' => array(
'username' => array()
),
'unique' => true
);
Drop Constraint
Now the opposite down() would look like the following:
// ... Listing
23-18
public function down()
{
$this->dropConstraint('table_name', 'constraint_name');
}
// ...
// ... Listing
23-19
public function up()
{
$definition = array(
'name' => 'email_foreign_key'
'local' => 'email_id',
'foreign' => 'id',
'foreignTable' => 'email',
'onDelete' => 'CASCADE'
);
$this->createForeignKey('table_name', $definition);
}
// ...
Name Description
Add Column
Listing // ...
23-21
public function up()
{
$this->addColumn('table_name', 'column_name', 'string', $options);
}
// ...
Rename Column
Some DBMS like sqlite do not implement the rename column operation. An exception is
thrown if you try and rename a column when using a sqlite connection.
Listing // ...
23-22
public function up()
{
$this->renameColumn('table_name', 'old_column_name', 'new_column_name');
}
// ...
Change Column
Change any aspect of an existing column:
Listing // ...
23-23
public function up()
{
$options = array('length' => 1);
$this->changeColumn('table_name', 'column_name', 'tinyint', $options);
}
// ...
Remove Column
// ... Listing
23-24
public function up()
{
$this->removeColumn('table_name', 'column_name');
}
// ...
Irreversible Migration
Sometimes you may perform some operations in the up() method that cannot be reversed.
For example if you remove a column from a table. In this case you need to throw a new
Doctrine_Migration_IrreversibleMigrationException exception.
// ... Listing
23-25
public function down()
{
throw new Doctrine_Migration_IrreversibleMigrationException(
'The remove column operation cannot be undone!'
);
}
// ...
Add Index
// ... Listing
23-26
public function up()
{
$options = array('fields' => array(
'username' => array(
'sorting' => 'ascending'
),
'last_login' => array()));
Remove Index
// ... Listing
23-27
public function down()
{
$this->removeIndex('table_name', 'index_name');
}
// ...
Listing // migrations/1_add_table.php
23-28
The above example assumes you have created and made available the MigrationTest
model. Once the up() method is executed the migration_test table is created so the
MigrationTest model can be used. We have provided the definition of this model below.
Listing // models/MigrationTest.php
23-29
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
Listing ---
23-30
# schema.yml
# ...
MigrationTest:
columns:
field1: string
Up/Down Automation
In Doctrine migrations it is possible most of the time to automate the opposite of a migration
method. For example if you create a new column in the up of a migration, we should be able
to easily automate the down since all we need to do is remove the column that was created.
This is possible by using the migrate() function for both the up and down.
The migrate() method accepts an argument of $direction and it will either have a value of
up or down. This value is passed to the first argument of functions like column, table, etc.
Now when we run up with the above migration, the column will be added and when we run
down the column will be removed.
Here is a list of the following migration methods that can be automated:
Generating Migrations
Doctrine offers the ability to generate migration classes a few different ways. You can
generate a set of migrations to re-create an existing database, or generate migration classes
to create a database for an existing set of models. You can even generate migrations from
differences between two sets of schema information.
From Database
To generate a set of migrations from the existing database connections it is simple, just use
Doctrine::generateMigrationsFromDb().
Doctrine::generateMigrationsFromDb('/path/to/migration/classes'); Listing
23-32
Diff Tool
Sometimes you may want to alter your models and be able to automate the migration process
for your changes. In the past you would have to write the migration classes manually for your
changes. Now with the diff tool you can make your changes then generate the migration
classes for the changes.
The diff tool is simple to use. It accepts a "from" and a "to" and they can be one of the
following:
A simple example would be to create two YAML schema files, one named schema1.yml and
another named schema2.yml.
The schema1.yml contains a simple User model:
Listing ---
23-34
# schema1.yml
User:
columns:
username: string(255)
password: string(255)
Now imagine we modify the above schema and want to add a email_address column:
Listing ---
23-35
# schema1.yml
User:
columns:
username: string(255)
password: string(255)
email_address: string(255)
Now we can easily generate a migration class which will add the new column to our database:
Now you can easily migrate your database and add the new column!
Conclusion
Using migrations is highly recommended for altering your production database schemas as it
is a safe and easy way to make changes to your schema.
Migrations are the last feature of Doctrine that we will discuss in this book. The final chapters
will discuss some other topics that will help you be a better Doctrine developers on a day-to-
day basis. First lets discuss some of the other Utilities (page 333) that Doctrine provides.
Chapter 24
Utilities
Pagination
Introduction
In real world applications, display content from database tables is a commom task. Also,
imagine that this content is a search result containing thousands of items. Undoubtely, it will
be a huge listing, memory expensive and hard for users to find the right item. That is where
some organization of this content display is needed and pagination comes in rescue.
Doctrine implements a highly flexible pager package, allowing you to not only split listing in
pages, but also enabling you to control the layout of page links. In this chapter, we'll learn
how to create pager objects, control pager styles and at the end, overview the pager layout
object - a powerful page links displayer of Doctrine.
Until this place, the source you have is the same as the old Doctrine_Query object. The only
difference is that now you have 2 new arguments. Your old query object plus these 2
arguments are now encapsulated by the Doctrine_Pager object. At this stage,
Doctrine_Pager defines the basic data needed to control pagination. If you want to know
that actual status of the pager, all you have to do is to check if it's already executed:
Listing $pager->getExecuted();
24-2
If you try to access any of the methods provided by Doctrine_Pager now, you'll experience
Doctrine_Pager_Exception thrown, reporting you that Pager was not yet executed. When
executed, Doctrine_Pager offer you powerful methods to retrieve information. The API
usage is listed at the end of this topic.
To run the query, the process is similar to the current existent Doctrine_Query execute
call. It even allow arguments the way you usually do it. Here is the PHP complete syntax,
including the syntax of optional parameters:
There are some special cases where the return records query differ of the counter query. To
allow this situation, Doctrine_Pager has some methods that enable you to count and then
to execute. The first thing you have to do is to define the count query:
// ...
$rs = $pager->execute();
The first param of setCountQuery can be either a valid Doctrine_Query object or a DQL
string. The second argument you can define the optional parameters that may be sent in the
counter query. If you do not define the params now, you're still able to define it later by
calling the setCountQueryParams:
This method accepts 2 parameters. The first one is the params to be sent in count query and
the second parameter is if the $params should be appended to the list or if it should override
the list of count query parameters. The default behavior is to override the list. One last thing
to mention about count query is, if you do not define any parameter for count query, it will
still send the parameters you define in $pager->execute() call.
Count query is always enabled to be accessed. If you do not define it and call $pager-
>getCountQuery(), it will return the "fetcher" query to you.
If you need access the other functionalities that Doctrine_Pager provides, you can access
them through the API:
$pager->getPage();
// Defines a new current page (need to call execute again to adjust offsets and
values)
$pager->setPage($page);
// Defined a new maximum number of records per page (need to call execute again to
adjust offset and values)
$pager->setMaxPerPage($maxPerPage);
// Returns the Doctrine_Query object that is used to make the count results to
pager
$pager->getCountQuery();
Sliding
Sliding page range style, the page range moves smoothly with the current page. The current
page is always in the middle, except in the first and last pages of the range. Check out how
does it work with a chunk length of 5 items:
Listing Listing 1 2 3 4 5 6 7 8 9 10 11 12 13 14
24-7
Page 1: o-------|
Page 2: |-o-----|
Page 3: |---o---|
Page 4: |---o---|
Page 5: |---o---|
Page 6: |---o---|
Page 7: |---o---|
Page 8: |---o---|
Jumping
In Jumping page range style, the range of page links is always one of a fixed set of "frames":
1-5, 6-10, 11-15, and so on.
Listing Listing 1 2 3 4 5 6 7 8 9 10 11 12 13 14
24-8
Page 1: o-------|
Page 2: |-o-----|
Page 3: |---o---|
Page 4: |-----o-|
Page 5: |-------o
Page 6: o---------|
Page 7: |-o-------|
Page 8: |---o-----|
Now that we know how the different of styles of pager range works, it's time to learn how to
use them:
What is the advantage to use this object, instead of the Doctrine_Pager? Just one; it allows
you to retrieve ranges around the current page.
Look at the example:
// Outputs: [1][2][3][4][5]
echo '['. implode('][', $pages) .']';
If you build your Doctrine_Pager inside the range object, the API gives you enough power
to retrieve information related to Doctrine_Pager_Range subclass instance:
// Return the range around the current page (obtained from Doctrine_Pager
// associated to the $pager_range instance)
$pager_range->rangeAroundPage();
Mask
A piece of string that is defined inside template as replacements. They are defined as
{%mask_name} and are replaced by what you define in options or what is defined internally
by Doctrine_Pager_Layout component. Currently, these are the internal masks available:
• {%page} Holds the page number, exactly as page_number, but can be overwritable
by addMaskReplacement() to behavior like another mask or value
• {%page_number} Stores the current page number, but cannot be overwritable
• {%url} Available only in setTemplate() and setSelectedTemplate() methods.
Holds the processed URL, which was defined in constructor
Template
As the name explains itself, it is the skeleton of HTML or any other resource that is applied to
each page returned by Doctrine_Pager_Range::rangeAroundPage() subclasses. There
are 3 distinct templates that can be defined:
• setTemplate() Defines the template that can be used in all pages returned by
Doctrine_Pager_Range::rangeAroundPage() subclass call
• setSelectedTemplate() Template that is applied when it is the page to be
processed is the current page you are. If nothing is defined (a blank string or no
definition), the template you defined in setTemplate() is used
• setSeparatorTemplate() Separator template is the string that is applied
between each processed page. It is not included before the first call and after the
last one. The defined template of this method is not affected by options and also it
cannot process masks
Now we know how to create the Doctrine_Pager_Layout and the types that are around
this component, it is time to view the basic usage:
Creating the pager layout is simple:
// Fetching users
$users = $pager->execute(); // This is possible too!
Explaining this source, the first part creates the pager layout instance. Second, it defines the
templates for all pages and for the current page. The last part, it retrieves the
Doctrine_Pager object and executes the query, returning in variable $users. The last part
calls the displar without any optional mask, which applies the template in all pages found by
Doctrine_Pager_Range::rangeAroundPage() subclass call.
As you may see, there is no need to use other masks except the internals ones. Lets suppose
we implement a new functionality to search for Users in our existent application, and we need
to support this feature in pager layout too. To simplify our case, the search parameter is
named "search" and is received through $_GET superglobal array. The first change we need
to do is tho adjust the Doctrine_Query object and also the URL, to allow it to be sent to
other pages.
Check out the code and notice we added a new mask, called {%search}. We'll need to send it
to the template processing at a later stage. We then assign the templates, just as defined
before, without any change. And also, we do not need to change execution of query.
Assigning templates for page links creation:
// Fetching users
$users = $pagerLayout->execute();
The method display() is the place where we define the custom mask we created. This
method accepts 2 optional arguments: one array of optional masks and if the output should be
returned instead of printed on screen. In our case, we need to define a new mask, the
{%search}, which is the search offset of $_GET superglobal array. Also, remember that since
it'll be sent as URL, it needs to be encoded. Custom masks are defined in key => value pairs.
So all needed code is to define an array with the offset we desire and the value to be
replaced:
// Handy method to execute the query without need to retrieve the Pager instance
$pagerLayout->execute($params = array(), $hydrationMode = null);
There are a couple of other methods that are available if you want to extend the
Doctrine_Pager_Layout to create you custom layouter. We will see these methods in the
next section.
// Parse the template of a given page and return the processed template
$this->_parseTemplate($options = array());
// Parse the url mask to return the correct template depending of the options sent
// Already process the mask replacements assigned
$this->_parseUrlTemplate($options = array());
$this->_parseReplacementsTemplate($options = array());
// Parse the url mask of a given page and return the processed url
$this->_parseUrl($options = array());
// Parse the mask replacements, changing from to-be replaced mask with new masks/
values
$this->_parseMaskReplacements($str);
Now that you have a small tip of useful methods to be used when extending
Doctrine_Pager_Layout, it's time to see our implemented class:
// First page
$this->addMaskReplacement('page', '«', true);
$options['page_number'] = $pager->getFirstPage();
$str .= $this->processPage($options);
// Previous page
$this->addMaskReplacement('page', '‹', true);
$options['page_number'] = $pager->getPreviousPage();
$str .= $this->processPage($options);
// Pages listing
$this->removeMaskReplacement('page');
$str .= parent::display($options, true);
// Next page
$this->addMaskReplacement('page', '›', true);
$options['page_number'] = $pager->getNextPage();
$str .= $this->processPage($options);
// Last page
$this->addMaskReplacement('page', '»', true);
$options['page_number'] = $pager->getLastPage();
$str .= $this->processPage($options);
echo $str;
}
}
As you may see, I have to manual process the items <<, <, > and >>. I override the
{%page} mask by setting a raw value to it (raw value is achieved by setting the third
parameter as true). Then I define the only MUST HAVE information to process the page and
call it. The return is the template processed as a string. I do it to any of my custom buttons.
Now supposing a totally different situation. Doctrine is framework agnostic, but many of our
users use it together with Symfony. Doctrine_Pager and subclasses are 100% compatible
with Symfony, but Doctrine_Pager_Layout needs some tweaks to get it working with
Symfony's link_to helper function. To allow this usage with Doctrine_Pager_Layout,
you have to extend it and add your custom processor over it. For example purpose (it works in
Symfony), I used {link_to}...{/link_to} as a template processor to do this job. Here is the
extended class and usage in Symfony:
return preg_replace(
'/\{link_to\}(.*?)\{\/link_to\}/', link_to('$1',
$this->_parseUrl($options)), $str
);
}
}
Usage:
$pagerLayout->setTemplate('[{link_to}{%page}{/link_to}]');
Facade
Creating & Dropping Databases
Doctrine offers the ability to create and drop your databases from your defined Doctrine
connections. The only trick to using it is that the name of your Doctrine connection must be
the name of your database. This is required due to the fact that PDO does not offer a method
for retrieving the name of the database you are connected to. So in order to create and drop
the database Doctrine itself must be aware of the name of the database.
Convenience Methods
Doctrine offers static convenience methods available in the main Doctrine class. These
methods perform some of the most used functionality of Doctrine with one method. Most of
these methods are using in the Doctrine_Task system. These tasks are also what are executed
from the Doctrine_Cli.
if (Doctrine::debug()) {
echo 'debugging is on';
} else {
echo 'debugging is off';
}
// Load your models so that they are present and loaded for Doctrine to work with
// Returns an array of the Doctrine_Records that were found and loaded
$models = Doctrine::loadModels('/path/to/models',
Doctrine::MODEL_LOADING_CONSERVATIVE); // or Doctrine::MODEL_LOADING_AGGRESSIVE
print_r($models);
// Pass an array of classes to the above method and it will filter out the ones
that are not Doctrine_Records
$models = Doctrine::filterInvalidModels(array('User', 'Formatter',
'Doctrine_Record'));
print_r($models); // would return array('User') because Formatter and
Doctrine_Record are not valid
$conn = Doctrine_Manager::connection();
Doctrine::dump($conn);
Tasks
Tasks are classes which bundle some of the core convenience methods in to tasks that can be
easily executed by setting the required arguments. These tasks are directly used in the
Doctrine command line interface.
BuildAll Listing
24-24
BuildAllLoad
BuildAllReload
Compile
CreateDb
CreateTables
Dql
DropDb
DumpData
Exception
GenerateMigration
GenerateMigrationsDb
GenerateMigrationsModels
GenerateModelsDb
GenerateModelsYaml
GenerateSql
GenerateYamlDb
GenerateYamlModels
LoadData
Migrate
RebuildDb
You can read below about how to execute Doctrine Tasks standalone in your own scripts.
Tasks
Below is a list of available tasks for managing your Doctrine implementation.
$ ./doctrine Listing
24-25
Doctrine Command Line Interface
./doctrine build-all
./doctrine build-all-load
./doctrine build-all-reload
./doctrine compile
./doctrine create-db
./doctrine create-tables
./doctrine dql
./doctrine drop-db
./doctrine dump-data
./doctrine generate-migration
./doctrine generate-migrations-db
./doctrine generate-migrations-models
./doctrine generate-models-db
./doctrine generate-models-yaml
./doctrine generate-sql
./doctrine generate-yaml-db
./doctrine generate-yaml-models
./doctrine load-data
./doctrine migrate
./doctrine rebuild-db
The tasks for the CLI are separate from the CLI and can be used standalone. Below is an
example.
$task->setArguments($args);
try {
if ($task->validate()) {
$task->execute();
}
} catch (Exception $e) {
throw new Doctrine_Exception($e->getMessage());
}
Usage
File named "doctrine" that is set to executable
chdir(dirname(__FILE__));
include('doctrine.php');
?>
Listing // Include your Doctrine configuration/setup here, your connections, models, etc.
24-28
Sandbox
Installation
You can install the sandbox by downloading the special sandbox package from
http://www.phpdoctrine.org/download43 or you can install it via svn below.
./doctrine
The above steps should give you a functioning sandbox. Execute the ./doctrine command
without specifying a task will show you an index of all the available cli tasks in Doctrine.
Conclusion
I hope some of these utilities discussed in this chapter are of use to you. Now lets discuss how
Doctrine maintains stability and avoids regressions by using Unit Testing (page 348).
43. http://www.phpdoctrine.org/download
Chapter 25
Unit Testing
Doctrine is programmatically tested using UnitTests. You can read more about unit testing
here44 on Wikipedia.
Running tests
In order to run the tests that come with doctrine you need to check out the entire project, not
just the lib folder.
Listing $ cd /path/to/co/doctrine
25-2
Listing CHANGELOG
25-3
COPYRIGHT
lib/
LICENSE
package.xml
tests/
tools/
vendor/
It is not uncommon for the test suite to have fails that we are aware of. Often Doctrine will
have test cases for bugs or enhancement requests that cannot be committed until later
versions. Or we simply don't have a fix for the issue yet and the test remains failing. You
can ask on the mailing list or in IRC for how many fails should be expected in each version
of Doctrine.
CLI
To run tests on the command line, you must have php-cli installed.
Navigate to the /path/to/co/doctrine/tests folder and execute the run.php script:
Listing
25-4
44. http://en.wikipedia.org/wiki/Unit_testing
$ cd /path/to/co/doctrine/tests
$ php run.php
This will print out a progress bar as it is running all the unit tests. When it is finished it will
report to you what has passed and failed.
The CLI has several options available for running specific tests, groups of tests or filtering
tests against class names of test suites. Run the following command to check out these
options.
Browser
You can run the unit tests in the browser by navigating to doctrine/tests/run.php. Options can
be set through _GET variables.
For example:
• http://localhost/doctrine/tests/run.php45
• http://localhost/doctrine/tests/
run.php?filter=Limit&group[]=query&group[]=record46
Please note that test results may very depending on your environment. For example if
php.ini apc.enable_cli is set to 0 then some additional tests may fail.
Writing Tests
When writing your test case, you can copy TemplateTestCase.php to start off. Here is a
sample test case:
45.http://localhost/doctrine/tests/run.php
46.http://localhost/doctrine/tests/
run.php?filter=Limit&group[]=query&group[]=record
The model definitions can be included directly in the test case file or they can be put in
/path/to/co/doctrine/tests/models and they will be autoloaded for you.
Once you are finished writing your test be sure to add it to run.php like the following.
Now when you execute run.php you will see the new failure reported to you.
Ticket Tests
In Doctrine it is common practice to commit a failing test case for each individual ticket that
is reported in trac. These test cases are automatically added to run.php by reading all test
case files found in the /path/to/co/doctrine/tests/Ticket/ folder.
You can create a new ticket test case easily from the CLI:
If the ticket number 9999 doesn't already exist then the blank test case class will be
generated for you at /path/to/co/doctrine/tests/Ticket/9999TestCase.php.
Assert Equal
Listing // ...
25-11
public function test1Equals1()
{
$this->assertEqual(1, 1);
}
// ...
// ... Listing
25-12
public function test1DoesNotEqual2()
{
$this->assertNotEqual(1, 2);
}
// ...
Assert Identical
The assertIdentical() method is the same as the assertEqual() except that its logic is
stricter and uses the === for comparing the two values.
// ... Listing
25-13
public function testAssertIdentical()
{
$this->assertIdentical(1, '1');
}
// ...
The above test would fail obviously because the first argument is the number 1 casted as
PHP type integer and the second argument is the number 1 casted as PHP type string.
Assert True
// ... Listing
25-14
public function testAssertTrue()
{
$this->assertTrue(5 > 2);
}
// ...
Assert False
// ... Listing
25-15
public function testAssertFalse()
{
$this->assertFalse(5 < 2);
}
// ...
Mock Drivers
Doctrine uses mock drivers for all drivers other than sqlite. The following code snippet shows
you how to use mock drivers:
Now when you execute queries they won't actually be executed against a real database.
Instead they will be collected in an array and you will be able to analyze the queries that were
executed and make test assertions against them.
$sql = $this->dbh->getAll();
// print the sql array to find the query you're looking for
// print_r($sql);
Conclusion
Unit testing in a piece of software like Doctrine is so incredible important. Without it, it would
be impossible to know if a change we make has any kind of negative affect on existing
working use cases. With our collection of unit tests we can be sure that the changes we make
won't break existing functionality.
Now lets move on to learn about how we can improve performance (page 354) when using
Doctrine.
Chapter 26
Improving Performance
Introduction
Performance is a very important aspect of all medium to large sized applications. Doctrine is
a large abstraction library that provides a database abstraction layer as well as object-
relational mapping. While this provides a lot of benefits like portability and ease of
development it's inevitable that this leads to drawbacks in terms of performance. This chapter
tries to help you to get the best performance out of Doctrine.
Compile
Doctrine is quite big framework and usually dozens of files are being included on each
request. This brings a lot of overhead. In fact these file operations are as time consuming as
sending multiple queries to database server. The clean separation of class per file works well
in developing environment, however when project goes commercial distribution the speed
overcomes the clean separation of class per file -convention.
Doctrine offers method called compile() to solve this issue. The compile method makes a
single file of most used Doctrine components which can then be included on top of your
script. By default the file is created into Doctrine root by the name
Doctrine.compiled.php.
Compiling is a method for making a single file of most used doctrine runtime components
including the compiled file instead of multiple files (in worst cases dozens of files) can
improve performance by an order of magnitude. In cases where this might fail, a
Doctrine_Exception is throw detailing the error.
Lets create a compile script named compile.php to handle the compiling of Doctrine:
Listing // compile.php
26-1
require_once('/path/to/doctrine/lib/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
Doctrine::compile('Doctrine.compiled.php');
If you wish to only compile in the database drivers you are using you can pass an array of
drivers as the second argument to compile(). For this example we are only using MySQL so
lets tell Doctrine to only compile the mysql driver:
// compile.php Listing
26-3
// ...
Doctrine::compile('Doctrine.compiled.php', array('mysql'));
Now you can change your bootstrap.php script to include the compiled Doctrine:
// bootstrap.php Listing
26-4
// ...
require_once('Doctrine.compiled.php');
// ...
Conservative Fetching
Maybe the most important rule is to be conservative and only fetch the data you actually
need. This may sound trivial but laziness or lack of knowledge about the possibilities that are
available often lead to a lot of unnecessary overhead.
Take a look at this example:
How often do you find yourself writing code like that? It's convenient but it's very often not
what you need. The example above will pull all columns of the record out of the database and
populate the newly created object with that data. This not only means unnecessary network
traffic but also means that Doctrine has to populate data into objects that is never used.
I'm sure you all know why a query like the following is not ideal:
SELECT Listing
26-6
*
FROM my_table
The above is bad in any application and this is also true when using Doctrine. In fact it's even
worse when using Doctrine because populating objects with data that is not needed is a waste
of time.
Another important rule that belongs in this category is: Only fetch objects when you really
need them. Doctrine has the ability to fetch "array graphs" instead of object graphs. At first
glance this may sound strange because why use an object-relational mapper in the first place
then? Take a second to think about it. PHP is by nature a precedural language that has been
enhanced with a lot of features for decent OOP. Arrays are still the most efficient data
structures you can use in PHP. Objects have the most value when they're used to accomplish
complex business logic. It's a waste of resources when data gets wrapped in costly object
structures when you have no benefit of that. Take a look at the following code that fetches all
comments with some related data for an article, passing them to the view for display
afterwards:
Listing
26-7
$q = Doctrine_Query::create()
->select('b.title, b.author, b.created_at')
->addSelect('COUNT(t.id) as num_comments')
->from('BlogPost b')
->leftJoin('b.Comments c')
->where('b.id = ?')
->orderBy('b.created_at DESC');
$blogPosts = $q->execute(array(1));
Now imagine you have a view or template that renders the most recent blog posts:
<small>
(<?php echo $blogPost['num_comments'] )
</small>
</li>
<?php endforeach;
Can you think of any benefit of having objects in the view instead of arrays? You're not going
to execute business logic in the view, are you? One parameter can save you a lot of
unnecessary processing:
Listing // ...
26-9
Listing // ...
26-10
$q->setHydrationMode(Doctrine::HYDRATE_ARRAY);
$blogPosts = $q->execute(array(1));
The above code will hydrate the data into arrays instead of objects which is much less
expensive.
One great thing about array hydration is that if you use the ArrayAccess on your objects
you can easily switch your queries to use array hydration and your code will work exactly
the same. For example the above code we wrote to render the list of the most recent blog
posts would work when we switch the query behind it to array hydration.
Sometimes, you may want the direct output from PDO instead of an object or an array. To do
this, set the hydration mode to Doctrine::HYDRATE_NONE. Here's an example:
Listing $q = Doctrine_Query::create()
26-11
->select('SUM(d.amount)')
->from('Donation d');
You will need to print the results and find the value in the array depending on your DQL
query:
print_r($results); Listing
26-12
In this example the result would be accessible with the following code:
There are two important differences between HYDRATE_ARRAY and HYDRATE_NONE which
you should consider before choosing which to use. HYDRATE_NONE is the fastest but the
result is an array with numeric keys and so results would be referenced as
$result[0][0] instead of $result[0]['my_field'] with HYDRATE_ARRAY. Best
practice would to use HYDRATE_NONE when retrieving large record sets or when doing
many similar queries. Otherwise, HYDRATE_ARRAY is more comfortable and should be
preferred.
Free Objects
As of version 5.2.5, PHP is not able to garbage collect object graphs that have circular
references, e.g. Parent has a reference to Child which has a reference to Parent. Since many
doctrine model objects have such relations, PHP will not free their memory even when the
objects go out of scope.
For most PHP applications, this problem is of little consequence, since PHP scripts tend to be
short-lived. Longer-lived scripts, e.g. bulk data importers and exporters, can run out of
memory unless you manually break the circular reference chains. Doctrine provides a free()
function on Doctrine_Record, Doctrine_Collection, and Doctrine_Query which
eliminates the circular references on those objects, freeing them up for garbage collection.
Usage might look like:
Free objects when mass inserting records:
$results = $q->fetchArray();
$q->free();
}
Or even better if you can reuse the same query object for each query in the loop that would
be ideal:
Listing $q = Doctrine_Query::create()
26-16
->from('User u');
You can automatically free query objects after executing by using the
Doctrine::ATTR_AUTO_FREE_QUERY_OBJECTS attribute.
Listing // test.php
26-17
// ...
$manager->setAttribute('auto_free_query_objects', true);
$q = Doctrine_Query::create()
->from('User u')
Now when we execute the above query, it will be automatically freed of resouces.
Other Tips
Helping the DQL parser
There are two possible ways when it comes to using DQL. The first one is writing the plain
DQL queries and passing them to Doctrine_Connection::query($dql). The second one
is to use a Doctrine_Query object and its fluent interface. The latter should be preferred for
all but very simple queries. The reason is that using the Doctrine_Query object and it's
methods makes the life of the DQL parser a little bit easier. It reduces the amount of query
parsing that needs to be done and is therefore faster.
Efficient relation handling
When you want to add a relation between two components you should NOT do something like
the following:
$user->Roles[] = $newRole;
The above code will load all roles of the user from the database if they're not yet loaded!
Just to add one new link!
Conclusion
Lots of methods exist for improving performance in Doctrine. It is highly recommended that
you consider some of the methods described above.
Now lets move on to learn about some of the technology (page 360) used in Doctrine.
Chapter 27
Technology
Introduction
Doctrine is a product of the work of many people. Not just the people who have coded and
documented this software are the only ones responsible for this great framework. Other
ORMs in other languages are a major resource for us as we can learn from what they have
already done.
Doctrine has also borrowed pieces of code from other open source projects instead of re-
inventing the wheel. Two of the projects borrowed from are symfony47 and the Zend
Framework48. The relevant license information can be found in the root of Doctrine when
you download49 it in a file named LICENSE.
Architecture
Doctrine is divided into three main packages: CORE, ORM and DBAL. Below is a list of some
of the main classes that make up each of the packages.
Doctrine CORE
• Doctrine
• Doctrine_Manager (page 159)
• Doctrine_Connection (page 160)
• Doctrine_Compiler (page 354)
• Doctrine_Exception (page 364)
• Doctrine_Formatter
• Doctrine_Object
• Doctrine_Null
• Doctrine_Event (page 304)
• Doctrine_Overloadable
• Doctrine_Configurable
• Doctrine_EventListener (page 304)
47. http://www.symfony-project.com
48. http://framework.zend.com
49. http://www.doctrine-project.org
Doctrine DBAL
• Doctrine_Expression_Driver (page 174)
• Doctrine_Export (page 287)
• Doctrine_Import (page 294)
• Doctrine_Sequence
• Doctrine_Transaction (page 299)
• Doctrine_DataDict (page 296)
Doctrine ORM
• Doctrine_Record (page 166)
• Doctrine_Table (page 161)
• Doctrine_Relation (page 64)
• Doctrine_Expression (page 174)
• Doctrine_Query (page 117)
• Doctrine_RawSql (page 190)
• Doctrine_Collection (page 177)
• Doctrine_Tokenizer
50. http://www.dofactory.com/Patterns/PatternSingleton.aspx
51. http://www.dofactory.com/Patterns/PatternComposite.aspx
52. http://www.dofactory.com/Patterns/PatternFactory.aspx
53. http://www.dofactory.com/Patterns/PatternObserver.aspx
54. http://www.dofactory.com/Patterns/PatternFlyweight.aspx
55. http://www.dofactory.com/Patterns/PatternFlyweight.aspx
Speed
• Lazy initialization - For collection elements
• Subselect fetching - Doctrine knows how to fetch collections efficiently using a
subselect.
• Executing SQL statements later, when needed : The connection never issues an
INSERT or UPDATE until it is actually needed. So if an exception occurs and you
need to abort the transaction, some statements will never actually be issued.
Furthermore, this keeps lock times in the database as short as possible (from the
late UPDATE to the transaction end).
• Join fetching - Doctrine knows how to fetch complex object graphs using joins and
subselects
• Multiple collection fetching strategies - Doctrine has multiple collection
fetching strategies for performance tuning.
• Dynamic mixing of fetching strategies - Fetching strategies can be mixed and
for example users can be fetched in a batch collection while users' phonenumbers
are loaded in offset collection using only one query.
• Driver specific optimizations - Doctrine knows things like bulk-insert on mysql.
• Transactional single-shot delete - Doctrine knows how to gather all the primary
keys of the pending objects in delete list and performs only one sql delete statement
per table.
• Updating only the modified columns. - Doctrine always knows which columns
have been changed.
• Never inserting/updating unmodified objects. - Doctrine knows if the the state
of the record has changed.
56. http://www.dofactory.com/Patterns/PatternState.aspx
57. http://www.dofactory.com/Patterns/PatternStrategy.aspx
58. http://www.martinfowler.com/eaaCatalog/activeRecord.html
59. http://www.martinfowler.com/eaaCatalog/unitOfWork.html
60. http://www.martinfowler.com/eaaCatalog/identityField.html
61. http://www.martinfowler.com/eaaCatalog/metadataMapping.html
62. http://www.martinfowler.com/eaaCatalog/dependentMapping.html
63. http://www.martinfowler.com/eaaCatalog/foreignKeyMapping.html
64. http://www.martinfowler.com/eaaCatalog/associationTableMapping.html
65. http://www.martinfowler.com/eaaCatalog/lazyLoad.html
66. http://www.martinfowler.com/eaaCatalog/queryObject.html
• PDO for database abstraction - PDO is by far the fastest availible database
abstraction layer for php.
Conclusion
This chapter should have given you a complete birds eye view of all the components of
Doctrine and how they are organized. Up until now you have seen them all used a part from
each other but the separate lists of the three main packages should have made things very
clear for you if it was not already.
Now we are ready to move on and learn about how to deal with Doctrine throwing exceptions
in the Exceptions and Warnings (page 364) chapter.
Chapter 28
Manager exceptions
Doctrine_Manager_Exception is thrown if something failed at the connection management
Listing try {
28-1
$manager->getConnection('unknown');
} catch (Doctrine_Manager_Exception) {
// catch errors
}
Relation exceptions
Relation exceptions are being thrown if something failed during the relation parsing.
Connection exceptions
Connection exceptions are being thrown if something failed at the database level. Doctrine
offers fully portable database error handling. This means that whether you are using sqlite or
some other database you can always get portable error code and message for the occurred
error.
Listing try {
28-2
$conn->execute('SELECT * FROM unknowntable');
} catch (Doctrine_Connection_Exception $e) {
echo 'Code : ' . $e->getPortableCode();
echo 'Message : ' . $e->getPortableMessage();
}
Query exceptions
An exception will be thrown when a query is executed if the DQL query is invalid in some
way.
Conclusion
Now that you know how to deal with Doctrine throwing exceptions lets move on and show you
some real world schemas (page 366) that would be used in common web applications found
today on the web.
Chapter 29
);
$this->hasColumn('permission_id', 'integer', null, array(
'primary' => true
)
);
}
);
$this->hasColumn('permission_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 (page 193) chapter:
Listing ---
29-2
User:
columns:
username: string(255)
password: string(255)
relations:
Roles:
class: Role
refClass: UserRole
foreignAlias: Users
Permissions:
class: Permission
refClass: UserPermission
foreignAlias: Users
Role:
columns:
name: string(255)
relations:
Permissions:
class: Permission
refClass: RolePermission
foreignAlias: Roles
Permission:
columns:
name: string(255)
RolePermission:
columns:
role_id:
type: integer
primary: true
permission_id:
type: integer
primary: true
relations:
Role:
Permission:
UserRole:
columns:
user_id:
type: integer
primary: true
role_id:
type: integer
primary: true
relations:
User:
Role:
UserPermission:
columns:
user_id:
type: integer
primary: true
permission_id:
type: integer
primary: true
relations:
User:
Permission:
Forum Application
Below is an example of a forum application where you have categories, boards, threads and
posts:
)
);
}
}
Here is the same example in YAML format. You can read more about YAML in the YAML
Schema Files (page 193) chapter:
--- Listing
29-4
Forum_Category:
columns:
root_category_id: integer(10)
parent_category_id: integer(10)
name: string(50)
description: string(99999)
relations:
Subcategory:
class: Forum_Category
local: parent_category_id
foreign: id
Rootcategory:
class: Forum_Category
local: root_category_id
foreign: id
Forum_Board:
columns:
category_id: integer(10)
name: string(100)
description: string(5000)
relations:
Category:
class: Forum_Category
local: category_id
foreign: id
Threads:
class: Forum_Thread
local: id
foreign: board_id
Forum_Entry:
columns:
author: string(50)
topic: string(100)
message: string(99999)
parent_entry_id: integer(10)
thread_id: integer(10)
date: integer(10)
relations:
Parent:
class: Forum_Entry
local: parent_entry_id
foreign: id
Thread:
class: Forum_Thread
local: thread_id
foreign: id
Forum_Thread:
columns:
board_id: integer(10)
updated: integer(10)
closed: integer(1)
relations:
Board:
class: Forum_Board
local: board_id
foreign: id
Entries:
class: Forum_Entry
local: id
foreign: thread_id
Conclusion
I hope that these real world schema examples will help you with using Doctrine in the real
world in your application. The last chapter of this book will discuss the coding standards (page
373) used in Doctrine and are recommended for you to use in your application as well.
Remember, consistency in your code is key!
Chapter 30
Coding Standards
Indentation
Use an indent of 4 spaces, with no tabs.
Line Termination
Line termination is the standard way for Unix text files to represent the end of a line. Lines
must end only with a linefeed (LF). Linefeeds are represented as ordinal 10, or hexadecimal
0x0A.
You should not use carriage returns (CR) like Macintosh computers (0x0D) and do not use the
carriage return/linefeed combination (CRLF) as Windows computers (0x0D, 0x0A).
Naming Conventions
Classes
The Doctrine ORM Framework uses the same class naming convention as PEAR and Zend
framework, where the names of the classes directly map to the directories in which they are
stored. The root level directory of the Doctrine Framework is the "Doctrine/" directory, under
which all classes are stored hierarchially.
Class names may only contain alphanumeric characters. Numbers are permitted in class
names but are discouraged. Underscores are only permitted in place of the path separator,
eg. the filename "Doctrine/Table/Exception.php" must map to the class name
"Doctrine_Table_Exception".
If a class name is comprised of more than one word, the first letter of each new word must be
capitalized. Successive capitalized letters are not allowed, e.g. a class "XML_Reader" is not
allowed while "Xml_Reader" is acceptable.
Interfaces
Interface classes must follow the same conventions as other classes (see above).
They must also end with the word "Interface" (unless the interface is approved not to contain
it such as Doctrine_Overloadable). Some examples:
Examples
• Doctrine_Adapter_Interface
• Doctrine_EventListener_Interface
Filenames
For all other files, only alphanumeric characters, underscores, and the dash character ("-")
are permitted. Spaces are prohibited.
Any file that contains any PHP code must end with the extension ".php". These examples
show the acceptable filenames for containing the class names from the examples in the
section above:
• Doctrine/Adapter/Interface.php
• Doctrine/EventListener/Interface
File names must follow the mapping to class names described above.
Functions in the global scope ("floating functions") are NOT permmitted. All static
functions should be wrapped in a static class.
Variables
Variable names may only contain alphanumeric characters. Underscores are not permitted.
Numbers are permitted in variable names but are discouraged. They must always start with a
lowercase letter and follow the "camelCaps" capitalization convention. Verbosity is
encouraged. Variables should always be as verbose as practical. Terse variable names such as
"$i" and "$n" are discouraged for anything other than the smallest loop contexts. If a loop
contains more than 20 lines of code, the variables for the indices need to have more
descriptive names. Within the framework certain generic object variables should always use
the following names:
There are cases when more descriptive names are more appropriate (for example when
multiple objects of the same class are used in same context), in that case it is allowed to use
different names than the ones mentioned.
Constants
Constants may contain both alphanumeric characters and the underscore. They must always
have all letters capitalized. For readablity reasons, words in constant names must be
separated by underscore characters. For example, ATTR_EXC_LOGGING is permitted but
ATTR_EXCLOGGING is not.Constants must be defined as class members by using the "const"
construct. Defining constants in the global scope with "define" is NOT permitted.
echo $Doctrine_SomeClass::MY_CONSTANT;
Record Columns
All record columns must be in lowercase and usage of underscores(_) are encouraged for
columns that consist of more than one word.
Foreign key fields must be in format [table_name]_[column]. The next example is a field that
is a foreign key that points to user(id):
Coding Style
PHP Code Demarcation
PHP code must always be delimited by the full-form, standard PHP tags and short tags are
never allowed. For files containing only PHP code, the closing tag must always be omitted
Strings
When a string is literal (contains no variable substitutions), the apostrophe or "single quote"
must always used to demarcate the string:
Literal String
Listing $string = 'something';
30-4
When a literal string itself contains apostrophes, it is permitted to demarcate the string with
quotation marks or "double quotes". This is especially encouraged for SQL statements:
Variable Substitution
Variable substitution is permitted using the following form:
String Concatenation
Strings may be concatenated using the "." operator. A space must always be added before
and after the "." operator to improve readability:
Arrays
Negative numbers are not permitted as indices and a indexed array may be started with any
non-negative number, however this is discouraged and it is recommended that all arrays have
a base index of 0. When declaring indexed arrays with the array construct, a trailing space
must be added after each comma delimiter to improve readability. It is also permitted to
declare multiline indexed arrays using the "array" construct. In this case, each successive line
must be padded with spaces. When declaring associative arrays with the array construct, it is
encouraged to break the statement into multiple lines. In this case, each successive line must
be padded with whitespace such that both the keys and the values are aligned:
$sampleArray = array(1, 2, 3,
$a, $b, $c,
56.44, $d, 500);
Classes
Classes must be named by following the naming conventions. The brace is always written
next line after the class name (or interface declaration). Every class must have a
documentation block that conforms to the PHPDocumentor standard. Any code within a class
must be indented four spaces and only one class is permitted per PHP file. Placing additional
code in a class file is NOT permitted.
This is an example of an acceptable class declaration:
/** Listing
30-10
* Documentation here
*/
class Doctrine_SampleClass
{
// entire content of class
// must be indented four spaces
}
/** Listing
30-11
* Documentation Block Here
*/
class Foo
{
/**
}
}
Functions must be separated by only ONE single new line like is done above between the
bar() and bar2() methods.
Listing /**
30-12
* Documentation Block Here
*/
class Foo
{
/**
* Documentation Block Here
*/
public function bar(&$baz)
{
}
}
Call-time pass by-reference is prohibited. The return value must not be enclosed in
parentheses. This can hinder readability and can also break code if a method is later changed
to return by reference.
Listing /**
30-13
* Documentation Block Here
*/
class Foo
{
/**
* WRONG
*/
public function bar() {
return($this->bar);
}
/**
* RIGHT
*/
public function bar()
{
return $this->bar;
}
}
Function arguments are separated by a single trailing space after the comma delimiter. This
is an example of an acceptable function call for a function that takes three arguments:
Call-time pass by-reference is prohibited. See above for the proper way to pass function
arguments by-reference. For functions whose arguments permitted arrays, the function call
may include the array construct and can be split into multiple lines to improve readability. In
these cases, the standards for writing arrays still apply:
threeArguments(array(1, 2, 3, 'Framework',
'Doctrine', 56.44, 500), 2, 3);
Control Statements
Control statements based on the if and elseif constructs must have a single space before the
opening parenthesis of the conditional, and a single space after the closing parenthesis.
Within the conditional statements between the parentheses, operators must be separated by
spaces for readability. Inner parentheses are encouraged to improve logical grouping of
larger conditionals. The opening brace is written on the same line as the conditional
statement. The closing brace is always written on its own line. Any content within the braces
must be indented four spaces.
if ($foo != 2) { Listing
30-16
$foo = 2;
}
For if statements that include elseif or else, the formatting must be as in these examples:
if ($foo != 1) { Listing
30-17
$foo = 1;
} else {
$foo = 3;
}
if ($foo != 2) {
$foo = 2;
} elseif ($foo == 1) {
$foo = 3;
} else {
$foo = 11;
}
if ( ! $foo) { Listing
30-18
Control statements written with the switch construct must have a single space before the
opening parenthesis of the conditional statement, and also a single space after the closing
parenthesis. All content within the switch statement must be indented four spaces. Content
under each case statement must be indented an additional four spaces but the breaks must
be at the same indentation level as the case statements.
Inline Documentation
Documentation Format:
All documentation blocks ("docblocks") must be compatible with the phpDocumentor format.
Describing the phpDocumentor format is beyond the scope of this document. For more
information, visit: http://phpdoc.org/67
Every method, must have a docblock that contains at a minimum:
Listing /*
30-20
* Test function
*
* @throws Doctrine_Exception
*/
public function test()
{
throw new Doctrine_Exception('This function did not work');
}
Conclusion
This is the last chapter of Doctrine ORM for PHP - Guide to Doctrine for PHP. I really hope
that this book was a useful piece of documentation and that you are now comfortable with
using Doctrine and will be able to come back to easily reference things as needed.
As always, follow the Doctrine :)
Thanks, Jon
67. http://phpdoc.org/