Doctrine

English Documentation

Doctrine 1.2 ORM Manual

Introduction

About this Version

This is the Doctrine 1.2 ORM Manual, covering up to version 1.2.4.

This manual is currently being “transliterated” to use reStructuredText. This will allow it to be built with Sphinx, hosted by readthedocs and hopefully integrated into the new official Doctrine documentation server. Chapters up to and including Working with Models should display properly, but the rest of the manual still hasn’t been fully rewritten. This can make code examples and tables hard to read.

Caution

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.

How to Contribute

If you’d like to help convert the manual to reST (particularly if you can read Japanese), you can fork and send pull requests via http://github.com/dominics/doctrine1-documentation. It might be a good idea to open an issue if you’re going to work on a page, so that work isn’t duplicated.

Eventually, this version will hopefully end up back in http://github.com/doctrine/doctrine1-documentation

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.

What is the Problem?

We are faced with many problems when building web applications. Instead of trying to explain it all it is best to read what Wikipedia has to say about object relational mappers.

Data management tasks in object-oriented (OO) programming are typically implemented by manipulating objects, which are almost always non-scalar values. For example, consider an address book entry that represents a single person along with zero or more phone numbers and zero or more addresses. This could be modeled in an object-oriented implementation by a “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.

—Pulled from Wikipedia

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.

Note

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.

Doctrine Layers: The ORM relies on the DBAL, which relies on PDO

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 Record, Data Mapper and Data Mapping patterns.

Through extending a specific base class named :php:class:`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 :php:class:`Doctrine_Table` class. This class has the typical Data Mapper interface, :php:meth:`createQuery`, :php:meth:`find(id)`, :php:meth:`findAll`, :php:meth:`findBy*`, :php:meth:`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 :php:class:`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.

The Relational Model and Object Model are distinct, but mostly overlap.

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 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).

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.

Notes

[1]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.
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:

FROM User u LEFT JOIN u.Phonenumbers where u.level > 1

The things to notice about this query:

  • We select from classes and not tables. We are selecting from the :php:class:`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 chapter.).

Note

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. :php:class:`User` is not a table / table name . It may be that the name of the database table that the :php:class:`User` class is mapped to is indeed named :php:class:`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 :php: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 :php:class:`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 Fowler cover a lot of the basic ORM terminology, the different approaches of modeling business logic and the patterns involved.

Another good read is about Driven Design. 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 chapter.

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 :php:meth:`phpinfo.php`` and upload it somewhere on your web server that is accessible to the web:

phpinfo();

Now execute it from your browser by going to http://localhost/phpinfo.php. 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.

Note

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:

php -v

Now check that you have PDO and the desired drivers installed with the following command:

php -i

You could also execute the phpinfo.php from the command line and get the same result as the above example:

php phpinfo.php

Note

Checking the requirements are required in order to run the examples used throughout this documentation.

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.

Tip

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 page.

Note

The sandbox implementation is not a recommend implementation for a production application. It’s only purpose is for exploring Doctrine and running small tests.

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.org

To check out a specific version you can use the following command from your terminal:

svn co http://svn.doctrine-project.org/branches/1.2 .

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/branches/1.2 in the path or repository url parameter. There is no need for a username or password to check out Doctrine.

Updating

Updating Doctrine with SVN is just as easy as installing. Simply execute the following command from your terminal:

svn update
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:

cd /var/www/my_project

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:

svn propedit svn:externals lib/vendor

The above command will open your editor and you need to place the following text inside and save:

doctrine http://svn.doctrine-project.org/branches/1.2/lib

Now you can install Doctrine by doing an svn update:

svn update

It will download and install Doctrine at the following path: /var/www/my_project/lib/vendor/doctrine

Tip

Don’t forget to commit your change to the SVN externals:

svn commit
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:

pear install pear.doctrine-project.org/Doctrine-1.2.x

Note

Replace the above 1.2.x with the version you wish to install. For example “1.2.1”.

Download Pear Package

If you do not wish to install via PEAR or do not have PEAR installed, you can always just manually download the package from the website. Once you download the package to your server you can extract it using the following command under linux.

tar xzf Doctrine-1.2.1.tgz
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:

mkdir doctrine_test
cd doctrine_test
Including Doctrine Libraries

The first thing we must do is find the Doctrine.php file containing the core class so that we can require it in to our application. The Doctrine.php file is in the lib folder from when you downloaded Doctrine in the previous section.

We need to move the Doctrine libraries in to the doctrine_test directory into a folder in doctrine_test/lib/vendor/doctrine:

mkdir lib
mkdir lib/vendor
mkdir lib/vendor/doctrine
mv /path/to/doctrine/lib doctrine

Or if you are using SVN, you can use externals:

svn co http://svn.doctrine-project.org/branches/1.2/lib lib/vendor/doctrine

Now add it to your svn externals:

svn propedit svn:externals lib/vendor

It will open up your editor and place the following inside and save:

doctrine http://svn.doctrine-project.org/branches/1.2/lib

Now when you do SVN update you will get the Doctrine libraries updated:

svn update lib/vendor
Require Doctrine Base Class

We need to create a php script for bootstrapping Doctrine and all the configuration for it. Create a file named bootstrap.php and place the following code in the file:

// bootstrap.php
/* 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 :php:class:`Doctrine` class present, we need to register the class autoloader function in the bootstrap file:

// bootstrap.php
spl_autoload_register(array('Doctrine', 'autoload'));

Lets also create the singleton :php:class:`Doctrine_Manager` instance and assign it to a variable named $manager:

// bootstrap.php
$manager = Doctrine_Manager::getInstance();
Autoloading Explained

Note

You can read about the PHP autoloading on the php website. 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:

$class = new Doctrine_Some_Class();

The above code will trigger a call to the :php:meth:`Doctrine_Core::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:

class Doctrine
{
    public function autoload($className)
    {
        $classPath = str_replace('_', '/', $className) . '.php';
        $path = '/path/to/doctrine/' . $classPath;
        require_once($path);
        return true;
    }
}

In the above example the :php:meth:`Doctrine\Some_Class` can be found at /path/to/doctrine/Doctrine/Some/Class.php.

Note

Obviously the real :php:meth:`Doctrine_Core::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

Tip

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
/* 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.

Note

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 [doc configuration :name] 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
require_once('bootstrap.php');
echo Doctrine_Core::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.

php test.php /path/to/doctrine/lib
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 chapter.

Introduction to Connections

DSN, the Data Source Name

In order to connect to a database through Doctrine, you have to create a valid DSN (Data Source Name).

Doctrine supports both PEAR DB/MDB2 like data source names as well as PDO style data source names. The following section deals with PEAR like data source names. If you need more info about the PDO-style data source names see the documentation on PDO.

Main Parts

The DSN consists in the following parts:

DSN part Description
phptype Database backend used in PHP (i.e. mysql, pgsql etc.)
dbsyntax Database used with regards to SQL syntax etc
protocol Communication protocol to use ( i.e. tcp, unix etc.)
hostspec Host specification (hostname[:port]) ||
database Database to use on the DBMS server
username User name for login
password Password for login
proto_opts Maybe used with protocol
option Additional connection options in URI query string format. Options are separated by ampersand (&). The following section shows a non-complete list of options.
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.
Providing the DSN

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:

phptype(dbsyntax)://username:password@protocol+hostspec/database?option=value

Most variations are allowed:

phptype://username:password@protocol+hostspec:110//usr/db_file.db
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

The currently supported PDO database drivers are:

Driver name Supported databases
fbsql FrontBase
ibase InterBase / Firebird (requires PHP 5)
mssql Microsoft SQL Server (NOT for Sybase. Compile PHP –with-mssql)
mysql MySQL
mysqli MySQL (supports new authentication protocol) (requires PHP 5)
oci Oracle 7/8/9/10
pgsql PostgreSQL
querysim QuerySim
sqlite SQLite 2

A second DSN format supported is

phptype(syntax)://user:pass@protocol(proto_opts)/database

If your database, option values, username or password contain characters used to delineate DSN parts, you can escape them via URI hex encodings:

Character Hex Code
: %3a
/ %2f
@ %40
+ %2b
( %28
) %29
? %3f
= %3d
& %26

Please note, that some features may be not supported by all database drivers.

Examples
  1. Connect to database through a socket

    mysql://user@unix(/path/to/socket)/pear
    
  2. Connect to database on a non standard port

    pgsql://user:pass@tcp(localhost:5555)/pear
    

    Note

    If you use, the IP address 127.0.0.1, the port parameter is ignored (default: 3306).

  3. Connect to SQLite on a Unix machine using options

    sqlite:////full/unix/path/to/file.db?mode=0666
    
  4. Connect to SQLite on a Windows machine using options

    sqlite:///c:/full/windows/path/to/file.db?mode=0666
    
  5. Connect to MySQLi using SSL

    mysqli://user:pass@localhost/pear?key=client-key.pem&cert=client-cert.pem
    
Opening New Connections

Opening a new database connection in Doctrine is very easy. If you wish to use PDO you can just initialize a new :php:class:`PDO` object.

Remember our bootstrap.php file we created in the Getting Started chapter? Under the code where we registered the Doctrine autoloader we are going to instantiate our new connection:

// bootstrap.php
$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

$dbh = new PDO($dsn, $user, $password);
$conn = Doctrine_Manager::connection($dbh);

Tip

Directly passing a PDO instance to Doctrine_Manager::connection will not allow Doctrine to be aware of the username and password for the connection, since their is no way to retrieve it from an existing PDO instance. The username and password is required in order for Doctrine to be able to create and drop databases. To get around this you can manually set the username and password option directly on the $conn object.

// bootstrap.php
$conn->setOption('username', $user);
$conn->setOption('password', $password);
Lazy Database Connecting

Lazy-connecting to database can save a lot of resources. There might be many times where you don’t need an actual database connection, hence its always recommended to use lazy-connecting (that means Doctrine will only connect to database when needed).

This feature can be very useful when using for example page caching, hence not actually needing a database connection on every request. Remember connecting to database is an expensive operation.

In the example below we will show you when you create a new Doctrine connection, the connection to the database isn’t created until it is actually needed.

// bootstrap.php
// At this point no actual connection to the database is created
$conn = Doctrine_Manager::connection('mysql://username:password@localhost/test');

// The first time the connection is needed, it is instantiated
// This query triggers the connection to be created
$conn->execute('SHOW TABLES');
Testing your Connection

After reading the previous sections of this chapter, you should now know how to create a connection. So, lets modify our bootstrap file to include the initialization of a connection. For this example we will just be using a sqlite memory database but you can use whatever type of database connection you prefer.

Add your database connection to bootstrap.php and it should look something like the following:

/* Bootstrap Doctrine.php, register autoloader and specify
   configuration attributes */

require_once('../doctrine/branches/1.2/lib/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();

$conn = Doctrine_Manager::connection('sqlite::memory:', 'doctrine');

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:

// 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
$stmt = $conn->prepare('SELECT \* FROM test');
$stmt->execute();
$results = $stmt->fetchAll();
print_r($results);

Execute test.php from your terminal and you should see:

php test.php
Array(
  [0] => Array(
    [name] => jwage,
    [0] => jwage
  )
)
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 chapter and learn how you can control functionality and configurations using the Doctrine attribute system.

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 :php:const:`Doctrine_Core::FETCH_BATCH` and then sets a table fetchmode to :php:const:`Doctrine_Core::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.

In the following example we set an attribute at the global level:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

In the next example above we override the global attribute on given connection:

// bootstrap.php
$conn->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_NONE);

In the last example we override once again the connection level attribute in the table level:

// bootstrap.php
$table = Doctrine_Core::getTable('User');
$table->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

Note

We haven’t introduced the above used :php:meth:`Doctrine_Core::getTable` method. You will learn more about the table objects used in Doctrine in the Component Overview section of the next chapter.

Default 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.

Default Column Options

It is possible to specify an array of default options to be used on every column in your model.

// bootstrap.php
$manager->setAttribute(
    Doctrine::ATTR_DEFAULT_COLUMN_OPTIONS,
    array('type' => 'string', 'length' => 255, 'notnull' => true)
);
Default Added Auto Id

You can customize the properties of the automatically added primary key in Doctrine models.

$manager->setAttribute(
    Doctrine::ATTR_DEFAULT_IDENTIFIER_OPTIONS,
    array('name' => '%s_id', 'type' => 'string', 'length' => 16)
);

Note

The %s string in the name is replaced with the table name.

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.

Tip

You can read more about the bitwise operators on the PHP website.

Portability Mode Attributes

Below is a list of all the available portability attributes and the description of what each one does:

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:

// bootstrap.php
$conn->setAttribute(
    Doctrine_Core::ATTR_PORTABILITY,
    Doctrine_Core::PORTABILITY_FIX_CASE | Doctrine_Core::PORTABILITY_RTRIM
);

Enable all portability options except trimming:

// bootstrap.php
$conn->setAttribute(
    Doctrine_Core::ATTR_PORTABILITY,
    Doctrine_Core::PORTABILITY_ALL ^ Doctrine_Core::PORTABILITY_RTRIM
);
Identifier quoting

You can quote the db identifiers (table and field names) with :php:meth:`quoteIdentifier`. The delimiting style depends on which database driver is being used.

Note

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:

Name Character Driver
backtick ` MySQL
double quote " Oracle
brackets [ or ] Access

Delimited identifiers are known to generally work correctly under the following drivers: Mssql, Mysql, Oracle, Pgsql, Sqlite and Firebird.

When using the :php:const:`Doctrine_Core::ATTR_QUOTE_IDENTIFIER` option, all of the field identifiers will be automatically quoted in the resulting SQL statements:

// bootstrap.php
$conn->setAttribute(Doctrine_Core::ATTR_QUOTE_IDENTIFIER, true);

Will result in a SQL statement that all the field names are quoted with the backtick ` operator (in MySQL):

SELECT * FROM sometable WHERE `id` = '123'

As opposed to:

SELECT * FROM sometable WHERE id = '123'
Hydration Overwriting

By default Doctrine is configured to overwrite any local changes you have on your objects if you were to query for some objects which have already been queried for and modified:

$user = Doctrine_Core::getTable('User')->find(1);
$user->username = 'newusername';

Now I have modified the above object and if I were to query for the same data again, my local changes would be overwritten:

$user = Doctrine_Core::getTable('User')->find(1);
echo $user->username;

You can disable this behavior by using the :php:const:`Doctrine_Core::ATTR_HYDRATE_OVERWRITE` attribute:

// bootstrap.php
$conn->setAttribute(Doctrine_Core::ATTR_HYDRATE_OVERWRITE, false);

Now if were to run the same test we ran above, the modified username would not be overwritten.

Configure Table Class

If you want to configure the class to be returned when using the :php:meth:`Doctrine_Core::getTable` method you can set the :php:const:`Doctrine_Core::ATTR_TABLE_CLASS` attribute. The only requirement is that the class extends :php:class:`Doctrine_Table`.

// bootstrap.php
$conn->setAttribute(Doctrine_Core::ATTR_TABLE_CLASS, 'MyTableClass');

Now the MyTableClass would look like the following:

class MyTableClass extends Doctrine_Table
{
    public function myMethod()
    {
        // run some query and return the results
    }
}

Now when you do the following it will return an instance of MyTableClass:

$user = $conn->getTable('MyModel')->myMethod();

If you want to customize the table class even further you can customize it for each model. Just create a class named MyModelTable and make sure it is able to be autoloaded.

class MyModelTable extends MyTableClass
{
    public function anotherMethod()
    {
        // run some query and return the results
    }
}

Now when I execute the following code it will return an instance of MyModelTable:

echo get_class($conn->getTable('MyModel')); // MyModelTable
Configure Query Class

If you would like to configure the base query class returned when you create new query instances, you can use the :php:const:`Doctrine_Core::ATTR_QUERY_CLASS` attribute. The only requirement is that it extends the Doctrine_Query class.

// bootstrap.php
$conn->setAttribute(Doctrine_Core::ATTR_QUERY_CLASS, 'MyQueryClass');

The MyQueryClass would look like the following:

class MyQueryClass extends Doctrine_Query
{
}

Now when you create a new query it will return an instance of MyQueryClass:

$q = Doctrine_Core::getTable('User') ->createQuery('u');
echo get_class($q); // MyQueryClass
Configure Collection Class

Since you can configure the base query and table class, it would only make sense that you can also customize the collection class Doctrine should use. We just need to set the :php:const:`Doctrine_Core::ATTR_COLLECTION_CLASS` attribute.

// bootstrap.php
$conn->setAttribute(Doctrine_Core::ATTR_COLLECTION_CLASS, 'MyCollectionClass');

The only requirement of the MyCollectionClass is that it must extend Doctrine_Collection:

$phonenumbers = $user->Phonenumber;
echo get_class($phonenumbers); // MyCollectionClass
Disabling Cascading Saves

You can optionally disable the cascading save operations which are enabled by default for convenience with the :php:const:`Doctrine_Core::ATTR_CASCADE_SAVES` attribute. If you set this attribute to false it will only cascade and save if the record is dirty. This means that you can’t cascade and save records who are dirty that are more than one level deep in the hierarchy, but you benefit with a significant performance improvement.

$conn->setAttribute(Doctrine_Core::ATTR_CASCADE_SAVES, false);
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:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_NONE);

For exporting tables only (but not constraints) you can use on of the following:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_TABLES);

You can also use the following syntax as it is the same as the above:

// bootstrap.php
$manager->setAttribute(
    Doctrine_Core::ATTR_EXPORT,
    Doctrine_Core::EXPORT_ALL ^ Doctrine_Core::EXPORT_CONSTRAINTS
);

For exporting everything (tables and constraints) you can use:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_ALL);
Naming convention attributes

Naming convention attributes affect the naming of different database related elements such as tables, indexes and sequences. Basically every naming convention attribute has affect in both ways. When importing schemas from the database to classes and when exporting classes into database tables.

So for example by default Doctrine naming convention for indexes is %s_idx. Not only do the indexes you set get a special suffix, also the imported classes get their indexes mapped to their non-suffixed equivalents. This applies to all naming convention attributes.

Index name format

:php:const:`Doctrine_Core::ATTR_IDXNAME_FORMAT` can be used for changing the naming convention of indexes. By default Doctrine uses the format [name]_idx. So defining an index called ‘ageindex’ will actually be converted into ‘ageindex_idx’.

You can change the index naming convention with the following code:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_IDXNAME_FORMAT, '%s_index');
Sequence name format

Similar to :php:const:`Doctrine_Core::ATTR_IDXNAME_FORMAT`, :php:const:`Doctrine_Core::ATTR_SEQNAME_FORMAT` can be used for changing the naming convention of sequences. By default Doctrine uses the format [name]_seq, hence creating a new sequence with the name of mysequence will lead into creation of sequence called mysequence_seq.

You can change the sequence naming convention with the following code:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_SEQNAME_FORMAT, '%s_sequence');
Table name format

The table name format can be changed the same as the index and sequence name format with the following code:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_TBLNAME_FORMAT, '%s_table');
Database name format

The database name format can be changed the same as the index, sequence and table name format with the following code:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_DBNAME_FORMAT, 'myframework_%s');
Validation attributes

Doctrine provides complete control over what it validates. The validation procedure can be controlled with :php:const:`Doctrine_Core::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.

Validation mode constants
Name Description
VALIDATE_NONE Turns off the whole validation procedure.
VALIDATE_LENGTHS Makes Doctrine validate all field lengths.
VALIDATE_TYPES Makes Doctrine validate all field types. Doctrine does loose typevalidation. This means that for example string with value ‘13.3’ willnot pass as an integer but ‘13’ will.
VALIDATE_CONSTRAINTS Makes Doctrine validate all fieldconstraints such as notnull, email etc.
VALIDATE_ALL Turns on all validations.

Note

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 :php:const:`Doctrine_Core::VALIDATE_ALL` attribute with the following code:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

You can also configure Doctrine to validate lengths and types, but not constraints with the following code:

// bootstrap.php
$manager->setAttribute(
    Doctrine_Core::ATTR_VALIDATE,
    Doctrine_Core::VALIDATE_LENGTHS | Doctrine_Core::VALIDATE_TYPES
);
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:

/* Bootstrap Doctrine.php, register autoloader and specify
   configuration attributes */

require_once('../doctrine/branches/1.2/lib/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();

$conn = Doctrine_Manager::connection('sqlite::memory:', 'doctrine');

$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);
$manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_ALL);
$manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

Now we are ready to move on to the next chapter where we will learn everything there is to know about Doctrine Connections.

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

:php:class:`Doctrine_Manager` provides the static method :php:meth:`Doctrine_Manager::connection` which opens new connections.

In this example we will show you to open a new connection:

// test.php
$conn = Doctrine_Manager::connection('mysql://username:password@localhost/test', 'connection 1');
Retrieve Connections

If you use the :php:meth:`Doctrine_Manager::connection` method and don’t pass any arguments it will return the current connection:

// test.php
$conn2 = Doctrine_Manager::connection();
if ($conn === $conn2) {
    echo 'Doctrine_Manager::connection() returns the current 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 :php:class:`Doctrine_Manager` instance:

// test.php
$conn2 = Doctrine_Manager::connection('mysql://username2:password2@localhost/test2', 'connection 2');
if ($conn2 === $manager->getCurrentConnection()) {
    echo 'Current connection is the connection we just created!';
}
Change Current Connection

You can change the current connection by calling :php:meth:`Doctrine_Manager::setCurrentConnection`:

// test.php
$manager->setCurrentConnection('connection 1');
echo $manager->getCurrentConnection()->getName(); // 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 :php:class:`Doctrine_Manager` implements special :php:class:`IteratorAggregate` interface.

Tip

The :php:class:`IteratorAggregate` is a special PHP interface for implementing iterators in to your objects.

// test.php
foreach($manager as $conn) {
    echo $conn->getName() . "";
}
Get Connection Name

You can easily get the name of a :php:class:`Doctrine_Connection` instance with the following code:

// test.php
$conn = Doctrine_Manager::connection();
$name = $manager->getConnectionName($conn);
echo $name; // connection 1
Close Connection

You can easily close a connection and remove it from the Doctrine connection registry with the following code:

// test.php
$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:

// test.php
$conn = Doctrine_Manager::connection();
$conn->close();
Get All Connections

You can retrieve an array of all the registered connections by using the :php:meth:`Doctrine_Manager::getConnections` method like below:

// test.php
$conns = $manager->getConnections();
foreach ($conns as $conn) {
    echo $conn->getName() . "";
}

The above is essentially the same as iterating over the :php:class:`Doctrine_Manager` object like we did earlier. Here it is again:

// test.php
foreach ($manager as $conn) {
    echo $conn->getName() . "";
}
Count Connections

You can easily get the number of connections from a :php:class:`Doctrine_Manager` object since it implements the Countable interface:

// test.php
$num = count($manager);
echo $num;

The above is the same as doing:

// test.php
$num = $manager->count();
Creating and Dropping Database

When you create connections using Doctrine, you gain the ability to easily create and drop the databases related to those connections.

This is as simple as using some functions provided in the :php:class:`Doctrine_Manager` or :php:class:`Doctrine_Connection` classes.

The following code will iterate over all instantiated connections and call the :php:meth:`dropDatabases`/:php:meth:`createDatabases` function on each one:

// test.php
$manager->createDatabases();
$manager->dropDatabases();
For a Specific Connection

You can easily drop or create the database for a specific :php:class:`Doctrine_Connection` instance by calling the :php:meth:`dropDatabase`/:php:meth:`createDatabase` function on the connection instance with the following code:

// test.php
$conn->createDatabase();
$conn->dropDatabase();
Writing Custom Connections

Sometimes you might need the ability to create your own custom connection classes and utilize them. You may need to extend mysql, or write your own connection type completely. This is possible by writing a few classes and then registering the new connection type with Doctrine.

So in order to create a custom connection first we need to write the following classes:

class Doctrine_Connection_Test extends Doctrine_Connection_Common
{
}

class Doctrine_Adapter_Test implements Doctrine_Adapter_Interface
{
    // All the methods defined in the interface
}

Now we register them with Doctrine:

// bootstrap.php
$manager->registerConnectionDriver('test', 'Doctrine_Connection_Test');

With those few changes something like this is now possible:

$conn = $manager->openConnection('test://username:password@localhost/dbname');

If we were to check what classes are used for the connection you will notice that they are the classes we defined above:

echo get_class($conn); // Doctrine_Connection_Test
echo get_class($conn->getDbh()); // Doctrine_Adapter_Test
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 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.

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:

class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('username', 'string', 255);
        $this->hasColumn('password', 'string', 255);
    }

    public function setUp()
    {
        $this->actAs('Timestampable');
    }
}

Note

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.

Each :php:class:`Doctrine_Record` child class can have a :php:meth:`setTableDefinition` and :php:meth:`setUp` method. The :php:meth:`setTableDefinition` method is for defining columns, indexes and other information about the schema of tables. The :php:meth:`setUp` method is for attaching behaviors and defining relationships between :php:class:`Doctrine_Record` child classes. In the above example we are enabling the Timestampable behavior which adds some automagic functionality. You will learn more about what all can be used in these functions in the Defining Models chapter.

Generating Models

Doctrine offers ways to generate these classes to make it easier to get started using Doctrine.

Note

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 :php:class:`Doctrine_Record` classes from your existing database. There is no need for you to manually write all the :php:class:`Doctrine_Record` classes for your domain model.

Making the first import

Let’s consider we have a mysql database called doctrine_test with a single table named user. The user table has been created with the following sql statement:

CREATE TABLE user (
    id BIGINT(20) NOT NULL AUTO_INCREMENT,
    first_name VARCHAR(255) DEFAULT NULL,
    last_name VARCHAR(255) DEFAULT NULL,
    username VARCHAR(255) DEFAULT NULL,
    password VARCHAR(255) DEFAULT NULL,
    type VARCHAR(255) DEFAULT NULL,
    is_active TINYINT(1) DEFAULT '1',
    is_super_admin TINYINT(1) DEFAULT '0',
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE=InnoDB

Now we would like to convert it into :php:class:`Doctrine_Record` class. With Doctrine this is easy! Remember our test script we created in the Getting Started 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
$conn = Doctrine_Manager::connection('mysql://root:mys3cr3et@localhost/doctrinetest', 'doctrine');

Note

You can use the :php:meth:`$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:

mkdir doctrine_test/models

Now we just need to add the code to our test.php script to generate the model classes:

// test.php
Doctrine_Core::generateModelsFromDb(
    'models',
    array('doctrine'),
    array('generateTableClasses' => true)
);

Note

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:

// models/generated/BaseUser.php
/**
 * 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:

// models/User.php
/**
 * This class has been auto-generated by the Doctrine ORM Framework
 */
class User extends BaseUser
{
}

Doctrine will automatically generate a skeleton :php:class:`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
/**
 * 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
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
$manager->setAttribute(Doctrine_Core::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
Doctrine_Core::loadModels('models');

Note

The model loading is fully explained later in the Autoloading Models section of this chapter.

Now we can modify test.php to include some code which will test the changes we made to the User model:

// test.php
$user = new User();
$user->username = 'jwage';
$user->password = 'changeme';

echo $user->password; // outputs md5 hash and not changeme

Now when you execute test.php from your terminal you should see the following:

php test.php 4cb9c8a8048fd02294477fcb1a41191a

Here is an example of some custom functions you might add to the UserTable class:

// models/UserTable.php
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 :php:class:`Doctrine_Table` classes to be loaded you must enable the autoload_table_classes attribute in your bootstrap.php file like done below:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);

Now you have access to this function when you are working with the UserTable instance:

// test.php
$usersCreatedToday = Doctrine_Core::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:

// test.php
Doctrine_Core::generateYamlFromModels('schema.yml', 'models');

Execute the test.php script:

php test.php

Now you should see a file named schema.yml created in the root of the doctrine_test directory. It should look like the following:

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
require_once('bootstrap.php');

Doctrine_Core::dropDatabases(); Doctrine_Core::createDatabases();
Doctrine_Core::generateModelsFromYaml('schema.yml', 'models');
Doctrine_Core::createTablesFromModels('models');

Now you can alter your schema.yml and re-generate your models by running the following command from your terminal:

php generate.php

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:

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)

Note

Notice some of the changes we made:

  1. Removed the explicit tableName definition as it will default to user.
  2. Attached the Timestampable behavior.
  3. Removed id column as it is automatically added if no primary key is defined.
  4. Removed updated_at and created_at columns as they can be handled automatically by the Timestampable behavior.

Now look how much cleaner the YAML is and is because we take advantage of defaults and utilize core behaviors it is much less work we have to do ourselves.

Now re-generate your models from the YAML schema file:

php generate.php

You can learn more about YAML Schema Files in its dedicated chapter.

Manually Writing Models

You can optionally skip all the convenience methods and write your models manually using nothing but your own PHP code. You can learn all about the models syntax in the Defining Models chapter.

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 :php:meth:`Doctrine_Core::modelsAutoload`.

To use Doctrine model loading you need to register the model autoloader in your bootstrap:

// bootstrap.php
spl_autoload_register(array('Doctrine_Core', 'modelsAutoload'));

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_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

Note

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 :php:meth:`Doctrine_Core::loadModels` functionality all found classes will be cached internally so the autoloader can require them later:

Doctrine_Core::loadModels('models');

Now when we instantiate a new class, for example a User class, the autoloader will be triggered and the class is required:

// triggers call to Doctrine_Core::modelsAutoload() and the class is included
$user = new User();

Instantiating the class above triggers a call to :php:meth:`Doctrine_Core::modelsAutoload` and the class that was found in the call to :php:meth:`Doctrine_Core::loadModels` will be required and made available.

Note

Conservative model loading is recommended in most cases, specifically for production environments as you do not want to require every single model class even when it is not needed as this is unnecessary overhead. You only want to require it when it is needed.

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_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_AGGRESSIVE);

Tip

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 :php:meth:`Doctrine_Core::loadModels` functionality all the classes found will be included right away:

Doctrine_Core::loadModels('/path/to/models');
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 Defining Models we will really get in to the API we use to define our models.

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.

Note

In Doctrine columns and column aliases are case sensitive. So when you are using columns in your DQL queries, the column/field names must match the case in your model definition.

Column Lengths

In Doctrine column length is an integer that specifies the column length. Some column types depend not only the given portable type but also on the given length. For example type string with length 1000 will be translated into native type TEXT on mysql.

The length is different depending on the type of column you are using:

  • integer

    Length is the the number of bytes the integer occupies.

  • string

    Number of the characters allowed in the string.

  • float/decimal

    Total number of characters allowed excluding the decimal.

  • enum

    If using native enum length does not apply but if using emulated enums then it is just the string length of the column value.

Column Aliases

Doctrine offers a way of setting column aliases. This can be very useful when you want to keep the application logic separate from the database logic. For example if you want to change the name of the database field all you need to change at your application is the column definition.

// models/Book.php
class Book extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('bookTitle as title', 'string');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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.

// test.php
$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.

// models/generated/BaseUser.php
class User extends BaseUser
{
    public function setTableDefinition()
    {
        $this->hasColumn('username', 'string', 255,
            array('default' => 'default username'));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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
$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 :php:meth:`createTable` method. The example array at the end of the data types section may be used with :php:meth:`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).

  1. Example: 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.
  2. Example: 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:

  • The notnull modifiers
  • The length modifiers
  • The default modifiers
  • unsigned modifiers for some field definitions, although not all DBMS’s support this modifier for integer field types.
  • collation modifiers (not supported by all drivers)
  • fixed length modifiers for some field definitions.

Building upon the above, we can say that the modifiers alter the field definition to create more specific field types for specific usage scenarios. The notnull modifier will be used in the following way to set the default DBMS NOT NULL Flag on the field to true or false, depending on the DBMS’s definition of the field value: In PostgreSQL the “NOT NULL” definition will be set to “NOT NULL”, whilst in MySQL (for example) the “NULL” option will be set to “NO”. In order to define a “NOT NULL” field type, we simply add an extra parameter to our definition array (See the examples in the following section)

'sometime' = array(
    'type' => 'time',
    'default' => '12:34:05',
    'notnull' => true,
),

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!

'sometext' = array(
    'type' => 'string',
    'length' => 12,
),

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.

Note

The next several examples are not meant for you to use and give them a try. They are simply for demonstrating purposes to show you how to use the different Doctrine data types using PHP code or YAML schema files.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('booltest', 'boolean');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('integertest', 'integer', 4, array(
            'unsigned' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('floattest', 'float');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('decimaltest', 'decimal');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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:

class Test extends Doctrine_Record
{
    public function setTableDefinition() {
        $this->hasColumn('decimaltest', 'decimal', 18,
            array('scale' => 2)
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('stringtest', 'string', 200, array(
            'fixed' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

Test:
  columns:
    stringtest:
      type: string(200)
      fixed: true
Array

This is the same as the ‘array’ type in PHP:

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('arraytest', 'array', 10000);
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('objecttest', 'object');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

Test:
  columns:
    objecttest: object

Note

The array and object types simply serialize the data when persisting to the database and unserialize the data when pulling from the database.

Blob

Blob (Binary Large OBject) data type is meant to store data of undefined length that may be too large to store in text fields, like data that is usually stored in files.

Blob fields are usually not meant to be used as parameters of query search clause (WHERE) unless the underlying DBMS supports a feature usually known as “full text search”.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('blobtest', 'blob');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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”.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('clobtest', 'clob');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('timestamptest', 'timestamp');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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 from 00 to 23 and MI and SS are respectively the number of the minute and of the second from 00 to 59. Hours, minutes and seconds numbered below 10 should be padded on the left with 0.

Some DBMS have native support for time of the day formats, but for others the DBMS driver may have to represent them as integers or text values. In any case, it is always possible to make comparisons between time values as well sort query results by fields of this type.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('timetest', 'time');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('datetest', 'date');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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 :php:meth:`Doctrine_Record::hasColumn`

Note

If you wish to use native enum types for your DBMS if it supports it then you must set the following attribute:

$conn->setAttribute(Doctrine_Core::ATTR_USE_NATIVE_ENUM, true);

Here is an example of how to specify the enum values:

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('enumtest', 'enum', null,
            array('values' => array('php', 'java', 'python'))
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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.

class Test extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('gziptest', 'gzip');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

Test:
  columns:
    gziptest: gzip

Note

The family of php functions for compressing are used internally for compressing and uncompressing the contents of the gzip column type.

Examples

Consider the following definition:

class Example extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('id', 'string', 32, array(
            'type' => 'string',
            'fixed' => 1,
            'primary' => true,
            'length' => '32' )
        );

        $this->hasColumn('someint', 'integer', 10, array(
            'type' => 'integer',
            'unsigned' => true,
            'length' => '10'
        ));

        $this->hasColumn('sometime', 'time', 25, array(
            'type' => 'time',
            'default' => '12:34:05',
            'notnull' => true,
            'length' => '25'
        ));

        $this->hasColumn('sometext', 'string', 12, array(
            'type' => 'string',
            'length' => '12'
        ));

        $this->hasColumn('somedate', 'date', 25, array(
            'type' => 'date',
            'length' => '25'
        ));

        $this->hasColumn('sometimestamp', 'timestamp', 25, array(
            'type' => 'timestamp',
            'length' => '25'
        ));

        $this->hasColumn('someboolean', 'boolean', 25, array(
            'type' => 'boolean',
            'length' => '25'
        ));

        $this->hasColumn('somedecimal', 'decimal', 18, array(
            'type' => 'decimal',
            'length' => '18'
        ));

        $this->hasColumn('somefloat', 'float', 2147483647, array(
            'type' => 'float',
            'length' => '2147483647'
        ));

        $this->hasColumn('someclob', 'clob', 2147483647, array(
            'type' => 'clob',
            'length' => '2147483647'
        ));

        $this->hasColumn('someblob', 'blob', 2147483647, array(
            'type' => 'blob',
            'length' => '2147483647'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

Example:
  tableName: example
  columns:
    id:
      type:    string(32)
      fixed:   true
      primary: true
    someint:
      type:     integer(10)
      unsigned: true
    sometime:
      type:    time(25)
      default: '12:34:05'
      notnull: true
    sometext:      string(12)
    somedate:      date(25)
    sometimestamp: timestamp(25)
    someboolean:   boolean(25)
    somedecimal:   decimal(18)
    somefloat:     float(2147483647)
    someclob:      clob(2147483647)
    someblob:      blob(2147483647)

The above example will create the following database table in Pgsql:

Column Type
id character(32)
someint integer
sometime time without time zone
sometext character or varying(12)
somedate date
sometimestamp timestamp without time zone
someboolean boolean
somedecimal numeric(18,2)
somefloat double precision
someclob text
someblob bytea

The schema will create the following database table in Mysql:

Field Type
id char(32)
someint integer
sometime time
sometext varchar(12)
somedate date
sometimestamp timestamp
someboolean tinyint(1)
somedecimal decimal(18,2)
somefloat double
someclob longtext
someblob longblob
Relationships
Introduction

In Doctrine all record relations are being set with :php:meth:`Doctrine_Record::hasMany`, :php:meth:`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 :php:meth:`Doctrine_Record::hasMany` and :php:meth:`Doctrine_Record::hasOne` methods are placed within a method called setUp(). Both methods take two arguments: the first argument is a string containing the name of the class and optional alias, the second argument is an array consisting of relation options. The option array contains the following keys:

Name Optional Description
local No The local field of the relation. Local field is the linked field inthe defining class.
foreign No The foreign fieldof the relation. Foreign field is the linked field in the linked class.
refClass Yes The name of the association class.This is only needed for many-to-many associations.
owningSide Yes Set to boolean true to indicate the owningside of the relation. The owning side is the side that owns the foreignkey. There can only be one owning side in an association between twoclasses. Note that this option is required if Doctrine can’t guess theowning side or it’s guess is wrong. An example where this is the case iswhen both ‘local’ and ‘foreign’ are part of the identifier (primarykey). It never hurts to specify the owning side in this way.
onDelete Yes The onDelete integrity action that is applied on the foreign key constraint when the tables are created byDoctrine.
onUpdate Yes The onUpdate integrity action that is applied on the foreign key constraint when thetables are created by Doctrine.
cascade Yes Specify application level cascading operations. Currently only delete issupported

So lets take our first example, say we have two classes Forum_Board and Forum_Thread. Here Forum_Board has many Forum_Threads, hence their relation is one-to-many. We don’t want to write Forum_ when accessing relations, so we use relation aliases and use the alias Threads.

First lets take a look at the Forum_Board class. It has three columns: name, description and since we didn’t specify any primary key, Doctrine auto-creates an id column for it.

We define the relation to the Forum_Thread class by using the :php:meth:`hasMany` method. Here the local field is the primary key of the board class whereas the foreign field is the board_id field of the Forum_Thread class.

// models/Forum_Board.php
class Forum_Board extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 100);
        $this->hasColumn('description', 'string', 5000);
    }

    public function setUp()
    {
        $this->hasMany('Forum_Thread as Threads', array(
            'local' => 'id',
            'foreign' => 'board_id'
        ));
    }
}

Note

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 chapter:

# 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 :php:meth:`hasOne` method. Also notice how we once again use aliases and how the local column here is board_id while the foreign column is the id column.

// models/Forum_Thread.php
class Forum_Thread extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer');
        $this->hasColumn('board_id', 'integer');
        $this->hasColumn('title', 'string', 200);
        $this->hasColumn('updated', 'integer', 10);
        $this->hasColumn('closed', 'integer', 1);
    }

    public function setUp()
    {
        $this->hasOne('Forum_Board as Board', array(
            'local' => 'board_id',
            'foreign' => 'id'
        ));

        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

 # 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:

// test.php
$board = new Forum_Board();
$board->name = 'Some board';

Now lets create a new thread under the board:

// ...
$board->Threads[0]->title = 'new thread 1';
$board->Threads[1]->title = 'new thread 2';

Each Thread needs to be associated to a user so lets create a new User and associate it to each Thread:

// ...
$user = new User();
$user->username = 'jwage';
$board->Threads[0]->User= $user;
$board->Threads[1]->User = $user;

Now we can save all the changes with one call. It will save the new board as well as its threads:

// ...
$board->save();

Lets do a little inspecting and see the data structure that is created when you use the code from above. Add some code to test.php to output an array of the object graph we’ve just populated:

print_r($board->toArray(true));

Tip

The :php:meth:`Doctrine_Record::toArray` takes all the data of a :php:class:`Doctrine_Record` instance and converts it to an array so you can easily inspect the data of a record. It accepts an argument named $deep telling it whether or not to include relationships. In this example we have specified true because we want to include the Threads data.

Now when you execute test.php with PHP from your terminal you should see the following

$ php test.php
Array (
    [id] => 2
    [name] => Some board
    [description] =>
    [Threads] => Array
        (
            [0] => Array
                (
                    [id] => 3
                    [user_id] => 1
                    [board_id] => 2
                    [title] => new thread 1
                    [updated] =>
                    [closed] =>
                    [User] => Array
                        (
                            [id] => 1
                            [is_active] => 1
                            [is_super_admin] => 0
                            [first_name] =>
                            [last_name] =>
                            [username] => jwage
                            [password] =>
                            [type] =>
                            [created_at] => 2009-01-20 16:41:57
                            [updated_at] => 2009-01-20 16:41:57
                        )
                )
            [1] => Array
                (
                    [id] => 4
                    [user_id] => 1
                    [board_id] => 2
                    [title] => new thread 2
                    [updated] =>
                    [closed] =>
                    [User] => Array
                        (
                            [id] => 1
                            [is_active] => 1
                            [is_super_admin] => 0
                            [first_name] =>
                            [last_name] =>
                            [username] => jwage
                            [password] =>
                            [type] =>
                            [created_at] => 2009-01-20 16:41:57
                            [updated_at] => 2009-01-20 16:41:57
                        )
                )
        )
)

Note

Notice how the auto increment primary key and foreign keys are automatically set by Doctrine internally. You don’t have to worry about the setting of primary keys and foreign keys at all!

Foreign Key Associations
One to One

One-to-one relations are probably the most basic relations. In the following example we have two classes, User and Email with their relation being one-to-one.

First lets take a look at the Email class. Since we are binding a one-to-one relationship we are using the :php:meth:`hasOne` method. Notice how we define the foreign key column (user_id) in the Email class. This is due to a fact that Email is owned by the User class and not the other way around. In fact you should always follow this convention - always place the foreign key in the owned class.

The recommended naming convention for foreign key columns is: [tableName]_[primaryKey]. As here the foreign table is ‘user’ and its primary key is ‘id’ we have named the foreign key column as ‘user_id’.

// models/Email.php
class Email extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer');
        $this->hasColumn('address', 'string', 150);
    }

    public function setUp()
    {
        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Email:
  columns:
      user_id: integer
      address: string(150)
  relations:
      User:
      local: user_id
      foreign: id
      foreignType: one

Tip

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 :php:meth:`hasOne` definition compared to the definition of the Email class.

// models/User.php
class User extends BaseUser
{
    public function setUp()
    {
        parent::setUp();
        $this->hasOne('Email', array(
            'local' => 'id',
            'foreign' => 'user_id'
        ));
    }
}

Note

Notice how we override the :php:meth:`setUp` method and call :php:meth:`parent::setUp`. This is because the BaseUser class which is generated from YAML or from an existing database contains the main :php:meth:`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 chapter:

# schema.yml
User:
    # ...
    relations:
        # ...
        Email:
            local: id
            foreign: user_id
One to Many and Many to One

One-to-Many and Many-to-One relations are very similar to One-to-One relations. The recommended conventions you came in terms with in the previous chapter also apply to one-to-many and many-to-one relations.

In the following example we have two classes: User and Phonenumber. We define their relation as one-to-many (a user can have many phonenumbers). Here once again the Phonenumber is clearly owned by the User so we place the foreign key in the Phonenumber class.

// models/User.php
class User extends BaseUser
{
    public function setUp()
    {
        parent::setUp();

        // ...

        $this->hasMany('Phonenumber as Phonenumbers', array(
            'local' => 'id',
            'foreign' => 'user_id'
        ));
    }
}
// models/Phonenumber.php
class Phonenumber extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer');
        $this->hasColumn('phonenumber', 'string', 50);
    }

    public function setUp()
    {
        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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
class Task extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 100);
        $this->hasColumn('parent_id', 'integer');
    }

    public function setUp()
    {
        $this->hasOne('Task as Parent', array(
            'local' => 'parent_id',
            'foreign' => 'id'
        ));
        $this->hasMany('Task as Subtasks', array(
            'local' => 'id',
            'foreign' => 'parent_id'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Task:
  columns:
    name: string(100)
    parent_id: integer
  relations:
    Parent:
      class: Task
      local: parent_id
      foreign: id
      foreignAlias: Subtasks

Note

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.

Join Table Associations
Many to Many

If you are coming from relational database background it may be familiar to you how many-to-many associations are handled: an additional association table is needed.

In many-to-many relations the relation between the two components is always an aggregate relation and the association table is owned by both ends. For example in the case of users and groups: when a user is being deleted, the groups he/she belongs to are not being deleted. However, the associations between this user and the groups he/she belongs to are instead being deleted. This removes the relation between the user and the groups he/she belonged to, but does not remove the user nor the groups.

Sometimes you may not want that association table rows are being deleted when user / group is being deleted. You can override this behavior by setting the relations to association component (in this case Groupuser) explicitly.

In the following example we have Groups and Users of which relation is defined as many-to-many. In this case we also need to define an additional class called Groupuser.

class User extends BaseUser
{
    public function setUp()
    {
        parent::setUp();

        // ...

        $this->hasMany('Group as Groups', array(
            'local' => 'user_id',
            'foreign' => 'group_id',
            'refClass' => 'UserGroup'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

User:
    # ...
    relations:
        # ...
        Groups:
            class: Group
            local: user_id
            foreign: group_id
            refClass: UserGroup

Note

The above refClass option is required when setting up many-to-many relationships.

// models/Group.php
class Group extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('groups');
        $this->hasColumn('name', 'string', 30);
    }

    public function setUp()
    {
        $this->hasMany('User as Users', array(
            'local' => 'group_id',
            'foreign' => 'user_id',
            'refClass' => 'UserGroup'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Group:
  tableName: groups
  columns:
    name: string(30)
  relations:
    Users:
      class: User
      local: group_id
      foreign: user_id
      refClass: UserGroup

Note

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 :php:const:`Doctrine_Core::ATTR_QUOTE_IDENTIFIER` attribute so that the reserved word is escaped with quotes.

$manager->setAttribute(Doctrine_Core::Doctrine_Core::ATTR_QUOTE_IDENTIFIER,
true);
// models/UserGroup.php
class UserGroup extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer', null, array(
            'primary' => true
        ));
        $this->hasColumn('group_id', 'integer', null, array(
            'primary' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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
$user = new User();

Now add two new groups to the User:

// ...
$user->Groups[0]->name = 'First Group';
$user->Groups[1]->name = 'Second Group';

Now you can save the groups to the database:

// ...
$user->save();

Now you can delete the associations between user and groups it belongs to:

// ...
$user->UserGroup->delete();

$groups = new Doctrine_Collection(Doctrine_Core::getTable('Group'));

$groups[0]->name = 'Third Group';
$groups[1]->name = 'Fourth Group';

$user->Groups[2] = $groups[0]; // $user will now have 3 groups

$user->Groups = $groups; // $user will now have two groups 'Third Group' and 'Fourth Group'

$user->save();

Now if we inspect the $user object data with the :php:meth:`Doctrine_Record::toArray`:

// ...
print_r($user->toArray(true));

The above example would produce the following output

$ php test.php
Array
    (
        [id] => 1
        [is_active] => 1
        [is_super_admin] => 0
        [first_name] =>
        [last_name] =>
        [username] => default username
        [password] =>
        [type] =>
        [created_at] => 2009-01-20 16:48:57
        [updated_at] => 2009-01-20 16:48:57
        [Groups] => Array
            (
                [0] => Array
                    (
                        [id] => 3
                        [name] => Third Group
                    )
            [1] => Array
                (
                    [id] => 4
                    [name] => Fourth Group
                )
            )
        [UserGroup] => Array
            (
            )
    )
Self Referencing (Nest Relations)
Non-Equal Nest Relations
// models/User.php
class User extends BaseUser
{
    public function setUp()
    {
        parent::setUp();

        // ...

        $this->hasMany('User as Parents', array(
            'local'    => 'child_id',
            'foreign'  => 'parent_id',
            'refClass' => 'UserReference'
        ));

        $this->hasMany('User as Children', array(
            'local'    => 'parent_id',
            'foreign'  => 'child_id',
            'refClass' => 'UserReference'
        ));
    }
}
// models/UserReference.php
class UserReference extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('parent_id', 'integer', null, array(
            'primary' => true
        ));
        $this->hasColumn('child_id', 'integer', null, array(
            'primary' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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
Equal Nest Relations

Equal nest relations are perfectly suitable for expressing relations where a class references to itself and the columns within the reference class are equal.

This means that when fetching related records it doesn’t matter which column in the reference class has the primary key value of the main class.

The previous clause may be hard to understand so lets take an example. We define a class called User which can have many friends. Notice here how we use the ‘equal’ option.

// models/User.php
class User extends BaseUser
{

    public function setUp()
    {
        parent::setUp();

        // ...

        $this->hasMany('User as Friends', array(
            'local'    => 'user1',
            'foreign'  => 'user2',
            'refClass' => 'FriendReference',
            'equal'    => true,
        ));
    }
}
// models/FriendReference.php
class FriendReference extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user1', 'integer', null, array(
            'primary' => true
        ));
        $this->hasColumn('user2', 'integer', null, array(
            'primary' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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
$daniels = new User();
$daniels->username = 'Jack Daniels';

$brandy = new User();
$brandy->username = 'John Brandy';

$koskenkorva = new User();
$koskenkorva->username = 'Mikko Koskenkorva';

$beer = new User();
$beer->username = 'Stefan Beer';

$daniels->Friends[0] = $brandy;

$koskenkorva->Friends[0] = $daniels;
$koskenkorva->Friends[1] = $brandy;
$koskenkorva->Friends[2] = $beer;

$conn->flush();

Note

Calling :php:meth:`Doctrine_Connection::flush` will trigger an operation that saves all unsaved objects and wraps it in a single transaction.

Now if we access for example the friends of Stefan Beer it would return one user ‘Mikko Koskenkorva’:

// ...
$beer->free();
unset($beer);
$user = Doctrine_Core::getTable('User')->findOneByUsername('Stefan Beer');

print_r($user->Friends->toArray());

Now when you execute test.php you will see the following:

$ php test.php
Array
    (
        [0] => Array
            (
                [id] => 4
                [is_active] => 1
                [is_super_admin] => 0
                [first_name] =>
                [last_name] =>
                [username] => Mikko Koskenkorva
                [password] =>
                [type] =>
                [created_at] => 2009-01-20 16:53:13
                [updated_at] => 2009-01-20 16:53:13
            )
    )
Foreign Key Constraints
Introduction

A foreign key constraint specifies that the values in a column (or a group of columns) must match the values appearing in some row of another table. In other words foreign key constraints maintain the referential integrity between two related tables.

Say you have the product table with the following definition:

// models/Product.php
class Product extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');
        $this->hasColumn('price', 'decimal', 18);
        $this->hasColumn('discounted_price', 'decimal', 18);
    }

    public function setUp()
    {
        $this->hasMany('Order as Orders', array(
            'local' => 'id',
            'foreign' => 'product_id'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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
class Order extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('orders');
        $this->hasColumn('product_id', 'integer');
        $this->hasColumn('quantity', 'integer');
    }

    public function setUp()
    {
        $this->hasOne('Product', array(
            'local' => 'product_id',
            'foreign' => 'id'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Order:
  tableName: orders
  columns:
    product_id: integer
    quantity: integer
    relations:
      Product:
        local: product_id
        foreign: id

Note

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:

CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    product_id INTEGER REFERENCES products (id),
    quantity INTEGER,
    INDEX product_id_idx (product_id)
)

Now it is impossible to create orders with a product_id that does not appear in the product table.

We say that in this situation the orders table is the referencing table and the products table is the referenced table. Similarly, there are referencing and referenced columns.

Foreign Key Names

When you define a relationship in Doctrine, when the foreign key is created in the database for you Doctrine will try to create a foreign key name for you. Sometimes though, this name may not be something you want so you can customize the name to use with the foreignKeyName option to your relationship setup.

// models/Order.php
class Order extends Doctrine_Record
{
    // ...

    public function setUp()
    {
        $this->hasOne('Product', array(
            'local' => 'product_id',
            'foreign' => 'id',
            'foreignKeyName' => 'product_id_fk'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Order:
    # ...
    relations:
        Product:
            local: product_id
            foreign: id
            foreignKeyName: product_id_fk
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.

Note

The integrity constraints listed above are case sensitive and must be in upper case when being defined in your schema.

Below is an example where the database delete cascading is used.

class Phonenumber extends Doctrine_Record
{
    // ...

    public function setUp()
    {
        parent::setUp();

        // ...

        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id',
            'onDelete' => 'CASCADE'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Phonenumber:
    # ...
    relations:
        # ...
        User:
            local: user_id
            foreign: id
            onDelete: CASCADE

Note

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:

Note

The following index examples are not meant for you to actually add to your test Doctrine environment. They are only meant to demonstrate the API for adding indexes.

class IndexTest extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');
        $this->index('myindex', array(
            'fields' => array('name')
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

IndexTest:
  columns:
    name: string
  indexes:
    myindex:
      fields: [name]

An example of adding a multi-column index to field called name:

class MultiColumnIndexTest extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');
        $this->hasColumn('code', 'string');

        $this->index('myindex', array(
            'fields' => array('name', 'code')
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

MultiColumnIndexTest:
  columns:
    name: string
    code: string
  indexes:
    myindex:
      fields: [name, code]

An example of adding multiple indexes on same table:

class MultipleIndexTest extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');
        $this->hasColumn('code', 'string'); $this->hasColumn('age', 'integer');

        $this->index('myindex', array(
            'fields' => array('name', 'code')
        ));

        $this->index('ageindex', array(
            'fields' => array('age')
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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 valuethat can be either ‘ASC’ or ‘DESC’.
length Index length (only some drivers support this).
primary Whether or not the index is a primary index.
type A string value that can be unique, ‘fulltext’, ‘gist’ or ‘gin’.

Here is an example of how to create a unique index on the name column.

class MultipleIndexTest extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');
        $this->hasColumn('code', 'string');
        $this->hasColumn('age', 'integer');

        $this->index('myindex', array(
            'fields' => array(
                'name' => array(
                    'sorting' => 'ASC',
                    'length'  => 10),
                'code'
            ),
            'type' => 'unique',
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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’.

// models/Article.php
class Article extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
        $this->hasColumn('content', 'string');

        $this->option('type', 'MyISAM');

        $this->index('content', array(
            'fields' => array('content'),
            'type'   => 'fulltext'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Article:
  options:
    type: MyISAM
  columns:
    name: string(255)
    content: string
  indexes:
    content:
      fields: [content]
      type: fulltext

Note

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 :php:meth:`check` method of the :php:class:`Doctrine_Record`. In the last example we add constraint to ensure that price is always higher than the discounted price.

// models/Product.php
class Product extends Doctrine_Record
{
    public function setTableDefinition()
    {
        // ...
        $this->check('price > discounted_price');
    }

    // ...
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Product:
  # ...
  checks:
    price_check: price > discounted_price

Generates (in pgsql):

CREATE TABLE product (
    id INTEGER,
    price NUMERIC,
    discounted_price NUMERIC,
    PRIMARY KEY(id),
    CHECK (price >= 0),
    CHECK (price <= 1000000),
    CHECK (price > discounted_price)
)

Note

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:

class MyInnoDbRecord extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');
        $this->option('type', 'INNODB');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

MyInnoDbRecord:
  columns:
    name: string
  options:
    type: INNODB

In the following example we set the collate and character set options:

class MyCustomOptionRecord extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string');

        $this->option('collate', 'utf8_unicode_ci');
        $this->option('charset', 'utf8');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

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:

$conn = Doctrine_Manager::connection();
$conn->setCharset('utf8');
Record Filters

Doctrine offers the ability to attach record filters when defining your models. A record filter is invoked whenever you access a property on a model that is invalid. So it allows you to essentially add properties dynamically to a model through the use of one of these filters.

To attach a filter you just need to add it in the :php:meth:`setUp` method of your model definition:

class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('username', 'string', 255);
        $this->hasColumn('password', 'string', 255);
    }

    public function setUp()
    {
        $this->hasOne('Profile', array(
            'local' => 'id',
            'foreign' => 'user_id'
        ));
        $this->unshiftFilter(new Doctrine_Record_Filter_Compound(array('Profile')));
    }
}

class Profile extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer');
        $this->hasColumn('first_name', 'string', 255);
        $this->hasColumn('last_name', 'string', 255);
    }

    public function setUp()
    {
        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        ));
    }
}

Now with the above example we can easily access the properties of the Profile relationship when using an instance of User. Here is an example:

$user = Doctrine_Core::getTable('User')
            ->createQuery('u')
            ->innerJoin('u.Profile p')
            ->where('p.username = ?', 'jwage')
            ->fetchOne();

echo $user->first_name . ' ' . $user->last_name;

When we ask for the first_name and last_name properties they do not exist on the $user instance so they are forwarded to the Profile relationship. It is the same as if you were to do the following:

echo $user->Profile->first_name . ' ' . $user->Profile->last_name;

You can write your own record filters pretty easily too. All that is required is you create a class which extends Doctrine_Record_Filter and implements the :php:meth:`filterSet` and :php:meth:`filterGet` methods. Here is an example:

class MyRecordFilter extends Doctrine_Record_Filter
{
    public function filterSet(Doctrine_Record $record, $name, $value)
    {
        // try and set the property
        throw new Doctrine_Record_UnknownPropertyException(sprintf(
            'Unknown record property / related component "%s" on "%s"',
            $name,
            get_class($record)
        ));
    }

    public function filterGet(Doctrine_Record, $name)
    {
        // try and get the property
        throw new Doctrine_Record_UnknownPropertyException(sprintf(
            'Unknown record property / related component "%s" on "%s"',
            $name,
            get_class($record)
        ));
    }
}

Now you can add the filter to your models:

class MyModel extends Doctrine_Record
{
    // ...

    public function setUp()
    {
        // ...
        $this->unshiftFilter(new MyRecordFilter());
    }
}

Note

Remember to be sure to throw an instance of the Doctrine_Record_UnknownPropertyException exception class if :php:meth:`filterSet` or :php:meth:`filterGet` fail to find the property.

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 :php:meth:`save` operations are already cascaded to associated objects by default.

Delete Cascades

Doctrine provides a second application-level cascade style: delete. Unlike the :php:meth:`save` cascade, the delete cascade needs to be turned on explicitly as can be seen in the following code snippet:

// models/User.php
class User extends BaseUser
{
    // ...
    public function setUp()
    {
        parent::setup();

        // ...

        $this->hasMany('Address as Addresses', array(
            'local' => 'id',
            'foreign' => 'user_id',
            'cascade' => array('delete')
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter

# 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.

Note

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 :php:meth:`$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:

  • Application-level delete cascades don’t perform well on many-valued associations when there are a lot of objects in the related collection (that is because they need to be fetched from the database, the actual deletion is pretty efficient).
  • Application-level delete cascades do not skip the object lifecycle as database-level cascades do (see next chapter). Therefore all registered event listeners and other callback methods are properly executed in an application-level cascade.
Database-Level Cascades

Some cascading operations can be done much more efficiently at the database level. The best example is the delete cascade.

Database-level delete cascades are generally preferrable over application-level delete cascades except:

  • Your database does not support database-level cascades (i.e. when using MySql with MYISAM tables).
  • You have listeners that listen on the object lifecycle and you want them to get invoked.

Database-level delete cascades are applied on the foreign key constraint. Therefore they’re specified on that side of the relation that owns the foreign key. Picking up the example from above, the definition of a database-level cascade would look as follows:

// models/Address.php
class Address extends Doctrine_Record
{
    public function setTableDefinition() {
        $this->hasColumn('user_id', 'integer');
        $this->hasColumn('address', 'string', 255);
        $this->hasColumn('country', 'string', 255);
        $this->hasColumn('city', 'string', 255);
        $this->hasColumn('state', 'string', 2);
        $this->hasColumn('postal_code', 'string', 25);
    }

    public function setUp()
    {
        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id',
            'onDelete' => 'CASCADE'
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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

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 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.

Working with Models

Define Test Schema

Note

Remember to delete any existing schema information and models from previous chapters.

rm schema.yml
touch schema.yml
rm -rf models/*

For the next several examples we will use the following schema:

// models/User.php
class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('username', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        ));
        $this->hasColumn('password', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        ));
    }

    public function setUp()
    {
        $this->hasMany('Group as Groups', array(
            'refClass' => 'UserGroup',
            'local' => 'user_id',
            'foreign' => 'group_id'
        ));
        $this->hasOne('Email', array(
            'local' => 'id',
            'foreign' => 'user_id'
        ));
        $this->hasMany('Phonenumber as Phonenumbers', array(
            'local' => 'id',
            'foreign' => 'user_id'
        ));
    }
}
// models/Email.php
class Email extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer', null, array(
            'type' => 'integer'
        ));
        $this->hasColumn('address', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        ));
    }

    public function setUp()
    {
        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        ));
    }
}
// models/Phonenumber.php
class Phonenumber extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer', null, array(
            'type' => 'integer'
        ));
        $this->hasColumn('phonenumber', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        ));
        $this->hasColumn('primary_num', 'boolean');
    }

    public function setUp()
    {
        $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        ));
    }
}
// models/Group.php
class Group extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('groups');
        $this->hasColumn('name', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        ));
    }

    public function setUp()
    {
        $this->hasMany('User as Users', array(
            'refClass' => 'UserGroup',
            'local' => 'group_id',
            'foreign' => 'user_id'
        ));
    }
}
// models/UserGroup.php
class UserGroup extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('user_id', 'integer', null, array(
            'type' => 'integer',
            'primary' => true
        ));
        $this->hasColumn('group_id', 'integer', null, array(
            'type' => 'integer',
            'primary' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# 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.

php generate.php
Dealing with Relations
Many-to-Many Relations

Caution

Doctrine requires that Many-to-Many relationships be bi-directional. For example: both User must have many Groups and Group must have many User.

Fetching Objects

Normally when you fetch data from database the following phases are executed:

  • Sending the query to database
  • Retrieve the returned data from the database

In terms of object fetching we call these two phases the ‘fetching’ phase. Doctrine also has another phase called hydration phase. The hydration phase takes place whenever you are fetching structured arrays / objects. Unless explicitly specified everything in Doctrine gets hydrated.

Lets consider we have Users and Phonenumbers with their relation being one-to-many. Now consider the following plain sql query:

// test.php
$sql = 'SELECT
            u.id, u.username, p.phonenumber
        FROM user u
           LEFT JOIN phonenumber p
               ON u.id = p.user_id';

$results = $conn->getDbh()->fetchAll($sql);

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:

index u.id u.username p.phonenumber
0 1 Jack Daniels 123 123
1 1 Jack Daniels 456 456
2 2 John Beer 111 111
3 3 John Smith 222 222
4 3 John Smith 333 333
5 3 John Smith 444 444

Here Jack Daniels has two Phonenumbers, John Beer has one whereas John Smith has three. You may notice how clumsy this result set is. Its hard to iterate over it as you would need some duplicate data checking logic here and there.

Doctrine hydration removes all duplicated data. It also performs many other things such as:

  • Custom indexing of result set elements
  • Value casting and preparation
  • Value assignment listening
  • Makes multi-dimensional array out of the two-dimensional result set array, the number of dimensions is equal to the number of nested joins

Now consider the DQL equivalent of the SQL query we used:

$q = Doctrine_Query::create()
      ->select('u.id, u.username, p.phonenumber')
      ->from('User u')
      ->leftJoin('u.Phonenumbers p');

$results = $q->execute(array(), Doctrine_Core::HYDRATE_ARRAY);

print_r($results);

The structure of this hydrated array would look like

$ php test.php
Array (
    [0] => Array
        (
            [id] => 1
            [username] =>
            [Phonenumbers] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [phonenumber] => 123 123
                        )
                    [1] => Array
                        (
                            [id] => 2
                            [phonenumber] => 456 123
                        )
                    [2] => Array
                        (
                            [id] => 3
                            [phonenumber] => 123 777
                        )
                )
        )
)

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 :php:class:`Doctrine_Record` objects and the arrays converted into :php:class:`Doctrine_Collection` objects. Whether dealing with arrays or objects you can:

  • Iterate over the results using foreach
  • Access individual elements using array access brackets
  • Get the number of elements using count() function
  • Check if given element exists using isset()
  • Unset given element using unset()

You should always use array hydration when you only need to data for access-only purposes, whereas you should use the record hydration when you need to change the fetched data.

The constant O(n) performance of the hydration algorithm is ensured by a smart identifier caching solution.

Tip

Doctrine uses an identity map internally to make sure that multiple objects for one record in a database don’t ever exist. If you fetch an object and modify some of its properties, then re-fetch that same object later, the modified properties will be overwritten by default. You can change this behavior by changing the ATTR_HYDRATE_OVERWRITE attribute to false.

Sample Queries
  • Count number of records for a relationship:

    // test.php
    $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'];
    
  • Retrieve Users and the Groups they belong to:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->leftJoin('u.Groups g');
    
    $users = $q->fetchArray();
    
    foreach ($users[0]['Groups'] as $group) {
        echo $group['name'];
    }
    
  • Simple WHERE with one parameter value:

    $q = Doctrine_Query::create()
        ->from('User u')
        ->where('u.username = ?', 'jwage');
    
    $users = $q->fetchArray();
    
  • Multiple WHERE with multiple parameters values:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->leftJoin('u.Phonenumbers p')
            ->where('u.username = ? AND p.id = ?', array(1, 1));
    
    $users = $q->fetchArray();
    

Tip

You can also optionally use the :php:meth:`andWhere` method to add to the existing where parts.

$q = Doctrine_Query::create()
        ->from('User u')
        ->leftJoin('u.Phonenumbers p')
        ->where('u.username = ?', 1)
        ->andWhere('p.id = ?', 1);

$users = $q->fetchArray();
  • Using :php:meth:`whereIn` convenience method:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->whereIn('u.id', array(1, 2, 3));
    
    $users = $q->fetchArray();
    
  • The following is the same as above example:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->where('u.id IN (1, 2, 3)');
    
    $users = $q->fetchArray();
    
  • Using DBMS function in your WHERE:

    $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:

    $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();
    
  • Join only primary phonenumbers using WITH:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->leftJoin('u.Phonenumbers p WITH p.primary_num = ?', true);
    
    $users = $q->fetchArray();
    
  • Selecting certain columns for optimization:

    $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:

    $q = Doctrine_Query::create()
            ->select('u.*, p.phonenumber')
            ->from('User u')
            ->leftJoin('u.Phonenumbers p');
    
    $users = $q->fetchArray();
    
  • Perform DQL delete with simple WHERE:

    $q = Doctrine_Query::create()
            ->delete('Phonenumber')
            ->addWhere('user_id = 5');
    
    $deleted = $q->execute();
    
  • Perform simple DQL update for a column:

    $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:

    $q = Doctrine_Query::create()
            ->update('User u')
            ->set('u.username', 'LOWER(u.username)');
    
    $updated = $q->execute();
    
  • Using mysql LIKE to search for records:

    $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:

    $q = Doctrine_Query::create()
            ->from('User u INDEXBY u.username');
    
    $users = $q->fetchArray();
    

    Now we can print the user with the username of jwage:

    print_r($users['jwage']);
    
  • Using positional parameters:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->where('u.username = ?', array('Arnold'));
    
    $users = $q->fetchArray();
    
  • Using named parameters:

    $q = Doctrine_Query::create()
            ->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:

    $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();
    

Tip

You can accomplish this without using subqueries. The two examples below would have the same result as the example above.

  • Use INNER JOIN to retrieve users who have groups, excluding the group named Group 2:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->innerJoin('u.Groups g WITH g.name != ?', 'Group 2');
    
    $users = $q->fetchArray();
    
  • Use WHERE condition to retrieve users who have groups, excluding the group named Group 2:

    $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:

    $q = Doctrine_Query::create() ->from('User u');
    
  • You can use array hydration with the :php:meth:`fetchArray` method:

    $users = $q->fetchArray();
    
  • You can also use array hydration by specifying the hydration method to the second argument of the :php:meth:`execute` method:

    $users = $q->execute(array(), Doctrine_Core::HYDRATE_ARRAY)
    
  • You can also specify the hydration method by using the :php:meth:`setHydrationMethod` method:

    $users = $q->setHydrationMode(Doctrine_Core::HYDRATE_ARRAY)->execute(); // So is this
    

Note

Custom accessors and mutators will not work when hydrating data as anything except records. When you hydrate as an array it is only a static array of data and is not object oriented. If you need to add custom values to your hydrated arrays you can use the some of the events such as preHydrate and postHydrate

  • Sometimes you may want to totally bypass hydration and return the raw data that PDO returns:

    $users = $q->execute(array(), Doctrine_Core::HYDRATE_NONE);
    

Tip

More can be read about skipping hydration in the improving performance chapter.

  • If you want to just fetch one record from the query:

    $user = $q->fetchOne();
    
    // Fetch all and get the first from collection
    $user = $q->execute()->getFirst();
    
Field Lazy Loading

Whenever you fetch an object that has not all of its fields loaded from database then the state of this object is called proxy. Proxy objects can load the unloaded fields lazily.

In the following example we fetch all the Users with the username field loaded directly. Then we lazy load the password field:

// test.php
$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:

$user->password;

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.

Arrays and Objects

:php:class:`Doctrine_Record` and :php:class:`Doctrine_Collection` provide methods to facilitate working with arrays: :php:meth:`toArray`, :php:meth:`fromArray` and :php:meth:`synchronizeWithArray`.

To Array

The :php:meth:`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.

print_r($user->toArray());

If you do not want to include the relationships in the array then you need to pass the $deep argument with a value of false:

print_r($user->toArray(false));
From Array

If you have an array of values you want to use to fill a record or even a collection, the :php:meth:`fromArray` method simplifies this common task.

$data = array(
    'name'   => 'John',
    'age'    => '25',
    'Emails' => array(
        array('address' => 'john@mail.com'),
        array('address' => 'john@work.com')
    )
);

$user = new User();
$user->fromArray($data);
$user->save();
Synchronize With Array

:php:meth:`synchronizeWithArray` allows you to... well, synchronize a record with an array. So if have an array representation of your model and modify a field, modify a relationship field or even delete or create a relationship, this changes will be applied to the record.

$q = Doctrine_Query::create()
        ->select('u.*, g.*')
        ->from('User u')
        ->leftJoin('u.Groups g')
        ->where('id = ?', 1);

$user = $q->fetchOne();

Now convert it to an array and modify some of the properties:

$arrayUser = $user->toArray(true);

$arrayUser['username'] = 'New name';
$arrayUser['Group'][0]['name'] = 'Renamed Group';
$arrayUser['Group'][] = array('name' => 'New Group');

Now use the same query to retrieve the record and synchronize the record with the $arrayUser variable:

$user = Doctrine_Query::create()
            ->select('u.*, g.*')
            ->from('User u')
            ->leftJoin('u.Groups g')
            ->where('id = ?', 1)
            ->fetchOne();

$user->synchronizeWithArray($arrayUser);
$user->save();
Overriding the Constructor

Sometimes you want to do some operations at the creation time of your objects. Doctrine doesn’t allow you to override the :php:meth:`Doctrine_Record::__construct` method but provides an alternative:

class User extends Doctrine_Record
{
    public function construct()
    {
        $this->username = 'Test Name';
        $this->doSomething();
    }

    public function doSomething()
    {
        // ...
    }

    // ...
}

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 there 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.

DQL: Doctrine Query Language

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 chapter. We will define one additional model for our testing purposes:

// models/Account.php
class Account extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
        $this->hasColumn('amount', 'decimal');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Account:
  columns:
    name: string(255)
    amount: decimal

When compared to using raw SQL, DQL has several benefits:

  • 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 the 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 for object population.

You may already be familiar with the following syntax:

Caution

DO NOT USE THE FOLLOWING CODE. It uses many SQL queries for object population.

// test.php
$users = Doctrine_Core::getTable('User')->findAll();

foreach($users as $user) {
    echo $user->username . " has phonenumbers: ";

    foreach($user->Phonenumbers as $phonenumber) {
        echo $phonenumber->phonenumber . "\n";
    }
}

Tip

Here is the same code but implemented more efficiently using only one SQL query for object population.

$q = Doctrine_Query::create()
        ->from('User u')
        ->leftJoin('u.Phonenumbers p');

echo $q->getSqlQuery();

Lets take a look at the SQL that would be generated by the above query:

SELECT
    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:

$users = $q->execute();

foreach($users as $user) {
    echo $user->username . " has phonenumbers: ";

    foreach($user->Phonenumbers as $phonenumber) {
        echo $phonenumber->phonenumber . "\n";
    }
}

Caution

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
    [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> OFFSET <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
$account = new Account();
$account->name = 'test 1';

$account->amount = '100.00';
$account->save();

$account = new Account();
$account->name = 'test 2';
$account->amount = '200.00';
$account->save();

Be sure to execute test.php:

php test.php

Now you can test the selecting of the data with these next few sample queries:

$q = Doctrine_Query::create()
        ->select('a.name')
        ->from('Account a');

echo $q->getSqlQuery();

Lets take a look at the SQL that would be generated by the above query:

SELECT a.id AS a__id, a.name AS a__name FROM account a
// ...
$accounts = $q->execute();
print_r($accounts->toArray());

The above example would produce the following output:

$ php test.php
Array (
    [0] => Array
        (
            [id] => 1
            [name] => test 1
            [amount] =>
        )
    [1] => Array
        (
            [id] => 2
            [name] => test 2
            [amount] =>
        )
)

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).

$q = Doctrine_Query::create()
        ->select('a.*')
        ->from('Account a');

echo $q->getSqlQuery();

Compare the generated SQL from the last query example to the SQL generated by the query right above:

SELECT
    a.id AS a__id,
    a.name AS a__name,
    a.amount AS a__amount
FROM account a

Note

Notice how the asterisk is replace by all the real column names that exist in the Account model.

Now lets execute the query and inspect the results:

$accounts = $q->execute();
print_r($accounts->toArray());

The above example would produce the following output:

$ php test.php
Array (
    [0] => Array
        (
            [id] => 1
            [name] => test 1
            [amount] => 100.00
        )
    [1] => Array
        (
            [id] => 2
            [name] => test 2
            [amount] => 200.00
        )
)

FROM clause components indicate the component or components from which to retrieve records.

$q = Doctrine_Query::create()
        ->select('u.username, p.*')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p')

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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.

$q = Doctrine_Query::create()
        ->select('a.name')
        ->from('Account a')
        ->where('a.amount > 2000');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT 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:

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p')
        ->having('COUNT(p.id) > 3');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
   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

The ORDER BY clause can be used for sorting the results:

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->orderBy('u.username');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->limit(20);

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT u.id AS u__id, u.username AS u__username FROM user u LIMIT 20
Aggregate values

Aggregate value SELECT syntax:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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

Now execute the query and inspect the results:

$users = $q->execute();

You can easily access the num_threads data with the following code:

echo $users->num_threads . ' threads found';
UPDATE queries

UPDATE statement syntax:

UPDATE <component_name>
SET
    <col_name1> = <expr1>,
    <col_name2> = <expr2>
WHERE <where_condition>
ORDER BY <order_by>
LIMIT <record_count>
  • The UPDATE statement updates columns of existing records in component_name with new values and returns the number of affected records.
  • The SET clause indicates which columns to modify and the values they should be given.
  • The optional WHERE clause specifies the conditions that identify which records to update. Without WHERE clause, all records are updated.
  • The optional ORDER BY clause specifies the order in which the records are being updated.
  • The LIMIT clause places a limit on the number of records that can be updated. You can use LIMIT row_count to restrict the scope of the UPDATE. A LIMIT clause is a rows-matched restriction not a rows-changed restriction. The statement stops as soon as it has found record_count rows that satisfy the WHERE clause, whether or not they actually were changed.
$q = Doctrine_Query::create()
        ->update('Account')
        ->set('amount', 'amount + 200')
        ->where('id > 200');

// If you just want to set the amount to a value
//   $q->set('amount', '?', 500);

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

UPDATE account SET amount = amount + 200 WHERE id > 200

Now to perform the update is simple. Just execute the query:

$rows = $q->execute();

echo $rows;
DELETE Queries
DELETE 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.
$q = Doctrine_Query::create()
        ->delete('Account a')
        ->where('a.id > 3');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

DELETE FROM account WHERE id > 3

Now executing the DELETE query is just as you would think:

$rows = $q->execute();

echo $rows;

Note

When executing DQL UPDATE and DELETE queries the executing of a query returns the number of affected rows.

FROM clause

Syntax:

FROM <component_reference>
    [[LEFT | INNER] JOIN <component_reference>] ...

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:

$q = Doctrine_Query::create()
        ->select('u.id')
        ->from('User u');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT 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:

[[LEFT | INNER] JOIN <component_reference1>] [ON | WITH] <join_condition1> [INDEXBY] <map_condition1>,
[[LEFT | INNER] JOIN <component_reference2>] [ON | WITH] <join_condition2> [INDEXBY] <map_condition2>,
...
[[LEFT | INNER] JOIN <component_referenceN>] [ON | WITH] <join_conditionN> [INDEXBY] <map_conditionN>

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:

$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->getSqlQuery();

Tip

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 :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
u.id AS u__id,
    p.id AS p__id
FROM user u
    LEFT JOIN phonenumber p
ON u.id = p.user_id

Note

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:

$q = Doctrine_Query::create()
        ->select('u.id, p.id')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

$q = Doctrine_Query::create()
        ->select('u.id, p.id')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p ON u.id = 2');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    u.id AS u__id,
    p.id AS p__id
FROM User u
    LEFT JOIN Phonenumbers p ON u.id = 2

Note

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.

$q = Doctrine_Query::create()
        ->select('u.id, p.id')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p WITH u.id = 2');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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

Note

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 :php:meth:`innerJoin` and :php:meth:`leftJoin`, which usage should be quite intuitive as shown below:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

$q = Doctrine_Query::create()
        ->from('User u INDEXBY u.username');

$users = $q->execute();

Note

The INDEXBY keyword does not alter the generated SQL. It is simply used internally by :php:class:`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:

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.

$q = Doctrine_Query::create()
        ->from('User u INDEXBY u.username')
        ->innerJoin('u.Groups g INDEXBY g.name');

$users = $q->execute();

Now lets print out the drinkers club’s creation date.

echo $users['jack daniels']->Groups['drinkers club']->createdAt;
WHERE clause

Syntax:

WHERE <where_condition>
  • 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

You can use the :php:meth:`addWhere`, :php:meth:`andWhere`, :php:meth:`orWhere`, :php:meth:`whereIn`, :php:meth:`andWhereIn`, :php:meth:`orWhereIn`, :php:meth:`whereNotIn`, :php:meth:`andWhereNotIn`, :php:meth:`orWhereNotIn` functions for building complex where conditions using :php:class:`Doctrine_Query` objects.

Here is an example where we retrieve all active registered users or super administrators:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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´´.

$q = Doctrine_Query::create()
        ->select('u.id, u.username')
        ->from('User u')
        ->where('u.username = ?', 'Vincent');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    u.id AS u__id,
    u.username AS u__username
FROM user u
WHERE
    u.username = ?

Note

Because we passed the value of the username as a parameter to the :php:meth:`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 :php:class:`Doctrine_Query` instance you can use the :php:meth:`getParams` method.

Integers

Integer literals support the use of PHP integer literal syntax.

$q = Doctrine_Query::create()
        ->select('a.id')
        ->from('User u')
        ->where('u.id = 4');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT u.id AS u__id FROM user u WHERE u.id = 4
Floats

Float literals support the use of PHP float literal syntax.

$q = Doctrine_Query::create()
        ->select('a.id')
        ->from('Account a')
        ->where('a.amount = 432.123');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT a.id AS a__id FROM account a WHERE a.amount = 432.123
Booleans

The boolean literals are true and false.

$q = Doctrine_Query::create()
        ->select('a.id')
        ->from('User u')
        ->where('u.is_super_admin = true');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT 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.

$q = Doctrine_Query::create()
        ->select('a.id')
        ->from('User u')
        ->where("u.type = 'admin'");

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT 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:

    $q = Doctrine_Query::create()
            ->select('u.id')
            ->from('User u')
            ->where('u.username = ?', array('Arnold'));
    
    echo $q->getSqlQuery();
    

    Note

    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 :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        u.id AS u__id
    FROM user u
    WHERE u.username = ?
    
  • Multiple positional parameters:

    $q = Doctrine_Query::create()
            ->from('User u')
            ->where('u.id > ? AND u.username LIKE ?', array(50, 'A%'));
    
    echo $q->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        u.id AS u__id
    FROM user u
    WHERE (
        u.id > ?
        AND u.username LIKE ?
    )
    

Here are some examples of using named parameters:

  • Single named parameter:

    $q = Doctrine_Query::create()
            ->select('u.id')
            ->from('User u')
            ->where('u.username = :name', array(':name' => 'Arnold'));
    
    echo $q->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        u.id AS u__id
    FROM user u
    WHERE
        u.username = :name
    
  • Named parameter with a LIKE statement:

    $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->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        u.id AS u__id
    FROM user u
    WHERE
        u.id > :id
        AND u.username LIKE :name
    
Operators and operator precedence

The operators are listed below in order of decreasing precedence.

Operator Description
. Navigation operator
Arithmetic operators:
+, - Unary
*, / Multiplication and division
+, - Addition and subtraction
Comparison operators:
=, >, >=, <, <=, <> (not equal)  
[NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY  
Logical operators:
NOT, AND, OR,  
In expressions

Syntax:

<operand> IN (<subquery>|<value list>)

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:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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 = ?
)

Here is an example where we just use a list of integers:

$q = Doctrine_Query::create()
        ->select('u.id')
        ->from('User u')
        ->whereIn('u.id', array(1, 3, 4, 5));

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT u.id AS u__id FROM user u WHERE u.id IN (?, ?, ?, ?)
Like Expressions

Syntax:

string_expression [NOT] LIKE pattern_value [ESCAPE escape_character]

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‘:

    $q = Doctrine_Query::create()
            ->select('u.id')
            ->from('User u')
            ->leftJoin('u.Email e')
            ->where('e.address LIKE ?', '%@gmail.com');
    
    echo $q->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        u.id AS u__id
    FROM user u
        LEFT JOIN email e ON u.id = e.user_id
    WHERE
        e.address LIKE ?
    
  • Find all users whose name starts with letter ‘A’:

    $q = Doctrine_Query::create()
            ->select('u.id')
            ->from('User u')
            ->where('u.username LIKE ?', 'A%');
    
    echo $q->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        u.id AS u__id
    FROM user u
    WHERE
        u.username LIKE ?
    
Exists Expressions

Syntax:

[NOT] EXISTS (<subquery>)

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.

Note

For the next few examples we need to add the ReaderLog model.

// models/ReaderLog.php
class ReaderLog extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('article_id', 'integer', null, array(
            'primary' => true
        ));
        $this->hasColumn('user_id', 'integer', null, array(
            'primary' => true
        ));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
ReaderLog:
  columns:
    article_id:
      type: integer
      primary: true
    user_id:
      type: integer
      primary: true

Note

After adding the ReaderLog model don’t forget to run the generate.php script!

php generate.php

Now we can run some tests! First, finding all articles which have readers:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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
    )

Finding all articles which don’t have readers:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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
    )
All and Any Expressions

Syntax:

operand comparison_operator ANY (subquery)
operand comparison_operator SOME (subquery)
operand comparison_operator ALL (subquery)

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()
       ->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.

$q = Doctrine_Query::create()
        ->from('C')
        ->where('C.col1 > ANY (FROM C2(col1))');

The keyword SOME is an alias for ANY.

$q = Doctrine_Query::create()
        ->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:

FROM C WHERE C.col1 <> ALL (FROM C2(col1));
FROM C WHERE C.col1 NOT IN (FROM C2(col1));
$q = Doctrine_Query::create()
        ->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:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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.

$q = Doctrine_Query::create()
        ->select('CONCAT(u.first_name, u.last_name) AS name')
        ->from('User u');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

$users = $q->execute();

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.

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->where("SUBSTRING(u.username, 0, 1) = 'z'");

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    u.id AS u__id,
    u.username AS u__username
FROM user u
WHERE
    SUBSTRING(u.username FROM 0 FOR 1) = 'z'

Note

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.

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->where('TRIM(u.username) = ?', 'Someone');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
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.

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->where("LOWER(u.username) = 'jon wage'");

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

  • 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.

Comparisons using subqueries

Find all the users which are not in a specific group.

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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.

$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->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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, HAVING clauses

DQL GROUP BY syntax:

GROUP BY groupby_item {, groupby_item}*

DQL HAVING syntax:

HAVING conditional_expression

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:

    $q = Doctrine_Query::create()
            ->select('MIN(a.amount)')
            ->from('Account a');
    
    echo $q->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT MIN(a.amount) AS a__0 FROM account a
    
  • Selecting the sum of all Account amounts:

    $q = Doctrine_Query::create()
            ->select('SUM(a.amount)')
            ->from('Account a');
    
    echo $q->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT SUM(a.amount) AS a__0 FROM account a
    
  • Using an aggregate function in a statement containing no GROUP BY clause, results in grouping on all rows. In the example below we fetch all users and the number of phonenumbers they have.

    $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->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        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 at least 2 phonenumbers:

    $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->getSqlQuery();
    

    The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

    SELECT
        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:

    $users = $q->execute();
    
    foreach($users as $user) {
        echo $user->name
            . ' has '
            . $user->num_phonenumbers
            . ' phonenumbers';
    }
    
ORDER BY clause
Introduction

Record collections can be sorted efficiently at the database level using the ORDER BY clause.

Syntax:

[
   ORDER BY {ComponentAlias.columnName}
   [ASC | DESC], ...
]

Examples:

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p')
        ->orderBy('u.username, p.phonenumber');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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.

$q = Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->leftJoin('u.Email e')
        ->orderBy('e.address DESC, u.id ASC');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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
Sorting by an aggregate value

In the following example we fetch all users and sort those users by the number of phonenumbers they have.

$q = Doctrine_Query::create()
        ->select('u.username, COUNT(p.id) count')
        ->from('User u')
        ->innerJoin('u.Phonenumbers p')
        ->orderby('count');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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
Using random order

In the following example we use random in the ORDER BY clause in order to fetch random post.

$q = Doctrine_Query::create()
        ->select('t.id, RANDOM() AS rand')
        ->from('Forum_Thread t')
        ->orderby('rand')
        ->limit(1);

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT f.id AS f__id, RAND() AS f__0 FROM forum__thread f ORDER BY f__0 LIMIT 1
LIMIT and OFFSET clauses

Propably the most complex feature DQL parser has to offer is its LIMIT clause parser. Not only does the DQL LIMIT clause parser take care of LIMIT database portability it is capable of limiting the number of records instead of rows by using complex query analysis and subqueries.

Retrieve the first 20 users and all their associated phonenumbers:

$q = Doctrine_Query::create()
        ->select('u.username, p.phonenumber')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p')
        ->limit(20);

echo $q->getSqlQuery();

Tip

You can also use the :php:meth:`offset` method of the :php:class:`Doctrine_Query` object in combination with the :php:meth:`limit` method to produce your desired LIMIT and OFFSET in the executed SQL query.

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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:

$table->setAttribute(Doctrine_Core::ATTR_QUERY_LIMIT, Doctrine_Core::LIMIT_ROWS);
$table->setAttribute(Doctrine_Core::ATTR_QUERY_LIMIT, Doctrine_Core::LIMIT_RECORDS); // revert

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.

$q = Doctrine_Query::create()
        ->select('u.id, u.username, p.*')
        ->from('User u')
        ->leftJoin('u.Phonenumbers p')
        ->limit(20);

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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
    )

Note

Notice how the IN clause with the subquery was added. This is so that the users are limited to 20 and the users phonenumbers are not limited.

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:

$q = Doctrine_Query::create()
        ->select('u.id, u.username, p.*')
        ->from('User u')
        ->innerJoin('u.Phonenumbers p')
        ->limit(20);

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    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 :php:meth:`add` method of the registry object. It takes two parameters, the query name and the actual DQL query.

$r = Doctrine_Manager::getInstance()->getQueryRegistry();
$r->add('User/all', 'FROM User u');

$userTable = Doctrine_Core::getTable('User');
// find all users $users = $userTable->find('all');

To simplify this support, :php:class:`Doctrine_Table` support some accessors to Doctrine_Query_Registry.

Creating a Named Query

When you build your models with option generateTableClasses defined as true, each record class will also generate a *Table class, extending from :php:class:`Doctrine_Table`.

Then, you can implement the method :php:meth:`construct` to include your Named Queries:

class UserTable extends Doctrine_Table
{
    public function construct()
    {
        // Named Query defined using DQL string
        $this->addNamedQuery('get.by.id', 'SELECT u.username FROM User u WHERE u.id = ?');

        // Named Query defined using Doctrine_Query object
        $this->addNamedQuery(
            'get.by.similar.usernames',
            Doctrine_Query::create()
                ->select('u.id, u.username')
                ->from('User u')
                ->where('LOWER(u.username) LIKE LOWER(?)')
        );
    }
}
Accessing Named Query

To reach the MyFooTable class, which is a subclass of :php:class:`Doctrine_Table`, you can do the following:

$userTable = Doctrine_Core::getTable('User');

To access the Named Query (will return you a :php:class:`Doctrine_Query` instance, always):

$q = $userTable->createNamedQuery('get.by.id');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT
    u.id AS u__id,
    u.username AS u__username
FROM user u
WHERE u.id = ?
Executing a Named Query

There are two ways to execute a Named Query. The first one is by retrieving the :php:class:`Doctrine_Query` and then executing it normally, as a normal instance:

$users = Doctrine_Core::getTable('User')
            ->createNamedQuery('get.by.similar.usernames')
            ->execute(array('%jon%wage%'));

You can also simplify the execution, by doing:

$users = Doctrine_Core::getTable('User')
            ->find('get.by.similar.usernames', array('%jon%wage%'));

The method :php:meth:`find` also accepts a third parameter, which is the hydration mode.

Cross-Accessing Named Query

If that’s not enough, Doctrine take advantage the Doctrine_Query_Registry and uses namespace queries to enable cross-access of Named Queries between objects. Suppose you have the *Table class instance of record Article. You want to call the “get.by.id” Named Query of record User. To access the Named Query, you have to do:

$articleTable = Doctrine_Core::getTable('Article');
$users = $articleTable->find('User/get.by.id', array(1, 2, 3));
BNF
QL_statement                                   ::=  select_statement | update_statement | delete_statement
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_spec "FETCH" 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_field
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_expression "OR" conditional_term
conditional_term                               ::=  conditional_factor
                                                        | conditional_term "AND" 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_expression "AND" arithmetic_expression
                                                        | string_expression ["NOT"] "BETWEEN" string_expression "AND" string_expression
                                                        | datetime_expression ["NOT"] "BETWEEN" datetime_expression "AND" 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_expression
                                                    "IS" ["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
string_expression                              ::=  string_primary` | "(" subquery ")"
string_primary                                 ::=  state_field_path_expression
                                                        | string_literal
                                                        | input_parameter
                                                        | functions_returning_strings
                                                        | aggregate_expression
datetime_expression                            ::=  datetime_primary` | "(" subquery ")"
datetime_primary                               ::=  state_field_path_expression
                                                        | input_parameter
                                                        | functions_returning_datetime
                                                        | aggregate_expression
boolean_expression                             ::=  boolean_primary` | "(" subquery ")"
boolean_primary                                ::=  state_field_path_expression | boolean_literal | input_parameter
enum_expression                                ::=  enum_primary` | "(" subquery ")"
enum_primary                                   ::=  state_field_path_expression` | enum_literal | input_parameter
entity_expression                              ::=  single_valued_association_path_expression | simple_entity_expression
simple_entity_expression                       ::=  identification_variable | input_parameter
functions_returning_numerics                   ::=  "LENGTH" "(" string_primary ")"
                                                        | "LOCATE" "(" `string_primary "," string_primary ["," simple_arithmetic_expression] ")"
                                                        | "ABS" "(" simple_arithmetic_expression ")"
                                                        | "SQRT" "(" simple_arithmetic_expression ")"
                                                        | "MOD" "(" simple_arithmetic_expression "," simple_arithmetic_expression ")"
                                                        | "SIZE" "(" collection_valued_path_expression ")"
functions_returning_datetime                   ::=  "CURRENT_DATE" | "CURRENT_TIME" | "CURRENT_TIMESTAMP"
functions_returning_strings                    ::=  CONCAT" "(" string_primary "," string_primary ")"
                                                        | "SUBSTRING" "(" string_primary "," simple_arithmetic_expression "," simple_arithmetic_expression ")"
                                                        | "TRIM" "(" [[trim_specification] [trim_character] "FROM"] string_primary ")"
                                                        | "LOWER" "(" string_primary ")"
                                                        | "UPPER" "(" string_primary ")"
trim_specification                             ::=  "LEADING" | "TRAILING" | "BOTH"
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 :php:class:`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:

$userTable = Doctrine_Core::getTable('User');

Now we can easily find a User record by its primary key by using the :php:meth:`find` method:

$user = $userTable->find(1);

Now if you want to find a single user by their username you can use the following magic finder:

$user = $userTable->findOneByUsername('jonwage');

You can also easily find records by using the relationships between records. Because User has many Phonenumbers we can find those Phonenumbers by passing the :php:meth:`findBy**` method a User instance:

$phonenumberTable = Doctrine_Core::getTable('Phonenumber');
$phonenumbers = $phonenumberTable->findByUser($user);

The magic finders will even allow a little more complex finds. You can use the And and Or keywords in the method name to retrieve record by multiple properties.

$user = $userTable->findOneByUsernameAndPassword('jonwage', md5('changeme'));

You can even mix the conditions.

$users = $userTable->findByIsAdminAndIsModeratorOrIsSuperAdmin(true, true, true);

Caution

These are very limited magic finders and it is always recommended to expand your queries to be manually written DQL queries. These methods are meant for only quickly accessing single records, no relationships, and are good for prototyping code quickly.

Note

The documented magic finders above are made possibly by using PHP’s __call() overloading functionality. The undefined functions are forwarded to :php:meth:`Doctrine_Table::{*}call` where the :php:class:`Doctrine_Query` objects are built, executed and returned to the user.

Debugging Queries

The :php:class:`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 SQL string of your :php:class:`Doctrine_Query` object:

$q = Doctrine_Query::create()
        ->select('u.id')
        ->from('User u')
        ->orderBy('u.username');

echo $q->getSqlQuery();

The above call to :php:meth:`getSqlQuery()` would output the following SQL query:

SELECT u.id AS u__id FROM user u ORDER BY u.username

Note

The SQL returned above by the :php:meth:`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 :php:meth:`Doctrine_Query::getParams` method.

Get the array of parameters for the :php:class:`Doctrine_Query` instance:

print_r($q->getParams());
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 chapter.

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 :php:class:`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.

$manager = Doctrine_Manager::getInstance();
Retrieving Connections
$connections = $manager->getConnections();
foreach (connections as $connection) {
    echo $connection->getName() . "";
}

The :php:class:`Doctrine_Manager` implements an iterator so you can simple loop over the $manager variable to loop over the connections.

foreach ($manager as $connection) {
    echo $connection->getName() . "";
}
Connection

:php:class:`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 :php:class:`Doctrine_Connection` class handles several things:

  • Handles database portability things missing from PDO (eg. LIMIT / OFFSET emulation)
  • Keeps track of :php:class:`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 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:

  • FreeTDS / Microsoft SQL Server / Sybase
  • Firebird/Interbase 6
  • Informix
  • Mysql
  • Oracle
  • Odbc
  • PostgreSQL
  • Sqlite
Creating Connections
// bootstrap.php
$conn = Doctrine_Manager::connection('mysql://username:password@localhost/test', 'connection 1');

Note

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 :php:meth:`Doctrine_Manager::connection` method.

Flushing the Connection

When you create new User records you can flush the connection and save all un-saved objects for that connection. Below is an example:

$conn = Doctrine_Manager::connection();

$user1 = new User();
$user1->username = 'Jack';

$user2 = new User();
$user2->username = 'jwage';

$conn->flush();

Calling :php:meth:`Doctrine_Connection::flush` will save all unsaved record instances for that connection. You could of course optionally call :php:meth:`save` on each record instance and it would be the same thing.

$user1->save();
$user2->save();
Table

:php:class:`Doctrine_Table` holds the schema information specified by the given component (record). For example if you have a User class that extends :php:class:`Doctrine_Record`, each schema definition call gets delegated to a unique table object that holds the information for later use.

Each :php:class:`Doctrine_Table` is registered by :php:class:`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 :php:meth:`Doctrine_Core::getTable` method.

Getting a Table Object

In order to get table object for specified record just call :php:meth:`Doctrine_Record::getTable`.

// test.php
$accountTable = Doctrine_Core::getTable('Account');
Getting Column Information

You can retrieve the column definitions set in :php:class:`Doctrine_Record` by using the appropriate :php:class:`Doctrine_Table` methods. If you need all information of all columns you can simply use:

// test.php
$columns = $accountTable->getColumns();

foreach ($columns as $column) {
    print_r($column);
}

The above example would output the following when executed:

$ php test.php
Array (
    [type] => integer
    [length] => 20
    [autoincrement] => 1
    [primary] => 1
)
Array (
    [type] => string
    [length] => 255
)
Array (
    [type] => decimal
    [length] => 18
)

Sometimes this can be an overkill. The following example shows how to retrieve the column names as an array:

// test.php
$names = $accountTable->getColumnNames();
print_r($names);

The above example would output the following when executed:

$ php test.php
Array (
    [0] => id
    [1] => name
    [2] => amount
)
Getting Relation Information

You can also get an array of all the Doctrine_Relation objects by simply calling :php:meth:`Doctrine_Table::getRelations` like the following:

// test.php
$userTable = Doctrine_Core::getTable('User');
$relations = $userTable->getRelations();
foreach ($relations as $name => $relation) {
    echo $name . ":\n";
    echo "Local - " . $relation->getLocal() . "\n";
    echo "Foreign - " .    $relation->getForeign() . "\n\n";
}

The above example would output the following when executed:

$ php test.php
Email:
Local - id
Foreign - user_id

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 :php:meth:`Doctrine_Table::getRelation` method.

// test.php
$relation = $userTable->getRelation('Phonenumbers');

echo 'Name: ' . $relation['alias'] . "\n";
echo 'Local - ' . $relation['local'] . "\n";
echo 'Foreign - ' .  $relation['foreign'] . "\n";
echo 'Relation Class - ' . get_class($relation);

The above example would output the following when executed:

$ php test.php
Name: Phonenumbers
Local - id
Foreign - user_id
Relation Class - Doctrine_Relation_ForeignKey

Note

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 :php:meth:`toArray` method and using :php:meth:`print_r` to inspect it.

$array = $relation->toArray();
print_r($array);
Finder Methods

:php:class:`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 :php:meth:`Doctrine_Connection::query`.

You can easily find an individual user by its primary key by using the :php:meth:`find` method:

$user = $userTable->find(2);
print_r($user->toArray());

The above example would output the following when executed:

$ php test.php
Array (
    [id] => 2
    [is_active] => 1
    [is_super_admin] => 0
    [first_name] =>
    [last_name] =>
    [username] => jwage
    [password] =>
    [type] =>
    [created_at] => 2009-01-21 13:29:12
    [updated_at] => 2009-01-21 13:29:12
)

You can also use the :php:meth:`findAll` method to retrieve a collection of all User records in the database:

foreach ($userTable->findAll() as $user) {
    echo $user->username . "\n";
}

The above example would output the following when executed:

$ php test.php
Jack
jwage

Caution

The :php:meth:`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 chapter.

You can also retrieve a set of records with a DQL where condition by using the :php:meth:`findByDql` method:

$users = $userTable->findByDql('username LIKE ?', '%jw%');

foreach($users as $user) {
    echo $user->username . "";
}

The above example would output the following when executed:

$ php test.php
jwage

Doctrine also offers some additional magic finder methods that can be read about in the [doc dql-doctrine-query-language:magic-finders :name] section of the DQL chapter.

Note

All of the finders below provided by :php:class:`Doctrine_Table` use instances of :php:class:`Doctrine_Query` for executing the queries. The objects are built dynamically internally and executed.

Using :php:class:`Doctrine_Query` instances are highly recommend when accessing multiple objects through relationships. If you don’t you will have high query counts as the data will be lazily loaded. You can read more about this in the DQL: Doctrine Query Language chapter.

Custom Table Classes

Adding custom table classes is very easy. Only thing you need to do is name the classes as [componentName]Table and make them extend :php:class:`Doctrine_Table`. So for the User model we would create a class like the following:

// models/UserTable.php
class UserTable extends Doctrine_Table
{
}
Custom Finders

You can add custom finder methods to your custom table object. These finder methods may use fast :php:class:`Doctrine_Table` finder methods or [doc dql-doctrine-query-language DQL API] (:php:meth:`Doctrine_Query::create`).

// models/UserTable.php
class UserTable extends Doctrine_Table
{
    public function findByName($name)
    {
        return Doctrine_Query::create()
                ->from('User u')
                ->where('u.name LIKE ?', "%$name%")
                ->execute();
    }
}

Doctrine will check if a child :php:class:`Doctrine_Table` class called UserTable exists when calling :php:meth:`getTable` and if it does, it will return an instance of that instead of the default :php:class:`Doctrine_Table`.

Note

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.

// boostrap.php
// ...
$manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);

Now when we ask for the User table object we will get the following:

$userTable = Doctrine_Core::getTable('User');

echo get_class($userTable); // UserTable

$users = $userTable->findByName("Jack");

Note

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 [doc dql-doctrine-query-language:magic-finders :name] section of the DQL chapter.

Record

Doctrine represents tables in your RDBMS with child :php:class:`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 :php:class:`Doctrine_Record` represents a database table column. You will learn more about how to define your models in the Defining Models chapter.

Now accessing the columns is easy:

// test.php
$userTable = Doctrine_Core::getTable('User');
$user = $userTable->find(1);
  • Access property through overloading:

    // ...
    echo $user->username;
    
  • Access property with get():

    // ...
    echo $user->get('username);
    
  • Access property with ArrayAccess:

    // ...
    echo $user['username'];
    

Tip

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 :php:class:`Doctrine_Record` implements a magic IteratorAggregate interface.

foreach ($user as $field => $value) {
    echo $field . ': ' . $value . "";
}

As with arrays you can use the :php:meth:`isset` for checking if given property exists and :php:meth:`unset` for setting given property to null.

We can easily check if a property named ‘name’ exists in a if conditional:

if (isset($user['username'])) {
}

If we want to unset the name property we can do it using the :php:meth:`unset` function in php:

unset($user['username']);

When you have set values for record properties you can get an array of the modified fields and values using :php:meth:`Doctrine_Record::getModified`:

// test.php
$user['username'] = 'Jack Daniels';
print_r($user->getModified());

The above example would output the following when executed:

$ php test.php
Array (
    [username] => Jack Daniels
)

You can also simply check if a record is modified by using the :php:meth:`Doctrine_Record::isModified` method:

echo $user->isModified() ? 'Modified' : 'Not Modified';

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 :php:meth:`count` function. This is possible since :php:class:`Doctrine_Record` implements a magic Countable interface. The other way would be calling the :php:meth:`count` method.

echo $record->count();
echo count($record);

:php:class:`Doctrine_Record` offers a special method for accessing the identifier of given record. This method is called :php:meth:`identifier` and it returns an array with identifier field names as keys and values as the associated property values.

$user['username'] = 'Jack Daniels';
$user->save();

print_r($user->identifier()); // array('id' => 1)

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, :php:class:`Doctrine_Record` offers a way for merging a given array or record to another

The :php:meth:`merge` method iterates through the properties of the given record or array and assigns the values to the object

$values = array(
    'username' => 'someone',
    'age' => 11,
);

$user->merge($values);

echo $user->username; // someone
echo $user->age; // 11

You can also merge a one records values in to another like the following:

$user1 = new User();
$user1->username = 'jwage';

$user2 = new User();
$user2->merge($user1);

echo $user2->username; // jwage

Note

:php:class:`Doctrine_Record` also has a :php:meth:`fromArray` method which is identical to :php:meth:`merge` and only exists for consistency with the :php:meth:`toArray` method.

Updating Records

Updating objects is very easy, you just call the :php:meth:`Doctrine_Record::save` method. The other way is to call :php:meth:`Doctrine_Connection::flush` which saves all objects. It should be noted though that flushing is a much heavier operation than just calling save method.

$userTable = Doctrine_Core::getTable('User');
$user = $userTable->find(2);

if ($user !== false) {
    $user->username = 'Jack Daniels';
    $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:

$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 :php:meth:`Doctrine_Record::assignIdentifier` method it sets the record identifier and changes the state so that calling :php:meth:`Doctrine_Record::save` performs an update instead of insert.

$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
$user = new User();
$user->username = 'jwage';
$user->password = 'changeme';
$user->save();

Issues the following query

INSERT INTO user
    (username, password)
VALUES
    (?, ?),
    ('jwage', 'changeme')

Now lets create another new object and set the same username but a different password.

$user = new User();
$user->username = 'jwage';
$user->password = 'newpassword';
$user->replace();

Issues the following query

REPLACE INTO user
    (id, username, password)
VALUES
    (?, ?, ?),
    (null, 'jwage', 'newpassword')

The record is replaced/updated instead of a new one being inserted

Refreshing Records

Sometimes you may want to refresh your record with data from the database, use :php:meth:`Doctrine_Record::refresh`.

$user = Doctrine_Core::getTable('User')->find(2);
$user->username = 'New name';

Now if you use the :php:meth:`Doctrine_Record::refresh` method it will select the data from the database again and update the properties of the instance.

$user->refresh();
Refreshing relationships

The :php:meth:`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:

$q = Doctrine_Query::create()
        ->from('User u')
        ->leftJoin('u.Groups')
        ->where('id = ?');

$user = $q->fetchOne(array(1));

Now lets retrieve a Group with its associated Users:

$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:

$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:

$user->Groups[] = $group;
$user->save()

Now if we call Doctrine_Record::refresh(true) it will refresh the record and its relationships loading the newly created reference we made above:

$user->refresh(true);
$group->refresh(true);

You can also lazily refresh all defined relationships of a model using :php:meth:`Doctrine_Record::refreshRelated`:

$user = Doctrine_Core::getTable('User')->findOneByName('jon');
$user->refreshRelated();

If you want to refresh an individual specified relationship just pass the name of a relationship to the :php:meth:`refreshRelated` function and it will lazily load the relationship:

$user->refreshRelated('Phonenumber');
Deleting Records

Deleting records in Doctrine is handled by :php:meth:`Doctrine_Record::delete`, :php:meth:`Doctrine_Collection::delete` and :php:meth:`Doctrine_Connection::delete` methods.

$userTable = Doctrine_Core::getTable("User");

$user = $userTable->find(2);

// deletes user and all related composite objects
if($user !== false) {
    $user->delete();
}

If you have a :php:class:`Doctrine_Collection` of User records you can call :php:meth:`delete` and it will loop over all records calling :php:meth:`Doctrine_Record::delete` for you.

$users = $userTable->findAll();

Now you can delete all users and their related composite objects by calling :php:meth:`Doctrine_Collection::delete`. It will loop over all Users in the collection calling delete one each one:

$users->delete();
Using Expression Values

There might be situations where you need to use SQL expressions as values of columns. This can be achieved by using Doctrine_Expression which converts portable DQL expressions to your native SQL expressions.

Lets say we have a class called event with columns timepoint(datetime) and name(string). Saving the record with the current timestamp can be achieved as follows:

// test.php
$user = new User();
$user->username = 'jwage';
$user->updated_at = new Doctrine_Expression('NOW()');
$user->save();

The above code would issue the following SQL query:

INSERT INTO user (username, updated_at) VALUES ('jwage', NOW())

Tip

When you use Doctrine_Expression with your objects in order to get the updated value you will have to manually call :php:meth:`refresh` to get the updated value from the database.

$user->refresh();
Getting Record State

Every :php:class:`Doctrine_Record` has a state. First of all records can be transient or persistent. Every record that is retrieved from database is persistent and every newly created record is considered transient. If a :php:class:`Doctrine_Record` is retrieved from database but the only loaded property is its primary key, then this record has a state called proxy.

Every transient and persistent :php:class:`Doctrine_Record` is either clean or dirty. :php:class:`Doctrine_Record` is clean when none of its properties are changed and dirty when at least one of its properties has changed.

A record can also have a state called locked. In order to avoid infinite recursion in some rare circular reference cases Doctrine uses this state internally to indicate that a record is currently under a manipulation operation.

Below is a table containing all the different states a record can be in with a short description of it:

Name Description
:php:const:`Doctrine_Record::STATE_PROXY` Record is in proxy state meaning its persistent but not all of its properties are loaded from the database.
:php:const:`Doctrine_Record::STATE_TCLEAN` Record is transient clean, meaning its transient and none of its properties are changed.
:php:const:`Doctrine_Record::STATE_TDIRTY` Record is transient dirty, meaning its transient and some of its properties are changed.
:php:const:`Doctrine_Record::STATE_DIRTY` Record is dirty, meaning its persistent and some of its properties are changed.
:php:const:`Doctrine_Record::STATE_CLEAN` Record is clean, meaning its persistent and none of its properties are changed.
:php:const:`Doctrine_Record::STATE_LOCKED` Record is locked.

You can easily get the state of a record by using the :php:meth:`Doctrine_Record::state` method:

$user = new User();

if ($user->state() == Doctrine_Record::STATE_TDIRTY) {
    echo 'Record is transient dirty';
}

Note

values specified in the schema. If we use an object that has no default values and instantiate a new instance it will return TCLEAN.

$account = new Account();

if ($account->state() == Doctrine_Record::STATE_TCLEAN) {
    echo 'Record is transient clean';
}
Getting Object Copy

Sometimes you may want to get a copy of your object (a new object with all properties copied). Doctrine provides a simple method for this: :php:meth:`Doctrine_Record::copy`.

$copy = $user->copy();

Notice that copying the record with :php:meth:`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:

$copy = $user->copy(false);

Using the PHP clone functionality simply uses this :php:meth:`copy` functionality internally:

$copy = clone $user;
Saving a Blank Record

By default Doctrine doesn’t execute when :php:meth:`save` is being called on an unmodified record. There might be situations where you want to force-insert the record even if it has not been modified. This can be achieved by assigning the state of the record to :php:const:`Doctrine_Record::STATE_TDIRTY`:

$user = new User();
$user->state('TDIRTY');
$user->save();

Note

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 :php:const:`Doctrine_Record::STATE_TDIRTY`.

Mapping Custom Values

There might be situations where you want to map custom values to records. For example values that depend on some outer sources and you only want these values to be available at runtime not persisting those values into database. This can be achieved as follows:

$user->mapValue('isRegistered', true);

$user->isRegistered; // true
Serializing

Sometimes you may want to serialize your record objects (possibly for caching purposes):

$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 :php:meth:`exists` method for checking if given record has a database row equivalent:

$record = new User();

echo $record->exists() ? 'Exists' : 'Does Not Exist'; // Does Not Exist

$record->username = 'someone'; $record->save();

echo $record->exists() ? 'Exists' : 'Does Not Exist'; // Exists
Function Callbacks for Columns

:php:class:`Doctrine_Record` offers a way for attaching callback calls for column values. For example if you want to trim certain column, you can simply use:

$record->call('trim', 'username');
Collection

:php:class:`Doctrine_Collection` is a collection of records (see Doctrine_Record). As with records the collections can be deleted and saved using :php:meth:`Doctrine_Collection::delete` and :php:meth:`Doctrine_Collection::save` accordingly.

When fetching data from database with either DQL API (see :php:class:`Doctrine_Query`) or rawSql API (see Doctrine_RawSql) the methods return an instance of :php:class:`Doctrine_Collection` by default.

The following example shows how to initialize a new collection:

$users = new Doctrine_Collection('User');

Now add some new data to the collection:

$users[0]->username = 'Arnold';
$users[1]->username = 'Somebody';

Now just like we can delete a collection we can save it:

$users->save();
Accessing Elements

You can access the elements of :php:class:`Doctrine_Collection` with :php:meth:`set` and :php:meth:`get` methods or with ArrayAccess interface.

$userTable = Doctrine_Core::getTable('User');
$users = $userTable->findAll();
  • Accessing elements with ArrayAccess interface:

    $users[0]->username = "Jack Daniels";
    $users[1]->username = "John Locke";
    
  • Accessing elements with :php:meth:`get`:

    echo $users->get(1)->username;
    
Adding new Elements

When accessing single elements of the collection and those elements (records) don’t exist Doctrine auto-adds them.

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
$users = $userTable->findAll();

echo count($users); // 5

$users[5]->username = "new user 1";
$users[6]->username = "new user 2";

You could also optionally omit the 5 and 6 from the array index and it will automatically increment just as a PHP array would:

$users[]->username = 'new user 3'; // key is 7
$users[]->username = 'new user 4'; // key is 8
Getting Collection Count

The :php:meth:`Doctrine_Collection::count` method returns the number of elements currently in the collection:

$users = $userTable->findAll();

echo $users->count();

Since :php:class:`Doctrine_Collection` implements Countable interface a valid alternative for the previous example is to simply pass the collection as an argument for the count() function:

echo count($users);
Saving the Collection

Similar to :php:class:`Doctrine_Record` the collection can be saved by calling the :php:meth:`save` method. When :php:meth:`save` gets called Doctrine issues :php:meth:`save` operations an all records and wraps the whole procedure in a transaction.

$users = $userTable->findAll();

$users[0]->username = 'Jack Daniels';
$users[1]->username = 'John Locke';

$users->save();
Deleting the Collection

Doctrine Collections can be deleted in very same way is Doctrine Records you just call :php:meth:`delete` method. As for all collections Doctrine knows how to perform single-shot-delete meaning it only performs one database query for the each collection.

For example if we have collection of users. When deleting the collection of users doctrine only performs one query for this whole transaction. The query would look something like:

DELETE 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:

    // test.php
    $userTable = Doctrine_Core::getTable('User');
    $userTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, 'id');
    
  • Now user collections will use the values of id column as element indexes:

    $users = $userTable->findAll();
    
    foreach($users as $id => $user) {
        echo $id . $user->username;
    }
    
  • You may want to map the name column:

    $userTable = Doctrine_Core::getTable('User');
    $userTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, 'username');
    
  • Now user collections will use the values of name column as element indexes:

    $users = $userTable->findAll();
    
    foreach($users as $username => $user) {
        echo $username . ' - ' . $user->created_at . "";
    }
    

Caution

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.

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 :php:class:`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 :php:class:`Doctrine_Manager`.

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

Once you enabled validation, you’ll get a bunch of validations automatically:

  • 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:

// bootstrap.php
$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, VALIDATE_ALL & ~VALIDATE_LENGTHS);

You can read more about this topic in the Data Validation 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 [doc data-validation :name] chapter. You can specify which validators apply to which column through the 4th argument of the :php:meth:`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:

  • You can write the validator on your own.
  • You can propose your need for a new validator to a Doctrine developer.
  • You can use validation hooks.

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 :php:class:`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
class User extends BaseUser
{
    protected function validate()
    {
        if ($this->username == 'God') {
            // Blasphemy! Stop that! ;-)
            // syntax: add(<fieldName>, <error code/identifier>)
            $errorStack = $this->getErrorStack();
            $errorStack->add('name', 'You cannot use this username!');
        }
    }
}
// models/Email.php
class Email extends BaseEmail
{
    // ...

    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...

        // validators 'email' and 'unique' used
        $this->hasColumn('address', 'string', 150, array('email', 'unique'));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

# schema.yml
Email:
  columns:
    address:
      type: string(150)
      email: true
      unique: true
Valid or Not Valid

Now that you know how to specify your business rules in your models, it is time to look at how to deal with these rules in the rest of your application.

Implicit Validation

Whenever a record is going to be saved to the persistent data store (i.e. through calling :php:meth:`$record->save`) the full validation procedure is executed. If errors occur during that process an exception of the type :php:exc:`Doctrine_Validator_Exception` will be thrown. You can catch that exception and analyze the errors by using the instance method :php:meth:`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 :php:meth:`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 :php:class:`Doctrine_Record` provides the instance method :php:meth:`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 :php:meth:`Doctrine_Record::getErrorStack`.

The following code snippet shows an example of handling implicit validation which caused a :php:exc:`Doctrine_Validator_Exception`.

// test.php
$user = new User();

try {
    $user->username = str_repeat('t', 256);
    $user->Email->address = "drink@@notvalid..";
    $user->save();
} catch(Doctrine_Validator_Exception $e) {
    $userErrors = $user->getErrorStack();
    $emailErrors = $user->Email->getErrorStack();

    foreach($userErrors as $fieldName => $errorCodes) {
        echo $fieldName . " - " . implode(', ', $errorCodes) . "\n";
    }
    foreach($emailErrors as $fieldName => $errorCodes) {
        echo $fieldName . " - " . implode(', ', $errorCodes) . "\n";
    }
}

Tip

You could also use :php:meth:`$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
echo $user->getErrorStackAsString();

It would output an error string that looks something like the following:

Validation failed in class User

1 field had validation error:

* 1 validator failed on username (length)
Profiler

Doctrine_Connection_Profiler is an event listener for :php:class:`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:

$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:

// test.php
$time = 0;
foreach ($profiler as $event) {
    $time += $event->getElapsedSecs();

    printf(
        "%s %f\n%s\n",
        $event->getName(),
        $event->getElapsedSecs(),
        $event->getQuery()
    );

    $params = $event->getParams();
    if (!empty($params)) {
        print_r($params);
    }
}
echo "Total time: " . $time . "\n";

Tip

Frameworks like symfony, Zend, 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

Note

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.

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
$lockingManager = new Doctrine_Locking_Manager_Pessimistic();

Tip

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 :php:meth:`releaseAgedLocks` method:

// test.php
$user = Doctrine_Core::getTable('User')->find(1);

try {
    $lockingManager->releaseAgedLocks(300);
    $gotLock = $lockingManager->getLock($user, 'jwage');

    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
$user = Doctrine_Core::getTable('User')->find(1);

$lockingManager = new Doctrine_Locking_Manager_Pessimistic();

try {
    if ($lockingManager->releaseLock($user, 'jwage')) {
        echo "Lock released";
    } else {
        echo "Record was not locked. No locks released.";
    }
} catch(Doctrine_Locking_Exception $dle) {
    echo $dle->getMessage(); // handle the error
}
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 :php:class:`Doctrine_Query` class by saving the SQL that would be executed by :php:class:`Doctrine_Query`.

First lets create a new :php:class:`Doctrine_Query` instance to work with:

$q = Doctrine_Query::create()
        ->from('User u')
        ->leftJoin('u.Phonenumber p')
        ->limit(20);

Now lets create the Doctrine_View instance and pass it the :php:class:`Doctrine_Query` instance as well as a name for identifying that database view:

$view = new Doctrine_View($q, 'RetrieveUsersAndPhonenumbers');

Now we can easily create the view by using the :php:meth:`Doctrine_View::create` method:

try {
    $view->create();
} catch (Exception $e) {
}

Alternatively if you want to drop the database view you use the :php:meth:`Doctrine_View::drop` method:

try {
    $view->drop();
} catch (Exception $e) {
}

Using views are extremely easy. Just use the :php:meth:`Doctrine_View::execute` for executing the view and returning the results just as a normal :php:class:`Doctrine_Query` object would:

$users = $view->execute();

foreach ($users as $user) {
    print_r($us->toArray());
}
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 day basis.

Lets move on to the next chapter 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.

Native SQL

Introduction

:php:class:`Doctrine_RawSql` provides a convenient interface for building raw sql queries. Similar to :php:class:`Doctrine_Query`, :php:class:`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 :php:class:`Doctrine_RawSql` object is easy:

// test.php
$q = new Doctrine_RawSql();

Optionally a connection parameter can be given and it accepts an instance of :php:class:`Doctrine_Connection`. You learned how to create connections in the Connections chapter.

// test.php
$conn = Doctrine_Manager::connection();
$q    = new Doctrine_RawSql($conn);
Component Queries

The first thing to notice when using :php:class:`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:

// test.php
$q->select('{u.*}') ->from('user u') ->addComponent('u', 'User');
$users = q->execute();

print_r($users->toArray());

Note

Note above that we tell that user table is bound to class called User by using the addComponent() method.

Pay attention to following things:

  • Fields must be in curly brackets.
  • For every selected table there must be one addComponent() call.
Fetching from Multiple Components

When fetching from multiple components the addComponent() calls become a bit more complicated as not only do we have to tell which tables are bound to which components, we also have to tell the parser which components belongs to which.

In the following example we fetch all users and their phonenumbers. First create a new :php:class:`Doctrine_RawSql` object and add the select parts:

// test.php
$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
$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
$q->addComponent('u', 'User u');

Now we add another component that is bound to table phonenumber:

// test.php
$q->addComponent('p', 'u.Phonenumbers p');

Note

Notice how we reference that the Phonenumber class is the User‘s phonenumber.

Now we can execute the :php:class:`Doctrine_RawSql` query just like if you were executing a :php:class:`Doctrine_Query` object:

// test.php
$users = $q->execute();
echo get_class($users) . "";
echo get_class($users[0]) . "\n";
echo get_class($users[0]['Phonenumbers'][0]) . "";

The above example would output the following when executed:

$ php test.php Doctrine_Collection User Phonenumber
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 :php:class:`Doctrine_Query` is capable of then :php:class:`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.

YAML Schema Files

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.

Note

The detect_relations option will attempt to guess relationships based on column names. In the example below Doctrine knows that User has one Contact and will automatically define the relationship between the models.

---
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:

---
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.

---
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.

---
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
---
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
---
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
---
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.

Features & Examples
Connection Binding

If you’re not using schema files to manage your models, you will normally use this code to bind a component to a connection name with the following code:

Create a connection with code like below:

Doctrine_Manager::connection('mysql://jwage:pass@localhost/connection1', 'connection1');

Now somewhere in your Doctrine bootstrapping of Doctrine you would bind the model to that connection:

Doctrine_Manager::connection()->bindComponent('User', 'conn1');

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 :php:class:`Doctrine_Manager` instance.

---
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 :php:class:`Doctrine_Record` child classes.

---
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.

---
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:

---
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

Note

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.

---
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.

---
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:

class BaseUser extends Doctrine_Record
{
   // ...
   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.

---
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 section of the Defining Models for more information about indexes and their options.

---
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:

$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
---
Entity:
  columns:
    name: string(255)
    username: string(255)
    password: string(255)

User:
  inheritance:
    extends: Entity
    type: simple

Group:
  inheritance:
    extends: Entity
    type: simple

Note

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 Inheritance chapter.

Concrete Inheritance
---
TextItem:
  columns:
    topic: string(255)

Comment:
  inheritance:
    extends: TextItem
    type: concrete
    columns:
     content: string(300)

You can read more about this topic in the Inheritance chapter.

Column Aggregation Inheritance

Note

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:

---
Entity:
  columns:
    name: string(255)
    type: string(255)

Note

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:

---
User:
  inheritance:
    extends: Entity
    type: column_aggregation
    keyField: type
    keyValue: User
  columns:
    username: string(255)
    password: string(255)

Note

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:

---
Group:
  inheritance:
    extends: Entity
    type: column_aggregation
    keyField: type
    keyValue: Group
  columns:
    description: string(255)

Note

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 Inheritance chapter.

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:

---
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.

---
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

Package Custom Path

You can also completely by pass the automatic generation of packages to the appropriate path by specifying a completely custom path to generate the package files:

---
User:
  package: User
  package_custom_path: /path/to/generate/package
  columns:
    username: string(255)
Global Schema Information

Doctrine schemas allow you to specify certain parameters that will apply to all of the models defined in the schema file. Below you can find an example on what global parameters you can set for schema files.

List of global parameters:

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.
options Array of tables options for the models.
package Package to put the models in.
inheritance Array of inheritance information for models
detect_relations Whether or not to try and detect foreign key relations

Now here is an example schema where we use some of the above global parameters:

---
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.

Using Schema Files

Once you have defined your schema files you need some code to build the models from the YAML definition.

$options = array(
    'packagesPrefix' => 'Plugin',
    'baseClassName'  => 'MyDoctrineRecord',
    'suffix' => '.php'
);

Doctrine_Core::generateModelsFromYaml('/path/to/yaml', '/path/to/model', $options);

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.

Name Default Description
packagesPrefix Package What to prefix the middle package models with.
packagesPath #models_path#/packages Path to write package files.
packagesFolderName packages The name of the folder to put packages in, inside of the packages path.
generateBaseClasses true Whether or not to generate abstract base models containing the definition and a top level class which is empty extends the base.
generateTableClasses true Whether or not to generate a table class for each model.
baseClassPrefix Base The prefix to use for generated base class.
baseClassesDirectory generated Name of the folder to generate the base class definitions in.
baseTableClassName Doctrine_Table The base table class to extend the other generated table classes from.
baseClassName Doctrine_Record Name of the base Doctrine_Record class.
classPrefix   The prefix to use on all generated classes.
classPrefixFiles true Whether or not to use the class prefix for the generated file names as well.
pearStyle false Whether or not to generated PEAR style class names and file names. This option if set to true will replace underscores(_) with the DIRECTORY_SEPARATOR in the path to the generated class file.
suffix .php Extension for your generated models.
phpDocSubpackage   The phpDoc subpackage name to generate in the doc blocks.
phpDocName   The phpDoc author name to generate in the doc blocks.
phpDocEmail   The phpDoc e-mail to generate in the doc blocks.
Conclusion

Now that we have learned all about YAML Schema files we are ready to move on to a great topic regarding Data Validation. 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.

Data Validation

Introduction

Hint

PostgreSQL Documentation: 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:

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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('username', 'string', 255, array(
                'minlength' => 12
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('username', 'string', 255, array(
                'notnull' => true,
                'primary' => true,
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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):

CREATE TABLE user (username VARCHAR(255) NOT NULL,
PRIMARY KEY(username))

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 :php:class:`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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('email', 'string', 255, array(
                'email'   => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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:

// test.php
// ...
$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
// ...
$user           = new User();
$user->username = 'jwage';
$user->email    = 'jonwage@somefakedomainiknowdoesntexist.com';

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() returned false.

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
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('username', 'string', 255, array(
                'notblank'   => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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:

// test.php

// ...
$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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('username', 'string', 255, array(
                'nospace'   => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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:

$user           = new User();
$user->username = 'jon wage';

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
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('birthday', 'timestamp', null, array(
                'past' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('next_appointment_date', 'timestamp', null, array(
                'future' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

// models/User.php
class User extends BaseUser
{
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('password', 'timestamp', null, array(
                'minlength' => 5
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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

// ...
$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
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('country', 'string', 2, array(
                'country' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

// test.php

// ...
$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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('ip_address', 'string', 15, array(
                'ip' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

$user             = new User();
$user->username   = 'jwage';
$user->ip_address = '123.123';

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
class User extends BaseUser
{
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('favorite_color', 'string', 7, array(
                'htmlcolor' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

Now if you try and save a User with an invalid html color value for the favorite_color column the validation will fail.

// test.php

// ...
$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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('age', 'integer', 3, array(
                'range' => array(10, 100)
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
User:
  columns:
# ...
    age:
      type: integer(3)
      range: [10, 100]
# ...

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

// ...
$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
class User extends BaseUser
{
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('age', 'integer', 3, array(
                'range' => array(1 => 100)
            )
        );
    }
}

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:

---
# 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 username.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('username', 'string', 255, array(
                'unique' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
User:
  columns:
    username:
      type: string(255)
      unique: true
# ...

Note

You should only use unique constraints for columns other than the primary key because they are always unique already.

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
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('username', 'string', 255, array(
                'regexp' => '/[a-zA-Z0-9]/'
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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

// ...
$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.

// models/User.php
class User extends BaseUser
{
    // ...

    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('cc_number', 'integer', 16, array(
                'creditcard' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

// models/User.php
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('readonly_value', 'string', 255, array(
                'readonly' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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
class User extends BaseUser
{
    // ...
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        // ...
        $this->hasColumn('age', 'integer', 3, array(
                'unsigned' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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:

// test.php

// ...
$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.

// models/State.php
class State extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
        $this->hasColumn('code', 'string', 2, array(
                'usstate' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

$state       = new State();
$state->name = 'Tennessee';
$state->code = 'ZZ';

if ( ! $state->isValid() )
{
    echo 'State is invalid because "ZZ" is not a valid state code';
}
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 Inheritance is an important one as we will discuss a great feature of Doctrine, Inheritance! 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.

Data Hydrators

Doctrine has a concept of data hydrators for transforming your :php:class:`Doctrine_Query` instances to a set of PHP data for the user to take advantage of. The most obvious way to hydrate the data is to put it into your object graph and return models/class instances. Sometimes though you want to hydrate the data to an array, use no hydration or return a single scalar value. This chapter aims to document and demonstrate the different hydration types and even how to write your own new hydration types.

Core Hydration Methods

Doctrine offers a few core hydration methods to help you with the most common hydration needs.

Record

The first type is HYDRATE_RECORD and is the default hydration type. It will take the data from your queries and hydrate it into your object graph. With this type this type of thing is possible.

$q = Doctrine_Core::getTable('User')
    ->createQuery('u')
    ->leftJoin('u.Email e')
    ->where('u.username = ?', 'jwage');

$user = $q->fetchOne(array(), Doctrine_Core::HYDRATE_RECORD);

echo $user->Email->email;

The data for the above query was retrieved with one query and was hydrated into the object graph by the record hydrator. This makes it much easier to work with data since you are dealing with records and not result sets from the database which can be difficult to work with.

Array

The array hydration type is represented by the HYDRATE_ARRAY constant. It is identical to the above record hydration except that instead of hydrating the data into the object graph using PHP objects it uses PHP arrays. The benefit of using arrays instead of objects is that they are much faster and the hydration does not take as long.

So if you were to run the same example you would have access to the same data but it would be via a PHP array.

$q = Doctrine_Core::getTable('User')
    ->createQuery('u')
    ->leftJoin('u.Email e')
    ->where('u.username = ?', 'jwage');

$user = $q->fetchOne(array(), Doctrine_Core::HYDRATE_ARRAY);

echo $user['Email']['email'];
Scalar

The scalar hydration type is represented by the HYDRATE_SCALAR constant and is a very fast and efficient way to hydrate your data. The downside to this method is that it does not hydrate your data into the object graph, it returns a flat rectangular result set which can be difficult to work with when dealing with lots of records.

$q = Doctrine_Core::getTable('User')
    ->createQuery('u')
    ->where('u.username = ?', 'jwage');

$user = $q->fetchOne(array(), Doctrine_Core::HYDRATE_SCALAR);

echo $user['u_username'];

The above query would produce a data structure that looks something like the following:

$user = array(
    'u_username' => 'jwage',
    'u_password' => 'changeme',
    // ...
);

If the query had a many relationship joined than the data for the user would be duplicated for every record that exists for that user. This is the downside as it is difficult to work with when dealing with lots of records.

Single Scalar

Often times you want a way to just return a single scalar value. This is possible with the single scalar hydration method and is represented by the HYDRATE_SINGLE_SCALAR attribute.

With this hydration type we could easily count the number of phonenumbers a user has with the following:

$q = Doctrine_Core::getTable('User')
    ->createQuery('u')
    ->select('COUNT(p.id)')
    ->leftJoin('u.Phonenumber p')
    ->where('u.username = ?', 'jwage');

$numPhonenumbers = $q->fetchOne(array(), Doctrine_Core::HYDRATE_SINGLE_SCALAR);

echo $numPhonenumbers;

This is much better than hydrating the data with a more complex method and grabbing the value from those results. With this it is very fast and efficient to get the data you really want.

On Demand

If you wish to use a less memory intensive hydration method you can use the on demand hydration which is represented by the HYDRATE_ON_DEMAND constant. It will only hydrate one records graph at a time so that means less memory footprint overall used.

// Returns instance of Doctrine_Collection_OnDemand
$result = $q->execute(array(), Doctrine_Core::HYDRATE_ON_DEMAND);
foreach ($result as $obj)
{
    // ...
}

:php:class:`Doctrine_Collection_OnDemand` hydrates each object one at a time as you iterate over it so this results in less memory being used because we don’t have to first load all the data from the database to PHP then convert it to the entire data structure to return.

Nested Set Record Hierarchy

For your models which use the nested set behavior you can use the record hierarchy hydration method to hydrate your nested set tree into an actual hierarchy of nested objects.

$categories = Doctrine_Core::getTable('Category')
    ->createQuery('c')
    ->execute(array(), Doctrine_Core::HYDRATE_RECORD_HIERARCHY);

Now you can access the children of a record by accessing the mapped value property named __children. It is named with the underscores prefixed to avoid any naming conflicts.

foreach ($categories->getFirst()->get('__children') as $child)
{
    // ...
}
Nested Set Array Hierarchy

If you wish to hydrate the nested set hierarchy into arrays instead of objects you can do so using the HYDRATE_ARRAY_HIERARCHY constant. It is identical to the HYDRATE_RECORD_HIERARCHY except that it uses PHP arrays instead of objects.

$categories = Doctrine_Core::getTable('Category')
    ->createQuery('c')
    ->execute(array(), Doctrine_Core::HYDRATE_ARRAY_HIERARCHY);

Now you can do the following:

foreach ($categories[0]['__children'] as $child)
{
    // ...
}
Writing Hydration Method

Doctrine offers the ability to write your own hydration methods and register them with Doctrine for use. All you need to do is write a class that extends :php:class:`Doctrine_Hydrator_Abstract` and register it with :php:class:`Doctrine_Manager`.

First lets write a sample hydrator class:

class Doctrine_Hydrator_MyHydrator extends Doctrine_Hydrator_Abstract
{
    public function hydrateResultSet($stmt)
    {
        $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
        // do something to with $data
        return $data;
    }
}

To use it make sure we register it with :php:class:`Doctrine_Manager`:

// bootstrap.php

// ...
$manager->registerHydrator('my_hydrator', 'Doctrine_Hydrator_MyHydrator');

Now when you execute your queries you can pass my_hydrator and it will use your class to hydrate the data.

$q->execute(array(), 'my_hydrator');

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
$ 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
class Entity extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 30);
        $this->hasColumn('username', 'string', 20);
        $this->hasColumn('password', 'string', 16);
        $this->hasColumn('created_at', 'timestamp');
        $this->hasColumn('update_at', 'timestamp');
    }
}

Now lets create a User model that extends Entity:

// models/User.php
class User extends Entity
{ }

Do the same thing for the Group model:

// 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 chapter:

---
# 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

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('Entity', 'User', 'Group'));
echo $sql[0];

The above code would output the following SQL query:

CREATE TABLE entity (id BIGINT AUTO_INCREMENT,
username VARCHAR(20),
password VARCHAR(16),
created_at DATETIME,
updated_at DATETIME,
name VARCHAR(30),
PRIMARY KEY(id)) ENGINE = INNODB

Note

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
class TextItem extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('topic', 'string', 100);
    }
}

Now lets create a model named Comment that extends TextItem and add an extra column named content:

// models/Comment.php
class Comment extends TextItem
{
    public function setTableDefinition()
    {
        parent::setTableDefinition();

        $this->hasColumn('content', 'string', 300);
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

TextItem:
  columns:
    topic: string(100)

Comment:
  inheritance:
    extends: TextItem
    type: concrete
  columns:
    content: string(300)

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('TextItem', 'Comment'));
echo $sql[0] . "";
echo $sql[1];

The above code would output the following SQL query:

CREATE TABLE text_item (id BIGINT AUTO_INCREMENT,
topic VARCHAR(100),
PRIMARY KEY(id)) ENGINE = INNODB
CREATE TABLE comment (id BIGINT AUTO_INCREMENT,
topic VARCHAR(100),
content TEXT,
PRIMARY KEY(id)) ENGINE = INNODB

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.

// models/Entity.php
class Entity extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 30);
        $this->hasColumn('username', 'string', 20);
        $this->hasColumn('password', 'string', 16);
        $this->hasColumn('created', 'integer', 11);
    }
}

// models/User.php
class User extends Entity
{
    public function setTableDefinition()
    {
        // the following method call is needed in
        // concrete inheritance
        parent::setTableDefinition();
    }
}

// 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 chapter:

---
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

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('Entity', 'User', 'Group'));
echo $sql[0] . "";
echo $sql[1] . "";
echo $sql[2] . "";

The above code would output the following SQL query:

CREATE TABLE user (id BIGINT AUTO_INCREMENT,
name VARCHAR(30),
username VARCHAR(20),
password VARCHAR(16),
created BIGINT,
PRIMARY KEY(id)) ENGINE = INNODB
CREATE TABLE groups (id BIGINT AUTO_INCREMENT,
name VARCHAR(30),
username VARCHAR(20),
password VARCHAR(16),
created BIGINT,
PRIMARY KEY(id)) ENGINE = INNODB
CREATE TABLE entity (id BIGINT AUTO_INCREMENT,
name VARCHAR(30),
username VARCHAR(20),
password VARCHAR(16),
created BIGINT,
PRIMARY KEY(id)) ENGINE = INNODB
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.

// models/Entity.php
class Entity extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 30);
        $this->hasColumn('username', 'string', 20);
        $this->hasColumn('password', 'string', 16);
        $this->hasColumn('created_at', 'timestamp');
        $this->hasColumn('update_at', 'timestamp');

        $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 chapter:

---
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

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('Entity', 'User', 'Group'));
echo $sql[0];

The above code would output the following SQL query:

CREATE TABLE entity (id BIGINT AUTO_INCREMENT,
username VARCHAR(20),
password VARCHAR(16),
created_at DATETIME,
updated_at DATETIME,
type VARCHAR(255),
PRIMARY KEY(id)) ENGINE = INNODB

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

// ...
$user           = new User();
$user->name     = 'Bjarte S. Karlsen';
$user->username = 'meus';
$user->password = 'rat';
$user->save();

Now lets save a new Group object:

// test.php

// ...
$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 :php:class:`Doctrine_Query` will return an instance of User.

// test.php

// ...
$q = Doctrine_Query::create()
    ->from('Entity e')
    ->where('e.id = ?');

$user = $q->fetchOne(array($user->id));

echo get_class($user); // User

If we do the same thing as above but for the Group record, it will return an instance of Group.

// test.php

// ...
$q = Doctrine_Query::create()
    ->from('Entity e')
    ->where('e.id = ?');

$group = $q->fetchOne(array($group->id));

echo get_class($group); // Group

We can also query the individual User or Group models:

$q = Doctrine_Query::create()
    ->select('u.id')
    ->from('User u');

echo $q->getSqlQuery();

The above call to getSql() would output the following SQL query:

SELECT e.id AS
e_id FROM entity e WHERE (e.type = '1')
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. This is one of the most sophisticated and useful features in Doctrine for accomplishing complex models with small and easy to maintain code.

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 :php:class:`Doctrine_Template` is much more powerful and flexible than using inheritance.

:php:class:`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 :php:class:`Doctrine_Template` and :php:class:`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 :php:class:`Doctrine_Template`). The common workflow is:

  • A new template is being initialized
  • The template creates the generator and calls initialize() method
  • The template is attached to given class

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.

// models/TimestampListener.php
class TimestampListener extends Doctrine_Record_Listener
{
    public function preInsert(Doctrine_Event $event)
    {
        $event->getInvoker()->created = date('Y-m-d', time());
        $event->getInvoker()->updated = date('Y-m-d', time());
    }

    public function preUpdate(Doctrine_Event $event)
    {
        $event->getInvoker()->updated = date('Y-m-d', time());
    }
}

Now lets create a child :php:class:`Doctrine_Template` named TimestampTemplate so we can attach it to our models with the actAs() method:

// models/TimestampBehavior.php
    class TimestampTemplate extends Doctrine_Template
    {
        public function setTableDefinition()
        {
            $this->hasColumn('created', 'date');
            $this->hasColumn('updated', 'date');

            $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.

class BlogPost extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 200);
        $this->hasColumn('body', 'clob');
    }

    public function setUp()
    {
        $this->actAs('TimestampBehavior');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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:

$blogPost        = new BlogPost();
$blogPost->title = 'Test';
$blogPost->body  = 'test';
$blogPost->save();

print_r($blogPost->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 1
    [title] => Test
    [body] => test
    [created] => 2009-01-22
    [updated] => 2009-01-22
)

Note

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 behaviors:core-behaviors:timestampable section of this chapter.

Templates with Relations

Many times the situations tend to be much more complex than the situation in the previous chapter. You may have model classes with relations to other model classes and you may want to replace given class with some extended class.

Consider we have two classes, User and Email, with the following definitions:

class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('username', 'string', 255);
        $this->hasColumn('password', 'string', 255);
    }

    public function setUp()
    {
        $this->hasMany('Email', array(
                'local' => 'id',
                'foreign' => 'user_id'
            )
        );
    }
}

class Email extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('address', 'string');
        $this->hasColumn('user_id', 'integer');
    }

    public function setUp()
    {
        $this->hasOne('User', array(
                'local' => 'user_id',
                'foreign' => 'id'
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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. :php:class:`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.

// models/UserTemplate.php
class UserTemplate extends Doctrine_Template
{
    public function setTableDefinition()
    {
        $this->hasColumn('username', 'string', 255);
        $this->hasColumn('password', 'string', 255);
    }

    public function setUp()
    {
        $this->hasMany('EmailTemplate as Emails', array(
                'local' => 'id',
                'foreign' => 'user_id'
            )
        );
    }
}

Now lets define the EmailTemplate:

// models/EmailTemplate.php
class EmailTemplate extends Doctrine_Template
{
    public function setTableDefinition()
    {
        $this->hasColumn('address', 'string');
        $this->hasColumn('user_id', 'integer');
    }

    public function setUp()
    {
        $this->hasOne('UserTemplate as User', array(
                'local' => 'user_id',
                'foreign' => 'id'
            )
        );
    }
}

Notice how we set the relations. We are not pointing to concrete Record classes, rather we are setting the relations to templates. This tells Doctrine that it should try to find concrete Record classes for those templates. If Doctrine can’t find these concrete implementations the relation parser will throw an exception, but before we go ahead of things, here are the actual record classes:

class User extends Doctrine_Record
{
    public function setUp()
    {
        $this->actAs('UserTemplate');
    }
}

class Email extends Doctrine_Record
{
    public function setUp()
    {
        $this->actAs('EmailTemplate');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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.

// test.php

// ...
$user = new User();
$user->Emails; // throws an exception

The following version works. Notice how we set the concrete implementations for the templates globally using :php:class:`Doctrine_Manager`:

// bootstrap.php

// ...
$manager->setImpl('UserTemplate', 'User')
        ->setImpl('EmailTemplate', 'Email');

Now this code will work and won’t throw an exception like it did before:

$user                     = new User();
$user->Emails[0]->address = 'jonwage@gmail.com';
$user->save();

print_r($user->toArray(true));

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 1
    [username] =>
    [password] =>
    [Emails] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [address] => jonwage@gmail.com
                    [user_id] => 1
                )
        )
)

Tip

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, :php:class:`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
class UserTemplate extends Doctrine_Template
{
    // ...
    public function authenticate($username, $password)
    {
        $invoker = $this->getInvoker();
        if ($invoker->username == $username && $invoker->password == $password)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

Now take a look at the following code and how we can use it:

$user           = new User();
$user->username = 'jwage';
$user->password = 'changeme';

if ($user->authenticate('jwage', 'changemte'))
{
    echo 'Authenticated successfully!';
}
else
{
    echo 'Could not authenticate user!';
}

You can also delegate methods to :php:class:`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
class UserTemplate extends Doctrine_Template
{
    // ...
    public function findUsersWithEmailTableProxy()
    {
        return Doctrine_Query::create()
            ->select('u.username')
            ->from('User u')
            ->innerJoin('u.Emails e')
            ->execute();
    }
}

Now we can access that function from the :php:class:`Doctrine_Table` object for the User model:

$userTable = Doctrine_Core::getTable('User');
$users = $userTable->findUsersWithEmail();

Tip

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:

public function initOptions()
public function buildLocalRelation()
public function buildForeignKeys(Doctrine_Table $table)
public function buildForeignRelation($alias = null)
public function buildRelation() // calls buildForeignRelation() and buildLocalRelation()
class EmailBehavior extends Doctrine_Record_Generator
{
    public function initOptions()
    {
        $this->setOption('className', '%CLASS%Email');

        // Some other options
        // $this->setOption('appLevelDelete', true);
        // $this->setOption('cascadeDelete', false);
    }

    public function buildRelation()
    {
        $this->buildForeignRelation('Emails');
        $this->buildLocalRelation();
    }

    public function setTableDefinition()
    {
        $this->hasColumn('address', 'string', 255, array(
                'email'  => true,
                'primary' => true
            )
        );
    }
}
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
$ 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 :php:class:`Doctrine_Record`s 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
class BlogPost extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 255);
        $this->hasColumn('body', 'clob');
    }

    public function setUp()
    {
        $this->actAs('Versionable', array(
                'versionColumn' => 'version',
                'className' => '%CLASS%Version',
                'auditLog' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
BlogPost:
  actAs:
    Versionable:
      versionColumn: version
      className: %CLASS%Version
      auditLog: true
  columns:
    title: string(255)
    body: clob

Note

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.

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('BlogPost'));
echo $sql[0] . "\n";
echo $sql[1];

The above code would output the following SQL query:

CREATE TABLE blog_post_version (id BIGINT,
title VARCHAR(255),
body LONGTEXT,
version BIGINT,
PRIMARY KEY(id,
version)) ENGINE = INNODB
CREATE TABLE blog_post (id BIGINT AUTO_INCREMENT,
title VARCHAR(255),
body LONGTEXT,
version BIGINT,
PRIMARY KEY(id)) ENGINE = INNODB
ALTER TABLE blog_post_version ADD FOREIGN KEY (id) REFERENCES blog_post(id) ON UPDATE CASCADE ON DELETE CASCADE

Note

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:

  • It creates a class called BlogPostVersion on-the-fly, the table this record is pointing at is blog_post_version
  • Everytime a BlogPost object is deleted / updated the previous version is stored into blog_post_version
  • Everytime a BlogPost object is updated its version number is increased.

Now lets play around with the BlogPost model:

$blogPost        = new BlogPost();
$blogPost->title = 'Test blog post';
$blogPost->body  = 'test';
$blogPost->save();

$blogPost->title = 'Modified blog post title';
$blogPost->save();

print_r($blogPost->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 1
    [title] => Modified blog post title
    [body] => test
    [version] => 2
)

Note

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.

Lets revert back to the first version:

$blogPost->revert(1);
print_r($blogPost->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 2
    [title] => Test blog post
    [body] => test
    [version] => 1
)

Note

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 lets expand our BlogPost model and add the Timestampable behavior to automatically set these dates for us.

// models/BlogPost.php
class BlogPost extends Doctrine_Record
{
    // ...
    public function setUp()
    {
        $this->actAs('Timestampable');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

---
BlogPost:
  actAs:
# ...
    Timestampable:
      created:
        name: created_at
        type: timestamp
        format: Y-m-d H:i:s
      updated:
        disabled: true
# ...

Now look what happens when we create a new post:

$blogPost        = new BlogPost();
$blogPost->title = 'Test blog post';
$blogPost->body  = 'test';
$blogPost->save();

print_r($blogPost->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 1
    [title] => Test blog post
    [body] => test
    [version] => 1
    [created_at] => 2009-01-21 17:54:23
    [updated_at] => 2009-01-21 17:54:23
)

Note

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:

Name Default Description
name created_at The name of the column.
type timestamp The column type.
options array() Any additional options for the column.
format Y-m-d H:i:s The format of the timestamp if you don’t use the timestamp column type. The date is built using PHP’s date() function.
disabled false Whether or not to disable the created date.
expression NOW() Expression to use to set the column value.

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:

Name Default Description
onInsert true Whether or not to set the updated date when the record is first inserted.
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
class BlogPost extends Doctrine_Record
{
    // ...
    public function setUp()
    {
        // ...
        $this->actAs('Sluggable', array(
                'unique'    => true,
                'fields'    => array('title'),
                'canUpdate' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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:

$blogPost        = new BlogPost();
$blogPost->title = 'Test blog post';
$blogPost->body  = 'test';
$blogPost->save();

print_r($blogPost->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 1
    [title] => Test blog post
    [body] => test
    [version] => 1
    [created_at] => 2009-01-21 17:57:05
    [updated_at] => 2009-01-21 17:57:05
    [slug] => test-blog-post
)

Note

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:

Name Default Description
name slug The name of the slug column.
alias null The alias of the slug column.
type string The type of the slug column.
length 255 The length of the slug column.
unique true Whether or not unique slug values are enforced.
options array() Any other options for the slug column.
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', 'urlize') The Class::method() used to build the slug.
indexName sluggable The name of the index to create.
I18n

:php:class:`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:

class NewsItem extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 255);
        $this->hasColumn('body', 'blog');
    }

    public function setUp()
    {
        $this->actAs('I18n', array(
                'fields' => array('title', 'body')
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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:

Name Default Description
className %CLASS%Translation The name pattern to use for generated class.
fields array() The fields to internationalize.
type string The type of lang column.
length 2 The length of the lang column.
options array() Other options for the lang column.

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('NewsItem'));
echo $sql[0] . "";
echo $sql[1];

The above code would output the following SQL query:

CREATE TABLE news_item_translation (id BIGINT,
title VARCHAR(255),
body LONGTEXT,
lang CHAR(2),
PRIMARY KEY(id,
lang)) ENGINE = INNODB
CREATE TABLE news_item (id BIGINT AUTO_INCREMENT,
PRIMARY KEY(id)) ENGINE = INNODB

Note

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:

// test.php

// ...
$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());

The above example would produce the following output:

$ php test.php
Array
(
    [id] => 1
    [Translation] => Array
        (
            [en] => Array
                (
                    [id] => 1
                    [title] => some title
                    [body] => test
                    [lang] => en
                )
            [fi] => Array
                (
                    [id] => 1
                    [title] => joku otsikko
                    [body] => test
                    [lang] => fi
                )
        )
)

How do we retrieve the translated data now? This is easy! Lets find all items and their Finnish translations:

// test.php

// ...
$newsItems = Doctrine_Query::create()
    ->from('NewsItem n')
    ->leftJoin('n.Translation t')
    ->where('t.lang = ?')
    ->execute(array('fi'));

echo $newsItems[0]->Translation['fi']->title;

The above example would produce the following output:

$ php test.php
joku otsikko
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
class Category extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
    }

    public function setUp()
    {
        $this->actAs('NestedSet', array(
                'hasManyRoots' => true,
                'rootColumnName' => 'root_id'
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
Category:
  actAs:
    NestedSet:
      hasManyRoots: true
      rootColumnName: root_id
  columns:
    name: string(255)

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('Category'));
echo $sql[0];

The above code would output the following SQL query:

CREATE TABLE category (id BIGINT AUTO_INCREMENT,
name VARCHAR(255),
root_id INT,
lft INT,
rgt INT,
level SMALLINT,
PRIMARY KEY(id)) ENGINE = INNODB

Note

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 Hierarchical Data.

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:

// models/Job.php
class Job extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 255);
        $this->hasColumn('description', 'clob');
    }

    public function setUp()
    {
        $this->actAs('Searchable', array(
                'fields' => array('title', 'content')
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
Job:
  actAs:
    Searchable:
      fields: [title, description]
  columns:
    title: string(255)
    description: clob

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('Job'));
echo $sql[0] . "";
echo $sql[1] . "";
echo $sql[2];

The above code would output the following SQL query:

CREATE TABLE job_index (id BIGINT,
keyword VARCHAR(200),
field VARCHAR(50),
position BIGINT,
PRIMARY KEY(id,
keyword,
field,
position)) ENGINE = INNODB
CREATE TABLE job (id BIGINT AUTO_INCREMENT,
title VARCHAR(255),
description LONGTEXT,
PRIMARY KEY(id)) ENGINE = INNODB
ALTER TABLE job_index ADD FOREIGN KEY (id) REFERENCES job(id) ON UPDATE CASCADE ON DELETE CASCADE

Note

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 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.

// models/Zipcode.php
class Zipcode extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('zipcode', 'string', 255);
        $this->hasColumn('city', 'string', 255);
        $this->hasColumn('state', 'string', 2);
        $this->hasColumn('county', 'string', 255);
        $this->hasColumn('zip_class', 'string', 255);
    }

    public function setUp()
    {
        $this->actAs('Geographical');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
Zipcode:
  actAs: [Geographical]
  columns:
    zipcode: string(255)
    city: string(255)
    state: string(2)
    county: string(255)
    zip_class: string(255)

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('Zipcode'));
echo $sql[0];

The above code would output the following SQL query:

CREATE TABLE zipcode (id BIGINT AUTO_INCREMENT,
zipcode VARCHAR(255),
city VARCHAR(255),
state VARCHAR(2),
county VARCHAR(255),
zip_class VARCHAR(255),
latitude DOUBLE,
longitude DOUBLE,
PRIMARY KEY(id)) ENGINE = INNODB

Note

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.

First lets retrieve two different zipcode records:

// test.php

// ...
$zipcode1 = Doctrine_Core::getTable('Zipcode')->findOneByZipcode('37209');
$zipcode2 = Doctrine_Core::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

// ...
echo $zipcode1->getDistance($zipcode2, $kilometers = false);

Note

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

// ...
$q = $zipcode1->getDistanceQuery();

$q->orderby('miles asc')
    ->addWhere($q->getRootAlias() . '.city != ?', $zipcode1->city)
    ->limit(50);

echo $q->getSqlQuery();

The above call to getSql() would output the following SQL query:

SELECT
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

Note

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:

// test.php

// ...
$result = $q->execute();

foreach ($result as $zipcode) {
    echo $zipcode->city . " - " . $zipcode->miles . "";
    // You could also access $zipcode->kilometers
}

Get some sample zip code data to test this

http://www.populardata.com/zip_codes.zip

Download and import the csv file with the following function:

// test.php

// ...
function parseCsvFile($file, $columnheadings = false, $delimiter = ',', $enclosure = "\"")
{
    $row    = 1;
    $rows   = array();
    $handle = fopen($file, 'r');

    while (($data = fgetcsv($handle, 1000, $delimiter, $enclosure)) !== FALSE) {

        if (!($columnheadings == false) && ($row == 1)) {
            $headingTexts = $data;
        } elseif (!($columnheadings == false)) {
            foreach ($data as $key => $value) {
                unset($data[$key]);
                $data[$headingTexts[$key]] = $value;
            }
            $rows[] = $data;
        } else {
            $rows[] = $data;
        }
        $row++;
    }

    fclose($handle);
    return $rows;
}

$array = parseCsvFile('zipcodes.csv', false);

foreach ($array as $key => $value) {
    $zipcode = new Zipcode();
    $zipcode->fromArray($value);
    $zipcode->save();
}
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, a delete_at date is set. Below is an example of how to create a model with the SoftDelete behavior being used.

// models/SoftDeleteTest.php
class SoftDeleteTest extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', null, array(
                'primary' => true
            )
        );
    }

    public function setUp()
    {
        $this->actAs('SoftDelete');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
SoftDeleteTest:
  actAs: [SoftDelete]
  columns:
    name:
      type: string(255)
      primary: true

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('SoftDeleteTest'));
echo $sql[0];

The above code would output the following SQL query:

CREATE TABLE soft_delete_test (name VARCHAR(255),
deleted_at DATETIME DEFAULT NULL,
PRIMARY KEY(name)) ENGINE = INNODB

Now lets put the behavior in action.

Note

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.

Enable DQL Callbacks

// bootstrap.php

// ...
$manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);

Now save a new record so we can test the SoftDelete functionality:

// test.php

// ...
$record       = new SoftDeleteTest();
$record->name = 'new record';
$record->save();

Now when we call delete() the deleted_at flag will be set to true:

// test.php

// ...
$record->delete();

print_r($record->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [name] => new record
    [deleted_at] => 2009-09-01 00:59:01
)

Also, when we select some data the query is modified for you:

// test.php

// ...
$q = Doctrine_Query::create()
    ->from('SoftDeleteTest t');

echo $q->getSqlQuery();

The above call to getSql() would output the following SQL query:

SELECT
s.name AS s**name,
s.deleted_at AS s**deleted_at
FROM soft_delete_test s
WHERE (s.deleted_at IS NULL)

Note

Notice how the where condition is automatically added to only return the records that have not been deleted.

Now if we execute the query:

// test.php

// ...
$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.

class Wiki extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 255);
        $this->hasColumn('content', 'string');
    }

    public function setUp()
    {
        $options  = array('fields' => array('title', 'content'));
        $auditLog = new Doctrine_Template_Versionable($options);
        $search   = new Doctrine_Template_Searchable($options);
        $slug     = new Doctrine_Template_Sluggable(array(
                'fields' => array('title')
            )
        );
        $i18n = new Doctrine_Template_I18n($options);

        $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 chapter:

---
WikiTest:
  actAs:
    I18n:
      fields: [title, content]
      actAs:
        Versionable:
          fields: [title, content]
        Searchable:
          fields: [title, content]
        Sluggable:
          fields: [title]
  columns:
    title: string(255)
    content: string

Note

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.

class NewsArticle extends Doctrine_Record
{
    public function setTableDefinition() {
        $this->hasColumn('title', 'string', 255);
        $this->hasColumn('body', 'string', 255); $this->hasColumn('author', 'string', 255);
    }

    public function setUp()
    {
        $this->actAs('I18n', array(
                'fields'          => array('title', 'body'),
                'generateFiles'   => true,
                'generatePath'    => '/path/to/generate'
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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() to evaluate it at runtime.

Querying Generated Classes

If you want to query the auto generated models you will need to make sure the model with the behavior attached is loaded and initialized. You can do this by using the static Doctrine_Core::initializeModels() method.

For example if you want to query the translation table for a BlogPost model you will need to run the following code:

Doctrine_Core::initializeModels(array('BlogPost'));

$q = Doctrine_Query::create()
    ->from('BlogPostTranslation t')
    ->where('t.id = ? AND t.lang = ?', array(1, 'en'));

$translations = $q->execute();

Note

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 Searching behavior in more detail in the Searching chapter. As it is a large topic we have devoted an entire chapter to it.

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:

// models/NewsItem.php
class NewsItem extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 255);
        $this->hasColumn('body', 'clob');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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:

// test.php

// ...
$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
class NewsItem extends Doctrine_Record
{
    // ...
    public function setUp()
    {
        $this->actAs('Searchable', array(
                'fields' => array('title', 'content')
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
NewsItem:
  actAs:
    Searchable:
      fields: [title, content]
# ...

Lets check the SQL that is generated by the above models:

// test.php

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('NewsItem'));
echo $sql[0] . "";
echo $sql[1] . "";
echo $sql[2];

The above code would output the following SQL query:

CREATE TABLE news_item_index (id BIGINT,
keyword VARCHAR(200),
field VARCHAR(50),
position BIGINT,
PRIMARY KEY(id,
keyword,
field,
position)) ENGINE = INNODB
CREATE TABLE news_item (id BIGINT AUTO_INCREMENT,
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 :php:class:`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:

keyword field position id
database title 3 1

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 :php:class:`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:

// models/NewsItem.php
class NewsItem extends Doctrine_Record
{
    // ...
    public function setUp()
    {
        $this->actAs('Searchable', array(
                'fields' => array('title', 'content')
                'batchUpdates' => true
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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

// ...
$newsItem        = new NewsItem();
$newsItem->title = 'Test';
$newsItem->body  = 'test';
$newsItem->save();

Note

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

// ...
$newsItemTable = Doctrine_Core::getTable('NewsItem');
$newsItemTable->batchUpdateIndex();
Text Analyzers

By default Doctrine uses :php:class:`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 :php:class:`Doctrine_Search_Analyzer_Interface`. Here is an example where we create an analyzer named MyAnalyzer:

// models/MyAnalyzer.php
class MyAnalyzer implements Doctrine_Search_Analyzer_Interface
{
    public function analyze($text)
    {
        $text = trim($text);
        return $text;
    }
}

Note

The search analyzers must only contain one method named analyze() and it should return the modified inputted text to be indexed.

This analyzer can then be applied to the search object as follows:

// test.php

// ...
$newsItemTable = Doctrine_Core::getTable('NewsItem');
$search = $newsItemTable
    ->getTemplate('Doctrine_Template_Searchable')
    ->getPlugin();

$search->setOption('analyzer', new MyAnalyzer());
Query language

:php:class:`Doctrine_Search` provides a query language similar to Apache Lucene. The :php:class:`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

// ...
$newsItemTable = Doctrine_Core::getTable('NewsItem');
$results       = $newsItemTable->search('test');
print_r($results);

The above code executes the following query:

SELECT
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

The output of the code above would be the following:

$ php test.php
Array
(
    [0] => Array
        (
            [relevance] => 1
            [id] => 1
        )
)

Now you can use those results in another query to retrieve the actual NewsItem objects:

// test.php

// ...
$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());

The above example would produce the following output:

$ php test.php
Array
(
    [0] => Array
        (
            [id] => 1
            [title] => Test
            [body] => test
        )
)

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.

// test.php

// ...
$q = Doctrine_Query::create()
    ->from('NewsItem i');

$q = Doctrine_Core::getTable('Article')
    ->search('test', $q);

echo $q->getSqlQuery();

The above call to getSql() would output the following SQL query:

SELECT
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:

// test.php

// ...
$newsItems = $q->execute();

print_r($newsItems->toArray());

The above example would produce the following output:

$ php test.php
Array
(
    [0] => Array
        (
            [id] => 1
            [title] => Test
            [body] => test
        )
)
File searches

As stated before :php:class:`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 :php:class:`Doctrine_Search_File` which is a child of :php:class:`Doctrine_Search` providing some extra functionality needed for the file searches.

// test.php

// ...
$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

// ...
$sql = Doctrine_Core::generateSqlFromArray(array('FileIndex'));

The above code would output the following SQL query:

CREATE TABLE file_index (url VARCHAR(255),
keyword VARCHAR(200),
field VARCHAR(50),
position BIGINT,
PRIMARY KEY(url,
keyword,
field,
position)) ENGINE = INNODB

You can create the actual table in the database by using the Doctrine_Core::createTablesFromArray() method:

// test.php

// ...
Doctrine_Core::createTablesFromArray(array('FileIndex'));

Now we can start using the file searcher. In this example lets just index the models directory:

// test.php

// ...
$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:

// test.php

// ...
$results = $search->search('hasColumn');
print_r($results);

The above example would produce the following output:

$ php test.php
Array
(
    [0] => Array
        (
            [relevance] => 2
            [url] => models/generated/BaseNewsItem.php
        )
)
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 chapter. The NestedSet is a large topic like the Searchable behavior so it got its own dedicated chapter as well.

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 here.

There are three major approaches to managing tree structures in relational databases, these are:

  • the adjacency list model
  • the nested set model (otherwise known as the modified pre-order tree traversal algorithm)
  • materialized path model

These are explained in more detail at the following links:

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:

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:

// models/Category.php
class Category extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
    }

    public function setUp()
    {
        $this->actAs('NestedSet');
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# schema.yml

# ...
Category:
  actAs: [NestedSet]
  columns:
    name: string(255)

Detailed information on Doctrine’s templating model can be found in chapter Behaviors. These templates add some functionality to your model. In the example of the nested set, your model gets 3 additional fields: lft, rgt and level. You never need to care about the lft and rgt fields. These are used internally to manage the tree structure. The level field however, is of interest for you because it’s an integer value that represents the depth of a node within it’s tree. A level of 0 means it’s a root node. 1 means it’s a direct child of a root node and so on. By reading the level field from your nodes you can easily display your tree with proper indention.

Caution

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
class Category extends Doctrine_Record
{
    // ...
    public function setUp()
    {
        $options = array(
            'hasManyRoots'   => true,
            'rootColumnName' => 'root_id'
        );
        $this->actAs('NestedSet', $options);
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

Working with Trees

After you successfully set up your model as a nested set you can start working with it. Working with Doctrine’s nested set implementation is all about two classes: :php:class:`Doctrine_Tree_NestedSet` and :php:class:`Doctrine_Node_NestedSet`. These are nested set implementations of the interfaces :php:class:`Doctrine_Tree_Interface` and :php:class:`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:

// test.php

// ...
$treeObject = Doctrine_Core::getTable('Category')->getTree();

In the next example $category is an instance of Category:

// test.php

// ...
$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.

Creating a Root Node
// test.php

// ...
$category       = new Category();
$category->name = 'Root Category 1';
$category->save();

$treeObject = Doctrine_Core::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:

// test.php

// ...
$child1       = new Category();
$child1->name = 'Child Category 1';

$child2       = new Category();
$child2->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:

// test.php

// ...
$category = Doctrine_Core::getTable('Category')->findOneByName('Child Category 1');
$category->getNode()->delete();

Caution

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

// ...
$category       = new Category();
$category->name = 'Root Category 2';
$category->save();

$categoryTable = Doctrine_Core::getTable('Category');
$treeObject    = $categoryTable->getTree();
$treeObject->createRoot($category);

$childCategory = $categoryTable->findOneByName('Child Category 1');
$childCategory->getNode()->moveAsLastChildOf($category);
...

Below is a list of the methods available for moving nodes around:

  • moveAsLastChildOf($other)
  • moveAsFirstChildOf($other)
  • moveAsPrevSiblingOf($other)
  • moveAsNextSiblingOf($other)

The method names should be self-explanatory to you.

Examining a Node

You can examine the nodes and what type of node they are by using some of the following functions:

// test.php

// ...
$isLeaf = $category->getNode()->isLeaf();
$isRoot = $category->getNode()->isRoot();

Note

The above used functions return true/false depending on whether or not they are a leaf or root node.

Examining and Retrieving Siblings

You can easily check if a node has any next or previous siblings by using the following methods:

// test.php

// ...
$hasNextSib = $category->getNode()->hasNextSibling();
$hasPrevSib = $category->getNode()->hasPrevSibling();

You can also retrieve the next or previous siblings if they exist with the following methods:

// test.php

// ...
$nextSib = $category->getNode()->getNextSibling();
$prevSib = $category->getNode()->getPrevSibling();

Note

The above methods return false if no next or previous sibling exists.

If you want to retrieve an array of all the siblings you can simply use the getSiblings() method:

// test.php

// ...
$siblings = $category->getNode()->getSiblings();
Examining and Retrieving Descendants

You can check if a node has a parent or children by using the following methods:

// test.php

// ...
$hasChildren = $category->getNode()->hasChildren();
$hasParent   = $category->getNode()->hasParent();

You can retrieve a nodes first and last child by using the following methods:

// test.php

// ...
$firstChild = $category->getNode()->getFirstChild();
$lastChild  = $category->getNode()->getLastChild();

Or if you want to retrieve the parent of a node:

// test.php

// ...
$parent = $category->getNode()->getParent();

You can get the children of a node by using the following method:

// test.php

// ...
$children = $category->getNode()->getChildren();

Caution

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

// ...
$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

// ...
$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).

Rendering a Simple Tree

Note

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

// ...
$treeObject = Doctrine_Core::getTable('Category')->getTree();
$tree       = $treeObject->fetchTree();

foreach ($tree as $node) {
    echo str_repeat('&nbsp;&nbsp;', $node['level']) . $node['name'] . "\n";
}
Advanced Usage

The previous sections have explained the basic usage of Doctrine’s nested set implementation. This section will go one step further.

Fetching a Tree with Relations

If you’re a demanding software developer this question may already have come into your mind: “How do I fetch a tree/branch with related data?”. Simple example: You want to display a tree of categories, but you also want to display some related data of each category, let’s say some details of the hottest product in that category. Fetching the tree as seen in the previous sections and simply accessing the relations while iterating over the tree is possible but produces a lot of unnecessary database queries. Luckily, :php:class:`Doctrine_Query` and some flexibility in the nested set implementation have come to your rescue. The nested set implementation uses :php:class:`Doctrine_Query` objects for all it’s database work. By giving you access to the base query object of the nested set implementation you can unleash the full power of :php:class:`Doctrine_Query` while using your nested set.

First lets create the query we want to use to retrieve our tree data with:

// test.php

// ...
$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:

$treeObject = Doctrine_Core::getTable('Category')->getTree();
$treeObject->setBaseQuery($q);
$tree       = $treeObject->fetchTree();

There it is, the tree with all the related data you need, all in one query.

Note

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:

// test.php

// ...
$treeObject->resetBaseQuery();

You can take it even further. As mentioned in the chapter Improving Performance 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:

// test.php

// ...
$q = Doctrine_Query::create()
    ->select('c.name, p.name, m.name')
    ->from('Category c')
    ->leftJoin('c.HottestProduct p')
    ->leftJoin('p.Manufacturer m')
    ->setHydrationMode(Doctrine_Core::HYDRATE_ARRAY);

$treeObject = Doctrine_Core::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.

Rendering with Indention

Below you will find an example where all trees are rendered with proper indention. You can retrieve the roots using the fetchRoots() method and retrieve each individual tree by using the fetchTree() method.

// test.php

// ...
$treeObject     = Doctrine_Core::getTable('Category')->getTree();
$rootColumnName = $treeObject->getAttribute('rootColumnName');

foreach ($treeObject->fetchRoots() as $root) {
    $options = array(
        'root_id' => $root->$rootColumnName
    );
    foreach($treeObject->fetchTree($options) as $node) {
        echo str_repeat(' ', $node['level']) . $node['name'] . "\n";
    }
}

After doing all the examples above the code above should render as follows:

$ php test.php
Root Category 1
Root Category 2
Child Category 1
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. 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.

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:

Doctrine_Core::loadData('/path/to/data.yml');

You can either specify an individual yml file like we have done above, or you can specify an entire directory:

Doctrine_Core::loadData('/path/to/directory');

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:

Doctrine_Core::loadData('/path/to/data.yml', true);
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:

Doctrine_Core::dumpData('/path/to/data.yml');

Or you can optionally dump all data to individual files. One YAML file per model like the following:

Doctrine_Core::dumpData('/path/to/directory', true);
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:

$ mkdir fixtures
$ touch fixtures/data.yml

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

// ...
Doctrine_Core::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
class Resource extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
        $this->hasColumn('resource_type_id', 'integer');
    }

    public function setUp()
    {
        $this->hasOne('ResourceType as Type', array(
                'local' => 'resource_type_id',
                'foreign' => 'id'
            )
        );

        $this->hasMany('Tag as Tags', array(
                'local' => 'resource_id',
                'foreign' => 'tag_id',
                'refClass' => 'ResourceTag'
            )
        );
    }
}

// models/ResourceType.php
class ResourceType extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
    }

    public function setUp()
    {
        $this->hasMany('Resource as Resouces', array(
                'local' => 'id',
                'foreign' => 'resource_type_id'
            )
        );
    }
}

// models/Tag.php
class Tag extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255);
    }

    public function setUp()
    {
        $this->hasMany('Resource as Resources', array(
                'local' => 'tag_id',
                'foreign' => 'resource_id',
                'refClass' => 'ResourceTag'
            )
        );
    }
}

// models/ResourceTag.php
class ResourceTag extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('resource_id', 'integer');
        $this->hasColumn('tag_id', 'integer');
    }
}

// models/Category.php
class BaseCategory extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('name', 'string', 255, array(
                'type' => 'string', 'length' => '255'
            )
        );
    }

    public function setUp()
    {
        $this->actAs('NestedSet');
    }
}

class BaseArticle extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('title', 'string', 255, array(
                'type' => 'string', 'length' => '255'
            )
        );

        $this->hasColumn('body', 'clob', null, array(
                'type' => 'clob'
            )
        );
    }

    public function setUp()
    {
        $this->actAs('I18n', array('fields' => array('title', 'body')));
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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

Note

All row keys across all YAML data fixtures must be unique. For example below tutorial, doctrine, help, cheat are all unique.

---
# 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.

---
# 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]
Fixtures For Nested Sets

Writing a fixtures file for a nested set tree is slightly different from writing regular fixtures files. The structure of the tree is defined like the following:

---
# 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

Tip

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.

---
# 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.

---
# 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.

Fixtures For I18n

The fixtures for the I18n aren’t anything custom since the I18n really is just a normal set of relationships that are built on the fly dynamically:

---
# 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 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.

Database Abstraction Layer

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

Name Type Primary Auto Increment
id integer true true
name string(255) false false
datetime timestamp false false

people

Name Type Primary Auto Increment
id integer true true
name string(255) false false

event_participants

Name Type Primary Auto Increment
event_id integer true false
person_id string(255) true false
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.

// test.php

// ...
$conn->export->createDatabase('events_db');

Now lets change the connection in our bootstrap.php file to connect to the new events_db:

// bootstrap.php

/**
 * 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:

// test.php

//
$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.

Datatype length default not null unsigned autoincrement
string x x x    
boolean   x x    
integer x x x x x
decimal   x x    
float   x x    
timestamp   x x    
time   x x    
date   x x    
clob x   x    
blob x   x    

And now we can go ahead and create the people table:

// test.php

// ...
$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

// ...
$options = array(
    'comment'       => 'Repository of people',
    'character_set' => 'utf8',
    'collate'       => 'utf8_unicode_ci',
    'type'          => 'innodb',
);

// ...

$conn->export->createTable('people', $definition, $options);
Creating Foreign Keys

Creating the event_participants table with a foreign key:

// test.php

// ...
$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
    ),
);

$conn->export->createTable('event_participants', $definition, $options);

Tip

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:

// test.php

// ...
$definition = array('local' => 'person_id',
                    'foreign' => 'id',
                    'foreignTable' => 'people',
                    'onDelete' => 'CASCADE');

$conn->export->createForeignKey('event_participants', $definition);
Altering table

:php:class:`Doctrine_Export` drivers provide an easy database portable way of altering existing database tables.

// test.php

// ...
$alter = array(
    'add' => array(
        'new_column' => array(
            'type' => 'string',
            'length' => 255
        ),
        'new_column2' => array(
            'type' => 'string',
            'length' => 255
        )
    )
);

echo $conn->export->alterTableSql('events', $alter);

The above call to alterTableSql() would output the following SQL query:

ALTER TABLE events ADD new_column VARCHAR(255),
ADD new_column2 VARCHAR(255)

Note

If you only want execute generated sql and not return it, use the alterTable() method.

// test.php

// ...
$conn->export->alterTable('events', $alter);

The alterTable() method requires two parameters and has an optional third:

Name Type Description
$name string Name of the table that is intended to be changed.
$changes array Associative array that contains the details of each type of change that is intended to be performed.

An optional third parameter (default: false):

Name Type Description
$check boolean Check if the DBMS can actually perform the operation before executing.

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.

// test.php

// ...
$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);

Note

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

// ...
$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

// ...
$definition = array(
    'fields' => array(
        'datetime' => array()
    )
);

$conn->export->createIndex('events', 'datetime', $definition);
Deleting database elements

For every create*() method as shown above, there is a corresponding drop*() method to delete a database, a table, field, index or constraint. The drop*() methods do not check if the item to be deleted exists, so it’s developer’s responsibility to check for exceptions using a try catch block:

// test.php

// ...
try {
    $conn->export->dropSequence('nonexisting');
} catch(Doctrine_Exception $e) {

}

You can easily drop a constraint with the following code:

// test.php

// ...
$conn->export->dropConstraint('events', 'PRIMARY', true);

Note

The third parameter gives a hint that this is a primary key constraint.

// test.php

// ... $conn->export->dropConstraint('event_participants', 'event_id');

You can easily drop an index with the following code:

$conn->export->dropIndex('events', 'event_timestamp');

Tip

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.

Drop a table from the database with the following code:

// test.php

// ...
$conn->export->dropTable('events');

We can drop the database with the following code:

// test.php

// ...
$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.
listSequences($dbName) List the available sequences. Takes optional database name as a parameter. If not supplied, the currently selected database is assumed.
listTableConstraints($tableName) Lists the available tables. takes a table name
listTableColumns($tableName) List the columns available in a table.
listTableIndexes($tableName) List the indexes defined in a table.
listTables($dbName) List the tables in a database.
listTableTriggers($tableName) List the triggers in a table.
listTableViews($tableName) List the views available in a table.
listUsers() List the users for the database.
listViews($dbName) List the views available for a database.

Below you will find examples on how to use the above listed functions:

Listing Databases
// test.php

// ...
$databases = $conn->import->listDatabases();
print_r($databases);
Listing Sequences
// test.php

// ... $sequences = $conn->import->listSequences('events_db');
print_r($sequences);
Listing Constraints
// test.php

// ...
$constraints = $conn->import->listTableConstraints('event_participants');
print_r($constraints);
Listing Table Columns
// test.php

// ... $columns = $conn->import->listTableColumns('events');
print_r($columns);
Listing Table Indexes
// test.php

// ... $indexes = $conn->import->listTableIndexes('events');
print_r($indexes);
Listing Tables
$tables = $conn->import->listTables();
print_r($tables);
Listing Views

Note

Currently there is no method to create views, so let’s do it manually.

$sql = "CREATE VIEW names_only AS SELECT name FROM people";
$conn->exec($sql);

$sql = "CREATE VIEW last_ten_events AS SELECT * FROM events ORDER BY id DESC LIMIT 0,10";
$conn->exec($sql);

Now we can list the views we just created:

$views = $conn->import->listViews();
print_r($views);
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:

  • getPortableDeclaration(), which is used for converting native RDBMS type declaration to portable Doctrine declaration
  • getNativeDeclaration(), which is used for converting portable Doctrine declaration to driver specific type declaration
Getting portable declaration
// test.php

// ...
$declaration = $conn->dataDict->getPortableDeclaration('VARCHAR(255)');

print_r($declaration);

The above example would output the following:

$ php test.php
Array
(
    [type] => Array
        (
            [0] => string
        )
    [length] => 255
    [unsigned] =>
    [fixed] =>
)
Getting Native Declaration
// test.php

// ...
$portableDeclaration = array(
    'type' => 'string',
    'length' => 20,
    'fixed' => true
);

$nativeDeclaration = $conn->dataDict->getNativeDeclaration($portableDeclaration);

echo $nativeDeclaration;

The above example would output the following:

$ php test.php
CHAR(20)
Drivers
Mysql
Setting table type
// test.php

// ...
$fields = array(
    'id' => array(
        'type' => 'integer',
        'autoincrement' => true
    ),
    'name' => array(
        'type' => 'string',
        'fixed' => true,
        'length' => 8
    )
);

Note

The following option is mysql specific and skipped by other drivers.

$options = array('type' => 'INNODB');

$sql = $conn->export->createTableSql('test_table', $fields);
echo $sql[0];

The above will output the following SQL query:

CREATE TABLE test_table (id INT AUTO_INCREMENT,
name CHAR(8)) ENGINE = INNODB
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.

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.

  • Atomicity 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.
  • Consistency 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.
  • Isolation 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 schedule) is serializable. For performance reasons, this ability is the most often relaxed constraint. See the isolation article for more details.
  • Durability 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 system has checked the integrity constraints and won’t need to abort the transaction. Typically, all transactions are written into a log 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.

In Doctrine all operations are wrapped in transactions by default. There are some things that should be noticed about how Doctrine works internally:

  • Doctrine uses application level transaction nesting.
  • Doctrine always executes INSERT / UPDATE / DELETE queries at the end of transaction (when the outermost commit is called). The operations are performed in the following order: all inserts, all updates and last all deletes. Doctrine knows how to optimize the deletes so that delete operations of the same component are gathered in one query.

First we need to begin a new transation:

$conn->beginTransaction();

Next perform some operations which result in queries being executed:

$user       = new User();
$user->name = 'New user';
$user->save();

$user       = Doctrine_Core::getTable('User')->find(5);
$user->name = 'Modified user';
$user->save();

Now we can commit all the queries by using the commit() method:

$conn->commit();
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():

function saveUserAndGroup(Doctrin_Connection $conn, User $user, Group $group)
{
    $conn->beginTransaction();

    $user->save();

    $group->save();

    $conn->commit();
}

Now we make use of the function inside another transaction:

try {
    $conn->beginTransaction();

    saveUserAndGroup($conn,$user,$group);
    saveUserAndGroup($conn,$user2,$group2);
    saveUserAndGroup($conn,$user3,$group3);

    $conn->commit();

} catch(Doctrine_Exception $e) {
    $conn->rollback();
}

Note

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 {
    $conn->beginTransaction();
    // do some operations here

    // creates a new savepoint called mysavepoint
    $conn->beginTransaction('mysavepoint');
    try {
        // do some operations here

        $conn->commit('mysavepoint');
    } catch(Exception $e) {
        $conn->rollback('mysavepoint');
    }
    $conn->commit();
} catch(Exception $e) {
    $conn->rollback();
}

The Doctrine_Transaction::rollback($savepoint) rolls back a transaction to the named savepoint. Modifications that the current transaction made to rows after the savepoint was set are undone in the rollback.

Note

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 {
    $conn->beginTransaction();
    // do some operations here

    // creates a new savepoint called mysavepoint
    $conn->beginTransaction('mysavepoint');

    // do some operations here

    $conn->commit();   // deletes all savepoints
} catch(Exception $e) {
    $conn->rollback(); // deletes all savepoints
}
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.

To get the transaction module use the following code:

$tx = $conn->transaction;

Set the isolation level to READ COMMITTED:

$tx->setIsolation('READ COMMITTED');

Set the isolation level to SERIALIZABLE:

$tx->setIsolation('SERIALIZABLE');

Tip

Some drivers (like Mysql) support the fetching of current transaction isolation level. It can be done as follows:

$level = $tx->getIsolation();
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.

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 :php:class:`Doctrine_Record` hooks:

// models/BlogPost.php

class BlogPost extends Doctrine_Record
{
    // ...

    public function preInsert($event)
    {
        $invoker = $event->getInvoker();

        $invoker->created = date('Y-m-d', time());
    }
}

Note

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:

// test.php

// ...
$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 :php:class:`Doctrine_Event` object. :php:class:`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 :php:class:`Doctrine_Connection` and its modules (such as :php:class:`Doctrine_Transaction`). All listener methods take one argument :php:class:`Doctrine_Event` which holds information about the listened event.

Creating a New Listener

There are three different ways of defining a listener. First you can create a listener by making a class that inherits :php:class:`Doctrine_EventListener`:

class MyListener extends Doctrine_EventListener
{
    public function preExec( Doctrine_Event $event )
    {

    }
}

Note that by declaring a class that extends :php:class:`Doctrine_EventListener` you don’t have to define all the methods within the :php:class:`Doctrine_EventListener_Interface`. This is due to a fact that :php:class:`Doctrine_EventListener` already has empty skeletons for all these methods.

Sometimes it may not be possible to define a listener that extends :php:class:`Doctrine_EventListener` (you might have a listener that inherits some other base class). In this case you can make it implement :php:class:`Doctrine_EventListener_Interface`.

class MyListener implements Doctrine_EventListener_Interface
{
    public function preTransactionCommit( Doctrine_Event $event ) {}
    public function postTransactionCommit( Doctrine_Event $event ) {}

    public function preTransactionRollback( Doctrine_Event $event ) {}
    public function postTransactionRollback( Doctrine_Event $event ) {}

    public function preTransactionBegin( Doctrine_Event $event ) {}
    public function postTransactionBegin( Doctrine_Event $event ) {}

    public function postConnect( Doctrine_Event $event ) {}
    public function preConnect( Doctrine_Event $event ) {}

    public function preQuery( Doctrine_Event $event ) {}
    public function postQuery( Doctrine_Event $event ) {}

    public function prePrepare( Doctrine_Event $event ) {}
    public function postPrepare( Doctrine_Event $event ) {}

    public function preExec( Doctrine_Event $event ) {}
    public function postExec( Doctrine_Event $event ) {}

    public function preError( Doctrine_Event $event ) {}
    public function postError( Doctrine_Event $event ) {}

    public function preFetch( Doctrine_Event $event ) {}
    public function postFetch( Doctrine_Event $event ) {}

    public function preFetchAll( Doctrine_Event $event ) {}
    public function postFetchAll( Doctrine_Event $event ) {}

    public function preStmtExecute( Doctrine_Event $event ) {}
    public function postStmtExecute( Doctrine_Event $event ) {}
}

Caution

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 :php:class:`Doctrine_Overloadable`. This interface has only one method: __call(), which can be used for catching all the events.

class MyDebugger implements Doctrine_Overloadable
{
    public function __call( $methodName, $args )
    {
        echo $methodName . ' called !';
    }
}
Attaching listeners

You can attach the listeners to a connection with setListener().

$conn->setListener( new MyDebugger() );

If you need to use multiple listeners you can use addListener().

$conn->addListener( new MyDebugger() );
$conn->addListener( new MyLogger() );
Pre and Post Connect

All of the below listeners are invoked in the :php:class:`Doctrine_Connection` class. And they are all passed an instance of :php:class:`Doctrine_Event`.

Methods Listens Params
preConnect() connection()  
postConnect() connection()  
Transaction Listeners

All of the below listeners are invoked in the :php:class:`Doctrine_Transaction` class. And they are all passed an instance of :php:class:`Doctrine_Event`.

Methods Listens Params
preTransactionBegin() beginTransaction()  
postTransactionBegin() beginTransaction()  
preTransactionRollback() rollback()  
postTransactionRollback() rollback()  
preTransactionCommit() commit()  
postTransactionCommit() commit()  
preCreateSavepoint() createSavepoint() savepoint
postCreateSavepoint() createSavepoint() savepoint
preRollbackSavepoint() rollbackSavepoint() savepoint
postRollbackSavepoint() rollbackSavepoint() savepoint
preReleaseSavepoint() releaseSavepoint() savepoint
postReleaseSavepoint() releaseSavepoint() savepoint
class MyTransactionListener extends Doctrine_EventListener
{
    public function preTransactionBegin( Doctrine_Event $event )
    {
        echo 'beginning transaction... ';
    }

    public function preTransactionRollback( Doctrine_Event $event )
    {
        echo 'rolling back transaction... ';
    }
}
Query Execution Listeners

All of the below listeners are invoked in the :php:class:`Doctrine_Connection` and :php:class:`Doctrine_Connection_Statement` classes. And they are all passed an instance of :php:class:`Doctrine_Event`.

Methods Listens Params
prePrepare() prepare() query
postPrepare() prepare() query
preExec() exec() query
postExec() exec() query,rows
preStmtExecute() execute() query
postStmtExecute() execute() query
preExecute() execute() * query
postExecute() execute() * query
preFetch() fetch() query, data
postFetch() fetch() query, data
preFetchAll() fetchAll() query, data
postFetchAll() fetchAll() query, data

Note

preExecute() and postExecute() only get invoked when Doctrine_Connection::execute() is being called without prepared statement parameters. Otherwise Doctrine_Connection::execute() invokes prePrepare(), postPrepare(), preStmtExecute() and postStmtExecute().

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

// ...
class HydrationListener extends Doctrine_Record_Listener
{
    public function preHydrate( Doctrine_Event $event )
    {
        $data              = $event->data;
        $data['full_name'] = $data['first_name'] . ' ' . $data['last_name'];
        $event->data       = $data;
    }
}

Now all we need to do is attach this listener to the User record and fetch some users:

// test.php

// ...
$userTable = Doctrine_Core::getTable('User');
$userTable->addRecordListener( new HydrationListener() );

$q = Doctrine_Query::create()
    ->from('User');

$users = $q->execute();

foreach ( $users as $user )
{
    echo $user->full_name;
}
Record Listeners

:php:class:`Doctrine_Record` provides listeners very similar to :php:class:`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 :php:class:`Doctrine_Record` and :php:class:`Doctrine_Validator` classes. And they are all passed an instance of :php:class:`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 :php:class:`Doctrine_Record_Listener`, by implementing :php:class:`Doctrine_Record_Listener_Interface` or by implementing :php:class:`Doctrine_Overloadable`.

In the following we’ll create a global level listener by implementing :php:class:`Doctrine_Overloadable`:

class Logger implements Doctrine_Overloadable
{
    public function __call( $m, $a )
    {
        echo 'caught event ' . $m;

        // do some logging here...
    }
}

Attaching the listener to manager is easy:

$manager->addRecordListener( new Logger() );

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:

class Debugger extends Doctrine_Record_Listener
{
    public function preInsert( Doctrine_Event $event )
    {
        echo 'inserting a record ...';
    }

    public function preUpdate( Doctrine_Event $event )
    {
        echo 'updating a record...';
    }
}

Attaching the listener to a connection is as easy as:

$conn->addRecordListener( new Debugger() );

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:

class Debugger extends Doctrine_Record_Listener
{
    public function postDelete( Doctrine_Event $event )
    {
        echo 'deleted ' . $event->getInvoker()->id;
    }
}

Attaching this listener to given table can be done as follows:

class MyRecord extends Doctrine_Record
{
    // ...
    public function setUp()
    {
        $this->addListener( new Debugger() );
    }
}
Record Hooks

All of the below listeners are invoked in the :php:class:`Doctrine_Record` and :php:class:`Doctrine_Validator` classes. And they are all passed an instance of :php:class:`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:

class BlogPost extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'title', 'string', 200 );
        $this->hasColumn( 'content', 'string' );
        $this->hasColumn( 'created', 'date' );
        $this->hasColumn( 'updated', 'date' );
    }

    public function preInsert( $event )
    {
        $this->created = date( 'Y-m-d', time() );
    }

    public function preUpdate( $event )
    {
        $this->updated = date( 'Y-m-d', time() );
    }
}
DQL Hooks

Doctrine allows you to attach record listeners globally, on each connection, or on specific record instances. :php:class:`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.

Tip

The SoftDelete functionality is included in Doctrine as a behavior. This code is used to demonstrate how to use the select, delete, and update DQL listeners to modify executed queries. You can use the SoftDelete behavior by specifying $this->actAs('SoftDelete') in your Doctrine_Record::setUp() definition.

class UserListener extends Doctrine_EventListener
{
    /**
     * Skip the normal delete options so we can override it with our own
     *
     * @param Doctrine_Event $event
     * @return void
     */
    public function preDelete( Doctrine_Event $event )
    {
        $event->skipOperation();
    }

    /**
     * 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:

class User extends Doctrine_Record
{
    // ...
    public function preDqlSelect()
    {
        // ...
    }

    public function preDqlUpdate()
    {
        // ...
    }

    public function preDqlDelete()
    {
        // ...
    }
}

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:

// bootstrap.php

// ...
$manager->setAttribute( Doctrine_Core::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:

$user           = new User();
$user->username = 'jwage';
$user->password = 'changeme';
$user->save();
$user->delete();

Note

The above call to $user->delete() does not actually delete the record instead it sets the deleted flag to true.

$q = Doctrine_Query::create()
    ->from('User u');

echo $q->getSqlQuery();
SELECT
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 = ?

Note

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 :php:class:`Doctrine_EventListener`:

$conn->addListener( new Debugger() );
$conn->addListener( new Logger() );
The Event object
Getting the Invoker

You can get the object that invoked the event by calling getInvoker():

class MyListener extends Doctrine_EventListener
{
    public function preExec( Doctrine_Event $event )
    {
        $event->getInvoker(); // Doctrine_Connection
    }
}
Event Codes

:php:class:`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:

class MyListener extends Doctrine_EventListener
{
    public function preExec( Doctrine_Event $event )
    {
        $event->getCode(); // Doctrine_Event::CONN_EXEC
    }
}

class MyRecord extends Doctrine_Record
{
    public function preUpdate( Doctrine_Event $event )
    {
        $event->getCode(); // Doctrine_Event::RECORD_UPDATE
    }
}
Getting the Invoker

The method getInvoker() returns the object that invoked the given event. For example for event Doctrine_Event::CONN_QUERY the invoker is a :php:class:`Doctrine_Connection` object.

Here is an example of using the record hook named preUpdate() that is invoked when a :php:class:`Doctrine_Record` instance is saved and an update is issued to the database:

class MyRecord extends Doctrine_Record
{
    public function preUpdate( Doctrine_Event $event )
    {
        $event->getInvoker(); // Object(MyRecord)
    }
}
Skip Next Operation

:php:class:`Doctrine_Event` provides many methods for altering the execution of the listened method as well as for altering the behavior of the listener chain.

For some reason you may want to skip the execution of the listened method. It can be done as follows (note that preExec() could be any listener method):

class MyListener extends Doctrine_EventListener
{
    public function preExec( Doctrine_Event $event )
    {
        // some business logic, then:
        $event->skipOperation();
    }
}

When using a chain of listeners you might want to skip the execution of the next listener. It can be achieved as follows:

class MyListener extends Doctrine_EventListener
{
    public function preExec( Doctrine_Event $event )
    {
        // some business logic, then:
        $event->skipNextListener();
    }
}
Conclusion

Event listeners are a great feature in Doctrine and combined with Behaviors 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.

Caching

Introduction

:php:class:`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. :php:class:`Doctrine_Cache` has many options for fine-tuning performance.

All the cache drivers are instantiated like the following:

// bootstrap.php

// ...
$options = array();
$cacheDriver = new Doctrine_Cache_Memcache( $options );

Note

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:

// bootstrap.php

// ...
$servers = array(
    'host'       => 'localhost',
    'port'       => 11211,
    'persistent' => true
);

$cacheDriver = new Doctrine_Cache_Memcache(
    array(
        'servers' => $servers,
        'compression' => false
    )
);

Note

Memcache allows multiple servers.

Available options for Memcache driver:

Option Data Type Default Value Description
servers array array(array('host' => 'localhost','port' => 11211, 'persistent' => true)) An array of memcached servers ; each memcached server is described by an associative array : 'host' => (string) : the name of the memcached server, 'port' => (int) : the port of the memcached server, 'persistent' => (bool) : use or not persistent connections to this memcached server
compression boolean false true if you want to use on-the-fly compression
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

// ...
$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

// ...
$cacheConn   = Doctrine_Manager::connection( new PDO( 'sqlite::memory:' ) );
$cacheDriver = new Doctrine_Cache_Db( array( 'connection' => $cacheConn ) );

Query Cache & Result Cache
Introduction

Doctrine provides means for caching the results of the DQL parsing process, as well as the end results of DQL queries (the data). These two caching mechanisms can greatly increase performance. Consider the standard workflow of DQL query execution:

  • Init new DQL query
  • Parse DQL query
  • Build database specific SQL query
  • Execute the SQL query
  • Build the result set
  • Return the result set

Now these phases can be very time consuming, especially phase 4 which sends the query to your database server. When Doctrine query cache is being used only the following phases occur:

  • Init new DQL query
  • Execute the SQL query (grabbed from the cache)
  • Build the result set
  • Return the result set

If a DQL query has a valid cache entry the cached SQL query is used, otherwise the phases 2-3 are executed normally and the result of these steps is then stored in the cache. The query cache has no disadvantages, since you always get a fresh query result.

Note

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):

  • Init new DQL query
  • 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
Using the Query Cache

You can set a connection or manager level query cache driver by using the Doctrine_Core::ATTR_QUERY_CACHE attribute. Setting a connection level cache driver means that all queries executed with this connection use the specified cache driver whereas setting a manager level cache driver means that all connections (unless overridden at connection level) will use the given cache driver.

Setting a manager level query cache driver:

// bootstrap.php

// ...
$manager->setAttribute( Doctrine_Core::ATTR_QUERY_CACHE, $cacheDriver );

Note

The value of $cacheDriver above could be any of the drivers we instantiated in the previous section of this chapter.

Setting a connection level cache driver:

// bootstrap.php

// ...
$conn->setAttribute( Doctrine_Core::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()
    ->useQueryCache(new Doctrine_Cache_Apc() );
Result Cache
Using the Result Cache

You can set a connection or manager level result cache driver by using Doctrine_Core::ATTR_RESULT_CACHE. Setting a connection level cache driver means that all queries executed with this connection use the specified cache driver whereas setting a manager level cache driver means that all connections (unless overridden at connection level) will use the given cache driver.

Setting a manager level cache driver:

// bootstrap.php

// ...
$manager->setAttribute( Doctrine_Core::ATTR_RESULT_CACHE, $cacheDriver );

Setting a connection level cache driver:

// bootstrap.php

// ...
$conn->setAttribute( Doctrine_Core::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_Core::ATTR_RESULT_CACHE_LIFESPAN.

Set the lifespan as one hour (60 seconds * 60 minutes = 1 hour = 3600 secs):

// bootstrap.php

// ...
$manager->setAttribute( Doctrine_Core::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:

$q = Doctrine_Query::create()
    ->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.

$q = Doctrine_Query::create()
    ->useResultCache(new Doctrine_Cache_Apc() );

Also you can override the lifespan attribute by calling setResultCacheLifeSpan():

$q = Doctrine_Query::create()
    ->setResultCacheLifeSpan( 60 * 30 );
Conclusion

Using the caching feature of Doctrine is highly recommended in both development and production environments. There 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 Technology, Coding Standards and Unit Testing. Lets move on to discuss the last feature of Doctrine, Migrations.

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 :php:class:`Doctrine_Migration` and pass it the path to our migration classes:

$migration = new Doctrine_Migration( '/path/to/migration_classes', $conn );

Note

Notice the second argument to the :php:class:`Doctrine_Migration` constructor. You can pass an optional :php:class:`Doctrine_Connection` instance. If you do not pass the connection for the migration class to use, it will simply grab the current connection.

Now we can migrate to the latest version by calling the migrate() method:

$migration->migrate();

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 );

Now you can migrate back to version 0 from 3:

$migration->migrate( 0 );

If you want to get the current version of the database you can use the getCurrentVersion() method:

echo $migration->getCurrentVersion();

Tip

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.

Note

Transactions in Migrations Internally Doctrine does not wrap migration versions in transactions. It is up to you as the developer to handle exceptions and transactions in your migration classes. Remember though, that very few databases support transactional DDL. So on most databases, even if you wrap the migrations in a transaction, any DDL statements like create, alter, drop, rename, etc., will take effect anyway.

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:

$ mkdir migrations

Now create the migrate.php script in your favorite editor and place the following code inside:

// migrate.php
require_once('bootstrap.php');

$migration = new Doctrine_Migration( 'migrations' );
$migration->migrate();
Writing Migration Classes

Migration classes consist of a simple class that extends from :php:class:`Doctrine_Migration`. You can define a up() and down() method that is meant for doing and undoing changes to a database for that migration version.

Note

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:

// migrations/1_add_table.php
class AddTable extends Doctrine_Migration_Base
{
    public function up()
    {
        $this->createTable( 'migration_test', array( 'field1' => array( 'type' => 'string' ) ) );
    }

    public function down()
    {
        $this->dropTable( 'migration_test' );
    }
}

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
class AddColumn extends Doctrine_Migration_Base
{
    public function up()
    {
        $this->addColumn( 'migration_test', 'field2', 'string' );
    }

    public function down()
    {
        $this->removeColumn( 'migration_test', 'field2' );
    }
}

Finally, lets change the type of the field1 column in the table we created previously:

// migrations/3_change_column.php
class ChangeColumn extends Doctrine_Migration_Base
{
    public function up()
    {
        $this->changeColumn( 'migration_test', 'field2', 'integer' );
    }

    public function down()
    {
        $this->changeColumn( 'migration_test', 'field2', 'string' );
    }
}

Now that we have created the three migration classes above we can run our migrate.php script we implemented earlier:

$ php migrate.php

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

// ...
$migration = new Doctrine_Migration( 'migrations' );
$migration->migrate( 0 );

Now run the migrate.php script:

$ php migrate.php

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
// ...
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'
    );

    $this->createTable( 'table_name', $columns, $options );
}
// ...

Note

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
// ...
public function down()
{
    $this->dropTable( 'table_name' );
}
// ...
Rename Table
// ...
public function up()
{
    $this->renameTable( 'old_table_name', 'new_table_name' );
}
// ...
Create Constraint
// ...
public function up()
{
    $definition = array(
        'fields' => array(
            'username' => array()
        ),
        'unique' => true
    );

    $this->createConstraint( 'table_name', 'constraint_name', $definition );
}
// ...
Drop Constraint

Now the opposite down() would look like the following:

// ...
public function down()
{
    $this->dropConstraint( 'table_name', 'constraint_name' );
}
// ...
Create Foreign Key
// ...
public function up()
{
    $definition = array(
        'local'        => 'email_id',
        'foreign'      => 'id',
        'foreignTable' => 'email',
        'onDelete'     => 'CASCADE',
    );

    $this->createForeignKey( 'table_name', 'email_foreign_key', $definition );
}
// ...

The valid options for the $definition are:

Name Description
name Optional constraint name
local The local field(s)
foreign The foreign reference field(s)
foreignTable The name of the foreign table
onDelete Referential delete action
onUpdate Referential update action
deferred Deferred constraint checking
Drop Foreign Key
// ...
public function down()
{
    $this->dropForeignKey( 'table_name', 'email_foreign_key' );
}
// ...
Add Column
// ...
public function up()
{
    $this->addColumn( 'table_name', 'column_name', 'string', $length, $options );
}
// ...
Rename Column

Note

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.

// ...
public function up()
{
    $this->renameColumn( 'table_name', 'old_column_name', 'new_column_name' );
}
// ...
Change Column

Change any aspect of an existing column:

// ...
public function up()
{
    $options = array( 'length' => 1 );
    $this->changeColumn( 'table_name', 'column_name', 'tinyint', $options );
}
// ...
Remove Column
// ...
public function up()
{
    $this->removeColumn( 'table_name', 'column_name' );
}
// ...
Irreversible Migration

Tip

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 :php:class:`Doctrine_Migration_IrreversibleMigrationException` exception.

// ...
public function down()
{
    throw new Doctrine_Migration_IrreversibleMigrationException( 'The remove column operation cannot be undone!' );
}
// ...
Add Index
// ...
public function up()
{
    $options = array(
        'fields' => array(
            'username' => array(
                'sorting' => 'ascending'
            ),
            'last_login' => array()
        )
    );

    $this->addIndex( 'table_name', 'index_name', $options );
}
// ...
Remove Index
// ...
public function down()
{
    $this->removeIndex( 'table_name', 'index_name' );
}
// ...
Pre and Post Hooks

Sometimes you may need to alter the data in the database with your models. Since you may create a table or make a change, you have to do the data altering after the up() or down() method is processed. We have hooks in place for this named preUp(), postUp(), preDown(), and postDown(). Define these methods and they will be triggered:

// migrations/1_add_table.php
class AddTable extends Doctrine_Migration_Base
{
    public function up()
    {
        $this->createTable( 'migration_test', array(
            'field1' => array(
                'type' => 'string'
            )
        );
    }

    public function postUp()
    {
        $migrationTest         = new MigrationTest();
        $migrationTest->field1 = 'Initial record created by migrations';
        $migrationTest->save();
    }

    public function down()
    {
        $this->dropTable( 'migration_test' );
    }
}

Note

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.

// models/MigrationTest.php
class MigrationTest extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'field1', 'string' );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
# 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.

Here is an example where we automate the adding and removing of a column

class MigrationTest extends Doctrine_Migration_Base
{
    public function migrate( $direction )
    {
        $this->column( $direction, 'table_name', 'column_name', 'string', '255' );
    }
}

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:

Automate Method Name Automates
table() createTable()/dropTable()
constraint() createConstraint()/dropConstraint()
foreignKey() createForeignKey()/dropForeignKey()
column() addColumn()/removeColumn()
index() addInex()/removeIndex()
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_Core::generateMigrationsFromDb().

Doctrine_Core::generateMigrationsFromDb( '/path/to/migration/classes' );
From Existing Models

To generate a set of migrations from an existing set of models it is just as simple as from a database, just use Doctrine_Core::generateMigrationsFromModels().

Doctrine_Core::generateMigrationsFromModels( '/path/to/migration/classes', '/path/to/models' );
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:

  • Path to yaml schema files
  • Name of an existing database connection
  • Path to an existing set of models

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:

---
# 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:

---
# 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:

Doctrine_Core::generateMigrationsFromDiff( '/path/to/migration/classes', '/path/to/schema1.yml', '/path/to/schema2.yml' );

This will produce a file at the path /path/to/migration/classes/1236199329_version1.php

class Version1 extends Doctrine_Migration_Base
{
    public function up()
    {
        $this->addColumn( 'user', 'email_address', 'string', '255', array() );
    }

    public function down()
    {
        $this->removeColumn( 'user', 'email_address' );
    }
}

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 that Doctrine provides.

Extensions

The Doctrine extensions are a way to create reusable Doctrine extensions that can be dropped into any project and enabled. An extension is nothing more than some code that follows the Doctrine standards for code naming, autoloading, etc.

In order to use the extensions you must first configure Doctrine to know the path to where your extensions live:

Doctrine_Core::setExtensionsPath( '/path/to/extensions' );

Lets checkout an existing extension from SVN to have a look at it. We’ll have a look at the Sortable extension which bundles a behavior for your models which give you up and down sorting capabilities.

$ svn co http://svn.doctrine-project.org/extensions/Sortable/branches/1.2-1.0/ /path/to/extensions/Sortable

If you have a look at /path/to/extensions/Sortable you will see a directory structure that looks like the following:

Sortable/
  lib/
    Doctrine/
      Template/
        Listener/
          Sortable.php
          Sortable.php
  tests/
    run.php
    Template/
      SortableTestCase.php

To test that the extension will run on your machine you can run the test suite for the extension. All you need to do is set the DOCTRINE_DIR environment variable.

$ export DOCTRINE_DIR=/path/to/doctrine

Note

The above path to Doctrine must be the path to the main folder, not just the lib folder. In order to run the tests it must have access to the tests directory included with Doctrine.

It is possible now to run the tests for the Sortable extension:

$ cd /path/to/extensions/Sortable/tests
$ php run.php

You should see the tests output the following showing the tests were successful:

Doctrine Unit Tests
===================
Doctrine_Template_Sortable_TestCase.............................................passed

Tested: 1 test cases.
Successes: 26 passes.
Failures: 0 fails.
Number of new Failures: 0
Number of fixed Failures: 0

Tests ran in 1 seconds and used 13024.9414062 KB of memory

Now if you want to use the extension in your project you will need register the extension with Doctrine and setup the extension autoloading mechanism.

First lets setup the extension autoloading.

// bootstrap.php

// ...
spl_autoload_register( array( 'Doctrine', 'extensionsAutoload' ) );

Now you can register the extension and the classes inside that extension will be autoloaded.

$manager->registerExtension( 'Sortable' );

Note

If you need to register an extension from a different location you can specify the full path to the extension directory as the second argument to the registerExtension() method.

Utilities

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 :php:class:`Doctrine_Task` system. These tasks are also what are executed from the :php:class:`Doctrine_Cli`.

// Turn debug on/off and check for whether it is on/off
Doctrine_Core::debug( true );

if ( Doctrine_Core::debug() )
{
    echo 'debugging is on';
}
else
{
    echo 'debugging is off';
}

// Get the path to your Doctrine libraries
$path = Doctrine_Core::getPath();

// Set the path to your Doctrine libraries if it is some non-default location
Doctrine_Core::setPath( '/path/to/doctrine/libs' );

// 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_Core::loadModels( '/path/to/models', Doctrine_Core::MODEL_LOADING_CONSERVATIVE );
// or Doctrine_Core::MODEL_LOADING_AGGRESSIVE
print_r( $models );

// Get array of all the models loaded and present to Doctrine
$models = Doctrine_Core::getLoadedModels();

// Pass an array of classes to the above method and it will filter out the ones that are not Doctrine_Records
$models = Doctrine_Core::filterInvalidModels( array( 'User', 'Formatter', 'Doctrine_Record' ) );
print_r( $models ); // would return array( 'User' ) because Formatter and Doctrine_Record are not valid

// Get Doctrine_Connection object for an actual table name
$conn = Doctrine_Core::getConnectionByTableName( 'user' ); // returns the connection object that the table name is associated with.

// Generate YAML schema from an existing database
Doctrine_Core::generateYamlFromDb( '/path/to/dump/schema.yml', array( 'connection_name' ), $options );

// Generate your models from an existing database
Doctrine_Core::generateModelsFromDb( '/path/to/generate/models', array( 'connection_name' ), $options );

// Array of options and the default values
$options = array(
    'packagesPrefix'       => 'Package',
    'packagesPath'         => '',
    'packagesFolderName'   => 'packages',
    'suffix'               => '.php',
    'generateBaseClasses'  => true,
    'baseClassesPrefix'    => 'Base',
    'baseClassesDirectory' => 'generated',
    'baseClassName'        => 'Doctrine_Record'
);

// Generate your models from YAML schema
Doctrine_Core::generateModelsFromYaml( '/path/to/schema.yml', '/path/to/generate/models', $options );

// Create the tables supplied in the array
Doctrine_Core::createTablesFromArray( array( 'User', 'Phoneumber' ) );

// Create all your tables from an existing set of models
// Will generate sql for all loaded models if no directory is given
Doctrine_Core::createTablesFromModels( '/path/to/models' );

// Generate string of sql commands from an existing set of models
// Will generate sql for all loaded models if no directory is given
Doctrine_Core::generateSqlFromModels( '/path/to/models' );

// Generate array of sql statements to create the array of passed models
Doctrine_Core::generateSqlFromArray( array( 'User', 'Phonenumber' ) );

// Generate YAML schema from an existing set of models
Doctrine_Core::generateYamlFromModels( '/path/to/schema.yml', '/path/to/models' );

// Create all databases for connections.
// Array of connection names is optional
Doctrine_Core::createDatabases( array( 'connection_name' ) );

// Drop all databases for connections
// Array of connection names is optional
Doctrine_Core::dropDatabases( array( 'connection_name' ) );

// Dump all data for your models to a yaml fixtures file
// 2nd argument is a bool value for whether or not to generate individual fixture files for each model. If true you need
// to specify a folder instead of a file.
Doctrine_Core::dumpData( '/path/to/dump/data.yml', true );

// Load data from yaml fixtures files
// 2nd argument is a bool value for whether or not to append the data when loading or delete all data first before loading
Doctrine_Core::loadData( '/path/to/fixture/files', true );

// Run a migration process for a set of migration classes
$num = 5; // migrate to version #5
Doctrine_Core::migration( '/path/to/migrations', $num );

// Generate a blank migration class template
Doctrine_Core::generateMigrationClass( 'ClassName', '/path/to/migrations' );

// Generate all migration classes for an existing database
Doctrine_Core::generateMigrationsFromDb( '/path/to/migrations' );

// Generate all migration classes for an existing set of models
// 2nd argument is optional if you have already loaded your models using loadModels()
Doctrine_Core::generateMigrationsFromModels( '/path/to/migrations', '/path/to/models' );

// Get Doctrine_Table instance for a model
$userTable = Doctrine_Core::getTable( 'User' );

// Compile doctrine in to a single php file
$drivers = array( 'mysql' ); // specify the array of drivers you want to include in this compiled version
Doctrine_Core::compile( '/path/to/write/compiled/doctrine', $drivers );

// Dump doctrine objects for debugging
$conn = Doctrine_Manager::connection();
Doctrine_Core::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
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.

Command Line Interface
Introduction The Doctrine Cli is a collection of tasks that help you

with your day to do development and testing with your Doctrine implementation. Typically with the examples in this manual, you setup php scripts to perform whatever tasks you may need. This Cli tool is aimed at providing an out of the box solution for those tasks.

Tasks

Below is a list of available tasks for managing your Doctrine implementation.

$ ./doctrine
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 = new Doctrine_Task_GenerateModelsFromYaml();
$args = array(
    'yaml_schema_path' => '/path/to/schema',
    'models_path'      => '/path/to/models'
);

$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

#!/usr/bin/env php
[php]

chdir( dirname( __FILE__ ) );
include( 'doctrine.php' );

Actual php file named “doctrine.php” that implements the :php:class:`Doctrine_Cli`.

// Include your Doctrine configuration/setup here, your connections, models, etc.

// Configure Doctrine Cli
// Normally these are arguments to the cli tasks but if they are set here the arguments will be auto-filled and are not required for you to enter them.

$config = array(
    'data_fixtures_path' => '/path/to/data/fixtures',
    'models_path'        => '/path/to/models',
    'migrations_path'    => '/path/to/migrations',
    'sql_path'           => '/path/to/data/sql',
    'yaml_schema_path'   => '/path/to/schema'
);

$cli = new Doctrine_Cli( $config );
$cli->run( $_SERVER['argv'] );

Now you can begin executing commands.

./doctrine generate-models-yaml
./doctrine create-tables
Sandbox
Installation

You can install the sandbox by downloading the special sandbox package from http://www.doctrine-project.org/download or you can install it via svn below.

svn co http://www.doctrine-project.org/svn/branches/0.11 doctrine
cd doctrine/tools/sandbox
chmod 0777 doctrine

./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.

Unit Testing

Doctrine is programmatically tested using UnitTests. You can read more about unit testing here 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.

$ svn co http://svn.doctrine-project.org/branches/1.2 /path/to/co/doctrine

Now change directory to the checked out copy of doctrine.

$ cd /path/to/co/doctrine

You should see the following files and directories listed.

CHANGELOG
COPYRIGHT
lib/
LICENSE
package.xml
tests/
tools/
vendor/

Tip

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:

$ 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.

$ php run.php -help

You can run an individual group of tests like this:

$ php run.php --group data_dict
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:

Caution

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:

class Doctrine_Sample_TestCase extends Doctrine_UnitTestCase
{
    public function prepareTables()
    {
        $this->tables[] = "MyModel1";
        $this->tables[] = "MyModel2";

        parent::prepareTables();
    }

    public function prepareData()
    {
        $this->myModel = new MyModel1();
        //$this->myModel->save();
    }

    public function testInit()
    {

    }

    // This produces a failing test
    public function testTest()
    {
        $this->assertTrue( $this->myModel->exists() );
        $this->assertEqual( 0, 1 );
        $this->assertIdentical( 0, '0' );
        $this->assertNotEqual( 1, 2 );
        $this->assertTrue( ( 5 < 1 ) );
        $this->assertFalse( (1 > 2 ) );
    }
}

class Model1 extends Doctrine_Record
{
}

class Model2 extends Doctrine_Record
{
}

Note

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.

$test->addTestCase( new Doctrine_Sample_TestCase() );

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:

$ php run.php --ticket 9999

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.

class Doctrine_Ticket_9999_TestCase extends Doctrine_UnitTestCase
{
}
Methods for testing
Assert Equal
// ...
public function test1Equals1()
{
    $this->assertEqual( 1, 1 );
}
// ...
Assert Not Equal
// ...
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.

// ...
public function testAssertIdentical()
{
    $this->assertIdentical( 1, '1' );
}
// ...

Note

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
// ...
public function testAssertTrue()
{
    $this->assertTrue( 5 > 2 );
}
// ...
Assert False
// ...
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:

class Doctrine_Sample_TestCase extends Doctrine_UnitTestCase
{
    public function testInit()
    {
        $this->dbh  = new Doctrine_Adapter_Mock( 'oracle' );
        $this->conn = Doctrine_Manager::getInstance()->openConnection( $this->dbh );
    }
}

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.

class Doctrine_Sample_TestCase extends Doctrine_UnitTestCase
{
    // ...
    public function testMockDriver()
    {
        $user           = new User();
        $user->username = 'jwage';
        $user->password = 'changeme';
        $user->save();

        $sql = $this->dbh->getAll();

        // print the sql array to find the query you're looking for
        // print_r( $sql );

        $this->assertEqual( $sql[0], 'INSERT INTO user (username, password) VALUES (?, ?)' );
    }
}
Test Class Guidelines

Every class should have at least one TestCase equivalent and they should inherit :php:class:`Doctrine_UnitTestCase`. Test classes should refer to a class or an aspect of a class, and they should be named accordingly.

Some examples:

Test Method Guidelines

Methods should support agile documentation and should be named so that if it fails, it is obvious what failed. They should also give information of the system they test

For example the method test name Doctrine_Export_Pgsql_TestCase::testCreateTableSupportsAutoincPks() is a good name.

Test method names can be long, but the method content should not be. If you need several assert-calls, divide the method into smaller methods. There should never be assertions within any loops, and rarely within functions.

Note

Commonly used testing method naming convention TestCase::test[methodName] is not allowed in Doctrine. So in this case Doctrine_Export_Pgsql_TestCase::testCreateTable() would not be allowed!

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 Improving Performance when using Doctrine.

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 :php:meth:`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 :php:class:`Doctrine_Exception` is throw detailing the error.

Lets create a compile script named compile.php to handle the compiling of DoctrineL:

// compile.php

require_once( '/path/to/doctrine/lib/Doctrine.php' );
spl_autoload_register( array( 'Doctrine', 'autoload' ) );

Doctrine_Core::compile( 'Doctrine.compiled.php' );

Now we can execute compile.php and a file named Doctrine.compiled.php will be generated in the root of your doctrine_test folder:

$ php compile.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 :php:meth:`compile`. For this example we are only using MySQL so lets tell Doctrine to only compile the mysql driver:

// compile.php

// ...
Doctrine_Core::compile( 'Doctrine.compiled.php', array( 'mysql' ) );

Now you can change your bootstrap.php script to include the compiled Doctrine:

// bootstrap.php

// ...
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:

$record = $table->find( $id );

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
*
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:

$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:

<?php foreach ( $blogPosts as $blogPost ): ?>
    <li>
        <strong>
            <?php echo $blogPost['title'] ?>
        </strong>

        - Posted on <?php echo $blogPost['created_at'] ?>
        by <?php echo $blogPost['author'] ?>.

        <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:

$blogPosts = $q->execute( array( 1 ), Doctrine_Core::HYDRATE_ARRAY );

If you prefer you can also use the :php:meth:`setHydrationMethod` method:

$q->setHydrationMode( Doctrine_Core::HYDRATE_ARRAY );
$blogPosts = $q->execute( array( 1 ) );

The above code will hydrate the data into arrays instead of objects which is much less expensive.

Note

One great thing about array hydration is that if you use the :php:class:`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 :php:const:`Doctrine_Core::HYDRATE_NONE`. Here’s an example:

$q = Doctrine_Query::create()
    ->select( 'SUM(d.amount)' )
    ->from( 'Donation d' );

$results = $q->execute( array(), Doctrine_Core::HYDRATE_NONE );

You will need to print the results and find the value in the array depending on your DQL query:

print_r( $results );

In this example the result would be accessible with the following code:

$total = $results[0][1];

Tip

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.

Bundle your Class Files

When using Doctrine or any other large OO library or framework the number of files that need to be included on a regular HTTP request rises significantly. 50-100 includes per request are not uncommon. This has a significant performance impact because it results in a lot of disk operations. While this is generally no issue in a dev environment, it’s not suited for production. The recommended way to handle this problem is to bundle the most-used classes of your libraries into a single file for production, stripping out any unnecessary whitespaces, linebreaks and comments. This way you get a significant performance improvement even without a bytecode cache (see next section). The best way to create such a bundle is probably as part of an automated build process i.e. with Phing.

Use a Bytecode Cache

A bytecode cache like APC will cache the bytecode that is generated by php prior to executing it. That means that the parsing of a file and the creation of the bytecode happens only once and not on every request. This is especially useful when using large libraries and/or frameworks. Together with file bundling for production this should give you a significant performance improvement. To get the most out of a bytecode cache you should contact the manual pages since most of these caches have a lot of configuration options which you can tweak to optimize the cache to your needs.

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 :php:meth:`free` function on :php:class:`Doctrine_Record`, :php:class:`Doctrine_Collection`, and :php:class:`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:

for ( $i = 0; $i < 1000; $i++ )
{
    $object = createBigObject();
    $object->save();
    $object->free( true );
}

You can also free query objects in the same way:

for ( $i = 0; $i < 1000; $i++ )
{
    $q = Doctrine_Query::create()
        ->from( 'User u' );

    $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:

$q = Doctrine_Query::create()
    ->from('User u');

for ( $i = 0; $i < 1000; $i++ )
{
    $results = $q->fetchArray();
    $q->free();
}
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 :php:class:`Doctrine_Query` object and its fluent interface. The latter should be preferred for all but very simple queries. The reason is that using the :php:class:`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:

    Note

    The following example assumes a many-many between Role and User.

    $role = new Role();
    $role->name = 'New Role Name';
    
    $user->Roles[] = $newRole;
    

    Caution

    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!

    The following is the recommended way instead:

    $userRole          = new UserRole();
    $userRole->role_id = $role_id;
    $userRole->user_id = $user_id;
    $userRole->save();
    
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 used in Doctrine.

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.

Note

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 symfony and the Zend Framework. The relevant license information can be found in the root of Doctrine when you download 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 DBAL

Doctrine DBAL is also divided into driver packages.

Design Patterns Used

GoF (Gang of Four) design patterns used:

Enterprise application design patterns used:

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.
  • 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 chapter.

Exceptions and Warnings

Manager exceptions

:php:class:`Doctrine_Manager_Exception` is thrown if something failed at the connection management

try
{
    $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.

try
{
    $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 that would be used in common web applications found today on the web.

Real World Examples

User Management System

In almost all applications you need to have some kind of security or authentication system where you have users, roles, permissions, etc. Below is an example where we setup several models that give you a basic user management and security system.

class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'username', 'string', 255,
            array(
                'unique' => true,
            )
        );
        $this->hasColumn( 'password', 'string', 255 );
    }
}

class Role extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'name', 'string', 255 );
    }
}

class Permission extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'name', 'string', 255 );
    }
}

class RolePermission extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'role_id', 'integer', null,
            array(
                'primary' => true,
            )
        );
        $this->hasColumn( 'permission_id', 'integer', null,
            array(
                'primary' => true,
            )
        );
    }

    public function setUp()
    {
        $this->hasOne( 'Role',
            array(
                'local'   => 'role_id',
                'foreign' => 'id',
            )
        );
        $this->hasOne( 'Permission',
            array(
                'local'   => 'permission_id',
                'foreign' => 'id',
            )
        );
    }
}

class UserRole extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'user_id', 'integer', null,
            array(
                'primary' => true,
            )
        );
        $this->hasColumn( 'role_id', 'integer', null,
            array(
                'primary' => true,
            )
        );
    }

    public function setUp()
    {
        $this->hasOne( 'User',
            array(
                'local'   => 'user_id',
                'foreign' => 'id',
            )
        );
        $this->hasOne( 'Role',
            array(
                'local'   => 'role_id',
                'foreign' => 'id',
            )
        );
    }
}

class UserPermission extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'user_id', 'integer', null,
            array(
                'primary' => true,
            )
        );
        $this->hasColumn( 'permission_id', 'integer', null,
            array(
                'primary' => true,
            )
        );
    }

    public function setUp()
    {
        $this->hasOne( 'User',
            array(
                'local'   => 'user_id',
                'foreign' => 'id',
            )
        );
        $this->hasOne( 'Permission',
            array(
                'local'   => 'permission_id',
                'foreign' => 'id',
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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:

class Forum_Category extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'root_category_id', 'integer', 10 );
        $this->hasColumn( 'parent_category_id', 'integer', 10 );
        $this->hasColumn( 'name', 'string', 50 );
        $this->hasColumn( 'description', 'string', 99999 );
    }

    public function setUp()
    {
        $this->hasMany( 'Forum_Category as Subcategory',
            array(
                'local'   => 'parent_category_id',
                'foreign' => 'id',
            )
        );
        $this->hasOne( 'Forum_Category as Rootcategory',
            array(
                'local'   => 'root_category_id',
                'foreign' => 'id',
            )
        );
    }
}

class Forum_Board extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'category_id', 'integer', 10 );
        $this->hasColumn( 'name', 'string', 100 );
        $this->hasColumn( 'description', 'string', 5000 );
    }

    public function setUp()
    {
        $this->hasOne( 'Forum_Category as Category',
            array(
                'local'   => 'category_id',
                'foreign' => 'id',
            )
        );
        $this->hasMany( 'Forum_Thread as Threads',
            array(
                'local'   => 'id',
                'foreign' => 'board_id'
            )
        );
    }
}

class Forum_Entry extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'author', 'string', 50 );
        $this->hasColumn( 'topic', 'string', 100 );
        $this->hasColumn( 'message', 'string', 99999 );
        $this->hasColumn( 'parent_entry_id', 'integer', 10 );
        $this->hasColumn( 'thread_id', 'integer', 10 );
        $this->hasColumn( 'date', 'integer', 10 );
    }

    public function setUp()
    {
        $this->hasOne( 'Forum_Entry as Parent',
            array(
                'local'   => 'parent_entry_id',
                'foreign' => 'id',
            )
        );
        $this->hasOne( 'Forum_Thread as Thread',
            array(
                'local'   => 'thread_id',
                'foreign' => 'id',
            )
        );
    }
}

class Forum_Thread extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'board_id', 'integer', 10 );
        $this->hasColumn( 'updated', 'integer', 10 );
        $this->hasColumn( 'closed', 'integer', 1 );
    }

    public function setUp()
    {
        $this->hasOne( 'Forum_Board as Board',
            array(
                'local'   => 'board_id',
                'foreign' => 'id',
            )
        );
        $this->hasMany( 'Forum_Entry as Entries',
            array(
                'local'   => 'id',
                'foreign' => 'thread_id',
            )
        );
    }
}

Here is the same example in YAML format. You can read more about YAML in the YAML Schema Files chapter:

---
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 used in Doctrine and are recommended for you to use in your application as well. Remember, consistency in your code is key!

Coding Standards

PHP File Formatting
General

For files that contain only PHP code, the closing tag (“?>”) is never permitted. It is not required by PHP. Not including it prevents trailing whitespace from being accidentally injected into the output.

Note

Inclusion of arbitrary binary data as permitted by __HALT_COMPILER() is prohibited from any Doctrine framework PHP file or files derived from them. Use of this feature is only permitted for special installation scripts.

Indentation

Use an indent of 4 spaces, with no tabs.

Maximum Line Length

The target line length is 80 characters, i.e. developers should aim keep code as close to the 80-column boundary as is practical. However, longer lines are acceptable. The maximum length of any line of PHP code is 120 characters.

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 “:php:class:`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 :php:class:`Doctrine_Overloadable`). Some examples:

Examples

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 and Methods

Function names may only contain alphanumeric characters and underscores are not permitted. Numbers are permitted in function names but are highly discouraged. They must always start with a lowercase letter and when a function name consists of more than one word, the first letter of each new word must be capitalized. This is commonly called the “studlyCaps” or “camelCaps” method. Verbosity is encouraged and function names should be as verbose as is practical to enhance the understandability of code.

For object-oriented programming, accessors for objects should always be prefixed with either “get” or “set”. This applies to all classes except for :php:class:`Doctrine_Record` which has some accessor methods prefixed with ‘obtain’ and ‘assign’. The reason for this is that since all user defined ActiveRecords inherit :php:class:`Doctrine_Record`, it should populate the get / set namespace as little as possible.

Note

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:

Object type Variable name
Doctrine_Connection $conn
Doctrine_Collection $coll
Doctrine_Manager $manager
Doctrine_Query $q

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.

class Doctrine_SomeClass
{
    const MY_CONSTANT = 'something';
}

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.

class User
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'home_address', 'string' );
    }
}

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):

class Phonenumber extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn( 'user_id', 'integer' );
    }
}
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
$string = 'something';

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:

String Containing Apostrophes
$sql = "SELECT id, name FROM people WHERE name = 'Fred' OR name = 'Susan'";
Variable Substitution

Variable substitution is permitted using the following form:

// variable substitution
$greeting = "Hello $name, welcome back!";
String Concatenation

Strings may be concatenated using the ”.” operator. A space must always be added before and after the ”.” operator to improve readability:

$framework = 'Doctrine' . ' ORM ' . 'Framework';
Concatenation Line Breaking

When concatenating strings with the ”.” operator, it is permitted to break the statement into multiple lines to improve readability. In these cases, each successive line should be padded with whitespace such that the ”.”; operator is aligned under the “=” operator:

$sql = "SELECT id, name FROM user "
    . "WHERE name = ? "
    . "ORDER BY name ASC";
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( 'Doctrine', 'ORM', 1, 2, 3 );

$sampleArray = array( 1, 2, 3,
                      $a, $b, $c,
                      56.44, $d, 500 );

$sampleArray = array(
    'first'  => 'firstValue',
    'second' => 'secondValue'
);
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:

/**
 * Documentation here
 */
class Doctrine_SampleClass
{
    // entire content of class
    // must be indented four spaces
}
Functions and Methods

Methods must be named by following the naming conventions and must always declare their visibility by using one of the private, protected, or public constructs. Like classes, the brace is always written next line after the method name. There is no space between the function name and the opening parenthesis for the arguments. Functions in the global scope are strongly discouraged. This is an example of an acceptable function declaration in a class:

/**
 * Documentation Block Here
 */
class Foo
{
    /**
     * Documentation Block Here
     */
    public function bar()
    {
        // entire content of function
        // must be indented four spaces
    }

    public function bar2()
    {

    }
}

Note

Functions must be separated by only ONE single new line like is done above between the bar() and bar2() methods.

Passing by-reference is permitted in the function declaration only:

/**
 * 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.

/**
 * 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:

threeArguments( 1, 2, 3 );

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 ), 2, 3 );

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 )
{
    $foo = 2;
}

For if statements that include elseif or else, the formatting must be as in these examples:

if ( $foo != 1 )
{
    $foo = 1;
}
else
{
    $foo = 3;
}

if ( $foo != 2 )
{
    $foo = 2;
}
elseif ( $foo == 1 )
{
    $foo = 3;
}
else
{
    $foo = 11;
}

When ! operand is being used it must use the following formatting:

if ( ! $foo )
{

}

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.

switch ( $case )
{
    case 1:
    case 2:
    break;
    case 3:
    break;
    default:
    break;
}

The construct default may never be omitted from a switch statement.

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/ <http://phpdoc.org/>

Every method, must have a docblock that contains at a minimum:

  • A description of the function
  • All of the arguments
  • All of the possible return values
  • It is not necessary to use the @access tag because the access level is already known from the public, private, or protected construct used to declare the function.

If a function/method may throw an exception, use @throws:

/*
 * 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

About Sensio

Sensio Labs is a French web agency well known for its innovative ideas on web development. Founded in 1998 by Fabien Potencier, Gregory Pascal, and Samuel Potencier, Sensio benefited from the Internet growth of the late 1990s and situated itself as a major player for building complex web applications. It survived the Internet bubble burst by applying professional and industrial methods to a business where most players seemed to reinvent the wheel for each project. Most of Sensio’s clients are large French corporations, who hire its teams to deal with small- to middle-scale projects with strong time-to-market and innovation constraints.

Sensio Labs develops interactive web applications, both for dot-com and traditional companies. Sensio Labs also provides auditing, consulting, and training on Internet technologies and complex application deployment. It helps define the global Internet strategy of large-scale industrial players. Sensio Labs has projects in France and abroad.

For its own needs, Sensio Labs develops the symfony framework and sponsors its deployment as an Open-Source project. This means that symfony is built from experience and is employed in many web applications, including those of large corporations.

Since its beginnings ten years ago, Sensio has always based its strategy on strong technical expertise. The company focuses on Open-Source technologies, and as for dynamic scripting languages, Sensio offers developments in all LAMP platforms. Sensio acquired strong experience on the best frameworks using these languages, and often develops web applications in Django, Rails, and, of course, symfony.

Sensio Labs is always open to new business opportunities, so if you ever need help developing a web application, learning symfony, or evaluating a project using Doctrine, feel free to contact us at fabien.potencier@sensio.com. The consultants, project managers, web designers, and developers of Sensio can handle projects from A to Z.

About the Author

Jonathan is a software developer who resides in the rolling hills of Nashville, Tennessee. He joined the Doctrine project as a contributor in early 2007 and has been involved in one way or another since then. Jonathan currently works full-time as a software developer at Sensio Labs, a French web agency with 10 years of experience developing web applications.

It all began at the age of 12 when he first got his hands on the tools to build his first website. From then on he hopped from one thing to another learning the various aspects of programming in several different languages. He enjoys working on Doctrine as it is a daily challenge and allows him to explore uncharted territories in PHP.

Acknowledgements and Contributors

Contributors
Roman S. Borschel

Roman is a software developer from Berlin, Germany who joined the project as a user in its early stages in 2006. Though being mainly a Java and .NET developer who currently works in the field of medical information systems, Roman is also a long-time PHP user and likes to push PHP to its limits and beyond.

Having a special interest in Object-Relational Mapping, he finds it challenging to try to apply ideas and concepts of object persistence solutions of other languages and tools in the PHP world where this territory is still in its infancy.

Guilherme Blanco

Guilherme Blanco was born in 09/19/1983 and lives in Brazil. He has a bachelors degree in Computer Science from the Federal University of São Carlos and works in the web development industry. He is currently employed by his company named Bisna and holds a blog at http://blog.bisna.com.

He has worked with Java, .NET and the majority of server/client side languages and decided to pick PHP as his default development language. Instead of reinventing the wheel after he planned an entire ORM tool, he decided to jump on board the Doctrine project in November of 2007 and put his efforts in to help it move forward.

Konsta Vesterinen

Konsta Vesterinen is the founder of the Doctrine project and is responsible for getting things started for this great project. He is the creator of the first versions of all code and documentation for Doctrine. A majority of the content in this book has been evolved and transformed from his original version that was created years ago.

Companies

As Doctrine is an open source project, the names mentioned above are only the people who organize and manage the contributions of the community behind the project. Though the people above get most of the credit for the work, none of this would be possible without the companies that have gotten behind Doctrine and supported it in any way they can. Below are some acknowledgements to the companies that have helped get the project to where it is today.

centre{source}

Before working for Sensio Labs, Jonathan Wage was one of the original employees at centre{source}. The great people at this company saw the need for a powerful ORM in the PHP world and got behind the project right away. Jonathan was permitted to spend countless hours of company time on the project to help move things forward.

Without these contributions, the project would not have been able to make the jump from a small starting open source project to a large and trusted project. Now Doctrine is known across the globe and companies rely on the project to assist with the building of their high end web applications.

Note

centre{source} is a full-service interactive firm assisting organizations that view the web as a strategic asset. They provide their clients four essential services: strategy, planning, execution, and on-going management.

Sensio Labs
Sensio Labs

Sensio Labs

In September of 2008, Sensio Labs officially got behind the Doctrine project and hired Jonathan to work on the project full-time. This was a major turning point for the project as it showed that corporate companies were willing to sponsor not just existing resources, but real funds.

With this change, lots of new things became possible such as Doctrine trainings, certifications, this book and much more.

Note

Sensio created symfony, a PHP Web application framework. Sensio has over 10 years of experience developing high value web applications and is a major player in the open source world.

Other Contributors

Below are some other contributors that have played a role in the success of the project that deserve a mention here.

  • `Ian P. Christian <http://pookey.co.uk/blog/>`_ - For hosting the Doctrine infrastructure on his own dime.
  • Werner Mollentze - For providing the original concepts of the Doctrine logo.
  • `Phu Son Nguyen <http://www.phuson.com/>`_ - For providing a final copy of the Doctrine logo and the web design.
  • `Fabien Potencier <http://www.aide-de-camp.org/>`_ - For lending us code from the symfony project to help with building the Doctrine website.

Lots of other people have contributed a long the way, as I cannot mention everyone here, you can always read more about the contributors on the Doctrine about page.

Imprint

© 2008-2009 Jonathan Wage, and contributors

ISBN-13: 978-2-918390-26-8

Sensio SA 92-98, boulevard Victor Hugo 92 115 Clichy France info@sensio.com

This work is licensed under the “Attribution-Share Alike 3.0 Unported” license (http://creativecommons.org/licenses/by-sa/3.0/).

You are free to share (to copy, distribute and transmit the work), and to remix (to adapt the work) under the following conditions:

  • Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
  • Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work only under the same, similar or a compatible license. For any reuse or distribution, you must make clear to others the license terms of this work.

The information in this book is distributed on an “as is” basis, without warranty. Although every precaution has been taken in the preparation of this work, neither the author(s) nor Sensio shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in this work.

If you find typos or errors, feel free to report them by creating a ticket on the Doctrine ticketing system (http://www.doctrine-project.org/jira).

Based on tickets and users feedback, this book is continuously updated and thanks to the on-demand printing process, you can enjoy the latest version as this paper book is kept up-to-date each week.

You can contact the author about this book, Doctrine and Open-Source or for training, consulting, application development, or business related questions at jonathan.wage@sensio.com.

Attribution-Share Alike 3.0 Unported License

THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE (“CCPL” OR “LICENSE”). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.

BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.

  1. Definitions
  1. “Adaptation” means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image (“synching”) will be considered an Adaptation for the purpose of this License.
  2. “Collection” means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License.
  3. “Creative Commons Compatible License” means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License.
  4. “Distribute” means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership.
  5. “License Elements” means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike.
  6. “Licensor” means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License.
  7. “Original Author” means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast.
  8. “Work” means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work.
  9. “You” means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
  10. “Publicly Perform” means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images.
  11. “Reproduce” means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium.
  1. Fair Dealing Rights

Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws.

  1. License Grant

Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:

  1. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections;
  2. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked “The original work was translated from English to Spanish,” or a modification could indicate “The original work has been modified.”;
  3. to Distribute and Publicly Perform the Work including as incorporated in Collections; and,
  4. to Distribute and Publicly Perform Adaptations.
  5. For the avoidance of doubt:
    1. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License;
    2. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and,
    3. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License.

The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved.

  1. Restrictions

The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:

  1. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested.
  2. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the “Applicable License”), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License.
  3. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution (“Attribution Parties”) in Licensor’s copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., “French translation of the Work by Original Author,” or “Screenplay based on original Work by Original Author”). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
  4. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author’s honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author’s honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise.
  1. Representations, Warranties and Disclaimer

UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.

  1. Limitation on Liability

EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

  1. Termination
  1. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
  2. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
  1. Miscellaneous
  1. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
  2. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
  3. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
  4. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
  5. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
  6. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law.

Cookbook

Introduction

This is a tutorial & how-to on creating your first project using the fully featured PHP Doctrine ORM. This tutorial uses the the ready to go Doctrine sandbox package. It requires a web server, PHP and PDO + Sqlite.

Download

To get started, first download the latest Doctrine sandbox package: http://www.doctrine-project.org/download. Second, extract the downloaded file and you should have a directory named Doctrine-x.x.x-Sandbox. Inside of that directory is a simple example implementation of a Doctrine based web application.

Package Contents

The files/directory structure should look like the following $ cd Doctrine-0.10.1-Sandbox $ ls config.php doctrine index.php migrations schema data doctrine.php lib models

The sandbox does not require any configuration, it comes ready to use with a sqlite database. Below is a description of each of the files/directories and what its purpose is.

  • doctrine - Shell script for executing the command line interface. Run with ./doctrine to see a list of command or ./doctrine help to see a detailed list of the commands
  • doctrine.php - Php script which implements the Doctrine command line interface which is included in the above doctrine shell script
  • index.php - Front web controller for your web application
  • migrations - Folder for your migration classes
  • schema - Folder for your schema files
  • models - Folder for your model files
  • lib - Folder for the Doctrine core library files

Running the CLI

If you execute the doctrine shell script from the command line it will output the following:

$ ./doctrine 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

Defining Schema

Below is a sample yaml schema file to get started. You can place the yaml file in schemas/schema.yml. The command line interface looks for all *.yml files in the schemas folder.

User: columns: id: primary: true autoincrement: true type: integer(4) username: string(255) password: string(255) relations: Groups: class: Group refClass: UserGroup foreignAlias: Users

Group: tableName: groups columns: id: primary: true autoincrement: true type: integer(4) name: string(255)

UserGroup: columns: user_id: integer(4) group_id: integer(4) relations: User: onDelete: CASCADE Group: onDelete: CASCADE

Test Data Fixtures

Below is a sample yaml data fixtures file. You can place this file in data/fixtures/data.yml. The command line interface looks for all *.yml files in the data/fixtures folder.

User: zyne: username: zYne- password: changeme Groups: [founder, lead, documentation] jwage: username: jwage password: changeme Groups: [lead, documentation]

Group: founder: name: Founder lead: name: Lead documentation: name: Documentation

Building Everything Now that you have written your schema files and

data fixtures, you can now build everything and begin working with your models . Run the command below and your models will be generated in the models folder.

$ ./doctrine build-all-reload build-all-reload - Are you sure you wish

to drop your databases? (y/n) y build-all-reload - Successfully dropped database for connection “sandbox” at path “/Users/jwage/Sites/doctrine/branches/0.10/tools/sandbox/sandbox.db” build-all-reload - Generated models successfully from YAML schema build-all-reload - Successfully created database for connection “sandbox” at path “/Users/jwage/Sites/doctrine/branches/0.10/tools/sandbox/sandbox.db” build-all-reload - Created tables successfully build-all-reload - Data was successfully loaded

Take a peak in the models folder and you will see that the model classes were generated for you. Now you can begin coding in your index.php to play with Doctrine itself. Inside index.php place some code like the following for a simple test.

Running Tests

$query = new Doctrine_Query(); $query->from(‘User u, u.Groups g’);

$users = $query->execute();

echo ‘

‘; print_r($users->toArray(true));

The print_r() should output the following data. You will notice that this is the data that we populated by placing the yaml file in the data/fixtures files. You can add more data to the fixtures and rerun the build-all-reload command to reinitialize the database.

Array ( [0] => Array ( [id] => 1 [username] => zYne- [password] =>

changeme [Groups] => Array ( [0] => Array ( [id] => 1 [name] => Founder )

                [1] => Array
                    (
                        [id] => 2
                        [name] => Lead
                    )

                [2] => Array
                    (
                        [id] => 3
                        [name] => Documentation
                    )

            )

    )

[1] => Array
    (
        [id] => 2
        [username] => jwage
        [password] => changeme
        [Groups] => Array
            (
                [0] => Array
                    (
                        [id] => 2
                        [name] => Lead
                    )

                [1] => Array
                    (
                        [id] => 3
                        [name] => Documentation
                    )

            )

    )

)

You can also issue DQL queries directly to your database by using the dql command line function. It is used like the following.

jwage:sandbox jwage$ ./doctrine dql “FROM User u, u.Groups g” dql -

executing: “FROM User u, u.Groups g” () dql - - dql - id: 1 dql - username: zYne- dql - password: changeme dql - Groups: dql - - dql - id: 1 dql - name: Founder dql - - dql - id: 2 dql - name: Lead dql - - dql - id: 3 dql - name: Documentation dql - - dql - id: 2 dql - username: jwage dql - password: changeme dql - Groups: dql - - dql - id: 2 dql - name: Lead dql - - dql - id: 3 dql - name: Documentation

User CRUD

Now we can demonstrate how to implement Doctrine in to a super simple module for managing users and passwords. Place the following code in your index.php and pull it up in your browser. You will see the simple application.

require_once(‘config.php’);

Doctrine_Core::loadModels(‘models’);

module = isset(*REQUEST[‘module’]) ? $*REQUEST[‘module’]:’users’; action = isset(*REQUEST[‘action’]) ? $*REQUEST[‘action’]:’list’;

if ($module == ‘users’) { userId = isset(*REQUEST[‘id’]) && $*REQUEST[‘id’] > 0 ? $_REQUEST[‘id’]:null; $userTable = Doctrine_Core::getTable(‘User’);

if ($userId === null) {
    $user = new User();
} else {
    $user = $userTable->find($userId);
}

switch ($action) {
    case 'edit':
    case 'add':
        echo '<form action="index.php?module=users&action=save" method="POST">
              <fieldset>
                <legend>User</legend>
                <input type="hidden" name="id" value="' . $user->id . '" />
                <label for="username">Username</label> <input type="text" name="user[username]" value="' . $user->username . '" />
                <label for="password">Password</label> <input type="text" name="user[password]" value="' . $user->password . '" />
                <input type="submit" name="save" value="Save" />
              </fieldset>
              </form>';
        break;
    case 'save':
        $user->merge($_REQUEST['user']);
        $user->save();

        header('location: index.php?module=users&action=edit&id=' . $user->id);
        break;
    case 'delete':
        $user->delete();

        header('location: index.php?module=users&action=list');
        break;
    default:
        $query = new Doctrine_Query();
        $query->from('User u')
              ->orderby('u.username');

        $users = $query->execute();

        echo '<ul>';
        foreach ($users as $user) {
            echo '<li><a href="index.php?module=users&action=edit&id=' . $user->id . '">' . $user->username . '</a> &nbsp; <a href="index.php?module=users&action=delete&id=' . $user->id . '">[X]</a></li>';
        }
        echo '</ul>';
}

echo '<ul>
        <li><a href="index.php?module=users&action=add">Add</a></li>
        <li><a href="index.php?module=users&action=list">List</a></li>
      </ul>';

} else { throw new Exception(‘Invalid module’); }

CodeIgniter and Doctrine

This tutorial will get you started using Doctrine with Code Igniter

Download Doctrine

First we must get the source of Doctrine from svn and place it in the system/database folder.

$ cd system/database $ svn co

http://svn.doctrine-project.org/branches/0.11/lib doctrine $ cd ..

// If you use svn in your project you can set Doctrine // as an external so you receive bug fixes automatically from svn $ svn propedit svn:externals database

// In your favorite editor add the following line // doctrine http://svn.doctrine-project.org/branches/0.11/lib

Setup Doctrine

Now we must setup the configuration for Doctrine and load it in system/application/config/database.php

$ vi application/config/database.php

The code below needs to be added under this line of code

$db[‘default’][‘cachedir’] = “”;

Add this code // Create dsn from the info above $db[‘default’][‘dsn’] = $db[‘default’][‘dbdriver’] . ‘://’ . $db[‘default’][‘username’] . ‘:’ . $db[‘default’][‘password’]. ‘@’ . $db[‘default’][‘hostname’] . ‘/’ . $db[‘default’][‘database’];

// Require Doctrine.php require_once(realpath(dirname(FILE) . ‘/../..’) . DIRECTORY_SEPARATOR . ‘database/doctrine/Doctrine.php’);

// Set the autoloader spl_autoload_register(array(‘Doctrine’, ‘autoload’));

// Load the Doctrine connection Doctrine_Manager::connection($db[‘default’][‘dsn’], $db[‘default’][‘database’]);

// Set the model loading to conservative/lazy loading Doctrine_Manager::getInstance()->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

// Load the models for the autoloader Doctrine_Core::loadModels(realpath(dirname(FILE) . ‘/..’) . DIRECTORY_SEPARATOR . ‘models’);

Now we must make sure system/application/config/database.php is included in your front controller. Open your front controller in your favorite text editor.

$ cd .. $ vi index.php

Change the last 2 lines of code of index.php with the following

require_once APPPATH.’config/database.php’; require_once

BASEPATH.’codeigniter/CodeIgniter’.EXT;

Setup Command Line Interface

Create the following file: system/application/doctrine and chmod the file so it can be executed. Place the code below in to the doctrine file.

$ vi system/application/doctrine

Place this code in system/application/doctrine

#!/usr/bin/env php define(‘BASEPATH’,’.’); // mockup that this app was

executed from ci ;) chdir(dirname(FILE)); include(‘doctrine.php’);

Now create the following file: system/application/doctrine.php. Place the code below in to the doctrine.php file.

require_once(‘config/database.php’);

// Configure Doctrine Cli // Normally these are arguments to the cli tasks but if they are set here the arguments will be auto-filled $config = array(‘data_fixtures_path’ => dirname(FILE) . DIRECTORY_SEPARATOR . ‘/fixtures’, ‘models_path’ => dirname(FILE) . DIRECTORY_SEPARATOR . ‘/models’, ‘migrations_path’ => dirname(FILE) . DIRECTORY_SEPARATOR . ‘/migrations’, ‘sql_path’ => dirname(FILE) . DIRECTORY_SEPARATOR . ‘/sql’, ‘yaml_schema_path’ => dirname(FILE) . DIRECTORY_SEPARATOR . ‘/schema’);

cli = new Doctrine_Cli(config); cli->run(_SERVER[‘argv’]);

Now we must create all the directories for Doctrine to use

// Create directory for your yaml data fixtures files $ mkdir

system/application/fixtures

// Create directory for your migration classes $ mkdir system/application/migrations

// Create directory for your yaml schema files $ mkdir system/application/schema

// Create directory to generate your sql to create the database in $ mkdir system/application/sql

Now you have a command line interface ready to go. If you execute the doctrine shell script with no argument you will get a list of available commands

$ cd system/application $ ./doctrine 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 $

On Microsoft Windows, call the script via the PHP binary (because the script won’t invoke it automatically:

php.exe doctrine

Start Using Doctrine

It is simple to start using Doctrine now. First we must create a yaml schema file. (save it at schema with filename like : user.yml) — User: columns: id: primary: true autoincrement: true type: integer(4) username: string(255) password: string(255) relations: Groups: # Relation alias or class name class: Group # Class name. Optional if alias is the class name local: user_id # Local: User.id = UserGroup.user_id. Optional foreign: group_id # Foreign: Group.id = UserGroup.group_id. Optional refClass: UserGroup # xRefClass for relating Users to Groups foreignAlias: Users # Opposite relationship alias. Group hasMany Users

Group: tableName: groups columns: id: primary: true autoincrement: true type: integer(4) name: string(255)

UserGroup: columns: user_id: type: integer(4) primary: true group_id: type: integer(4) primary: true relations: User: local: user_id # Local key foreign: id # Foreign key onDelete: CASCADE # Database constraint Group: local: group_id foreign: id onDelete: CASCADE

Now if you run the following command it will generate your models in system/application/models

$ ./doctrine generate-models-yaml generate-models-yaml - Generated

models successfully from YAML schema

Now check the file system/application/models/generated/BaseUser.php. You will see a compclass definition like below.

/** * 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’, 4, array(‘primary’ => true, ‘autoincrement’ => true)); $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp() { $this->hasMany(‘Group as Groups’, array(‘refClass’ => ‘UserGroup’, ‘local’ => ‘user_id’, ‘foreign’ => ‘group_id’));

$this->hasMany('UserGroup', array('local' => 'id',
                                  'foreign' => 'user_id'));

}

}

// Add custom methods to system/application/models/User.php

/** * This class has been auto-generated by the Doctrine ORM Framework */ class User extends BaseUser { public function setPassword($password) { this->password = md5(password); } }

/** * This class has been auto-generated by the Doctrine ORM Framework */ class UserTable extends Doctrine_Table { public function retrieveAll() { $query = new Doctrine_Query(); $query->from(‘User u’); $query->orderby(‘u.username ASC’);

return $query->execute();

} }

Now we can create some sample data to load in to our application(this step requires you have a valid database configured and ready to go. The build-all-reload task will drop and recreate the database, create tables, and load data fixtures

Create a file in system/application/fixtures/users.yml

$ vi fixtures/users.yml

Add the following yaml to the file

User: jwage: username: jwage password: test

Now run the build-all-reload task to drop db, build models, recreate

$ ./doctrine build-all-reload build-all-reload - Are you sure you wish

to drop your databases? (y/n) y build-all-reload - Successfully dropped database named: “jwage_codeigniter” build-all-reload - Generated models successfully from YAML schema build-all-reload - Successfully created database named: “jwage_codeigniter” build-all-reload - Created tables successfully build-all-reload - Data was successfully loaded

Now we are ready to use Doctrine in our actual actions. Lets open our system/application/views/welcome_message.php and somewhere add the following code somewhere.

$user = new User(); $user->username = ‘zYne-‘;

$user->setPassword(‘password’); $user->save();

$userTable = Doctrine_Core::getTable(‘User’); $user = $userTable->findOneByUsername(‘zYne-‘);

echo $user->username; // prints ‘zYne-‘

$users = $userTable->retrieveAll();

echo users->count(); // echo '2'' foreach (users as $user) { echo $user->username; }

Plug and Play Schema Information with Templates

Doctrine templates essentially allow you to extract schema information so that it can be plugged in to multiple Doctrine classes without having to duplicate any code. Below we will show some examples of what a template could be used for and how it can make your schema easier to maintain.

Let’s get started. Imagine a project where you have multiple records which must have address attributes. Their are two basic approaches to solving this problem. One is to have a single table to store all addresses and each record will store a foreign key to the address record it owns. This is the “normalized” way of solving the problem. The “de-normalized” way would be to store the address attributes with each record. In this example a template will extract the attributes of an address and allow you to plug them in to as many Doctrine classes as you like.

First we must define the template so that we can use it in our Doctrine classes.

class Doctrine_Template_Address extends Doctrine_Template { public

function setTableDefinition() { $this->hasColumn(‘address1’, ‘string’, 255); $this->hasColumn(‘address2’, ‘string’, 255); $this->hasColumn(‘address3’, ‘string’, 255); $this->hasColumn(‘city’, ‘string’, 255); $this->hasColumn(‘state’, ‘string’, 2); $this->hasColumn(‘zipcode’, ‘string’, 15); } }

Now that we have our template defined, lets define some basic models that need to have address attributes added to them. Lets start first with a User.

class User extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp()
{
    $this->actAs('Address');
}

}

Now we also have a Company model which also must contain an address.

class Company extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); $this->hasColumn(‘description’, ‘clob’); }

public function setUp()
{
    $this->actAs('Address');
}

}

Now lets generate the SQL to create the tables for the User and Company model. You will see that the attributes from the template are automatically added to each table.

CREATE TABLE user (id BIGINT AUTO_INCREMENT, username VARCHAR(255),

password VARCHAR(255), address1 VARCHAR(255), address2 VARCHAR(255), address3 VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zipcode VARCHAR(15), PRIMARY KEY(id)) ENGINE = INNODB

CREATE TABLE company (id BIGINT AUTO_INCREMENT, name VARCHAR(255), description LONGTEXT, address1 VARCHAR(255), address2 VARCHAR(255), address3 VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zipcode VARCHAR(15), PRIMARY KEY(id)) ENGINE = INNODB

That’s it. Now you can maintain your Address schema information from one place and use the address functionality in as many places as you like.

Taking Advantage of Column Aggregation Inheritance

First, let me give a brief explanation of what column aggregation inheritance is and how it works. With column aggregation inheritance all classes share the same table, and all columns must exist in the parent. Doctrine is able to know which class each row in the database belongs to by automatically setting a “type” column so that Doctrine can cast the correct class when hydrating data from the database. Even if you query the top level column aggregation class, the collection will return instances of the class that each row belongs to.

Now that you have a basic understand of column aggregation inheritance lets put it to use. In this example we will setup some models which will allow us to use one address table for storing all of our addresses across the entire application. Any record will be able to have multiple addresses, and all the information will be stored in one table. First lets define our Address

class Address extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘address1’, ‘string’, 255); $this->hasColumn(‘address2’, ‘string’, 255); $this->hasColumn(‘address3’, ‘string’, 255); $this->hasColumn(‘city’, ‘string’, 255); $this->hasColumn(‘state’, ‘string’, 2); $this->hasColumn(‘zipcode’, ‘string’, 15); $this->hasColumn(‘type’, ‘string’, 255); $this->hasColumn(‘record_id’, ‘integer’);

    $this->option('export', 'tables');

    $this->setSubClasses(array('UserAddress'    => array('type' => 'UserAddress'),
                               'CompanyAddress' => array('type' => 'CompanyAddress')));
}

}

Note the option set above to only export tables because we do not want to export any foreign key constraints since record_id is going to relate to many different records.

We are going to setup a User so it can have multiple addresses, so we will need to setup a UserAddress child class that User can relate to.

class UserAddress extends Address { public function setUp() {

$this->hasOne(‘User’, array(‘local’ => ‘record_id’, ‘foreign’ => ‘id’)); } }

Now lets define our User and link it to the UserAddress model so it can have multiple addresses. class User extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp()
{
    $this->hasMany('UserAddress as Addresses', array('local'    => 'id',
                                                     'foreign'  => 'record_id'));
}

}

Now say we have a Company record which also needs ot have many addresses. First we need to setup the CompanyAddress child class

class CompanyAddress extends Address { public function setUp() {

$this->hasOne(‘Company’, array(‘local’ => ‘record_id’, ‘foreign’ => ‘id’)); } }

Now lets define our Company and link it to the CompanyAddress model so it can have multiple addresses. class Company extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); }

public function setUp()
{
    $this->hasMany('CompanyAddress as Addresses', array('local'    => 'id',
                                                        'foreign'  => 'record_id'));
}

}

Now both Users and Companies can have multiple addresses and the data is all stored in one address table.

Now lets create the tables and insert some records

Doctrine_Core::createTablesFromArray(array(‘User’, ‘Company’,

‘Address’));

$user = new User(); $user->username = ‘jwage’; $user->password = ‘changeme’; $user->Addresses[0]->address1 = ‘123 Road Dr.’; $user->Addresses[0]->city = ‘Nashville’; $user->Addresses[0]->state = ‘TN’; $user->save();

$company = new Company(); $company->name = ‘centre{source}’; $company->Addresses[0]->address1 = ‘123 Road Dr.’; $company->Addresses[0]->city = ‘Nashville’; $company->Addresses[0]->state = ‘TN’; $company->save();

Query for the user and its addresses

$users = Doctrine_Query::create() ->from(‘User u’)

->leftJoin(‘u.Addresses a’) ->execute();

echo $users[0]->username; // jwage echo users[0]->Addresses[0]->address1 = '123 Road Dr.'; echo get_class(users[0]->Addresses[0]); // UserAddress

Query for the company and its addresses

$companies = Doctrine_Query::create() ->from(‘Company c’)

->leftJoin(‘c.Addresses a’) ->execute();

echo $companies[0]->name; // centre{source} echo companies[0]->Addresses[0]->address1 = '123 Road Dr.'; echo get_class(companies[0]->Addresses[0]); // CompanyAddress

Now lets query the Addresses directly and you will notice each child record returned is hydrated as the appropriate child class that created the record initially.

addresses = Doctrine_Query::create() ->from('Address a') ->execute(); echo get_class(addresses[0]); // UserAddress echo get_class($addresses[1]); // CompanyAddress

Master and Slave Connections

In this tutorial we explain how you can setup Doctrine connections as master and slaves for both reading and writing data. This strategy is common when balancing load across database servers.

So, the first thing we need to do is configure all the available connections for Doctrine.

$connections = array( ‘master’ => ‘mysql://root:@master/dbname’,

‘slave_1’ => ‘mysql://root:@slave1/dbname’, ‘slave_2’ => ‘mysql://root:@slave2/dbname’, ‘slave_3’ => ‘mysql://root:@slave3/dbname’, ‘slave_4’ => ‘mysql://root:@slave4/dbname’ );

foreach ($connections as $name => dsn) { Doctrine_Manager::connection(dsn, $name); }

Now that we have one master connection and four slaves setup we can override the :php:class:`Doctrine_Record` and Doctrine_Query classes to add our logic for switching between the connections for read and write functionality. All writes will go to the master connection and all reads will be randomly distributed across the available slaves.

Lets start by adding our logic to Doctrine_Query by extending it with our own MyQuery class and switching the connection in the preQuery() hook.

class MyQuery extends Doctrine_Query { // Since php doesn’t support

late static binding in 5.2 we need to override // this method to instantiate a new MyQuery instead of Doctrine_Query public static function create(conn = null) { return new MyQuery(conn); }

public function preQuery()
{
    // If this is a select query then set connection to one of the slaves
    if ($this->getType() == Doctrine_Query::SELECT) {
        $this->_conn = Doctrine_Manager::getInstance()->getConnection('slave_' . rand(1, 4));
    // All other queries are writes so they need to go to the master
    } else {
        $this->_conn = Doctrine_Manager::getInstance()->getConnection('master');
    }
}

}

Now we have queries taken care of, but what about when saving records? We can force the connection for writes to the master by overriding Doctrine_Record and using it as the base for all of our models.

abstract class MyRecord extends Doctrine_Record { public function

save(Doctrine_Connection conn = null) { // If specific connection is not provided then lets force the connection // to be the master if (conn === null) { conn = Doctrine_Manager::getInstance()->getConnection('master'); } parent::save(conn); } }

All done! Now reads will be distributed to the slaves and writes are given to the master connection. Below are some examples of what happens now when querying and saving records.

First we need to setup a model to test with.

class User extends MyRecord { public function setTableDefinition() {

$this->setTableName(‘user’); $this->hasColumn(‘username’, ‘string’, 255, array(‘type’ => ‘string’, ‘length’ => ‘255’)); $this->hasColumn(‘password’, ‘string’, 255, array(‘type’ => ‘string’, ‘length’ => ‘255’)); } }

// The save() method will happen on the master connection because it is

a write $user = new User(); $user->username = ‘jwage’; $user->password = ‘changeme’; $user->save();

// This query goes to one of the slaves because it is a read $q = new MyQuery(); $q->from(‘User u’); $users = $q->execute();

print_r($users->toArray(true));

// This query goes to the master connection because it is a write $q = new MyQuery(); $q->delete(‘User’) ->from(‘User u’) ->execute();

Writing a Unit of Work in PHP Doctrine

Authors:
  • Jon Lebensold
Contact:

http://jon.lebensold.ca/

In this tutorial, we’re going to create a Unit Of Work object that will simplify performing transactions with Doctrine Models. The Goal here is to centralize all of our commits to the database into one class which will perform them transactionally.

Afterwards, we can extend this class to include logging and error handling in case a commit fails.

It is helpful to think of the Unit of Work as a way of putting everything that we would want to update, insert and delete into one bag before sending it to the database.

Let’s create a Doctrine YAML file with a Project Model:

Project: tableName: lookup_project columns: id: primary: true autoincrement: true type: integer(4) name: string(255)

With Doctrine models, saving a Project should be as simple as this:

$project = new Project(); $project->name = ‘new project’;

$project->save();

However, as soon as we want to perform database transactions or logging becomes a requirement, having save(); statements all over the place can create a lot of duplication.

To start with, let’s create a UnitOfWork class:

class UnitOfWork { protected $*createOrUpdateCollection = array();

protected $*deleteCollection = array(); }

Because Doctrine is clever enough to know when to UPDATE and when to INSERT, we can combine those two operations in one collection. We’ll store all the delete’s that we’re planning to form in $_deleteCollection.

Now we need to add some code to our class to make sure the same object isn’t added twice.

protected function

*existsInCollections(model) { // does the model already belong to the createOrUpdate collection? foreach (this->*createOrUpdateCollection as m) { if (model->getOid() == $m->getOid()) { return true; } }

// does the model already belong to the delete collection?
foreach ($this->_deleteCollection as $m) {
    if ($model->getOid() == $m->getOid()) {
        return true;
    }

}

return false; }

Now we can add our public methods that will be used by code outside of the UnitOfWork:

public function

registerModelForCreateOrUpdate(model) { // code to check to see if the model exists already if (this->_existsInCollections($model)) { throw new Exception(‘model already in another collection for this transaction’); }

// no? add it
$this->_createOrUpdateCollection[] = $model;

}

public function registerModelForDelete(model) { // code to check to see if the model exists already if (this->_existsInCollections($model)) { throw new Exception(‘model already in another collection for this transaction’); }

// no? add it
$this->_deleteCollection[] = $model;

}

Before we write the transaction code, we should also be able to let other code clear the Unit Of Work. We’ll use this method internally as well in order to flush the collections after our transaction is succesful.

public function clearAll() { $this->*deleteCollection = array();

$this->*createOrUpdateCollection = array(); }

With skeleton in place, we can now write the code that performs the Doctrine transaction:

public function commitAll() { $conn = Doctrine_Manager::connection();
try {
      $conn->beginTransaction();

      $this->_performCreatesOrUpdates($conn);
      $this->_performDeletes($conn);

      $conn->commit();
} catch(Doctrine_Exception $e) {
    $conn->rollback();
}

$this->clearAll();

}

Now we’re assuming that we’ve already started a Doctrine connection. In order for this object to work, we need to initialize Doctrine. It’s often best to put this kind of code in a config.php file which is loaded once using require_once();

define(‘SANDBOX_PATH’, dirname(FILE)); define(‘DOCTRINE_PATH’,

SANDBOX_PATH . DIRECTORY_SEPARATOR . ‘lib’); define(‘MODELS_PATH’, SANDBOX_PATH . DIRECTORY_SEPARATOR . ‘models’); define(‘YAML_SCHEMA_PATH’, SANDBOX_PATH . DIRECTORY_SEPARATOR . ‘schema’); define(‘DB_PATH’, ‘mysql://root:@localhost/database’);

require_once(DOCTRINE_PATH . DIRECTORY_SEPARATOR . ‘Doctrine.php’);

spl_autoload_register(array(‘Doctrine’, ‘autoload’)); Doctrine_Manager::getInstance()->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

$connection = Doctrine_Manager::connection(DB_PATH, ‘main’);

Doctrine_Core::loadModels(MODELS_PATH);

With all that done, we can now invoke the Unit of Work to perform a whole range of operations in one clean transaction without adding complexity to the rest of our code base.

$t = Doctrine_Core::getTable(‘Project’); $lastProjects =

$t->findByName(‘new project’);

$unitOfWork = new UnitOfWork();

// prepare an UPDATE $lastProjects[0]->name = ‘old project’; unitOfWork->registerModelForCreateOrUpdate(lastProjects[0]);

// prepare a CREATE $project = new Project(); $project->name = ‘new project name’;

unitOfWork->registerModelForCreateOrUpdate(project);

// prepare a DELETE unitOfWork->registerModelForDelete(lastProjects[3]);

// perform the transaction $unitOfWork->commitAll();

The end result should look like this:

class UnitOfWork { /** * Collection of models to be persisted * *

@var array Doctrine_Record */ protected $_createOrUpdateCollection = array();

/**
 * Collection of models to be persisted
 *
 * @var array Doctrine_Record
 */
protected $_deleteCollection = array();

/**
 * Add a model object to the create collection
 *
 * @param Doctrine_Record $model
 */
public function registerModelForCreateOrUpdate($model)
{
    // code to check to see if the model exists already
    if ($this->_existsInCollections($model)) {
        throw new Exception('model already in another collection for this transaction');
    }

    // no? add it
    $this->_createOrUpdateCollection[] = $model;
}

/**
 * Add a model object to the delete collection
 *
 * @param Doctrine_Record $model
 */
public function registerModelForDelete($model)
{
      // code to check to see if the model exists already
      if ($this->_existsInCollections($model)) {
          throw new Exception('model already in another collection for this transaction');
      }

      // no? add it
      $this->_deleteCollection[] = $model;
}

/**
 * Clear the Unit of Work
 */
public function ClearAll()
{
    $this->_deleteCollection = array();
    $this->_createOrUpdateCollection = array();
}

/**
 * Perform a Commit and clear the Unit Of Work. Throw an Exception if it fails and roll back.
 */
public function commitAll()
{
    $conn = Doctrine_Manager::connection();

    try {
        $conn->beginTransaction();

        $this->performCreatesOrUpdates($conn);
        $this->performDeletes($conn);

        $conn->commit();
    } catch(Doctrine_Exception $e) {
        $conn->rollback();
    }

    $this->clearAll();
}

protected function _performCreatesOrUpdates($conn)
{
    foreach ($this->_createOrUpdateCollection as $model) {
        $model->save($conn);
    }
}

protected function _performDeletes($conn)
{
    foreach ($this->_deleteCollection as $model) {
        $model->delete($conn);
    }
}

protected function _existsInCollections($model)
{
   foreach ($this->_createOrUpdateCollection as $m) {
        if ($model->getOid() == $m->getOid()) {
            return true;
        }
   }

   foreach ($this->_deleteCollection as $m) {
        if ($model->getOid() == $m->getOid()) {
            return true;
        }
   }

   return false;
}

}

Thanks for reading, feel free to check out http://jon.lebensold.ca or mail me at jon@lebensold.ca if you have any questions.

Record-based Retrieval Security Template

Introduction

This is a tutorial & how-to on using a security template and listener to restrict a user to specific records, or a range of specific records based on credentials and a user table association. Basically fine grained user access control.

This template was created for a project which had a few credentials, division_manager, district_manager, branch_manager, and salesperson. We have a list of accounts, their related sales and all sorts of sensitive information for each account. Each logged in user should be allowed to only view the accounts and related information based off their credentials + either the division, district, branch or salesperson they are allowed to view.

So a division manager can view all info for all accounts within his division. A salesperson can only view the accounts they are assign.

The template has been a work in progress so the code below may not actually be the final code I’m using today. But since it is now working for all situations I’m asking of it, I thought I would post it as is.

Template
class gsSecurityTemplate extends Doctrine_Template { protected

$_options = array();

/**
 * __construct
 *
 * @param string $options
 * @return void
 */
public function __construct(array $options)
{
    if (!isset($options['conditions']) || empty($options['conditions'])) {
        throw new Doctrine_Exception('Unable to create security template without conditions');
    }

    $this->_options = $options;
}

public function setUp()
{
    $this->addListener(new gsSecurityListener($this->_options));
}

}

class gsSecurityListener extends Doctrine_Record_Listener { private static $*user_id = 0, $*credentials = array(), $_alias_count = 30;

protected $_options = array();

/**
 * __construct
 *
 * @param string $options
 * @return void
 */
public function __construct(array $options)
{
    $this->_options = $options;
}

public function preDqlSelect(Doctrine_Event $event)
{
    $invoker = $event->getInvoker();
    $class   = get_class($invoker);
    $params  = $event->getParams();

    if($class == $params['alias']) {
        return;
    }

    $q       = $event->getQuery();

    // only apply to the main protected table not chained tables... may break some situations
    if(!$q->contains('FROM '.$class)) {
        return;
    }

    $wheres = array();
    $pars   = array();

    $from = $q->getDqlPart('from');

    foreach ($this->_options['conditions'] as $rel_name => $conditions) {
        $apply = false;
        foreach ($conditions['apply_to'] as $val) {
            if (in_array($val,self::$_credentials)) {
                $apply = true;
                break;
            }
        }

        if ($apply) {
            $alias = $params['alias'];
            $aliases = array();
            $aliases[] = $alias;

            foreach ($conditions['through'] as $key => $table) {
                $index = 0;
                $found = false;
                foreach ($from as $index => $val) {
                    if (strpos($val,$table) !== false) {
                        $found = true;
                        break;
                    }

                }

                if ($found) {
                    $vals = explode(' ', substr($from[$index],strpos($from[$index],$table)));
                    $alias = (count($vals) == 2) ? $vals[1]:$vals[0];
                    $aliases[] = $alias;
                } else {
                    $newalias = strtolower(substr($table,0,3)).self::$_alias_count++;
                    $q->leftJoin(end($aliases).'.'.$table.' '.$newalias);
                    $aliases[] = $newalias;
                }
            }

            $wheres[] = '('.end($aliases).'.'.$conditions['field'].' = ? )';
            $pars[] = self::$_user_id;
        }
    }

    if(!empty($wheres)) {
        $q->addWhere( '('.implode(' OR ',$wheres).')',$pars);
    }
}

static public function setUserId($id)
{
    self::$_user_id = $id;
}

static public function setCredentials($vals)
{
    self::$_credentials = $vals;
}

}

YAML schema syntax

Here is the schema I used this template with. I’ve removed lots of extra options, other templates I was using, indexes and table names. It may not work out of the box without the indexes - YMMV.

Account: actAs: gsSecurityTemplate: conditions: Division: through: [ Division, UserDivision ] field: user_id apply_to: [ division_manager ] Branch: through: [ Branch, UserBranch ] field: user_id apply_to: [ branch_manager ] Salesperson: through: [ Salesperson, UserSalesperson ] field: user_id apply_to: [ salesperson ] District: through: [ Branch, District, UserDistrict ] field: user_id apply_to: [ district_manager ] columns: id: { type: integer(4), primary: true, autoincrement: true, unsigned: true } parent_id: { type: integer(4), primary: false, autoincrement: false, unsigned: true} business_class_id: { type: integer(2), unsigned: true } salesperson_id: { type: integer(4), unsigned: true } branch_id: { type: integer(4), unsigned: true } division_id: { type: integer(1), unsigned: true } sold_to: { type: integer(4), unsigned: true }

Division: columns: id: { type: integer(1), autoincrement: true, primary: true, unsigned: true } name: { type: string(32) } code: { type: string(4) }

District: actAs: gsSecurityTemplate: conditions: Division: through: [ Division, UserDivision ] field: user_id apply_to: [ division_manager ] relations: Division: foreignAlias: Districts local: division_id onDelete: RESTRICT columns: id: { type: integer(4), autoincrement: true, primary: true, unsigned: true } name: { type: string(64) } code: { type: string(4) } division_id: { type: integer(1), unsigned: true }

Branch: actAs: gsSecurityTemplate: conditions: Division: through: [ Division, UserDivision ] field: user_id apply_to: [ division_manager ] District: through: [ District, UserDistrict ] field: user_id apply_to: [ district_manager ] relations: Division: local: division_id foreignAlias: Branches onDelete: CASCADE District: foreignAlias: Branches local: district_id onDelete: RESTRICT columns: id: { type: integer(4), primary: true, autoincrement: true, unsigned: true } name: { type: string(64) } code: { type: string(4) } district_id: { type: integer(4), unsigned: true } division_id: { type: integer(1), unsigned: true } is_active: { type: boolean, default: true }

User: relations: Divisions: class: Division refClass: UserDivision local: user_id foreign: division_id Districts: class: District refClass: UserDistrict local: user_id foreign: district_id Branches: class: Branch refClass: UserBranch local: user_id foreign: branch_id Salespersons: class: Salesperson refClass: UserSalesperson local: user_id foreign: salespersons_id columns: id: { type: integer(4), autoincrement: true, primary: true, unsigned: true } name: { type: string(128) } is_admin: { type: boolean, default: false } is_active: { type: boolean, default: true } is_division_manager: { type: boolean, default: false } is_district_manager: { type: boolean, default: false } is_branch_manager: { type: boolean, default: false } is_salesperson: { type: boolean, default: false } last_login: { type: timestamp }

UserDivision: tableName: user_divisions columns: id: { type: integer(4), autoincrement: true, primary: true, unsigned: true } user_id: { type: integer(4), primary: true, unsigned: true } division_id: { type: integer(1), primary: true, unsigned: true }

UserDistrict: tableName: user_districts columns: id: { type: integer(4), autoincrement: true, primary: true, unsigned: true } user_id: { type: integer(4), primary: true, unsigned: true } district_id: { type: integer(4), primary: true, unsigned: true }

UserBranch: tableName: user_branches columns: id: { type: integer(4), autoincrement: true, primary: true, unsigned: true } user_id: { type: integer(4), primary: true, unsigned: true } branch_id: { type: integer(4), primary: true, unsigned: true }

UserSalesperson: tableName: user_salespersons columns: id: { type: integer(4), autoincrement: true, primary: true, unsigned: true } user_id: { type: integer(4), primary: true, unsigned: true } salespersons_id: { type: integer(4), primary: true, unsigned: true }

You can see from the User model that the credentials are set within the db. All hardcoded in this situation.

Using the template

Once you’ve built your models from the schema, you should see something like the following in your model’s setUp function.

$gssecuritytemplate0 = new gsSecurityTemplate(array(‘conditions’ =>

array(‘Division’ => array( ‘through’ => array( 0 => ‘Division’, 1 => ‘UserDivision’, ), ‘field’ => ‘user_id’, ‘apply_to’ => array( 0 => ‘division_manager’, ), ‘exclude_for’ => array( 0 => ‘admin’, ), ), ‘Branch’ => array( ‘through’ => array( 0 => ‘Branch’, 1 => ‘UserBranch’, ), ‘field’ => ‘user_id’, ‘apply_to’ => array( 0 => ‘branch_manager’, ), ‘exclude_for’ => array( 0 => ‘admin’, 1 => ‘division_manager’, 2 => ‘district_manager’, ), ), ‘Salesperson’ => array( ‘through’ => array( 0 => ‘Salesperson’, 1 => ‘UserSalesperson’, ), ‘field’ => ‘user_id’, ‘apply_to’ => array( 0 => ‘salesperson’, ), ‘exclude_for’ => array( 0 => ‘admin’, 1 => ‘division_manager’, 2 => ‘district_manager’, 3 => ‘branch_manager’, ), ), ‘District’ => array( ‘through’ => array( 0 => ‘Branch’, 1 => ‘District’, 2 => ‘UserDistrict’, ), ‘field’ => ‘user_id’, ‘apply_to’ => array( 0 => ‘district_manager’, ), ‘exclude_for’ => array( 0 => ‘admin’, 1 => ‘division_manager’, ), )))); this->actAs(gssecuritytemplate0);

The last part you need to use is to provide the template with the running user’s credentials and id. In my project’s session bootstrapping I have the following ( I use the symfony MVC framework ).

public function initialize($context,

parameters = null) { parent::initialize(context, parameters = null); gsSecurityListener::setUserId(this->getAttribute(‘user_id’)); gsSecurityListener::setCredentials($this->listCredentials());

}

This provides the credentials the user was given when they logged in as well as their id.

User setup

In my case, I create users and provide a checkbox for their credentials, one for each type I have. Lets take Division Manager as an example. In my case we have 3 divisions, East, Central, West. When I create a user I assign it the West division, and check off that they are a division manager. I can then proceed to login, and my account listing page will restrict the accounts I see automatically to my division.

Querying

Now if you query the Account model, the template is triggered and based on your credentials the results will be restricted.

The query below

$accounts = Doctrine_Query::create()->from(‘Account

a’)->leftJoin(‘a.Branches b’)->where(‘a.company_name LIKE ?’,’A%’)->execute();

produces the resulting sql.

SELECT ... FROM accounts a2 LEFT JOIN branches b2 ON a2.branch_id =

b2.id LEFT JOIN divisions d2 ON a2.division_id = d2.id LEFT JOIN user_divisions u2 ON d2.id = u2.division_id WHERE a2.company_name LIKE ? AND u2.user_id = ? ORDER BY a2.company_name

The results you get back will always be restricted to the division you have been assigned. Since in our schema we’ve defined restrictions on the Branch and Districts as well if I were to want to provide a user with a drop down of potential branches, I can simply query the branches as I normally would, and only the ones in my division would be returned to choose from.

Restrictions

For the time being, this module only protects tables in the FROM clause, since doctrine currently runs the query listener for the new tables added to the query by the template, and thus we get a pretty nasty query in the end that doesn’t work. If I can figure out how to detect such situations reliably I’ll update the article.

Japanese Documentation

Manual

コードの例

CAUTION この本のテキストはたくさんのPHPコードの例を含みます。本の分量を減らすためにPHPの開始と終了タグはすべて取り除かれています。サンプルをコピー&ペーストするときは必ずPHPタグを含めてください。

Doctrineって何?

DoctrineはPHP5.2.3以降用のオブジェクトリレーショナルマッパー(ORM - Object Relational Mapper)で強力なデータベース抽象化レイヤー(DBAL - DataBase Abstraction Layer)のに。主要な機能の1つはDoctrine Query Language (DQL)と呼ばれるプロプリエタリなオブジェクト指向のSQL方言でデータベースクエリを書くオプションがあることです。HibernateのHQLにインスパイアされ、これは開発者に柔軟性を維持し不要なコードの重複がないSQLの強力な代替機能を提供します。

ORMって何?

オブジェクトリレーショナルマッピング(Object Relational Mapping)はプログラミング言語のテクニックです。リレーショナルデータベースで互換性のないデータ型を翻訳するためにデータベースで対処するとき、これによってプログラミング言語から利用できる”バーチャルなオブジェクトデータベース”を用意できます。これを可能にするたくさんの無料と商用パッケージが存在しますが時には開発者が独自のORMを作成する選択肢もあります。

何が問題なの?

ウェブアプリケーションを構築するときに多くの問題に直面します。ここでオブジェクトリレーショナルマッパーのすべてを説明するよりもWikipediaの説明を見るのがベストです。

[http://en.wikipedia.org/wiki/Object-relational_mapping Wikipedia]からの引用:

通常、オブジェクト指向のプログラミングにおけるデータ管理のタスクは、大抵の場合スカラーではない値であるオブジェクトの操作によって実装されます。例えば、ゼロ以上の電話番号とゼロ以上のアドレスに沿って個人を表すアドレスブックのエントリを考えてみましょう。これはオブジェクト指向の実装でモデル化できます。例えばエントリ: 個人の名前、電話番号のリスト(もしくは配列)、とアドレスのリストより構成されるデータを保有する”スロット”を持つ”個人オブジェクト”です。電話番号のリスト自身は”電話番号オブジェクト”などを含みます。アドレスブックのエントリはプログラミング言語による単独の変数として扱われます(例えば、単独の変数で参照される)。さまざまなメソッドはオブジェクト、望む電話番号、ホームアドレスなどを返すメソッドなどで関連付けできます。

しかしながら、SQLを生み出す多くのデータベースはテーブルの範囲内で編成されたスカラーの値、整数と文字列のようなものしか保存と操作ができません。

データベースに保存するためにプログラマはオブジェクトの値をよりシンプルな値に変換する(そしてこれらを読み取り用に変換する)、もしくはプログラムの範囲内でシンプルなスカラーの値のみを使用しなければなりません。オブジェクトリレーショナルマッピングは最初のアプローチを実装するために使われます。

より高度な問題は、永続的なオブジェクトのプロパティとリレーションが維持しながら、これらのオブジェクトがデータを読み取りやすくするためにデータベースに保存可能なフォームに翻訳することです、

最小要件

DoctrineはPHP5.2.3以降を必要とし、外部ライブラリは必要ありません。 データベース関数の呼び出しを抽象化するために、DoctrineはPDOを利用します。PDOはwww.php.netから入手できるPHPの公式リリースに搭載されています。

NOTE Windows用のUniform Server、MAMPもしくはその他の非公式パッケージを利用する場合、追加設定が必要な場合があります。

基本的な概要

DoctrineはPHP製のオブジェクトリレーショナルマッピング用のツールです。DoctrineはPDOを利用し2つの主要なレイヤー、DBALとORMに分割されます。下記の図はDoctrineのそれぞれのレイヤーが連携する様子を示しています。

[http://www.doctrine-project.org/images/doctrine-layers.jpg Doctrineレイヤー]

DBAL(Database Abstraction Layer)は基本的なデータベースのPDOによって既に提供される抽象化/独立性を完成および拡張します。PDOの元で強力なデータベース抽象化レイヤーを使いたいだけなら、DBALライブラリはスタンドアロンで利用可能です。ORMレイヤーはDBALに依存するので、ORMパッケージをロードするときDBALは既にインクルードされています。

Doctrineの説明

次のセクションではDoctrineがいるORMツールの世界を説明することにします。Doctrine ORMは主に [http://www.martinfowler.com/eaaCatalog/activeRecord.html Active Record]、[http://www.martinfowler.com/eaaCatalog/dataMapper.html Data Mapper]と [http://www.martinfowler.com/eaaCatalog/metadataMapping.html Meta Data Mapping]パターンでビルドされています。

Doctrine\_Record``という名前の特定の基底クラスを継承することで、すべての子クラスは典型的なActiveRecordインターフェイス(save/delete/etc.)を取得するのでDoctrineはレコードのライフサイクルの扱いとモニタリングを簡単にできます。しかしながら、大抵の場合実際の作業は``Doctrine_Table``クラスのような他のコンポーネントに向けられます。このクラスの典型例はData Mapperインターフェイス、``createQuery()find(id)findAll()findBy*()``findOneBy*()``などです。ですのでActiveRecordの基底クラスによってマッピングのフットワークがどこか他のところで行われる一方でDoctrineはレコードを管理して典型的なActiveRecordインターフェイスを提供できます。

ActiveRecordのアプローチには典型的な制限がついてまわります。もっとも明らかなことは永続的であるために特定の基底クラスを継承することが強制されることです(Doctrine_Record)。一般的に、ドメインモデルのデザインはリレーショナルモデルのデザインによってほとんど制限されます。しかし例外があります。継承構造を扱うとき、Doctrineによって洗練されたマッピング戦略が提供されるのでドメインモデルはリレーショナルモデルから少し分離されもう少し自由が得られます。

Doctrineは継続的な開発プロセスにありドメインのモデリングにより多くの自由を提供する新しい機能の追加を常に試みています。しかしながら、DoctrineがActiveRecordアプローチにとどまる限り、(強制された)これら2つのモデルのとても大きな類似性が常に存在するようになります。

現在の状況は次の図で記述されます。

[http://www.doctrine-project.org/relational-bounds.jpg リレーショナルモデルの範囲]

この図でわかるように、ドメインモデルはリレーショナルモデルの範囲から遠く離れることはできません。

これらの欠点を述べた後で、ActiveRecordのアプローチの利点を述べましょう。(ごくわずかな)シンプルなプログラミングモデルは別として、リレーショナルモデルとオブジェクト指向ドメインモデルの強い類似性も利点を持つことがわかります: これによって既存のリレーショナルスキーマから基本的なドメインモデルを作成できる強力な生成ツールを比較的簡単に提供できます。さらに、上記の理由のためドメインモデルはリレーショナルモデルから遠くに離れることができないので、生成と同期ツールなどは開発プロセスを通して簡単に使えます。このようなツールがDoctrineの強みの一つです。

大多数のウェブアプリケーションに対してActiveRecordのアプローチの制限はそれほど大きな問題ではないと筆者は考えます。多くの場合ビジネスドメインの複雑性が適度ですが、多すぎる制限が押しつけられドメインモデルに影響がありすぎるので、ActiveRecordのアプローチは複雑なビジネスロジック複雑なビジネスロジック(ドメイン駆動のデザインを利用してよく取り組まれる)には必ずしも適していないことも筆者は認めます。

Doctrineはシンプルもしくはほどほどの複雑性を持つドメインモデル``(1)``の一貫性を推し進めるための素晴らしいツールです。ドメインモデルをデータベース中心にすることと独自のものへのすべてのマッピングを実装することの間のトレードオフを考慮する場合、複雑なドメインモデルに対してよい選択肢ではないことがわかるかもしれません(執筆時点で筆者はActiveRecordアプローチに基づいたPHP製の強力なORMツールは知らないからです)。

NOTE (1) 複雑性は規模ではないことに注意してください。ドメインモデルは複雑性を伴わずにとても大きくなることがありますし逆もしかりです。もちろん、ドメインモデルが大きいほど複雑性である可能性は高くなります。

これでDoctrineができることとできないことがわかりました。すぐに始めたいのであれば、次の章の”Getting Started”に直行してください。

キーコンセプト

Doctrine Query Language(DQL)はオブジェクトクエリ言語です。ドメインモデル: クラスの名前、フィールドの名前、クラスの間のリレーション、などの用語を使い単独のオブジェクトもしくは完全なオブジェクトグラフ用のクエリを表現します。リレーショナルモデル(テーブルの名前、カラムの名前、など)からのドメインモデル(フィールドの名前、クラスの名前、など)の分離を壊さずにオブジェクトを読み取るもしくは操作するためにこれは強力なツールです。DQLはSQLにとてもよく似ており、またSQLを理解している人がわかりやすいようにそうなるように意図しています。しかしながら、ごくわずかですが、常に念頭においておく必要のある重要な違いがあります:

DQLクエリの例をあげます:

FROM User u LEFT JOIN u.Phonenumbers where u.level > 1

このクエリからわかることは次の通りです:

  • **テーブル**ではなく**クラス**をselectします。``User``クラス/モデルからselectしています。

  • アソシエーション (u.Phonenumbers)に従ってjoinします

  • フィールド (u.level)を参照できます

  • joinの条件(ON x.y = y.x)はありません。クラスの間のアソシエーションとデータベースで表現する方法はDoctrineに知らされます(もちろん、Doctrineにこのマッピングを知らせる必要があります。このやり方は[doc defining-models :name]の章で説明されます)。

    NOTE ドメインモデル (クラス、属性、他のクラスへのリレーションなど)の観点からDQLはクエリを表現します

クラス、フィールドとクラスの間のアソシエーションはそれほど重要ではありません。``User``はテーブル/テーブルの名前**ではありません**。``User``クラスにマッピングされるデータベーステーブルの名前は本当に``User``ですが専門用語の違いにはこだわります。あら探しに聞こえるかもしれませんが、ActiveRecordのアプローチのため、リレーショナルモデルはドメインモデルによく似ていますが実際にはとても重要です。カラムの名前はフィールドの名前と同じであることがまれ であり継承が関わると同時に、ドメインモデルから分岐するためにリレーショナルモデルが始まります。データベースの複数のテーブルに実際にマッピングされる``User``クラスを用意できます。この点で”``User``テーブルから選択する”を語るのは単に間違っていることは明らかです。Doctrineの開発は継続されるので2つのモデルより分岐できるようにする機能が追加されてゆきます。

さらなる読み物

オブジェクトリレーショナルマッピングと(object-oriented)ドメインモデルを知らない方には次の読み物をお勧めします:

[http://www.martinfowler.com/books.html Martin Fowlerの本]はたくさんのORMの基本用語、ビジネスロジックと関連パターンのモデリングの異なるアプローチをカバーします。

別の良い読み物は[http://domaindrivendesign.org/books/#DDD Domain Driven Design]です。現在Doctrineで本格的なドメイン駆動のデザインは利用できませんが、これはドメインモデリング用の優れたリソースで、とりわけ複雑なビジネスドメインにおいて、今日広く使われるようになったドメインモデル周辺の用語が詳しく説明されています(Entity、Value Object、Repositoryなど)。

まとめ

Doctrineの背景にある方法論と原則に関する教育上の読み物を少し提供しました。これでDoctrineのすべてに取り組む準備ができています。[doc getting-started :name]の章でDoctrineのセットアップに突入しましょう。

要件を確認する

最初にサーバーでDoctrineを実行できることを確認できます。方法は2つあります:

最初に``phpinfo.php``という名前の小さなPHPスクリプトを作り、ウェブサーバーのウェブからアクセスできる場所にアップロードします:

phpinfo();

http://localhost/phpinfo.php にブラウザでアクセスしてスクリプトを実行します。PHPの構成の詳細情報の一覧が表示されます。PHPのバージョンが//5.2.3//以降でありPDOと必要なドライバがインストールされていることを確認します。

NOTE ターミナルからコマンドを実行する方法でもインストールされているPHPが必要な要件を満たしていることを確認できます。次の例で示します。

次のコマンドでPHPのバージョンが5.2.3以降であることを確認してください:

$ php -v

次のコマンドでPDOとお望みのドライバがインストールされていることを確認します:

$ php -i

コマンドラインから``phpinfo.php``を実行しても上記と同じ結果を得られます:

$ php phpinfo.php

インストールする

現在Doctrineをインストールする方法は4通りあり一覧は次の通りです:

  • SVN (subversion)
  • SVN externals
  • PEARインストーラ
  • PEARパッケージをダウンロードする

SVN (subversion)を通してDoctrineをダウンロードする方法がお勧めです。この場合は更新が楽です。プロジェクトが既にSVNでバージョン管理している場合、SVN externalsを使います。

Tip

5分以内でDoctrineを試したいだけなら、サンドボックスのパッケージが推奨されます。次のセクションでサンドボックスパッケージを検討します。

サンドボックス

Doctrineは一行もテストを書かずにDoctrineをテストするために設定をまったくしなくてよいDoctrine実装の特別なパッケージも提供します。[http://www.doctrine-project.org/download ダウンロードページ]から入手できます。

NOTE サンドボックスの実装は本番アプリケーションには推奨されません。これはDoctrineの探求と地井さんテストの実行だけを目的としています。
SVN

SVNとexternalsオプションを通してDoctrineを使うことを大いにお勧めします。SVNから最新のバグ修正を入手できて最良の経験が保証されるのでこのオプションがベストです。

インストールする

SVNを通してDoctrineをインストールするのはとても簡単です。SVN サーバー: ``http://svn.doctrine-project.org``から任意のバージョンのDoctrineをダウンロードできます。

特定のバージョンをチェックアウトするにはターミナルから次のコマンドを使用します:

SVNクライアントがなければ、下記のリストから1つ選んでください。チェックアウトのオプションを見つけてパスもしくはリポジトリのURLパラメータに http://svn.doctrine-project.org/1.1 を入力します。Doctrineをチェックアウトするためにユーザー名もしくはパスワードは不要です。

更新する

インストール作業と同じようにSVNでDoctrineをアップグレードする作業は簡単です。ターミナルから次のコマンドを実行するだけです:

$ svn update
SVN Externals

プロジェクトで既にSVNでバージョン管理されている場合、DoctrineをインストールするためにSVN externalsを使うのがお勧めです。

ターミナルでプロジェクトをチェックアウトすることから始めます:

$ cd /var/www/my_project

プロジェクトをチェックアウトしたので、ターミナルから次のコマンドを実行してDoctrineをSVN externalとしてセットアップします:

$ svn propedit svn:externals lib/vendor

上記のコマンドでエディタを開き次のテキストを押し込んで保存します:

svn updateを実行することでDoctrineをインストールできます:

$ svn update

このコマンドによってDoctrineは次のパスでインストールされます: /var/www/my_project/lib/vendor/doctrine

Tip

SVN externalsへの変更をコミットすることをお忘れなく。

$ svn commit

PEARインストーラ

Doctrineはサーバーでインストールとアップデート用のPEARサーバーも提供します。 次のコマンドでDoctrineを簡単にインストールできます:

$ pear install pear.doctrine-project.org/Doctrine-1.1.x

NOTE 1.1.xをインストールしたいバージョンに置き換えます。例えば”1.2.1”です。
Pearパッケージをダウンロードする

PEARでインストールしたくないもしくはPEARがインストールされていない場合、[http://www.doctrine-project.org/download 公式サイト]からパッケージを手動でダウンロードできます。サーバーにパッケージをダウンロードした後でlinuxでは次のコマンドを利用してこれを展開できます。

$ tar xzf Doctrine-1.2.1.tgz

実装する

Doctrineを手に入れたので、アプリケーションでDoctrineを実装する準備ができています。 これはDoctrineを始めることに向けた最初のステップです。

最初に``doctrine_test``という名前のディレクトリを作ります。ここはすべてのテストコードを設置する場所です:

$ mkdir doctrine_test $ cd doctrine_test
Doctrineライブラリをインクルードする

最初に行わなければならないことはアプリケーションで読み込むことができるようにコアクラスを格納する``Doctrine.php``ファイルを見つけることです。``Doctrine.php``ファイルは前のセクションでダウンロードしたDoctrineのlibフォルダに存在します。

Doctrineライブラリを``doctrine_test``ディレクトリから``doctrine_test/lib/vendor/doctrine``フォルダに移動させる必要があります:

$ mkdir lib $ mkdir lib/vendor $ mkdir lib/vendor/doctrine $ mv

/path/to/doctrine/lib doctrine

もしくはSVNを利用しているのであれば、externalsを使います:

lib/vendor/doctrine

svn externalsにパスを追加します:

$ svn propedit svn:externals lib/vendor

テキストエディタを開き次の内容を入力して保存します:

SVN updateを行うとDoctrineのライブラリは更新されます:

$ svn update lib/vendor
Doctrineの基底クラスをrequireする

Doctrineとすべての設定をブートストラップするためのPHPコードが必要です。 ``bootstrap.php``という名前のファイルを作り次のコードをファイルに加えます:

// bootstrap.php

/** * Bootstrap Doctrine.php, register autoloader specify * configuration attributes and load models. */

require_once(dirname(FILE) . ‘/lib/vendor/doctrine/Doctrine.php’);

オートローダーを登録する

``Doctrine``クラスの準備が終わったので、ブートストラップファイルでクラスのオートローダー関数を登録する必要があります:

// bootstrap.php

// ... spl_autoload_register(array(‘Doctrine’, ‘autoload’));

``Doctrine_Manager``シングルトンインスタンスも作り``$manager``という名前の変数に割り当てます:

// bootstrap.php

// ... $manager = Doctrine_Manager::getInstance();

オートロード機能の説明
NOTE [http://www.php.net/spl_autoload_register PHPの公式サイト]でオートロード機能の使い方がわかります。オートローダーを利用することで予めロードされたすべてのクラスの代わりにリクエストされたクラスを遅延ロードできます。これはパフォーマンスの面で大きな恩恵があります。

Doctrineのオートローダーの動作方法はシンプルです。クラスの名前とパスは相対的なので、名前に基づいてDoctrineクラスへのパスを決定できます。

``Doctrine_Some_Class``という名前のクラスをインスタンス化することを考えてみましょう:

$class = new Doctrine_Some_Class();

上記のコードは``Doctrine::autoload()``関数の呼び出しを実行しインスタンス化するクラスの名前を渡します。クラスの名前の文字列は操作されパスに変換され読み込まれます。下記はクラスの発見と読み込み方法を示す疑似コードです:

class Doctrine { public function autoload($className) { $classPath =

str_replace(‘_’, ‘/’, $className) . ‘.php’; $path = ‘/path/to/doctrine/’ . classPath; require_once(path); return true; } }

上記の例では``Doctrine_Some_Class``は``/path/to/doctrine/Doctrine/Some/Class.php``で見つかります。

NOTE もちろん実際の``Doctrine::autoload()``メソッドはもっと複雑でファイルの存在を確認するエラーチェック機能を持ちますが上記のコードはどのように動作するのかを実演するためにあります。
ブートストラップファイル

Tip

後の章とセクションでこのブートストラップクラスを使うので作ってください!

作成したブートストラップファイルの内容は次のようになります:

// bootstrap.php

/** * 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();

この新しいブートストラップファイルは実装の変更を行う場所でありまた段階的にDoctrineの使い方を学ぶのでこのファイルはこの本で何度も参照されます。

NOTE 上記で説明された接続属性はDoctrineの機能です。[doc configuration :name]の章で属性の詳細とこれらのゲッター/セッターを学びます。
テストスクリプト

Doctrineの機能に関して学ぶので様々なテストを実行するために利用できるシンプルなテストスクリプトを作りましょう。

``doctrine_test``ディレクトリの中で``test.php``という名前の新しいファイルを作成し内部に次の内容を置きます:

// test.php

require_once(‘bootstrap.php’);

echo Doctrine::getPath();

これでコマンドラインからテストスクリプトを実行できます。これはこの章全体でDoctrineでテストを実行する方法です。動作しているか確認してください!Doctrineがインストールされている場所のパスが出力されます。

$ php test.php /path/to/doctrine/lib

まとめ

ふぅ!実際にコードに取り組んだ最初の章でした。ご覧の通り、最初にサーバーがDoctrineを実際に実行できることをチェックできました。それから異なる複数の方法でDoctrineのダウンロードとインストールができることを学びました。最後にこの本の残りの章で練習するために使う小さなテスト環境をセットアップすることでDoctrineを実装する方法を学びました。

[doc introduction-to-connections :name]の章に移動してDoctrineの接続を初体験しましょう。

DSN、Data Source Name

Doctrineを通してデータベースに接続するには、有効なDSN(Data Source Name)を作らなければなりません。

DoctrineはPDOスタイルと同じようにPEAR DB/MDB2のDSNをサポートします。次のセクションではPEARのDSN形式を扱います。PDOスタイルのDSNの詳しい情報が必要であれば[http://www.php.net/pdo PDO]のドキュメントを参照してください。

DSNは次の部分で構成されます:

||~ DSNの部分 ||~ 説明 || || phptype || PHPで使われるデータベース (すなわちmysql、pgsqlなど) || || dbsyntax || SQL構文などで使われるデータベース。 || || protocol || 使用するコミュニケーションプロトコル(すなわち、tcp、unixなど) || || hostspec || ホストのスペック(hostname[:port]) || || database || DBMSサーバーで使うデータベース || || username || ログイン用のユーザー名 || || password || ログイン用のパスワード || || proto_opts || プロトコルで使われる || || ``option `` || URIクエリ文字列形式の追加接続オプション。オプションはアンパサンド(&)で分離されます。次のテーブルでオプションの不完全なリストを示します: ||

オプションのリスト

||~ 名前 ||~ 説明 || || charset || クライアントの文字集合を設定するバックエンドサポート。|| || new_link|| 同じホストに複数回接続する場合RDBMSの中には新しい接続を作成しないものがあります。このオプションによって新しい接続の強制が試みられます。||

DSNは連想配列もしくは文字列のどちらかで提供できます。提供されたDSNの文字列フォーマットの完全な表記は次の通りです:

phptype(dbsyntax)://username:password@protocol+hostspec/database?option=value

大抵のバリアントは許容されます:

phptype://username:password@protocol+hostspec:110//usr/db_file.db

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

現在サポートされるPDOのデータベースドライバは次の通りです:

||~ ドライバの名前 ||~ サポートされるデータベース || || fbsql || FrontBase || || ibase || InterBase / Firebird (PHP 5が必須) || || mssql || Microsoft SQL Server (Sybase**ではない**。Compile PHP –with-mssql) || || mysql || MySQL || || mysqli || MySQL (新しい認証プロトコル) (PHP 5が必須) || || oci || Oracle 7/8/9/10 || || pgsql || PostgreSQL || || querysim || QuerySim || || sqlite || SQLite 2 ||

サポートされる2番目のDSNは次の通りです

phptype(syntax)://user:pass@protocol(proto_opts)/database

データベース、オプションの値、ユーザー名もしくはパスワードにDSNを区切るために使われる文字が含まれる場合、URIの16進法のエンコーディングを通してエスケープできます:

||~ 文字 ||~ 16進法 || || : || %3a || || / || %2f || || @ || %40 || || + || %2b || || ( || %28 || || ) || %29 || || `` ?`` || %3f || || = || %3d || || & || %26 ||

機能の中にはすべてのデータベースでサポートされないものがあることに十分注意してくださるようお願いします。

例 1. ソケットを通したデータベースへの接続

mysql://user@unix(/path/to/socket)/pear

例 2. 非標準ポートでのデータベースへの接続

pgsql://user:pass@tcp(localhost:5555)/pear

NOTE 使うのであれば、IPアドレス``{127.0.0.1}``、ポートパラメータ(デフォルト: 3306)は無視されます。

例 3. オプションを利用するUnixマシン上でのSQLiteへの接続

sqlite:////full/unix/path/to/file.db?mode=0666

例 4. オプションを利用してWindowsマシンでSQLiteに接続する

sqlite:///c:/full/windows/path/to/file.db?mode=0666

例 5. SSLを利用してMySQLiに接続する

mysqli://user:pass@localhost/pear?key=client-key.pem&cert=client-cert.pem

新しい接続を開く

Doctrineで新しいデータベース接続を開くのはとても簡単です。[http://www.php.net/PDO PDO]を使う場合、新しいPDOオブジェクトを初期化するだけです。

[doc getting-started :name]の章で作成した``bootstrap.php``ファイルを覚えていますか?Doctrineのオートローダーが登録されたコードで、新しい接続をインスタンス化します:

// bootstrap.php

// ... $dsn = ‘mysql:dbname=testdb;host=127.0.0.1’; $user = ‘dbuser’; $password = ‘dbpass’;

dbh = new PDO(dsn, $user, $password); conn = Doctrine_Manager::connection(dbh);

Tip

``Doctrine_Manager::connection()``にPDOインスタンスを直に渡すことでDoctrineは接続用のユーザー名とパスワードを認識できません。既存のPDOインスタンスから接続を読み取る方法がないからです。Doctrineがデータベースの作成と削除ができるようにユーザー名とパスワードが必要です。これを切り抜けるには``$conn``オブジェクトでユーザー名とパスワードオプションを直に設定します。

// bootstrap.php

// ... $conn->setOption(‘username’, $user); $conn->setOption(‘password’, $password);

データベースの遅延接続

データベースへの遅延接続は多くのリソースを節約できます。常に遅延接続を使うことが推奨されるので、実際にデータベース接続を必要とする機会はあまりないでしょう(すなわちDoctrineは必要なときだけデータベースに接続する)。

毎回のリクエストでデータベースの接続が必要ないページキャッシュなどでこの機能がとても役に立ちます。データベースへの接続は負荷の大きいオペレーションであることを覚えておいてください。

下記の例では、Doctrineの新しい接続を作成するときに、データベースへの接続は実際に必要になるまで作成されないことを示しています。

// bootstrap.php

// ...

// この時点でデータベースへの接続は作成されない $conn = Doctrine_Manager::connection(‘mysql://username:password@localhost/test’);

// 接続が必要な最初のときに、インスタンス化される // このクエリによって接続が作成される $conn->execute(‘SHOW TABLES’);

接続をテストする

この章の前のセクションを読んだ後で、接続を作成する方法を学ぶことにします。接続のインスタンス化をインクルードするためにブートストラップファイルを修正しましょう。この例ではSQLiteのメモリデータベースを使いますが、望むタイプのデータベース接続は何でも使えます。

``bootstrap.php``にデータベース接続を追加すると次のようになります:

/** * Bootstrap Doctrine.php、オートローダーを登録して *

接続属性を指定する */

require_once(‘../doctrine/branches/1.2/lib/Doctrine.php’); spl_autoload_register(array(‘Doctrine’, ‘autoload’)); $manager = Doctrine_Manager::getInstance();

$conn = Doctrine_Manager::connection(‘sqlite::memory:’, ‘doctrine’);

接続をテストするために``test.php``スクリプトを修正して小さなテストを実行しましょう。テストスクリプトが変数``$conn``が使えるようになったので接続が動作していることを確認するために小さなテストをセットアップしましょう:

最初に、testテーブルを作りレコードを挿入します:

// test.php

// ... $conn->export->createTable(‘test’, array(‘name’ => array(‘type’ => ‘string’))); $conn->execute(‘INSERT INTO test (name) VALUES (?)’, array(‘jwage’));

データが挿入されて読み取れることを確認するために作成したばかりの``test``テーブルからシンプルな``SELECT``クエリを実行してみましょう:

// test.php

// ... $stmt = $conn->prepare(‘SELECT * FROM test’); $stmt->execute(); $results = stmt->fetchAll(); print_r(results);

ターミナルから``test.php``を実行すると結果は次の通りです:

$ php test.php Array ( [0] => Array ( [name] => jwage [0] => jwage )

)

まとめ

すばらしい!Doctrine接続の基本的なオペレーションを学びました。新しい接続を用意するためにDoctrineのテスト環境を修正しました。次の章の例では接続を使うのでこの環境は必要です。

[doc configuration :name]の章に移動してDoctrineの属性システムを利用して機能と設定をコントロールする方法を学びます。

Doctrineは属性を利用して機能と機能性を制御します。このセクションではDoctrineの機能性を使うために、属性の設定と取得する方法および、存在する属性をオーバーライドする方法を検討します。

設定のレベル

Doctrineは3レベルの設定構造を持ちます。グローバル、接続とテーブルレベルで設定属性を設定できます。同じ属性が下側と上側の両方のレベルで設定される場合、一番上の属性が常に使われます。例えばユーザーが最初にグローバルレベルでデフォルトの取得モードを``Doctrine_Core::FETCH_BATCH``に設定してテーブルの取得モードを``Doctrine_Core::FETCH_LAZY``に設定すれば、遅延取得戦略はテーブルのレコードが取得されているときはいつでも使えます。

  • グローバルレベル - グローバルレベルで設定された属性はすべての接続と接続ごとのすべてのテーブルに影響を及ぼします。
  • 接続レベル - 接続レベルで設定される属性はその接続のそれぞれのテーブルのみに影響を及ぼします。
  • テーブルレベル - テーブルレベルで設定される属性はそのテーブルのみに影響を及ぼします。

次の例ではグローバルレーベルで1つの属性を設定します:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

次の例は与えられた接続のグローバル属性をオーバーライドします:

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_NONE);

最後の例ではテーブルレベルで接続レベルの属性を再度オーバーライドします:

// bootstrap.php

// ... $table = Doctrine_Core::getTable(‘User’);

$table->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

NOTE ``Doctrine_Core::getTable()``メソッドを使った例は紹介しませんでした。次の章の[doc component-overview:table :name]のセクションでテーブルオブジェクトを詳しく学びます。

ポータビリティ

それぞれのデータベース管理システム(DBMS - Database Management System)は独自の振る舞いを行います。例えば、出力する際にテーブルの名前の最初の文字を大文字にするものもがあれば、小文字にしたりそのままにするものがあります。これらの挙動によってアプリケーションを別の種類のデータベースに移植させるのが難しくなります。Doctrineはこれらの困難に打ち勝つために努力してくれるのでアプリケーションを変更せずにDBMSを切り替えることができます。例えばsqliteからmysqlに切り替えることです。

ポータビリティモードはビット単位なので、|を使い結合したり^を使い削除できます。これを行う方法の例は下記のセクションをご覧ください。

Tip

ビット演算子の詳細な情報は[http://www.php.net/language.operators.bitwise PHP公式サイト]をご覧ください。

ポータビリティモードの属性

すべてのポータビリティ属性と説明の一覧です:

||~ 名前 ||~ 説明 || || || PORTABILITY_ALL || すべてのポータビリティモードを有効にする。これはデフォルトの設定です。|| || PORTABILITY\_DELETE_COUNT || 削除される列の数のレポートを強制する。シンプルな``DELETE FROM``テーブル名のクエリを実行する際に削除される列の数をカウントしないDBMSがあります。このモードでは``DELETE``クエリの最後に``WHERE 1=1``を追加することでRDBMSをだましてカウントするようにします || || PORTABILITY\_EMPTY\_TO_NULL || データと出力において空の文字列の値をnullに変換する。必要なのはOracleは空の文字列をnullと見なす一方で、その他の主なDBMSは空とnullの違いを知っているからです。|| || PORTABILITY_ERRORS || 特定のドライバの特定のエラーメッセージを他のDBMSと互換性があるようにする || || PORTABILITY\_FIX\_ASSOC\_FIELD_NAMES || これは連想配列形式の取得においてキーから修飾子を削除します。SQLiteなどは、クエリで省略されていない場合に連想配列形式のカラムに対して省略されていない名前をデフォルトで使用します。|| || PORTABILITY\_FIX_CASE || すべてのメソッドで小文字もしくは大文字にするためにテーブルとフィールドの名前を変換する。事例はfield_caseオプションに依存し``CASE_LOWER``(デフォルト)もしくは``CASE_UPPER``のどちらかに設定できます。|| || PORTABILITY_NONE || すべてのポータビリティ機能を無効にする。|| || PORTABILITY_NUMROWS || Oracleで``numRows()``を動作させるためのハックを有効にする || || PORTABILITY_EXPR || ポータブルではない式が使われる場合にDQL APIが例外を投げる。|| || PORTABILITY_RTRIM || すべてのデータ取得する際にデータ出力の右トリミングする。固定長の文字の値を右トリミングしない場合でも、これは固定長の文字の値を自動的に右トリミングするRDBMSには適用されない。||

小文字化とトリミングのためにポータビリティモードを有効にする``setAttribute()``メソッドを次のコードのように使うことができます:

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_PORTABILITY, Doctrine_Core::PORTABILITY_FIX_CASE | Doctrine_Core::PORTABILITY_RTRIM);

トリミングを除いたすべてのポータビリティモードを有効にする

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_PORTABILITY, Doctrine_Core::PORTABILITY_ALL ^ Doctrine_Core::PORTABILITY_RTRIM);

識別子のクォート

``quoteIdentifier()``でDBの識別子(テーブルとフィールド名)をクォートできます。区切りのスタイルはデータベースドライバによります。

NOTE 区切られた識別子を使うことができるので、これらを使うべきであることを意味しません。一般的に、これらが解決する問題よりも多くの問題を引き起こします。ともかく、フィールドの名前として予約語がある場合に必要です(この場合、できるのであれば、予約語を変更することを提案します)。

Doctrineの内部メソッドの中にはクエリを生成するものがあります。``quote_identifier``属性を有効にすることで、これらの生成クエリの中で識別子をクォートするようDoctrineに伝えることができます。すべてのユーザー提供のクエリに対してこのオプションは無意味です。

区切られた識別子内部で次の文字を使うとポータビリティが壊れます:

||~ 名前 ||~ 文字 ||~ ドライバ || || backtick || \` || MySQL || || double quote || " || Oracle || || brackets || [ or ] || Access ||

次のドライバの元で識別子の区切りが一般的に正しく動作することが知られています: Mssql、Mysql、Oracle、Pgsql、SqliteとFirebird

``Doctrine_Core::ATTR_QUOTE_IDENTIFIER``オプションを使うとき、フィールドの識別子のすべては結果のSQL文において自動的にクォートされます:

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_QUOTE_IDENTIFIER, true);

結果のSQL文においてすべてのフィールド名はバッククォート演算子’`’でクォートされます(MySQL)。

SELECT * FROM sometable WHERE id = ‘123’

対照的に:

SELECT * FROM sometable WHERE id = ‘123’

ハイドレーションの上書き

デフォルトではあたかもすでに問い合わせされ修正されたオブジェクトを問い合わせしたようにDoctrineはオブジェクトでのローカルの変更を上書きするように設定されています。

$user = Doctrine_Core::getTable(‘User’)->find(1); $user->username =

‘newusername’;

上記のオブジェクトを修正したのであたかも同じデータを再度問い合わせしたように、ローカルな変更は上書きされます。

$user = Doctrine_Core::getTable(‘User’)->find(1); echo

$user->username; // データベースのオリジナルのユーザー名を出力する

``ATTR_HYDRATE_OVERWRITE``属性を使うことでこのふるまいを無効にできます:

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_HYDRATE_OVERWRITE, false);

これで上記で同じテストを実行したとしても、修正されたユーザー名は上書きされません。

テーブルクラスを設定する

``Doctrine_Core::getTable()``メソッドを使うときに返されるクラスを設定したい場合``ATTR_TABLE_CLASS``属性をセットできます。唯一の要件は``Doctrine_Table``を継承するクラスです。

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_TABLE_CLASS, ‘MyTableClass’);

``MyTableClass``は次のようになります:

class MyTableClass extends Doctrine_Table { public function myMethod()

{ // 何らかのクエリを実行し結果を返す } }

これで次のコードを実行するとき``MyTableClass``のインスタンスが返されるようになります:

$user = $conn->getTable(‘MyModel’)->myMethod();

テーブルクラスをさらにカスタマイズしたい場合それぞれのモデルごとにカスタマイズできます。 ``MyModelTable``という名前のクラスを作りオートロード可能であることを確認します。

class MyModelTable extends MyTableClass { public function

anotherMethod() { // 何らかのクエリを実行し結果を返す } }

次のコードを実行するとき``MyModelTable``のインスタンスが返されます:

echo get_class($conn->getTable(‘MyModel’)); // MyModelTable

クエリクラスを設定する

新しいクエリインスタンスを作るとき基底のクエリクラスを設定したいとき、``ATTR_QUERY_CLASS``属性を使うことができます。唯一の要件は``Doctrine_Query``クラスを継承することです。

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_QUERY_CLASS, ‘MyQueryClass’);

``MyQueryClass``は次のようになります:

class MyQueryClass extends Doctrine_Query {

}

これで新しいクエリを作ると``MyQueryClass``のインスタンスが返されるようになります:

$q = Doctrine::getTable(‘User’) ->createQuery(‘u’);

echo get_class($q); // MyQueryClass

コレクションクラスを設定する

基底クラスとテーブルクラスを設定できるので、Doctrineが使うコレクションクラスもカスタマイズできることのみに意味をなします。``ATTR_COLLECTION_CLASS``属性をセットする必要があるだけです。

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_COLLECTION_CLASS, ‘MyCollectionClass’);

``MyCollectionClass``の唯一の要件は``Doctrine_Collection``を継承しなければならないことです:

$phonenumbers = user->Phonenumber; echo get_class(phonenumbers);

// MyCollectionClass

カスケーディングセーブを無効にする

オプションとして利便性のために``ATTR_CASCADE_SAVES``属性によってデフォルトで有効になっているカスケーディングセーブオペレーションを無効にできます。この属性を``false``にするとレコードがダーティである場合のみカスケードとセーブが行われます。このことは階層において1つのレベルより深くダーティなレコードをカスケードしてセーブできないことを意味しますが、顕著なパフォーマンスの改善の効果を得られます。

$conn->setAttribute(Doctrine::ATTR_CASCADE_SAVES, false);

エクスポートする

テーブル作成用にデータベースにクラスをエクスポートする際にDoctrineにエクスポートするものを伝えるためにエクスポート属性が使われます。

何もエクスポートしたくない場合は次のように行います:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_NONE);

(制約は伴わずに)テーブルだけをエクスポートするためだけなら次のようにできます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_TABLES);

上記と同じ内容を次の構文でも実現できます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_ALL ^ Doctrine_Core::EXPORT_CONSTRAINTS);

すべて(テーブルと制約)をエクスポートするには:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_ALL);

命名規約の属性

命名規約の属性は、テーブル、インデックスとシーケンスのような要素に関連する異なるデータベースの命名に影響を及ぼします。データベースからクラスまでのスキーマをインポートするときとクラスをデータベーステーブルにエクスポートするとき、基本的にすべての命名規約属性は両方の方法で影響を及ぼします。

例えばDoctrineのインデックス用の命名規約のデフォルトは``%s_idx``です。インデックスだけでなく特別な接尾辞を設定可能で、インポートされるクラスは接尾辞を持たない対応物にマッピングされるインデックスを取得します。これはすべての命名規約属性に適用されます。

インデックス名のフォーマット

``Doctrine_Core::ATTR_IDXNAME_FORMAT``は命名規約のインデックスを変更するために使われます。デフォルトではDoctrineは``[name]_idx``のフォーマットを使用します。’ageindex’と呼ばれるインデックスの定義は実際には’ageindex_idx’に変換されます。

次のコードでインデックスの命名規約を変更できます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_IDXNAME_FORMAT, ‘%s_index’);

シーケンス名のフォーマット

``Doctrine_Core::ATTR_IDXNAME_FORMAT``と同じように、``Doctrine_Core::ATTR_SEQNAME_FORMAT``はシーケンスの命名規約を変更するために使うことができます。デフォルトではDoctrineは``[name]_seq``のフォーマットを使います。``mysequence``の名前を持つ新しいシーケンスを作ると``mysequence_seq``という名前のシーケンスに作成につながるからです。

次のコードでシーケンスの命名規約を変更できます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_SEQNAME_FORMAT, ‘%s_sequence’);

テーブル名のフォーマット

インデックスとシーケンス名のフォーマットと同じようにテーブル名のフォーマットは次のコードで変更できます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_TBLNAME_FORMAT, ‘%s_table’);

データベース名のフォーマット

インデックス、シーケンスとテーブル名のフォーマットと同じようにデータベース名のフォーマットを次のコードで変更できます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_DBNAME_FORMAT, ‘myframework_%s’);

バリデーション属性

Doctrineはバリデーションに対して完全なコントロール機能を提供します。バリデーション処理は``Doctrine_Core::ATTR_VALIDATE``でコントロールされます。

バリデーションモードはビット単位なので、``|``を使用して結合し``^``を使用して削除できます。これを行う方法は下記の例をご覧ください。

バリデーションモードの定数

||~ 名前 ||~ 説明 || || VALIDATE_NONE || バリデーション処理全体をオフに切り替える。|| || VALIDATE_LENGTHS || すべてのフィールドの長さをバリデートする。|| || VALIDATE_TYPES || すべてのフィールドの型をバリデートする。Doctrineは緩い型のバリデーションを行う。例えば‘13.3’などを含む文字列は整数としてパスしないが‘13’はパスする。|| || VALIDATE_CONSTRAINTS || notnull、emailなどのすべてのフィールド制約をバリデートする。|| || VALIDATE_ALL || すべてのバリデーションをオンにする。||

NOTE デフォルトのバリデーションは無効になっているのでデータをバリデートしたい場合有効にする必要があります。この設定を変更する方法の例のいくつかは下記で示されています。

次のコードで``Doctrine_Core::VALIDATE_ALL``属性を利用してすべてのバリデーションを有効にできます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);

次のコードで長さと型をバリデートし、制約には行わないようにDoctrineを設定できます:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_LENGTHS | Doctrine_Core::VALIDATE_TYPES);

まとめ

Doctrineを設定するために最も良く使われる属性の一部を検討してきました。今のところこれらの属性はよくわからないかもしれません。次の章を読めば必要な属性がわかります。

上記の値を変更したい属性がありましたら、これを``bootstrap.php``ファイルに追加するとコードは次のようになります:

/** * Bootstrap Doctrine.php, register autoloader and specify *

configuration attributes */

require_once(‘../doctrine/branches/1.2/lib/Doctrine.php’); spl_autoload_register(array(‘Doctrine’, ‘autoload’)); $manager = Doctrine_Manager::getInstance();

$conn = Doctrine_Manager::connection(‘sqlite::memory:’, ‘doctrine’);

$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL); $manager->setAttribute(Doctrine_Core::ATTR_EXPORT, Doctrine_Core::EXPORT_ALL); $manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

次の章に移動する準備ができました。Doctrineの[doc connections :name]に関するすべての内容を学びます。

はじめに

最初から複数の接続を扱えるようにDoctrineは設計されてきました。個別に指定しない限りDoctrineはクエリの実行には現在の接続を使います。

この章ではDoctrineの接続の作成と扱い方を示します。

接続を開く

``Doctrine_Manager``はスタティックメソッドの``Doctrine_Manager::connection()``を提供します。このメソッドは新しい接続を開きます。

この例では新しい接続を開く方法を示しています:

// test.php

// ... $conn = Doctrine_Manager::connection(‘mysql://username:password@localhost/test’, ‘connection 1’);

接続を読み取る

``Doctrine_Manager::connection()``メソッドを使用し引数を渡さない場合現在の接続が返されます:

// test.php

// ... $conn2 = Doctrine_Manager::connection();

if ($conn === $conn2) { echo ‘Doctrine_Manager::connection() returns the current connection’; }

現在の接続

現在の接続は最後に開いた接続です。次の例では``Doctrine_Manager``インスタンスから現在の接続を取得する方法が示されています:

// test.php

// ... $conn2 = Doctrine_Manager::connection(‘mysql://username2:password2@localhost/test2’, ‘connection 2’);

if ($conn2 === $manager->getCurrentConnection()) { echo ‘Current connection is the connection we just created!’; }

現在の接続を変更する

``Doctrine_Manager::setCurrentConnection()``を呼び出すことで現在の接続を変更できます。

// test.php

// ... $manager->setCurrentConnection(‘connection 1’);

echo $manager->getCurrentConnection()->getName(); // connection 1

接続を反復する

foreach句にマネージャーオブジェクトを渡すことで開いた接続をイテレートできます。``Doctrine_Manager``が特殊な``IteratorAggregate``インターフェイスを実装するのでこれは可能です。

// test.php

// ... foreach($manager as $conn) { echo $conn->getName() . “”; }

接続名を取得する

次のコードで``Doctrine_Connection``インスタンスの名前を簡単に取得できます:

// test.php

// ... $conn = Doctrine_Manager::connection();

$name = manager->getConnectionName(conn);

echo $name; // connection 1

接続を閉じる

接続を閉じたりDoctrine接続レジストリから削除のは簡単です:

// test.php

// ... $conn = Doctrine_Manager::connection();

manager->closeConnection(conn);

接続を閉じるがDoctrine接続レジストリから削除したくない場合は次のコードが利用できます:

// test.php

// ... $conn = Doctrine_Manager::connection(); $conn->close();

すべての接続を取得する

``Doctrine_Manager::getConnections()``メソッドを使用して登録されたすべての接続の配列を読み取ることができます:

// test.php

// ... $conns = manager->getConnections(); foreach (conns as $conn) { echo $conn->getName() . “”; }

上記のコードは初期の頃に``Doctrine_Manager``オブジェクトをイテレートすることと同じです。再度掲載します:

// test.php

// ... foreach ($manager as $conn) { echo $conn->getName() . “”; }

接続をカウントする

``Countable``インターフェイスを実装するので``Doctrine_Manager``オブジェクトから接続数を取得できます。

// test.php

// ... num = count(manager);

echo $num;

上記のコードは次のコードと同じです:

// test.php

// ... $num = $manager->count();

データベースの作成と削除

Doctrineを使用して接続を作成するとき、これらの接続に関連するデータベースの作成と削除する機能が簡単に手に入ります。

``Doctrine_Manager``もしくは``Doctrine_Connection``クラスで提供されるメソッドを使うことで簡単にできます。

次のコードではインスタンス化された接続をすべてイテレートして``dropDatabases()``/``createDatabases()``メソッドを呼び出します:

// test.php

// ... $manager->createDatabases();

$manager->dropDatabases();

特定の接続に対してデータベースを削除/作成する

接続インスタンスで``dropDatabase()``/``createDatabase()``メソッドを呼び出すことで特定の``Doctrine_Connection``インスタンス用のデータベースを削除もしくは作成できます:

// test.php

// ... $conn->createDatabase();

$conn->dropDatabase();

カスタム接続を書く

ときには独自のカスタム接続クラスを作りこれらを活用する機能が必要になることがあります。mysqlを拡張するもしくは独自のデータベース型を独自に書くことが必要になることがあります。これはいくつかのクラスを書き接続型をDoctrineに登録することで可能です。

カスタム接続を作成するにはまず次のクラスを書く必要があります。

class Doctrine_Connection_Test extends Doctrine_Connection_Common {

}

class Doctrine_Adapter_Test implements Doctrine_Adapter_Interface { // ... all the methods defined in the interface }

ではこれらをDoctrineに登録します:

// bootstrap.php

// ... $manager->registerConnectionDriver(‘test’, ‘Doctrine_Connection_Test’);

次のような少しの変更でこれが実現されます:

$conn =

$manager->openConnection(‘test://username:password@localhost/dbname’);

接続にどんなクラスが使われるのか確認すればそれらが上記で定義したクラスであることがわかります。

echo

get_class(conn); // Doctrine_Connection_Test echo get_class(conn->getDbh()); // Doctrine_Adapter_Test

まとめ

Doctrineの接続すべてを学びましたので[doc introduction-to-models :name]の章でモデルに直に飛び込む準備ができました。Doctrineのモデルも少し学びました。少し遊んで最初のテストモデルを作成しDoctrineが提供するマジックを見ることになります。

はじめに

最も低いレベルで、DoctrineはPHPクラスの一式でデータベーススキーマを表現します。これらのクラスはスキーマとモデルの振る舞いを定義します。

ウェブアプリケーションのユーザーを表す基本モデルは次のようになります。

class User extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp()
{
    $this->actAs('Timestampable');
}

}

NOTE 実際には上記のクラスは使いません。これらは単なる例です。この章の後の方で既存のデータベーステーブルから最初のクラスの定義を生成します。

``Doctrine_Record``のそれぞれの子クラスは``setTableDefinition()``と``setUp()``メソッドを持ちます。``setTableDefinition()``メソッドはカラム、インデックスとテーブルのスキーマに関するその他の情報を定義するためにあります。``setUp()``メソッドはビヘイビアと``Doctrine_Record``子クラスの間のリレーションを定義するためにあります。上記の例ではautomagic機能を追加するTimestampableビヘイビアを有効にしています。[doc defining-models :name]の章でこれらのメソッドすべてが使われている例を学びます。

モデルを生成する

Doctrineは使い始めを楽にするためにこれらのクラスを生成する方法を提供します。

NOTE 既存のデータベースの生成は始めるための利便性のみしか意味しません。データベースを生成した後で必要に応じて調整と整頓をしなければなりません。
既存のデータベース

よくある事例はORMにアクセスするデータベースとコードはより巨大/複雑になることです。SQLを手で書くよりも信頼のおけるツールが必要です。

Doctrineは既存のデータベースから``Doctrine_Record``クラスを生成する機能をサポートします。ドメインモデル用にすべての``Doctrine_Record``クラスを手動で書く必要はありません。

最初のインポートを行う

``doctrine_test``という名前のデータベースと``user``という名前の単独のテーブルがある場合を考えてみましょう。``user``テーブルは次のSQL文で作成されます:

CREATE TABLE user ( id bigint(20) NOT NULL auto_increment, first_name

varchar(255) default NULL, last_name varchar(255) default NULL, username varchar(255) default NULL, password varchar(255) default NULL, type varchar(255) default NULL, is_active tinyint(1) default ‘1’, is_super_admin tinyint(1) default ‘0’, created_at TIMESTAMP, updated_at TIMESTAMP, PRIMARY KEY (id) ) ENGINE=InnoDB

これを``Doctrine_Record``クラスに変換することを考えます。Doctrineによってこれは簡単です![doc getting-started :name]章で作成したテストスクリプトを覚えていますか?これを利用してモデルを生成します。

最初にSQLiteのメモリの代わりにMySQLデータベースを使うために``bootstrap.php``を修正する必要があります:

// bootstrap.php

// ... $conn = Doctrine_Manager::connection(‘mysql://root:mys3cr3et@localhost/doctrine_test’, ‘doctrine’); // ...

NOTE データベースがまだ存在せず接続ユーザーがデータベースを作成するパーミッションを持つ場合データベースを作成するために``$conn->createDatabase()``メソッドを使うことができます。テーブルを作成するために``CREATE TABLE``文を使用します。

生成クラスを置く場所が必要なので``doctrine_test``ディレクトリの中で``models``という名前のディレクトリを作りましょう:

$ mkdir doctrine_test/models

モデルクラスを生成するには``test.php``スクリプトにコードを追加することだけが必要です:

// test.php

// ... Doctrine_Core::generateModelsFromDb(‘models’, array(‘doctrine’), array(‘generateTableClasses’ => true));

NOTE ``generateModelsFromDb``メソッドは1つのパラメータのみを必要としこのパラメータはディレクトリです(生成レコードが書き込まれるディレクトリ)。2番目の引数はモデルを生成するためのデータベースの接続名の配列で、3番目はモデルのビルド用のオプションの配列です。

これだけです!``doctrine_test/models/generated``ディレクトリで``BaseUser.php``という名前のファイルがあります。ファイルは次のようになります:

// models/generated/BaseUser.php

/** * 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)); } }

``doctrine_test/models``ディレクトリで``User.php``という名前のファイルもあります。ファイルは次のようになります:

// models/User.php

/** * This class has been auto-generated by the Doctrine ORM Framework */ class User extends BaseUser {

}

Doctrineは``doctrine_test/models/UserTable.php``で``Doctrine_Table``スケルトンクラスを自動生成します。``true``の値を持つ``generateTableClasses``オプションを渡したからです。ファイルは次のようになります:

// models/UserTable.php

/** * This class has been auto-generated by the Doctrine ORM Framework */ class UserTable extends Doctrine_Table {

}

モデルの機能をカスタマイズするために``User``と``UserTable``クラスの中でカスタムメソッドを設置できます。下記のコードは例です:

// models/User.php

// ... class User extends BaseUser { public function setPassword($password) { return this->_set('password', md5(password)); } }

適切に動作させるために``password``アクセサをオーバーライドするには``bootstrap.php``ファイルで``auto_accessor_override``属性を有効にしなければなりません。

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE, true);

ユーザーパスワードを設定しようとするとmd5に暗号化されます。最初に``models``ディレクトリからモデルをオートロードするために次のように``bootstrap.php``ファイルを修正する必要があります:

// bootstrap.php

// ... Doctrine_Core::loadModels(‘models’);

NOTE モデルのロードはこの章の[doc introduction-to-models:autoloading-models :name]セクションで説明されます。

``User``モデルに行った変更をテストするコードを含めるために``test.php``を修正します:

// test.php

// ...

$user = new User(); $user->username = ‘jwage’; $user->password = ‘changeme’;

echo $user->password; // changemeではなくmd5ハッシュを出力する

ターミナルから``test.php``を実行するとき次の内容が表示されます:

$ php test.php 4cb9c8a8048fd02294477fcb1a41191a

``UserTable``クラスに追加するカスタムメソッドの例は次の通りです:

// models/UserTable.php

// ... 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(); } }

カスタムの``Doctrine_Table``クラスをロードするには``bootstrap.php``ファイルで``autoload_table_classes``属性を有効にしなければなりません。

// boostrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);

``UserTable``インスタンスを扱っているときにこのメソッドにアクセスできます:

// test.php

// ... $usersCreatedToday = Doctrine_Core::getTable(‘User’)->getCreatedToday();

スキーマファイル

代わりにYAMLスキーマファイルでモデルを管理してそれらのファイルからPHPクラスを生成できます。最初に作業をやりやすくするために手元にある既存のモデルからYAMLスキーマファイルを生成しましょう。次のコードを内部に取り込むために``test.php``を変更します:

// test.php

// ...

Doctrine_Core::generateYamlFromModels(‘schema.yml’, ‘models’);

``test.php``スクリプトを実行します:

$ php test.php

``doctrine_test``ディレクトリのrootに作成された``schema.yml``という名前のファイルを見ます。内容は次の通りです:

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)

有効なYAMLスキーマファイルが手元にあるので、ここからスキーマを維持管理してPHPクラスを生成できます。``generate.php``という名前の新しいPHPスクリプトを作りましょう。このスクリプトはすべてを再生成しスクリプトが呼び出されるたびにデータベースを再インスタンス化します:

// generate.php

require_once(‘bootstrap.php’);

Doctrine_Core::dropDatabases(); Doctrine_Core::createDatabases(); Doctrine_Core::generateModelsFromYaml(‘schema.yml’, ‘models’); Doctrine_Core::createTablesFromModels(‘models’);

``schema.yml``を変更してターミナルから次のコマンドを実行してモデルを再生成できます:

$ php generate.php

YAMLスキーマファイルをセットアップしてスキーマファイルを再生成したのでファイルの内容を少し整頓してDoctrineの力を利用しましょう:

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)

NOTE 変更の注意点:

1.) デフォルトなので明示的な``tableName``の定義を削除した。 2.) ``Timestampable``ビヘイビアを添付した。 3.) 主キーが定義されていない場合自動的に追加されるので``id``カラムを削除した。 4.) ``Timestampable``ビヘイビアで自動的に処理できるので``updated_at``と``created_at``カラムを削除した。

デフォルトを利用することでYAMLはきれいになりコアのビヘイビアを活用するほど自分自身で行わなければならない作業は少なくなります。

YAMLスキーマファイルからモデルを再生成します:

$ php generate.php

[doc yaml-schema-files 専用の章]でYAMLスキーマファイルに関する詳しい内容を学びます。

モデルを書く

オプションとしてすべてのコンビニエンスメソッドをスキップして独自のPHPコードだけでモデルを書くことができます。[doc defining-models :name]の章でモデルの構文のすべてを学びます。

モデルをオートロードする

Doctrineはモデルをロードするための方法を2つ:コンサーバティブ(遅延)ロード、アグレッシブロードを提供します。コンサーバティブロードは初期にはPHPファイルを必要としません。代わりにクラスの名前へのパスをキャッシュしこのパスはspl_autoload_register()で初期に登録した``Doctrine_Core::autoload()``で使われます。両方のモデルのロード方法を利用した例は次の通りです。

コンサーバティブ

コンサーバティブ(conservative - 慎重な・控えめな)なモデルロードは本番環境では理想的なモデルのロードメソッドになりつつあります。このメソッドはモデルのロードが実行されるときすべてのモデルをロードする代わりに遅延ロードします。

コンサーバティブなモデルロードはそれぞれが1つのクラスを持ち、ファイルの名前はクラスから名付けなければなりません。例えば、``User``というクラスがある場合、``User.php``という名前のファイルに含まれなければなりません。

コンサーバティブなモデルロードを使うにはモデルロードの属性をコンサーバティブにする必要があります:

$manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING,

Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

NOTE 以前のステップで``bootstrap.php``ファイルでこの変更をすでに行っているので再度同じ変更する必要はありません。

``Doctrine_Core::loadModels()``の機能を使うとき見つかるすべてのクラスは内部でキャッシュされるのでオートローダーは後でそれらを読み込むことができます。

Doctrine_Core::loadModels(‘models’);

新しいクラス、例えば``User``クラスをインスタンス化するとき、オートローダーが起動しクラスが読み込まれます。

// Doctrine_Core::autoload()の呼び出しが行われクラスが読み込まれる

$user = new User();

上記でクラスをインスタンス化することで``Doctrine_Core::autoload()``の呼び出しが行われ``Doctrine_Core::loadModels()``のコールで見つかったクラスが読み込まれ利用可能になります。

NOTE 必要がないときにモデルをクラスをすべて読み込むと不要なオーバーヘッドが生じるので、必要なときだけ読み込みたい場合、とりわけ本番環境でコンサーバティブなモデルロードは推奨されます。
アグレッシブ

アグレッシブ(aggressive - 積極的な)なモデルロードはデフォルトのモデルロードメソッドでとても便利です。``.php``拡張子を持つファイルをすべて探し読み込みます。Doctrineは継承を満たすことができないで、モデルが別のクラスを継承する場合、正しい順序でそれらのクラスを読み込むことはできません。なのですべての依存関係がそれぞれのクラスで満たされるようにするのはあなたの仕事です。

アグレッシブなモデルロードではファイルごとに複数のクラスを用意しファイルの名前はファイル内部のクラスの名前と関連する必要はありません。

アグレッシブなモデルロードの欠点はすべてのPHPファイルがすべてのリクエストに含まれるので、たくさんのモデルがある場合コンサーバティブなモデルロードを使うことをお勧めします。

アグレッシブなモデルロードを使うにはモデルロード属性をアグレッシブに設定する必要があります:

$manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING,

Doctrine_Core::MODEL_LOADING_AGGRESSIVE);

TIP アグレッシブなモデルロードはデフォルトのロード属性なので使う場合は明示的に設定する必要はありません。

``Doctrine_Core::loadModels()``の機能を使うとき見つかるすべてのクラスは直ちに読み込まれます:

Doctrine_Core::loadModels(‘/path/to/models’);

まとめ

この章はこれまでで最もハードだと思いますが良い内容です。モデルの使い方、既存のデータベースからモデルを生成する方法、独自のモデルを書く方法、とモデルをYAMLスキーマファイルとして管理する方法を少し学びました。モデルディレクトリからモデルをロードする機能を実装するためにDoctrineのテスト環境も修正しました。

Doctrineのモデルのトピックは非常に大きいので開発者がすべての情報を吸収しやすいように章を3つのピースに分割します。[doc defining-models 次の章]においてモデルを定義するために使うAPIに入ります。

以前述べたように、Doctrineの最も低いレベルにおいてスキーマはデータベーステーブル用のスキーマメタデータをマッピングするPHPクラスの一式で表現されます。

この章ではPHPコードを使用してスキーマ情報をマッピングする方法を詳しく説明します。

カラム

データベースの互換性の問題の1つは多くのデータベースにおいて返されるクエリの結果セットが異なることです。MySQL はフィールドの名前はそのままにします。このことは``”SELECT myField FROM ...”``形式のクエリを発行する場合、結果セットは``myField``のフィールドを含むことを意味します。

不幸にして、これはMySQLとその他のいくつかのデータベースだけの挙動です。例えばPostgresはすべてのフィールド名を小文字で返す一方でOracleは大文字ですべてのフィールド名を返します。”だから何?Doctrineを使う際にこれがどのような方法で影響を及ぼすの?”、と疑問に思うかもしれません。幸いにして、この問題を悩む必要はまったくありません。

Doctrineはこの問題を透過的に考慮します。Record派生クラスを定義し``myField``という名前のフィールドを定義する場合、MySQLもしくはPostgresもしくはOracleその他を使おうが、:code:`record->myField (もしくは```record[‘myField’]``、好きな方で)を通してアクセスできることを意味します。

要するに: under_scores(アンダースコア)、camelCase(キャメルケース)もしくは望む形式を使用してフィールドを好きなように名付けることができます。

NOTE Doctrineにおいてカラムとカラムのエイリアスは大文字と小文字を区別します。DQLクエリでカラムを使用するとき、カラム/フィールドの名前はモデルの定義のケースにマッチしなければなりません。
カラムのエイリアス

Doctrineはカラムのエイリアスを定義する方法を提供します。これはデータベースのロジックからアプリケーションのロジックを分離するのを維持したい場合にとても役に立ちます。例えば データベースフィールドの名前を変更したい場合、アプリケーションで変更する必要のあるのはカラムの定義だけです。

// models/Book.php

class Book extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘bookTitle as title’, ‘string’); } }

下記のコードはYAMLフォーマットのサンプルです。[doc yaml-schema-files :name]の章でYAMLの詳しい情報を読むことができます:

# schema.yml

Book: columns: bookTitle: name: bookTitle as title type: string

Now データベースのカラムはbookTitleという名前ですがtitleを使用してオブジェクトのプロパティにアクセスできます。

// test.php

// ... $book = new Book(); $book->title = ‘Some book’; $book->save();

デフォルトの値

Doctrineはすべてのデータ型のデフォルト値をサポートします。デフォルト値がレコードのカラムに付属するとき2つのことを意味します。まずこの値はすべての新しく作成されたRecordに添付されDoctrineがデータベースを作成するときにcreate tableステートメントにデフォルト値を含めます。

// models/generated/BaseUser.php

class User extends BaseUser { public function setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255, array(‘default’ => ‘default username’));

    // ...
}

// ...

}

YAMLフォーマットのサンプルコードは次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細情報を読むことができます:

# schema.yml

User: # ... columns: username: type: string(255) default: default username # ...

真新しいUserレコードで名前を表示するときデフォルト値が表示されます:

// test.php

// ... $user = new User(); echo $user->username; // デフォルトのユーザー名

データの型
はじめに

データベースフィールドに保存できる情報用にすべてのDBMSはデータの型の複数の選択肢を提供します。しかしながら、利用可能なデータ型の一式はDBMSによって異なります。

DoctrineによってサポートされるDBMSでインターフェイスを簡略化するために、内在するDBMSにおいてアプリケーションが個別にアクセスできるデータ型の基本セットが定義されました

Doctrineのアプリケーションプログラミングインターフェイスはデータベースオプションを管理する際にデータ型のマッピングを考慮します。それぞれのドライバを使用して内在するDBMSに送るかつDBMSから受け取るものを変換することも可能です。

次のデータ型の例ではDoctrineの``createTable()``メソッドを使います。データ型セクションの最後の配列の例では選んだDBMSでポータブルなテーブルを作成するために``createTable()``メソッドを使うことがあります(何のDBMSが適切にサポートされているか理解するためにDoctrineのメインドキュメントを参照してくださるようお願いします)。次の例ではインデックスの作成と維持はカバーされないことも注意してください。この章はデータ型と適切な使い方のみを考慮します。

アプリケーションレベルでバリデートされた長さ(Doctrineバリデータでバリデートされた長さ)と同様に、カラムの長さはデータベースレベルで影響があることを気を付けてください。

例 1. ‘string’型と長さ3000の’content’という名前のカラムはデータベースレベルの長さ4000を持つ’TEXT’データベースの型になります。しかしながらレコードがバリデートされるとき最大長が3000である’content’カラムを持つことのみ許可されます。

例 2. 多くのデータベースでは’integer’型と長さ1を持つカラムは’TINYINT’になります。

一般的に Doctrineは指定された長さによってどのinteger/string型を使うのか知っているほど賢いです。

型修飾子

Doctrine APIの範囲内で オプションのテーブルデザインに役立つように設計された修飾子が少しあります:

  • notnull修飾子
  • length修飾子
  • default修飾子
  • フィールド定義用のunsigned修飾子、integerフィールド型に対して すべてのDBMSはこの修飾子をサポートしません。
  • collation修飾子(すべてのドライバでサポートされない)
  • フィールド定義用の固定長修飾子

上記の内容に基づいて話しを進めると、特定の使い方のシナリオ用の特定のフィールドの型を作成するために、修飾子がフィールド定義を変更することが言えます。DBMSのフィールド値の定義によって、フィールド上でデフォルトのDBMS NOT NULL Flagをtrueもしくはfalseに設定するためにnotnull修飾子は次の方法で使われます: PostgreSQLにおいて”NOT NULL”の定義は”NOT NULL”に設定される一方で、(例えば)MySQLでは”NULL”オプションは”NO”に設定されます。”NOT NULL”フィールド型を定義するために、定義配列に追加パラメータを追加するだけです(例は次のセクションを参照)。

‘sometime’ = array( ‘type’ => ‘time’, ‘default’ => ‘12:34:05’,

‘notnull’ => true, ),

上記の例を利用することで、デフォルトのフィールド演算子も研究できます。フィールド用のデフォルト値はnotnull演算子と同じ方法で設定されます。この値はDBMSがテキストフィールド用にサポートする文字集合、フィールドのデータ型用の他の有効な値に設定されます。上記の例において、”Time”データ型に対して有効な時間である‘12:34:05’を指定しました。datetimeと同じく日付と時間を設定するとき、調べて選択したDBMSのエポックの範囲に収まるようにすべきであることを覚えておいてください。さもなければエラーを診断するのが困難な状況に遭遇します!

‘sometext’ = array( ‘type’ => ‘string’, ‘length’ => 12, ),

上記の例ではデータベースのテーブルで長さ12のフィールドを変更する文字が作られます。長さの定義が省略される場合、Doctrineは指定されたデータ型で許容される最大長を作成されます。これはフィールドの型とインデックス作成において問題を引き起こす可能性があります。ベストプラクティスはすべてもしくは大抵のフィールドに対して長さを定義することです。

論理型

論理型は0か1の2つの値のどちらかだけを表します。効率性の観点からDBMSドライバの中には単独の文字のテキストフィールドで整数型を実装するものがあるのでこれらの論理型を整数型として保存されることを想定しないでください。この型のフィールドに割り当てできる3番目の利用可能な値としてnullを使うことで三値論理は可能です。

NOTE 次のいくつかの例では使ったり試したりすることを想定していません。これらは単にPHPコードもしくはYAMLスキーマファイルを利用してDoctrineの異なるデータ型を使う方法を示すことだけを目的としています。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘booltest’, ‘boolean’); } }

YAMLフォーマットでの同じ例です。[doc yaml-schema-files :name]の章でYAMLの詳細内容を見ることができます:

Test: columns: booltest: boolean
整数型

整数型はPHPの整数型と同じです。それぞれのDBMSが扱える大きさの整数型の値を保存します。

オプションとしてこの型のフィールドは符号なしの整数として作成されますがすべてのDBMSはこれをサポートしません。それゆえ、このようなオプションは無視されることがあります。本当にポータルなアプリケーションはこのオプションの利用可能性に依存すべきではありません。

整数型はカラムの長さによって異なるデータベースの型にマッピングされます。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘integertest’, ‘integer’, 4, array( ‘unsigned’ => true ) ); } }

YAMLフォーマットでの例です。[doc yaml-schema-files :name]の章っでYAMLの詳細情報を読むことができます:

Test: columns: integertest: type: integer(4) unsigned: true
浮動小数点型

浮動小数点のデータ型は10進法の浮動小数点数を保存できます。このデータ型は高い精度を必要としない大きなスケールの範囲の数値を表現するのに適しています。スケールと精度に関してデータベースに保存される値の制限は使用されるDBMSに依存します。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘floattest’, ‘float’); } }

下記のコードはYAMLフォーマットでの例です。[doc yaml-schema-files :name]の章でYAMLの詳細情報を読むことができます:

Test: columns: floattest: float
小数型

小数型のデータは固定精度の小数を保存できます。このデータ型は高い正確度と精度を必要とする数値を表現するのに適しています。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘decimaltest’, ‘decimal’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を学ぶことができます:

Test: columns: decimaltest: decimal

他のカラムの``length``を設定するように小数の長さを指定することが可能で3番目の引数でオプションとして``scale``を指定できます:

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘decimaltest’, ‘decimal’, 18, array( ‘scale’ => 2 ) ); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細情報をみることができます:

Test: columns: decimaltest: type: decimal(18) scale: 2
文字列型

テキストデータ型では長さに対して2つのオプションが利用可能です: 1つは明示的に制限された長さでもう一つはデータベースが許容する限りの大きさの未定義の長さです。

効率の点で制限オプション付きの長さは大抵の場合推奨されます。未定義の長さオプションはとても大きなフィールドを許可しますがインデックスとnullの利用を制限することがあり、その型のフィールド上でのソートを許可しません。

この型のフィールドは8ビットの文字を扱うことができます。文字列の値をこの型に変換することでドライバはDBMSで特別な意味を持つ文字のエスケープを考慮します。

デフォルトではDoctrineは可変長の文字型を使用します。固定長の型が使われる場合、fixed修飾子を通してコントロールできます。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘stringtest’, ‘string’, 200, array( ‘fixed’ => true ) ); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細情報を見ることができます:

Test: columns: stringtest: type: string(200) fixed: true
配列

これはPHPの’array’型と同じです。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘arraytest’, ‘array’, 10000); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細情報を見ることができます:

Test: columns: arraytest: array(10000)
オブジェクト

Doctrineはオブジェクトをカラム型としてサポートします。基本的にオブジェクトをフィールドに設定可能でDoctrineはそのオブジェクトのシリアライズ/アンシリアライズを自動的に処理します。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘objecttest’, ‘object’); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細情報を読むことができます:

Test: columns: objecttest: object

NOTE 配列とオブジェクト型はデータベースで永続化するときはデータをシリアライズしデータベースから引き出すときはデータをアンシリアライズします
blob

blob(Binary Large OBject)データ型は、通常はファイルに保存されるデータのようにテキストフィールドに大きすぎる未定義の長さのデータを保存することを意味します。

内在するDBMSが”全文検索”として知られる機能をサポートしない限りblobフィールドはエリーの検索句(WHERE)のパラメータを使用することを意味しません。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘blobtest’, ‘blob’); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: blobtest: blob
clob

clob (Character Large OBject)データ型は、通常はファイルに保存されるデータのように、テキストフィールドで保存するには大きすぎる未定義の長さのデータを保存することを意味します。

blogフィールドがデータのすべての型を保存するのが想定されているのに対してclobフィールドは印字可能なASCII文字で構成されるデータのみを保存することを想定しています。

内在するDBMSが”全文検索”として知られる機能をサポートしない限りclobフィールドはクエリ検索句(WHERE)のパラメータとして使われることが想定されています。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘clobtest’, ‘clob’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: clobtest: clob
timestamp

timestampデータ型は日付と時間のデータ型の組み合わせに過ぎません。timestamp型の値の表記は日付と時間の文字列の値は1つのスペースで連結することで実現されます。それゆえ、フォーマットのテンプレートは``YYYY-MM-DD HH:MI:SS``です。表される値は日付と時間データ型で説明したルールと範囲に従います。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘timestamptest’, ‘timestamp’); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: timestamptest: timestamp
time

timeデータ型はその日の与えられた瞬間の時間を表します。DBMS独自の時間の表記もISO-8601標準に従ってテキストの文字列を使用することで実現できます。

日付の時間用にISO-8601標準で定義されたフォーマットはHH:MI:SSでHHは時間で00から23まででMIとSSは分と秒で00から59までです。時間、分と秒は10より小さな数値の場合は左側に0が詰められます。

DBMSの中にはネイティブで時間フォーマットをサポートするものがありますが、DBMSドライバの中にはこれらを整数もしくはテキストの文字列として表現しなければならないものがあります。ともかく、この型のフィールドによるソートクエリの結果と同じように時間の値の間で比較することは常に可能です。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘timetest’, ‘time’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: timetest: time
date

dateデータ型は年、月と日にちのデータを表します。DBMS独自の日付の表記はISO-8601標準の書式のテキスト文字列を使用して実現されます。

日付用にISO-8601標準で定義されたフォーマットはYYYY-MM-DDでYYYYは西暦の数字(グレゴリオ暦)、MMは01から12までの月でDDは01か31までの日の数字です。10より小さい月の日にちの数字には左側に0が追加されます。

DBMSの中にはネイティブで日付フォーマットをサポートするものがありますが、他のDBMSドライバではこれらを整数もしくはテキストの値として表現しなければならないことがあります。どの場合でも、この型のフィールドによるソートクエリの結果によって日付の間の比較は常に可能です。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘datetest’, ‘date’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: datetest: date
enum

Doctrineはunifiedなenum型を持ちます。カラムに対して可能な値は``Doctrine_Record::hasColumn()``でカラム定義に指定できます。

NOTE DBMSに対してネイティブのenum型を使用したい場合次の属性を設定しなければなりません:

$conn->setAttribute(Doctrine_Core::ATTR_USE_NATIVE_ENUM, true);

次のコードはenumの値を指定する方法の例です:

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘enumtest’, ‘enum’, null, array(‘values’ => array(‘php’, ‘java’, ‘python’)) ); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: enumtest: type: enum values: [php, java, python]
gzip

gzipデータ型は存続するときに自動的に圧縮取得されたときに解凍される以外は文字列と同じです。ビットマップ画像など、大きな圧縮率でデータを保存するときにこのデータ型は役に立ちます。

class Test extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘gziptest’, ‘gzip’); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Test: columns: gziptest: gzip

NOTE 内部ではgzipカラム型の内容の圧縮と解凍を行うために[http://www.php.net/gzcompress 圧縮]系のPHP関数が使われています。

次の定義を考えましょう:

class Example extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘id’, ‘string’, 32, array( ‘type’ => ‘string’, ‘fixed’ => 1, ‘primary’ => true, ‘length’ => ‘32’ ) );

    $this->hasColumn('someint', 'integer', 10, array(
            'type' => 'integer',
            'unsigned' => true,
            'length' => '10'
        )
    );

    $this->hasColumn('sometime', 'time', 25, array(
            'type' => 'time',
            'default' => '12:34:05',
            'notnull' => true,
            'length' => '25'
        )
    );

    $this->hasColumn('sometext', 'string', 12, array(
            'type' => 'string',
            'length' => '12'
        )
    );

    $this->hasColumn('somedate', 'date', 25, array(
            'type' => 'date',
            'length' => '25'
        )
    );

    $this->hasColumn('sometimestamp', 'timestamp', 25, array(
            'type' => 'timestamp',
            'length' => '25'
        )
    );

    $this->hasColumn('someboolean', 'boolean', 25, array(
            'type' => 'boolean',
            'length' => '25'
        )
    );

    $this->hasColumn('somedecimal', 'decimal', 18, array(
            'type' => 'decimal',
            'length' => '18'
        )
    );

    $this->hasColumn('somefloat', 'float', 2147483647, array(
            'type' => 'float',
            'length' => '2147483647'
        )
    );

    $this->hasColumn('someclob', 'clob', 2147483647, array(
            'type' => 'clob',
            'length' => '2147483647'
        )
    );

    $this->hasColumn('someblob', 'blob', 2147483647, array(
            'type' => 'blob',
            'length' => '2147483647'
        )
    );
}

}

YAMLフォーマットでの同じ例です。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Example: tableName: example columns: id: type: string(32) fixed: true

primary: true someint: type: integer(10) unsigned: true sometime: type: time(25) default: ‘12:34:05’ notnull: true sometext: string(12) somedate: date(25) sometimestamp: timestamp(25) someboolean: boolean(25) somedecimal: decimal(18) somefloat: float(2147483647) someclob: clob(2147483647) someblob: blob(2147483647)

上記の例はPgsqlで次のテーブルが作成します:

||~ カラム ||~ 型 || || id || character(32) || || someint || integer || || sometime || タイムゾーンなしの``time`` || || sometext || character``もしくは``varying(12) || || somedate || date || || sometimestamp || タイムゾーンなしの``timestamp`` || || someboolean || boolean || || somedecimal || numeric(18,2) || || somefloat || double``の精度 \|\| \|\| ``someclob || text || || someblob || bytea ||

Mysqlではスキーマは次のデータベーステーブルを作成します:

||~ フィールド ||~ 型 || || id || char(32) || || someint || integer || || sometime || time || || sometext || varchar(12) || || somedate || date || || sometimestamp || timestamp || || someboolean || tinyint(1) || || somedecimal || decimal(18,2) || || somefloat || double || || someclob || longtext || || someblob || longblob ||

リレーション

はじめに

Doctrineにおいてすべてのレコードのリレーションは``Doctrine_Record::hasMany``、``Doctrine_Record::hasOne``メソッドで設定されます。Doctrineはほとんどの種類のデータベースリレーションをサポートします from 一対一のシンプルな外部キーのリレーションから自己参照型のリレーションまでサポートします。

カラムの定義とは異なり``Doctrine_Record::hasMany``と``Doctrine_Record::hasOne``メソッドは``setUp()``と呼ばれるメソッドの範囲内で設置されます。両方のメソッドは2つの引数を受け取ります: 最初の引数はクラスの名前とオプションのエイリアスを含む文字列で、2番目の引数はリレーションのオプションで構成される配列です。オプションの配列は次のキーを含みます:

||~ 名前 ||~ オプション ||~ 説明 || || local || No || リレーションのローカルフィールド。ローカルフィールドはクラスの定義ではリンク付きのフィールド。 || || foreign || No || リレーションの外部フィールド。外部フィールドはリンク付きのクラスのリンク付きフィールドです。|| || refClass || Yes || アソシエーションクラスの名前。これは多対多のアソシエーションに対してのみ必要です。|| || owningSide|| Yes || 所有側のリレーションを示すには論理型のtrueを設定します。所有側とは外部キーを所有する側です。2つのクラスの間のアソシエーションにおいて所有側は1つのみです。Doctrineが所有側を推測できないもしくは間違った推測をする場合このオプションが必須であることに注意してください。’local’と’foreign’の両方が識別子(主キー)の一部であるときこれが当てはまります。この方法で所有側を指定することは害になることはありません。|| || onDelete || Yes || Doctrineによってテーブルが適用されるときに``onDelete``整合アクションが外部キー制約に適用されます。 || || onUpdate || Yes || Doctrineによってテーブルが作成されたときに``onUpdate``整合アクションが外部キー制約に適用されます。|| || cascade || Yes || オペレーションをカスケーディングするアプリケーションレベルを指定する。現在削除のみサポートされる ||

最初の例として、``Forum_Board``と``Forum_Thread``の2つのクラスがあるとします。リレーションが一対多なので、``Forum_Board``は多くの``Forum_Threads``を持ちます。リレーションにアクセスする際に``Forum_``を書きたくないので、リレーションのエイリアスを使用しエイリアスの``Threads``を使用します。

最初に``Forum_Board``クラスを見てみましょう。これはカラム: 名前, 説明を持ち主キーを指定していないので、Doctrineはidカラムを自動作成します。

``hasMany()``メソッドを使用することで``Forum_Thread``クラスへのリレーションを定義します。localフィールドがboardクラスの主キーである一方でforeignフィールドが``Forum_Thread``クラスの``board_id``フィールドです。

// models/Forum_Board.php

class Forum_Board extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 100); $this->hasColumn(‘description’, ‘string’, 5000); }

public function setUp()
{
    $this->hasMany('Forum_Thread as Threads', array(
            'local' => 'id',
            'foreign' => 'board_id'
        )
    );
}

}

NOTE asキーワードが使われていることに注目してください。このことは``Forum_Board``が``Forum_Thread``に定義された多数のリレーションを持ちますが``Threads``のエイリアスが設定されることを意味します。

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Forum_Board: columns: name: string(100) description: string(5000)

``Forum_Thread``クラスの内容を少しのぞいて見ましょう。カラムの内容は適当ですが、リレーションの定義方法に注意をはらってください。それぞれの``Thread``は1つの``Board``のみを持つことができるので``hasOne()``メソッドを使っています。またエイリアスの使い方とlocalカラムが``board_id``である一方で外部カラムは``id``カラムであることに注目してください。

// models/Forum_Thread.php

class Forum_Thread extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’); $this->hasColumn(‘board_id’, ‘integer’); $this->hasColumn(‘title’, ‘string’, 200); $this->hasColumn(‘updated’, ‘integer’, 10); $this->hasColumn(‘closed’, ‘integer’, 1); }

public function setUp()
{
    $this->hasOne('Forum_Board as Board', array(
            'local' => 'board_id',
            'foreign' => 'id'
        )
    );

    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# 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

これらのクラスを使い始めることができます。プロパティに既に使用した同じアクセサはリレーションに対してもすべて利用できます。

最初に新しいboardを作りましょう:

// test.php

// ... $board = new Forum_Board(); $board->name = ‘Some board’;

boardの元で新しいthreadを作りましょう:

// test.php

// ... $board->Threads[0]->title = ‘new thread 1’; $board->Threads[1]->title = ‘new thread 2’;

それぞれの``Thread``はそれぞれのユーザーに関連付ける必要があるので新しい``User``を作りそれぞれの``Thread``に関連付けましょう:

$user = new User(); $user->username = ‘jwage’; $board->Threads[0]->User

= $user; $board->Threads[1]->User = $user;

これですべての変更を1つの呼び出しで保存できます。threadsと同じように新しいboardを保存します:

// test.php

// ... $board->save();

上記のコードを使うときに作成されるデータ構造を見てみましょう。投入したばかりのオブジェクトグラフの配列を出力するために``test.php``にコードを追加します:

print_r($board->toArray(true));

Tip

レコードのデータを簡単にインスペクトできるように``Doctrine_Record::toArray()``は``Doctrine_Record``インスタンスのすべてのデータを取り配列に変換します。これはリレーションを含めるかどうかを伝える``$deep``という名前の引数を受け取ります。この例では``Threads``のデータを含めたいので{[true]}を指定しました。

ターミナルで``test.php``を実行すると次の内容が表示されます:

$ php test.php Array ( [id] => 2 [name] => Some board [description] =>

[Threads] => Array ( [0] => Array ( [id] => 3 [user_id] => 1 [board_id] => 2 [title] => new thread 1 [updated] => [closed] => [User] => Array ( [id] => 1 [is_active] => 1 [is_super_admin] => 0 [first_name] => [last_name] => [username] => jwage [password] => [type] => [created_at] => 2009-01-20 16:41:57 [updated_at] => 2009-01-20 16:41:57 )

        )

    [1] => Array
        (
            [id] => 4
            [user_id] => 1
            [board_id] => 2
            [title] => new thread 2
            [updated] =>
            [closed] =>
            [User] => Array
                (
                    [id] => 1
                    [is_active] => 1
                    [is_super_admin] => 0
                    [first_name] =>
                    [last_name] =>
                    [username] => jwage
                    [password] =>
                    [type] =>
                    [created_at] => 2009-01-20 16:41:57
                    [updated_at] => 2009-01-20 16:41:57
                )

        )

)

)

NOTE Doctrine内部でautoincrementの主キーと外部キーが自動的に設定されることに注意してください。主キーと外部キーの設定に悩む必要はまったくありません!
外部キーのアソシエーション
一対一

一対一のリレーションは最も基本的なリレーションでしょう。次の例ではリレーションが一対一である``User``と``Email``の2つのクラスを考えます。

最初に``Email``クラスを見てみましょう。一対一のリレーションをバインドしているので``hasOne()``メソッドを使用しています。Email``クラスで外部キーのカラム(``user_id)を定義する方法に注目してください。これは``Email``が``User``によって所有され他の方法がないという事実に基づいています。実際次の慣習 - 所有側のクラスで外部キーを設置することに従うべきです。

外部キー用に推奨される命名規約は: ``[tableName]_[primaryKey]``です。外部テーブルは’user’で主キーは’id’なので外部キーのカラムは’user_id’と名付けました。

// models/Email.php

class Email extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’); $this->hasColumn(‘address’, ‘string’, 150); }

public function setUp()
{
    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Email: columns: user_id: integer address: string(150) relations: User: local: user_id foreign: id foreignType: one

Tip

リレーションは自動的に反転して追加されるので、YAMLスキーマファイルを使用するとき反対端(User)でリレーションを指定することは必須ではありません。リレーションはクラスの名前から名付けられます。ですのでこの場合``User``側のリレーションは``Email``と呼ばれ``many``になります。これをカスタマイズしたい場合``foreignAlias``と``foreignType``オプションを使用できます。

``Email``クラスは``User``クラスとよく似ています。localとforeignカラムは``Email``クラスの定義と比較される``hasOne()``の定義に切り替えられることに注目してください。

// models/User.php

class User extends BaseUser { public function setUp() { parent::setUp();

    $this->hasOne('Email', array(
            'local' => 'id',
            'foreign' => 'user_id'
        )
    );
}

}

NOTE ``setUp()``メソッドをオーバーライドして``parent::setUp()``を呼び出していることに注目してください。これはYAMLもしくは既存のデータベースから生成された``BaseUser``クラスがメインの``setUp()``メソッドを持ちリレーションを追加するために``User``クラスでこのメソッドをオーバーライドしているからです。

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: # ... relations: # ... Email: local: id foreign: user_id

一対多と多対一

一対多と多対一のリレーションは一対一のリレーションとよく似ています。以前の章で見た推奨される慣習は一対多と多対一のリレーションにも適用されます。

次の例では2つのクラス: ``User``と``Phonenumber``があります。一対多のリレーションとして定義します(ユーザーは複数の電話番号を持つ)。繰り返しますが``Phonenumber``は``User``によって所有されるので``Phonenumber``クラスに外部キーを設置します。

// models/User.php

class User extends BaseUser { public function setUp() { parent::setUp();

    // ...

    $this->hasMany('Phonenumber as Phonenumbers', array(
            'local' => 'id',
            'foreign' => 'user_id'
        )
    );
}

}

// models/Phonenumber.php

class Phonenumber extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’); $this->hasColumn(‘phonenumber’, ‘string’, 50); }

public function setUp()
{
    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

YAMLフォーマットでの同じです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# 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

ツリー構造

ツリー構造は自己参照の外部キーのリレーションです。次の定義は階層データの概念の用語では隣接リスト(Adjacency List)とも呼ばれます。

// models/Task.php

class Task extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 100); $this->hasColumn(‘parent_id’, ‘integer’); }

public function setUp()
{
    $this->hasOne('Task as Parent', array(
            'local' => 'parent_id',
            'foreign' => 'id'
        )
    );

    $this->hasMany('Task as Subtasks', array(
            'local' => 'id',
            'foreign' => 'parent_id'
        )
    );
}

}

YAMLフォーマットでの同じ例です。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Task: columns: name: string(100) parent_id: integer relations: Parent: class: Task local: parent_id foreign: id foreignAlias: Subtasks

NOTE 上記の実装は純粋な例で階層データを保存し読み取るための最も効率的な方法ではありません。階層データを扱い推奨方法に関してはDoctrineに含まれる``NestedSet``ビヘイビアを確認してください。
テーブルのアソシエーションをジョインする
多対多

リレーショナルデータベースの背景知識があれば、多対多のアソシエーションを扱う方法になれているかもしれません: 追加のアソシエーションテーブルが必要です。

多対多のリレーションにおいて2つのコンポーネントの間のリレーションは常に集約関係でアソシエーションテーブルは両端で所有されます。ユーザーとグループの場合: ユーザーが削除されているとき、ユーザーが所属するグループは削除されません。しかしながら、ユーザーとユーザーが所属するグループの間のアソシエーションが代わりに削除されています。これはユーザーとユーザーが所属するグループの間のリレーションを削除しますが、ユーザーとグループは削除しません。

ときにはユーザー/グループを削除するときアソシエーションテーブルの列を削除したくないことがあります。リレーションをアソシエーションコンポーネントに設定する(このケースでは``Groupuser``) ことで明示的にこのビヘイビアをオーバーライドできます。

次の例ではリレーションが多対多として定義されているGroupsとUsersがあります。このケースでは``Groupuser``と呼ばれる追加クラスも定義する必要があります。

class User extends BaseUser public function setUp() { parent::setUp();
    // ...

    $this->hasMany('Group as Groups', array(
            'local' => 'user_id',
            'foreign' => 'group_id',
            'refClass' => 'UserGroup'
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

User: # ... relations: # ... Groups: class: Group local: user_id

foreign: group_id refClass: UserGroup

NOTE 多対多のリレーションをセットアップするとき上記の``refClass``オプションは必須です。

// models/Group.php

class Group extends Doctrine_Record { public function setTableDefinition() { $this->setTableName(‘groups’); $this->hasColumn(‘name’, ‘string’, 30); }

public function setUp()
{
    $this->hasMany('User as Users', array(
            'local' => 'group_id',
            'foreign' => 'user_id',
            'refClass' => 'UserGroup'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Group: tableName: groups columns: name: string(30) relations: Users: class: User local: group_id foreign: user_id refClass: UserGroup

NOTE ``group``は予約語であることにご注意ください。これが``setTableName``メソッドを使用してテーブルを``groups``にリネームする理由です。予約語がクォートでエスケープされるように他のオプションは``Doctrine::ATTR_QUOTE_IDENTIFIER``属性を使用して識別子のクォート追加を有功にすることです。

$manager->setAttribute(Doctrine_Core::ATTR_QUOTE_IDENTIFIER, true);

// models/UserGroup.php

class UserGroup extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’, null, array( ‘primary’ => true ) );

    $this->hasColumn('group_id', 'integer', null, array(
            'primary' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

UserGroup: columns: user_id: type: integer primary: true group_id: type: integer primary: true

リレーションが双方向であることに注目してください。``User``は複数の``Group``を持ち``Group``は複数の``User``を持ちます。Doctrineで多対多のリレーションを完全に機能させるためにこれは必須です。

新しいモデルで遊んでみましょう。ユーザーを作成しこれにいくつかのグループを割り当てます。最初に新しい``User``インス場合も考えてみましょう。注文テーブルが実在する製品の注文のみが含まれることを保証したい場合を考えます。ですので製品テーブルを参照する注文テーブルで外部キー制約を定義します:

// models/Order.php

class Order extends Doctrine_Record { public function setTableDefinition() { $this->setTableName(‘orders’); $this->hasColumn(‘product_id’, ‘integer’); $this->hasColumn(‘quantity’, ‘integer’); }

public function setUp()
{
    $this->hasOne('Product', array(
            'local' => 'product_id',
            'foreign' => 'id'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Order: tableName: orders columns: product_id: integer quantity: integer relations: Product: local: product_id foreign: id

NOTE 外部キーを含むクエリを発行するときに最適なパフォーマンスを保証するために外部キーのカラムのインデックスは自動的に作成されます。

``Order``クラスがエクスポートされるとき次のSQLが実行されます:

CREATE TABLE orders ( id integer PRIMARY KEY auto_increment,

product_id integer REFERENCES products (id), quantity integer, INDEX product_id_idx (product_id) )

``product``テーブルに現れない``product_id``で``orders``を作成するのは不可能です。

この状況においてordersテーブルは参照するテーブルでproductsテーブルはは参照されるテーブルです。同じように参照と参照されるカラムがあります。

外部キーの名前

Doctrineでリレーションを定義し外部キーがデータベースで作成されるとき、Doctrineは外部キーの名前をつけようとします。ときには、その名前が望んだものとは違うことがあるのでリレーションのセットアップで``foreignKeyName``オプションを使うことで名前をカスタマイズできます。

// models/Order.php

class Order extends Doctrine_Record { // ...

public function setUp()
{
    $this->hasOne('Product', array(
            'local' => 'product_id',
            'foreign' => 'id',
            'foreignKeyName' => 'product_id_fk'
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。YAMLの詳細は[doc yaml-schema-files :name]の章で読むことができます:

# schema.yml

Order: # ... relations: Product: local: product_id foreign: id foreignKeyName: product_id_fk

整合アクション

CASCADE

親テーブルから列を削除もしくは更新しコテーブルでマッチするテーブルを自動的に削除もしくは更新します。``ON DELETE CASCADE``と``ON UPDATE CASCADE``の両方がサポートされます。2つのテーブルの間では、親テーブルもしくは子テーブルの同じカラムで振る舞う``ON UPDATE CASCADE``句を定義すべきではありません。

SET NULL

親テーブルから列を削除もしは更新し子テーブルで外部キーカラムを``NULL``に設定します。外部キーカラムが``NOT NULL``修飾子が指定されない場合のみこれは有効です。``ON DELETE SET NULL``と``ON UPDATE SET NULL``句の両方がサポートされます。

NO ACTION

標準のSQLにおいて、``NO ACTION``はアクションが行われないことを意味し、具体的には参照されるテーブルで関連する外部キーの値が存在する場合、主キーの値を削除するもしくは更新する処理が許可されません。

RESTRICT

親テーブルに対する削除もしくは更新オペレーションを拒否します。``NO ACTION``と``RESTRICT``は``ON DELETE``もしくは``ON UPDATE``句を省略するのと同じです。

SET DEFAULT

次の例において``User``と``Phonenumber``の2つのクラスのリレーションを一対多に定義します。``onDelete``カスケードアクションで外部キーの制約も追加します。このことは``user``が削除されるたびに関連する``phonenumbers``も削除されることを意味します。

NOTE 上記で示されている整合性制約は大文字小文字を区別しスキーマで定義するときは大文字でなければなりません。下記のコードは削除カスケードが使用されるデータベース削除の例です。

class Phonenumber extends Doctrine_Record { // ...

public function setUp()
{
    parent::setUp();

    // ...

    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id',
            'onDelete' => 'CASCADE'
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Phonenumber: # ... relations: # ... User: local: user_id foreign: id onDelete: CASCADE

NOTE 外部キーがあるところで整合性制約がおかれていることに注目してください。整合性制約がデータベースのプロパティにエクスポートされるためにこれは必須です。

インデックス

はじめに

インデックスは特定のカラムの値を持つ列を素早く見つけるために使われます。インデックスなしでは、データベースは最初の列から始め関連する列をすべて見つけるためにテーブル全体を読み込まなければなりません。

テーブルが大きくなるほど、時間がかかります。テーブルが問題のカラム用のインデックスを持つ場合、データベースはデータをすべて見ることなくデータの中ほどで位置を素早く決定できます。テーブルが1000の列を持つ場合、これは列を1つづつ読み込むよりも少なくとも100倍以上速いです。

インデックスはinsertとupdateを遅くなるコストがついてきます。しかしながら、一般的に SQLのwhere条件で使われるフィールドに対して**常に**インデックスを使うべきです。

インデックスを追加する

``Doctrine_Record::index``を使用してインデックスを追加できます。インデックスをnameという名前のフィールドに追加するシンプルな例です:

NOTE 次のインデックスの例はDoctrineの環境に実際に追加することは想定されていません。これらはインデックス追加用のAPIを示すためだけを意図しています。

class IndexTest extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’);

    $this->index('myindex', array(
            'fields' => array('name')
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

IndexTest: columns: name: string indexes: myindex: fields: [name]

``name``という名前のフィールドにマルチカラムインデックスを追加する例です:

class MultiColumnIndexTest extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’); $this->hasColumn(‘code’, ‘string’);

    $this->index('myindex', array(
            'fields' => array('name', 'code')
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を見ることができます:

MultiColumnIndexTest: columns: name: string code: string indexes:

myindex: fields: [name, code]

同じテーブルで複数のインデックスを追加する例です:

class MultipleIndexTest extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’); $this->hasColumn(‘code’, ‘string’); $this->hasColumn(‘age’, ‘integer’);

    $this->index('myindex', array(
            'fields' => array('name', 'code')
        )
    );

    $this->index('ageindex', array(
            'fields' => array('age')
        )
    );
}

}

YAMLフォーマットでの同じ例です。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

MultipleIndexTest: columns: name: string code: string age: integer

indexes: myindex: fields: [name, code] ageindex: fields: [age]

インデックスオプション

Doctrineは多くのインデックスオプションを提供します。これらの一部はデータベース固有のものです。利用可能なオプションの全リストは次の通りです:

||~ 名前 ||~ 説明 || || sorting || 文字列の値が’ASC’もしくは’DESC’の値を取れるか || || length || インデックスの長さ(一部のドライバのみサポート)。 || || primary || インデックスがプライマリインデックスであるか。 || || type || 文字列の値で’unique’、’fulltext’、’gist’もしくは’gin’が許可されるか||

nameカラムでユニークインデックスを作る方法の例は次の通りです。

class MultipleIndexTest extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’); $this->hasColumn(‘code’, ‘string’); $this->hasColumn(‘age’, ‘integer’);

    $this->index('myindex', array(
            'fields' => array(
                'name' => array(
                    'sorting' => 'ASC',
                    'length'  => 10),
                    'code'
                ),
            'type' => 'unique',
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。YAMLの詳細は[doc yaml-schema-files :name]の章で読むことができます:

MultipleIndexTest: columns: name: string code: string age: integer

indexes: myindex: fields: name: sorting: ASC length: 10 code: - type: unique

特別なインデックス

Doctrineは多くの特別なインデックスをサポートします。これらにはMysqlのFULLTEXTとPgsqlのGiSTインデックスが含まれます。次の例では’content’フィールドに対してMysqlのFULLTEXTインデックスを定義します。

// models/Article.php

class Article extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); $this->hasColumn(‘content’, ‘string’);

    $this->option('type', 'MyISAM');

    $this->index('content', array(
            'fields' => array('content'),
            'type'   => 'fulltext'
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Article: options: type: MyISAM columns: name: string(255) content: string indexes: content: fields: [content] type: fulltext

NOTE テーブルの型を``MyISAM``に設定していることに注目してください。これは``fulltext``インデックス型は``MyISAM``でのみサポートされるため``InnoDB``などを使う場合はエラーを受け取るからです。

チェック

``Doctrine_Record``の``check()``メソッドを使用することで任意の``CHECK``制約を作成できます。最後の例では価格がディスカウント価格よりも常に高いことを保証するために制約を追加します。

// models/Product.php

class Product extends Doctrine_Record { public function setTableDefinition() { // ...

    $this->check('price > discounted_price');
}

// ...

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Product: # ... checks: price_check: price > discounted_price

生成されるSQL(pgsql):

CREATE TABLE product ( id INTEGER, price NUMERIC, discounted_price

NUMERIC, PRIMARY KEY(id), CHECK (price >= 0), CHECK (price <= 1000000), CHECK (price > discounted_price))

NOTE データベースの中には``CHECK``制約をサポートしないものがあります。この場合Doctrineはチェック制約の作成をスキップします。

Doctrineバリデータが定義で有効な場合はレコードが保存されるとき価格が常にゼロ以上であることも保証されます。

トランザクションの範囲で保存される価格の中にゼロよりも小さいものがある場合、Doctrineは``Doctrine_Validator_Exception``を投げトランザクションを自動的にロールバックします。

テーブルオプション

Doctrineはさまざまなテーブルオプションを提供します。すべてのテーブルオプションは``Doctrine_Record::option``関数を通して設定できます。

例えばMySQLを使用しINNODBテーブルを利用したい場合は次のようにできます:

class MyInnoDbRecord extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’);

    $this->option('type', 'INNODB');
}

}

YAMLフォーマットの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を見ることができます:

MyInnoDbRecord: columns: name: string options: type: INNODB

次の例では照合順序と文字集合のオプションを設定します:

class MyCustomOptionRecord extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘name’, ‘string’);

    $this->option('collate', 'utf8_unicode_ci');
    $this->option('charset', 'utf8');
}

}

YAMLフォーマットでの同じ例です。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

MyCustomOptionRecord: columns: name: string options: collate:

utf8_unicode_ci charset: utf8

特定のデータベース(Firebird、MySqlとPostgreSQL)でcharsetオプションを設定しても無意味でDoctrineがデータを適切に返すのには不十分であることがあります。これらのデータベースに対して、データベース接続の``setCharset``関数を使うこともお勧めします:

$conn = Doctrine_Manager::connection(); $conn->setCharset(‘utf8’);

レコードフィルター

Doctrineはモデルを定義するときにレコードフィルターを添付する機能を持ちます。レコードフィルターは無効なモデルのプロパティにアクセスするときに起動されます。ですのでこれらのフィルターの1つを使うことを通してプロパティをモデルに追加することが本質的に可能になります。

フィルターを添付するにはこれをモデル定義の``setUp()``メソッドに追加することだけが必要です:

class User extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp()
{
    $this->hasOne('Profile', array(
        'local' => 'id',
        'foreign' => 'user_id'
    ));
    $this->unshiftFilter(new Doctrine_Record_Filter_Compound(array('Profile')));
}

}

class Profile extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’); $this->hasColumn(‘first_name’, ‘string’, 255); $this->hasColumn(‘last_name’, ‘string’, 255); }

public function setUp()
{
    $this->hasOne('Profile', array(
        'local' => 'user_id',
        'foreign' => 'id'
    ));
}

}

上記の例のコードによって``User``のインスタンスを使うとき``Profile``リレーションのプロパティに簡単にアクセスできます。次のコードは例です:

$user = Doctrine_Core::getTable(‘User’) ->createQuery(‘u’)

->innerJoin(‘u.Profile p’) ->where(‘p.username = ?’, ‘jwage’) ->fetchOne();

echo $user->first_name . ‘ ‘ . $user->last_name;

``first_name``と``last_name``プロパティに問い合わせるときこれらは``$user``インスタンスに存在しないのでこれらは``Profile``リレーションにフォワードされます。これは次の内容を行ったこととまったく同じです:

echo $user->Profile->first_name . ‘ ‘ . $user->Profile->last_name;

独自のレコードフィルターをとても簡単に書くこともできます。必要なことは``Doctrine_Record_Filter``を継承し``filterSet()``と``filterGet()``メソッドを実装するクラスを作ることです。例は次の通りです:

class MyRecordFilter extends Doctrine_Record_Filter { public function

filterSet(Doctrine_Record $record, $name, $value) { // プロパティをトライしてセットする

    throw new Doctrine_Record_UnknownPropertyException(sprintf('Unknown record property / related component "%s" on "%s"', $name, get_class($record)));
}

public function filterGet(Doctrine_Record, $name)
{
    // プロパティをトライしてゲットする

    throw new Doctrine_Record_UnknownPropertyException(sprintf('Unknown record property / related component "%s" on "%s"', $name, get_class($record)));
}

}

これでフィルターをモデルに追加できます:

class MyModel extends Doctrine_Record { // ...
public function setUp()
{
    // ...

    $this->unshiftFilter(new MyRecordFilter());
}

}

NOTE ``filterSet()``もしくは``filterGet()``がプロパティを見つけられない場合、例外クラスの``Doctrine_Record_UnknownPropertyException``のインスタンスが投げられていることをかならず確認してください。

遷移的な永続化

Doctrineはデータベースとアプリケーションレベルでカスケーディングオペレーションを提供します。このセクションではアプリケーションとデータベースレベルの両方でセットアップする詳細な方法を説明します。

アプリケーションレベルのカスケード

とりわけオブジェクトグラフを扱うとき、個別のオブジェクトの保存と削除はとても退屈です。Doctrineはアプリケーションレベルでオペレーションのカスケード機能を提供します。

保存カスケード

デフォルトでは``save()``オペレーションは関連オブジェクトに既にカスケードされていることにお気づきかもしれません。

削除カスケード

Doctrineは2番目のカスケードスタイル: deleteを提供します。``save()``カスケードとは異なり、``delete``カスケードは次のコードスニペットのように明示的に有効にする必要があります:

// models/User.php

class User extends BaseUser { // ...

public function setUp()
{
    parent::setup();

    // ...

    $this->hasMany('Address as Addresses', array(
            'local' => 'id',
            'foreign' => 'user_id',
            'cascade' => array('delete')
        )
    );
}

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: # ... relations: # ... Addresses: class: Address local: id foreign: user_id cascade: [delete]

アプリケーションレベルで関連オブジェクトにカスケードされるオペレーションを指定するために``cascade``オプションが使われます。

NOTE 現在サポートされる値は``delete``のみであることにご注意ください。より多くのオプションは将来のDoctrineのリリースで追加されます。

上記の例において、Doctrineは関連する``Address``に``User``の削除をカスケードします。次の説明は``$record->delete()``を通してレコードを削除する際の一般的な手続きです:

1. Doctrineは適用する必要のある削除カスケードが存在するかリレーションを探します。削除カスケードが存在しない場合、3に移動します)。

2. 指定された削除カスケードを持つそれぞれのリレーションに対して、Doctrineはカスケードのターゲットであるオブジェクトがロードされることを確認します。このことはDoctrineは関連オブジェクトがまだロードされていない場合データベースから関連オブジェクトが取得することを意味します。(例外: すべてのオブジェクトがロードされていることを確認するために多くの値を持つアソシエーションはデータベースから再取得されます)。それぞれの関連オブジェクトに対して、ステップ1に進みます)。

3. Doctrineは参照照合性を維持しながらすべての削除を並べ替え最も効果的な方法で実行します。

この説明から1つのことがすぐに明らかになります: アプリケーションレベルのカスケードはオブジェクトレベルで行われ、参加しているオブジェクトが利用可能にすることを行うために1つのオブジェクトから別にオブジェクトにオペレーションがカスケードされることを意味します。

このことは重要な意味を示します:

  • 関連の照合順序でたくさんのオブジェクトがあるとき多くの値を持つアソシエーションではアプリケーションレベルの削除カスケードはうまく実行されませんこれらがデータベースから取得される必要があるためで、実際の削除はとても効率的です)。
  • アプリケーションレベルの削除カスケードはデータベースレベルのカスケードが行うようにオブジェクトのライフサイクルをスキップしません(次の章を参照)。それゆえ登録されたすべてのイベントリスナーと他のコールバックメソッドはアプリケーションレベルのカスケードで適切に実行されます。
データベースレベルのカスケード

データベースレベルでカスケードオペレーションはとても効率的にできるものがあります。もっともよい例は削除カスケードです。

次のことを除いて一般的にデータベースレベルの削除カスケードはアプリケーションレベルよりも望ましいです:

  • データベースがデータベースレベルのカスケードをサポートしない(MySqlでMYISAMテーブルを使うとき)。
  • オブジェクトライフサイクルをリスニングするリスナーがありこれらを起動させたい。

データベースレベルの削除カスケードは外部キー制約に適用されます。それゆえこれらは外部キーを所有するリレーション側で指定されます。上記から例を拾うと、データベースレベルのカスケードの定義は次のようになります:

// models/Address.php

class Address extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’); $this->hasColumn(‘address’, ‘string’, 255); $this->hasColumn(‘country’, ‘string’, 255); $this->hasColumn(‘city’, ‘string’, 255); $this->hasColumn(‘state’, ‘string’, 2); $this->hasColumn(‘postal_code’, ‘string’, 25); }

public function setUp()
{
    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id',
            'onDelete' => 'CASCADE'
        )
    );
}

}

YAMLフォーマットの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を詳しく読むことができます:

# 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

Doctrineがテーブルを作成するとき``onDelete``オプションは適切なDDL/DMLステートメントに翻訳されます。

NOTE 'onDelete' => 'CASCADE'``がAddressクラスで指定されることに注目してください。Addressは外部キー(``user_id)を所有するのでデータベースレベルのカスケードは外部キーに適用されます。

現在、2つのデータベースレベルのカスケードスタイルは``onDelete``と``onUpdate``に対してのみです。Doctrineがテーブルを作成するとき両方とも外部キーを所有する側で指定されデータベーススキーマに適用されます。

まとめ

これでDoctrineのモデルを定義するすべての方法を知りました。アプリケーションで[doc work-with-models モデルと連携する]方法を学ぶ準備ができています。

これはとても大きなトピックなので、少し休憩を取り、マウンテンデューを飲んで[doc working-with-models 次の章]にすぐに戻ってください。

テストスキーマを定義する

NOTE 以前の章からの既存のスキーマ情報をモデルを削除しておいてください。

$ rm schema.yml $ touch schema.yml $ rm -rf models/*

次のいくつかの例に対して次のスキーマを使います:

// models/User.php

class User extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255, array( ‘type’ => ‘string’, ‘length’ => ‘255’ ) );

    $this->hasColumn('password', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        )
    );
}

public function setUp()
{
    $this->hasMany('Group as Groups', array(
            'refClass' => 'UserGroup',
            'local' => 'user_id',
            'foreign' => 'group_id'
        )
    );

    $this->hasOne('Email', array(
            'local' => 'id',
            'foreign' => 'user_id'
        )
    );

    $this->hasMany('Phonenumber as Phonenumbers', array(
            'local' => 'id',
            'foreign' => 'user_id'
        )
    );
}

}

// models/Email.php

class Email extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’, null, array( ‘type’ => ‘integer’ ) );

    $this->hasColumn('address', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        )
    );
}

public function setUp()
{
    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

// models/Phonenumber.php

class Phonenumber extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’, null, array( ‘type’ => ‘integer’ ) );

    $this->hasColumn('phonenumber', 'string', 255, array(
            'type' => 'string',
            'length' => '255'
        )
    );
    $this->hasColumn('primary_num', 'boolean');
}

public function setUp()
{
    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

// models/Group.php

class Group extends Doctrine_Record { public function setTableDefinition() { $this->setTableName(‘groups’); $this->hasColumn(‘name’, ‘string’, 255, array( ‘type’ => ‘string’, ‘length’ => ‘255’ ) ); }

public function setUp()
{
    $this->hasMany('User as Users', array(
            'refClass' => 'UserGroup',
            'local' => 'group_id',
            'foreign' => 'user_id'
        )
    );
}

}

// models/UserGroup.php

class UserGroup extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’, null, array( ‘type’ => ‘integer’, ‘primary’ => true ) );

    $this->hasColumn('group_id', 'integer', null, array(
            'type' => 'integer',
            'primary' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# 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

スキーマを定義したので以前の章で利便性のために作成した``generate.php``スクリプトを実行してデータベースをインスタンス化できます。

$ php generate.php

リレーションを扱う

関連レコードを作成する

Doctrineで関連レコードにアクセスするのは簡単です: レコードプロパティに関してまったく同じゲッターとセッターを使うことができます。

3つの方法はどれでも使えますが、配列のポータビリティを目的にするなら最後の方法がお勧めです。

// test.php

// ... $user = new User(); $user[‘username’] = ‘jwage’; $user-[‘password’] = ‘changeme’;

$email = $user->Email;

$email = $user->get(‘Email’);

$email = $user[‘Email’];

存在しない一対一の関連レコードにアクセスするとき、Doctrineは自動的にオブジェクトを作成します。That is why the above

// test.php

// ... $user->Email->address = 'jonwage@gmail.com‘; $user->save();

一対多の関連レコードにアクセスするとき、Doctrineは関連コンポーネント用の``Doctrine_Collection``を作成します。リレーションが一対多である``users``と``phonenumbers``を考えてみましょう。上記で示されるように``phonenumbers``を簡単に追加できます:

// test.php

// ... $user->Phonenumbers[]->phonenumber = ‘123 123’; $user->Phonenumbers[]->phonenumber = ‘456 123’; $user->Phonenumbers[]->phonenumber = ‘123 777’;

ユーザーと関連の電話番号を簡単に保存できます:

// test.php

// ... $user->save();

2つの関連コンポーネントの間でリンクを簡単に作る別の方法は``Doctrine_Record::link()``を使うことです。既存の2つのレコードをお互いに関連づける(もしくはリンクする)ことはよくあります。この場合、関わるレコードクラスの間で定義されたリレーションがある場合、関連レコードの識別子だけが必要です:

新しい ``Phonenumber``オブジェクトを作成し新しい電話番号の識別子を追跡しましょう:

// test.php

// ... $phoneIds = array();

$phone1 = new Phonenumber(); $phone1[‘phonenumber’] = ‘555 202 7890’; $phone1->save();

$phoneIds[] = $phone1[‘id’];

$phone2 = new Phonenumber(); $phone2[‘phonenumber’] = ‘555 100 7890’; $phone2->save();

$phoneIds[] = $phone2[‘id’];

``User``レコード用に存在するので電話番号をユーザーにリンクしましょう。

// test.php

$user = new User(); $user[‘username’] = ‘jwage’; $user[‘password’] = ‘changeme’; $user->save();

$user->link(‘Phonenumbers’, $phoneIds);

``User``レコードクラスへのリレーションが``Phonenumber``レコードクラスに対して定義された場合、次のようにもできます:

最初に連携するユーザーを作ります:

// test.php

// ... $user = new User(); $user[‘username’] = ‘jwage’; $user[‘password’] = ‘changeme’; $user->save();

新しい``Phonenumber``インスタンスを作成します:

// test.php

// ... $phone1 = new Phonenumber(); $phone1[‘phonenumber’] = ‘555 202 7890’; $phone1->save();

``User``を``Phonenumber``にリンクできます:

// test.php

// ... phone1->link('User', array(user[‘id’]));

別の電話番号を作成できます:

// test.php

// ... $phone2 = new Phonenumber(); $phone2[‘phonenumber’] = ‘555 100 7890’;

この``Phonenumber``も``User``にリンクしましょう:

// test.php

// ... phone2->link('User', array(user[‘id’]));

関連レコードを読み取る

前の節とまったく同じな``Doctrine_Record``メソッドで関連レコードを読み取ることができます。既にロードされていない関連コンポーネントにアクセスするときDoctrineが取得に1つのSELECT文を使用することに注意してください。次の例では3つの``SQL SELECT``が実行されます。

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->find(1);

echo $user->Email[‘address’];

echo $user->Phonenumber[0]->phonenumber;

これをもっと効率的に行うにはDQLを使います。次の例では関連コンポーネントの読み取りに1つのSQLクエリのみを使用します。

// test.php

// ... $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’];

関連レコードを更新する

それぞれの関連オブジェクト/コレクションに対してsaveを個別に呼び出すもしくは他のオブジェクトを所有するオブジェクトでsave()を呼び出すことで関連レコードを更新できます。すべての追加オブジェクトを保存する``Doctrine_Connection::flush``を呼び出すこともできます。

// test.php

// ... $user->Email[‘address’] = 'koskenkorva@drinkmore.info‘;

$user->Phonenumber[0][‘phonenumber’] = ‘123123’;

$user->save();

NOTE 上記の例では``$user->save()``を呼び出すことで``email``と``phonenumber``が保存されます。
関連レコードをクリアする

オブジェクトから関連レコードリファレンスをクリアすることができます。これはこれらのオブジェクトが関連しているという事実を変更することはなく、また保存するのであればデータベースでこれを変更することはありません。これはPHPの1つのオブジェクトの参照を別のものにクリアするだけです。

次のコードを実行することですべてのリファレンスをクリアできます:

// test.php

// ... $user->clearRelated();

もしくは特定のリレーションシップをクリアすることもできます:

// test.php

// ... $user->clearRelated(‘Email’);

これは次のようなことをしたい場合に便利です:

// test.php

// ... if ($user->Email->exists()) { // ユーザーがメールを持つ } else { // ユーザーがメールを持たない }

$user->clearRelated(‘Email’);

``Email``オブジェクトが存在しない場合Doctrineが新しい``Email``オブジェクトを自動的に作成するので、あたかも``$user->save()``を呼び出し``User``の空白の``Email``レコードを保存しないように、この参照をクリアする必要があります。

``relatedExists()``メソッドを使うことで上記のシナリオを簡略化できます。これで上記のチェックはより短いコードになり後で不必要な参照をクリアすることにわずらわずに済みます。

if ($user->relatedExists(‘Email’)) { // ユーザーがメールを持つ } else {

// ユーザーがメールを持たない }

関連レコードを削除する

レコードもしくはコレクション上で``delete()``を呼び出すことで関連レコードを個別に削除できます。

個別の関連レコードを削除できます:

// test.php

// ... $user->Email->delete();

レコードのコレクションの範囲から個別のレコードを削除できます:

// test.php

// ... $user->Phonenumber[3]->delete();

望むのであればコレクション全体を削除できます:

// test.php

// ... $user->Phonenumbers->delete();

もしくはユーザー全体とすべての関連オブジェクトを削除できます:

// test.php

// ... $user->delete();

典型的なウェブアプリケーションでは削除される関連オブジェクトの主キーはフォームからやってきます。この場合関連レコードの最も効率的な削除はDQLのDELETEステートメントを使用することです。リレーションが一対多である``Users``と``Phonenumbers``を再度考えてみましょう。与えられたユーザーidに対して``Phonenumbers``を削除することは次のように実現できます:

// test.php

// ... $q = Doctrine_Query::create() ->delete(‘Phonenumber’) ->addWhere(‘user_id = ?’, 5) ->whereIn(‘id’, array(1, 2, 3));

$numDeleted = $q->execute();

ときに``Phonenumber``レコードを削除したくないが外部キーをnullに設定することでリレーションのリンクを解除したいことがあります。もちろんこれはDQLを使えば実現できますが最もエレガントな方法は``Doctrine_Record::unlink()``を使う方法です。

NOTE ``unlink()``メソッドが非常にスマートであることに留意してください。このメソッドは関連する``Phonenumbers``用の外部キーをnullにする設定するだけでなく{User``オブジェクトから``Phonenumber``のすべての参照も削除します。

User``が3つの``Phonenumbers``(識別子は1、2と3)を持つことを考えましょう。``Phonenumbers 1と3のリンク解除は次のように実現できます:

// test.php

// ... $user->unlink(‘Phonenumber’, array(1, 3));

echo $user->Phonenumbers->count(); // 1

関連レコードに取り組む
リレーションの存在をテストする

下記の例ではリレーションがまだインスタンス化されないのでfalseが返されます:

// test.php

// ... user = new User(); if (isset(user->Email)) { // ... }

次の例では``Email``リレーションをインスタンス化したのでtrueが返されます:

// test.php

// ... $obj->Email = new Email();

if(isset($obj->Email)) { // ... }

多対多のリレーション

CAUTION Doctrineは多対多のリレーションが双方向であることを求めます。例: ``User``は複数の``Groups``を持たなければならず``Group``は複数の``User``を持たなければならない
新しいレコードを作成する

``User``と``Group``の2つのクラスを考えてみましょう。これらはGroupUserアソシエーションクラスを通してリンクされます。一時的な(新しい)レコードを扱うときに``User``と``Groups``の組を追加するための最速の方法は次の通りです:

// test.php

// ... $user = new User(); $user->username = ‘Some User’; $user->Groups[0]->username = ‘Some Group’; $user->Groups[1]->username = ‘Some Other Group’; $user->save();

しかしながら実際の世界のシナリオではユーザーを追加したい既存のグループがあることはよくあります、これを行う最も効率的な方法は次の通りです:

// test.php

// ... $groupUser = new GroupUser(); $groupUser->user_id = $userId; $groupUser->group_id = $groupId; $groupUser->save();

リンクを削除する

多対多の関連レコード間のリンクを削除する正しい方法はDQL DELETEステートメントを使うことです。DQL DELETEを利用する際に便利で推奨される方法はQuery APIを通して行われます。

// test.php

// ... $q = Doctrine_Query::create() ->delete(‘UserGroup’) ->addWhere(‘user_id = ?’, 5) ->whereIn(‘group_id’, array(1, 2));

$deleted = $q->execute();

関連オブジェクトの間のリレーションを``unlink``する別の方法は``Doctrine_Record::unlink``メソッドを通したものです。しかしながら、こnメソッドは最初にデータベースにクエリを行うので親モデルが既に存在しない限りこのメソッドは避けるべきです。

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->find(5); $user->unlink(‘Group’, array(1, 2)); $user->save();

2番目の引数を省略することで``Group``へのすべてのリレーションのリンクを解除することもできます:

// test.php

// ... $user->unlink(‘Group’);

``User``と``Group``の間のリンクを削除する明確で便利な方法が次であるとしても、これを行うべきでは*ありません*:

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->find(5); $user->GroupUser->remove(0)->remove(1); $user->save();

この方法は``$user->GroupUser``への呼び出しは与えられた``User``に対するすべての``Group``リンクをロードしているからです。``User``が多くの``Groups``に所属している場合この方法が時間のかかるタスクになる可能性があります。ユーザーがわずかな``groups``に所属する場合でも、これが不要なSELECTステートメントを実行します。

オブジェクトをフェッチする

通常データベースからデータをフェッチするとき次のフレーズが実行されます:

オブジェクトフェッチの観点からこれら2つのフェーズを’フェッチ’フェーズにします。Doctrineにはハイドレーションフェーズと呼ばれる別のフェーズもあります。ハイドレーションフェーズは構造化あれた配列/オブジェクトをフェッチするときに起こります。Doctrineで明示的に指定されないものはハイドレイトされます。

リレーションシップが1対多である``Users``と``Phonenumbers``がある場合を考えてみましょう。次のプレーンなSQLクエリを考えましょう:

// test.php

// ... $sql = ‘SELECT u.id, u.username, p.phonenumber FROM user u LEFT JOIN phonenumber p ON u.id = p.user_id’; $results = conn->getDbh()->fetchAll(sql);

この種の一対多のJOINに慣れている場合 基本の結果セットをコンストラクトする方法に親しみやすいかもしれません。ユーザーが複数の電話番号を持つときは結果セットに重複データが存在します。結果セットは次のようになります:

||~ index ||~ u.id ||~ u.username ||~ p.phonenumber || || 0 || 1 || Jack Daniels || 123 123 || || 1 || 1 || Jack Daniels || 456 456 || || 2 || 2 || John Beer || 111 111 || || 3 || 3 || John Smith || 222 222 || || 4 || 3 || John Smith || 333 333 || || 5 || 3 || John Smith || 444 444 ||

Jack Danielsが2つの``Phonenumbers``を持ち、John Beerは1つ持つのに対してJohn Smithは3つ持ちます。この結果セットがいかにぶかっこうなことにお気づきのことでしょう。あちらこちらで重複データのチェックが必要なのでイテレートするのは難しいです。

Doctrineのハイドレーションはすべての重複データを削除します。これは次のようなほかの多くのことも実行します:

SQLクエリに同等なDQLを考えてみましょう:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, u.username, p.phonenumber’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’);

$results = $q->execute(array(), Doctrine_Core::HYDRATE_ARRAY);

print_r($results);

ハイドレイトされた配列の構造は次の通りです:

$ php test.php Array ( [0] => Array ( [id] => 1 [username] =>

[Phonenumbers] => Array ( [0] => Array ( [id] => 1 [phonenumber] => 123 123 )

                [1] => Array
                    (
                        [id] => 2
                        [phonenumber] => 456 123
                    )

                [2] => Array
                    (
                        [id] => 3
                        [phonenumber] => 123 777
                    )

            )

    )
// ...

)

この構造はオブジェクト(レコード)のハイドレーションにも適用されます。これはDoctrineのデフォルトのハイドレーションモードです。唯一の違いは個別の要素が``Doctrine_Record``オブジェクトと``Doctrine_Collection``オブジェクトに変換される配列として表現されることです。オブジェクトの配列を扱うとき、次のことができます:

アクセスオンリーの目的でデータが必要なときはつねに配列ハイドレーションを使うべきである一方でフェッチされたデータを変更する必要があるときはレコードハイドレーションを使うべきです。

ハイドレーションアルゴリズムのコンスタントなO(n)パフォーマンスはスマートアイデンフィファーキャッシングソリューションによって保証されます。

Tip

データベースの1つのレコードで複数のオブジェクトが存在しないことを確認するためにDoctrineは内部でアイデンティティマップを使います。オブジェクトをフェッチしプロパティの一部を修正する場合、後で同じオブジェクトを取得すれば、修正されたプロパティはデフォルトでオーバーライドされます。``ATTR_HYDRATE_OVERWRITE``属性を``false``に変更することでこのふるまいを変更することができます。

サンプルのクエリ

リレーションに対してレコードの数をカウントする:

// test.php

// ... $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

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Groups g’);

$users = $q->fetchArray();

foreach ($users[0][‘Groups’] as $group) { echo $group[‘name’]; }

1つのパラメータの値を持つシンプルなWHERE:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->where(‘u.username = ?’, ‘jwage’);

$users = $q->fetchArray();

複数のパラメータの値を持つマルチプルWHERE:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->where(‘u.username = ? AND p.id = ?’, array(1, 1));

$users = $q->fetchArray();

Tip

オプションとして既存のwhere部分に追加するために``andWhere()``メソッドを使うこともできます。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->where(‘u.username = ?’, 1) ->andWhere(‘p.id = ?’, 1);

$users = $q->fetchArray();

``whereIn()``コンビニエンスメソッドを使用する:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->whereIn(‘u.id’, array(1, 2, 3));

$users = $q->fetchArray();

次のコードは上記と同じ:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->where(‘u.id IN (1, 2, 3)’);

$users = $q->fetchArray();

WHEREでDBMS関数を使う:

// test.php

// ... $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();

集約関数を使用して結果セットを制限する。1つ以上の電話番号を持つユーザーに制限する:

// test.php

// ... $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();

WITHを使用して最初の電話番号のみをJOINする:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p WITH p.primary_num = ?’, true);

$users = $q->fetchArray();

最適化用に特定のカラムを選択する:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username, p.phone’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’);

$users = $q->fetchArray();

1つの``Phonenumber``カラムのみ以外のすべての``User``カラムを選択するためにワイルドカードを使用する:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.*, p.phonenumber’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’);

$users = $q->fetchArray();

シンプルなWHEREでDQLのdeleteを実行する:

// test.php

// ... $q = Doctrine_Query::create() ->delete(‘Phonenumber’) ->addWhere(‘user_id = 5’);

$deleted = $q->execute();

1つのカラムに対してシンプルなDQLのupdateを実行する:

// test.php

// ... $q = Doctrine_Query::create() ->update(‘User u’) ->set(‘u.is_active’, ‘?’, true) ->where(‘u.id = ?’, 1);

$updated = $q->execute();

DBMSの関数でDQL updateを実行する。すべてのユーザー名を小文字にする:

// test.php

// ... $q = Doctrine_Query::create() ->update(‘User u’) ->set(‘u.username’, ‘LOWER(u.username)’);

$updated = $q->execute();

レコードを検索するためにMySQLのLIKEを使用する:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->where(‘u.username LIKE ?’, ‘%jwage%’);

$users = $q->fetchArray();

レコードエントリのキーが割り当てたカラムの名前であるデータをハイドレイトするためにINDEXBYキーワードを使用する:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u INDEXBY u.username’);

$users = $q->fetchArray();

jwageのユーザー名を持つユーザーを表示できます:

// test.php

// ... print_r($users[‘jwage’]);

位置パラメータを使用する

$q = Doctrine_Query::create() ->from(‘User u’) ->where(‘u.username =

?’, array(‘Arnold’));

$users = $q->fetchArray();

名前付きパラメータを使用する

$q = Doctrine_Query::create() ->from(‘User u’) ->where(‘u.username =

:username’, array(‘:username’ => ‘Arnold’));

$users = $q->fetchArray();

WHEREでサブクエリを使用する。Group 2という名前のグループに存在しないユーザーを見つける:

// test.php

// ... $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();

Tip

サブクエリなしでこれを実現できます。下記の2つの例は上記の例と同じ結果が得られます。

グループを持つユーザーを読み取るためにINNER JOINを使用する

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->innerJoin(‘u.Groups g WITH g.name != ?’, ‘Group 2’)

$users = $q->fetchArray();

グループを持つユーザーを読み取るためにWHERE条件を使用する

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Groups g’) ->where(‘g.name != ?’, ‘Group 2’);

$users = $q->fetchArray();

Doctrineはクエリを実行してデータを読み取るための多くの方法を持ちます。下記のコードはクエリを実行する異なるすべての方法の例です:

最初にテストするサンプルクエリを作成する:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’);

``fetchArray()``メソッドで配列のハイドレーションを実行できます:

$users = $q->fetchArray();

``execute()``メソッドの2番目の引数でハイドレーションメソッドを指定することでも配列のハイドレーションを利用できます:

// test.php

// ... $users = $q->execute(array(), Doctrine::HYDRATE_ARRAY)

``setHydrationMethod()``メソッドを利用することでもハイドレーションメソッドを指定できます:

$users = $q->setHydrationMode(Doctrine::HYDRATE_ARRAY)->execute(); //

So is this

ときにはハイドレーションを完全に回避してPDOが返す生のデータが欲しいことがあります:

// test.php

// ... $users = $q->execute(array(), Doctrine::HYDRATE_NONE);

クエリから1つのレコードだけを取得したい場合:

// test.php

// ... $user = $q->fetchOne();

// Fetch all and get the first from collection $user = $q->execute()->getFirst();

フィールドの遅延ロード

データベースからロードされるすべてのフィールドを持たないオブジェクトを取得するときこのオブジェクトの状態はプロキシ(proxy)と呼ばれます。プロキシオブジェクトはまたロードされていないフィールドを遅延ロードできます。

次の例では直接ロードされた``username``フィールドを持つすべてのUsersを取得します。それからpasswordフィールドを遅延ロードします:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->where(‘u.id = ?’, 1)

$user = $q->fetchOne();

次に``password``フィールドを遅延ロードし値を読み取るために追加のデータベースクエリを実行します:

// test.php

// ... $user->password;

Doctrineはロードされたフィールドのカウントに基づいてプロキシの評価を行います。フィールドごとにどのフィールドがロードされるのかは評価しません。この理由は単純でパフォーマンスです。PHPの世界ではフィールドの遅延ロードはほとんど必要ないので、どのフィールドがロードされるのかチェックするこの種の変数を導入することは基本的な取得に不要なオーバーロードを持ち込むことになります。

配列とオブジェクト

Doctrine\_Record``と``Doctrine_Collection``は配列との連携を円滑にするメソッドを提供します: ``toArray()fromArray()``と``synchronizeWithArray()

配列に

``toArray()``メソッドはレコードもしくはコレクションの配列表現です。これはオブジェクトが持つリレーションにもアクセスします。デバッグ目的でレコードを表示する必要がある場合オブジェクトの配列表現を取得して出力できます。

// test.php

// ... print_r($user->toArray());

配列にリレーションを格納したい場合、//true//の値を持つ引数``$deep``を渡す必要があります:

// test.php

// ... print_r($user->toArray(true));

配列から

配列の値がありレコードもしくはコレクションを満たすために使いたい場合、``fromArray()``メソッドはこの共通のタスクを簡略化します。

// test.php

// ... $data = array( ‘name’ => ‘John’, ‘age’ => ‘25’, ‘Emails’ => array( array(‘address’ => 'john@mail.com‘), array(‘address’ => 'john@work.com‘) ‘Groups’ => array(1, 2, 3) );

$user = new User(); user->fromArray(data); $user->save();

次のようにカスタムモデルのミューテータで``fromArray()``を使うことが可能です:

// models/User.php

class User extends Doctrine_Record { // ...

public function setEncryptedPassword($password)
{
    return $this->_set('password', md5($password));
}

}

``fromArray()``を使う場合``encrypted_password``という名前の値を渡すことで``setEncryptedPassword()``メソッドを使うことができます。

// test.php

// ... $user->fromArray(array(‘encrypted_password’ => ‘changeme’));

配列で同期する

``synchronizeWithArray()``によってレコードと配列を同期できます。モデルの配列表現がありフィールドを修正する場合、リレーションのフィールドを修正もしくはリレーションを削除もしくは作成します。この変更はレコードに適用されます。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.*, g.*’) ->from(‘User u’) ->leftJoin(‘u.Groups g’) ->where(‘id = ?’, 1);

$user = $q->fetchOne();

これを配列に変換してプロパティの一部を修正します:

// test.php

// ... $arrayUser = $user->toArray(true);

$arrayUser[‘username’] = ‘New name’; $arrayUser[‘Group’][0][‘name’] = ‘Renamed Group’; $arrayUser[‘Group’][] = array(‘name’ => ‘New Group’);

レコードを読み取るために同じクエリを使いレコードと変数``$arrayUser``を同期します:

// test.php

// ... $user = Doctrine_Query::create() ->select(‘u.*, g.*’) ->from(‘User u’) ->leftJoin(‘u.Groups g’) ->where(‘id = ?’, 1) ->fetchOne();

user->synchronizeWithArray(arrayUser); $user->save();

レコードをリンクするidの配列を指定することでリレーションをシンクロナイズすることもできます。

$user->synchronizeWithArray(array(‘Group’ => array(1, 2, 3)));

$user->save();

上記のコードは既存のグループを削除しユーザーをグループid 1、2、3にリンクします。

Tip

リレーションを一緒にリンクするために内部では``Doctrine_Record::link()``と``Doctrine_Record::unlink()``が使われています。

コンストラクタをオーバーライドする

ときどきオブジェクト作成時に同じオペレーションを行いたい場合があります。``Doctrine_Record::__construct()``メソッドをオーバーライドできませんが代わりの方法があります:

class User extends Doctrine_Record { public function construct() {

$this->username = ‘Test Name’; $this->doSomething(); }

public function doSomething()
{
    // ...
}

// ...

}

唯一の欠点はコンストラクタにパラメータを渡す方法がないことです。

まとめ

これでモデルのことがよくわかりました。これらを作り、ロードする方法を知っています。最も大事なことはモデルとカラムとリレーションを連携させる方法です。[doc dql-doctrine-query-language :name]の章に移動して使い方を学びます。

はじめに

Doctrine Query Language (DQL)は複雑なオブジェクト読み取りを手助けするためのObject Query Languageです。リレーショナルデータを効率的に読み取るときに(例えばユーザーと電話番号を取得するとき)DQL(もしくは生のSQL)を使うことを常に考えるべきです。

この章ではDoctrine Query Languageの使い方の例をたくさん実演します。これらすべての例では[doc defining-models :name]の章で定義したスキーマを使うことを想定します。またテスト用に1つの追加モデルを定義します。

// models/Account.php

class Account extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); $this->hasColumn(‘amount’, ‘decimal’); } }

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Account: columns: name: string(255) amount: decimal

生のSQLを使う場合と比較すると、DQLは次の恩恵があります:

  • 始めから結果セットの列ではなくレコード(オブジェクト)の読み取りのために設計されている
  • DQLはリレーションを理解するのでSQLのjoinとjoinの条件を手動で入力する必要がない
  • DQLは異なるデータベースでポータブルである
  • DQLはレコード制限などとても複雑なアルゴリズムが組み込まれておりこれは開発者がオブジェクトを効率的に読み取るのを手助けする
  • 条件付きの取得で一対多、多対多のリレーショナルデータを扱うときに時間を節約できる機能をサポートする

DQLの力が十分でなければ、オブジェクト投入に対して[doc native-sql RawSql API]を使うことを考えるべきです。

既に次の構文に慣れている方もいらっしゃるでしょうx:

CAUTION 次のコードは決して使わないでください。 これはオブジェクト投入用に多くのSQLクエリを使います。

// test.php

// ... $users = Doctrine_Core::getTable(‘User’)->findAll();

foreach($users as $user) { echo $user->username . ” has phonenumbers: ”;

foreach($user->Phonenumbers as $phonenumber) {
    echo $phonenumber->phonenumber . "\n";
}

}

Tip

上記と同じ内容ですがオブジェクト投入のために1つのSQLクエリのみを使うより効率的な実装です。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’);

echo $q->getSqlQuery();

上記のクエリによって生成されるSQLを見てみましょう:

SELECT 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

クエリを実行してデータで遊んでみましょう:

// test.php

// ... $users = $q->execute();

foreach($users as $user) { echo $user->username . ” has phonenumbers: ”;

foreach($user->Phonenumbers as $phonenumber) {
    echo $phonenumber->phonenumber . "\n";
}

}

CAUTION DQLの文字列で二重引用符(”)を使うのは非推奨です。これはMySQLの標準では使えますがDQLにおいて識別子と混同される可能性があります。代わりに値に対してプリペアードステートメントを使うことが推奨されます。これによって適切にエスケープされます。

SELECTクエリ

``SELECT``文の構文:

SELECT [ALL | DISTINCT] , ... [FROM [WHERE ] [GROUP BY [ASC | DESC],

... ] [HAVING ] [ORDER BY [ASC | DESC], ...] [LIMIT OFFSET }]

``SELECT``文は1つもしくは複数のコンポーネントからデータを読み取るために使われます。

それぞれの``select_expr``は読み取りたいカラムもしくは集約関数の値を示します。 すべての``SELECT``文で少なくとも1つの``select_expr``がなければなりません。

最初にサンプルの``Account``レコードをinsertします:

// test.php

// ... $account = new Account(); $account->name = ‘test 1’; $account->amount = ‘100.00’; $account->save();

$account = new Account(); $account->name = ‘test 2’; $account->amount = ‘200.00’; $account->save();

``test.php``を実行します:

$ php test.php

次のサンプルクエリでデータのselectをテストできます:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.name’) ->from(‘Account a’);

echo $q->getSqlQuery();

上記のクエリによって生成されたSQLを見てみましょう:

SELECT a.id AS a**id, a.name AS a**name FROM account a

// test.php

// ... $accounts = q->execute(); print_r(accounts->toArray());

上記の例では次の出力が生み出されます:

$ php test.php Array ( [0] => Array ( [id] => 1 [name] => test 1

[amount] => )

[1] => Array
    (
        [id] => 2
        [name] => test 2
        [amount] =>
    )

)

アスタリスクは任意のコンポーネントからすべてのカラムをselectするために使われます。アスタリスクを使うときでも実行されるSQLクエリは実際にはそれを使いません(Doctrineはアスタリスクを適切なカラムの名前に変換することで、データベースでのパフォーマンスの向上につながります)。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.*’) ->from(‘Account a’);

echo $q->getSqlQuery();

最後のクエリの例から生成されたSQLとすぐ前に生成されたクエリで生成されたSQLを比較します:

SELECT a.id AS a**id, a.name AS a**name, a.amount AS a__amount FROM

account a

NOTE アスタリスクは``Account``モデルに存在する実際のすべてのカラム名に置き換えられることに留意してください。

クエリを実行して結果を検査してみましょう:

// test.php

// ... $accounts = q->execute(); print_r(accounts->toArray());

上記の例は次の出力を生み出します:

$ php test.php Array ( [0] => Array ( [id] => 1 [name] => test 1

[amount] => 100.00 )

[1] => Array
    (
        [id] => 2
        [name] => test 2
        [amount] => 200.00
    )

)

``FROM``句はレコードから読み取るコンポーネントを示します。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username, p.*’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’)

echo $q->getSqlQuery();

``getSql()``への上記の呼び出しは次のSQLクエリを出力します:

SELECT 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

``WHERE``句は、選択されるためにレコードが満たさなければならない条件を示します。``where_condition``は選択されるそれぞれの列に対してtrueに表示する式です。``WHERE``句が存在しない場合ステートメントはすべての列を選択します。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.name’) ->from(‘Account a’) ->where(‘a.amount > 2000’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT a.id AS a**id, a.name AS a**name FROM account a WHERE a.amount >

2000

``WHERE``句において、集約(要約)関数を除いて、DQLがサポートする任意の関数と演算子を使うことができます。``HAVING``句は集約関数で結果を絞るために使うことができます:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->having(‘COUNT(p.id) > 3’);

echo $q->getSqlQuery();

``getSql()``を呼び出すと次のSQLクエリが出力されます:

SELECT 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

``ORDER BY``句は結果のソートに使われます。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->orderBy(‘u.username’);

echo $q->getSqlQuery();

上記の``getSql()``を呼び出すと次のSQLクエリが出力されます:

SELECT u.id AS u**id, u.username AS u**username FROM user u ORDER BY

u.username

``LIMIT``と``OFFSET``句はレコードの数を``row_count``に効率的に制限するために使われます。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->limit(20);

echo $q->getSqlQuery();

上記の``getSql()``を呼び出すと次のSQLクエリが出力されます:

SELECT u.id AS u**id, u.username AS u**username FROM user u LIMIT 20
DISTINCTキーワード
集約値

集約値用の``SELECT``構文:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

クエリを実行して結果をインスペクトします:

// test.php

// ... $users = $q->execute();

次のコードで``num_threads``のデータに簡単にアクセスできます:

// test.php

// ... echo $users->num_threads . ‘ threads found’;

UPDATEクエリ

``UPDATE``文の構文:

UPDATE SET = , = WHERE ORDER BY LIMIT
  • ``UPDATE``文は``component_name``の既存のレコードのカラムを新しい値で更新し影響を受けたレコードの数を返します。
  • ``SET``句は修正するカラムとそれらに渡される値を示します。
  • オプションの``WHERE``句は更新するレコードを特定する条件を指定します。``WHERE``句がなければ、すべてのレコードが更新されます。
  • オプションの``ORDER BY``句はレコードが更新される順序を指定します。
  • ``LIMIT``句は更新できるレコードの数に制限をおきます。``UPDATE``の範囲を制限するために``LIMIT row_count``を使うことができます。``LIMIT``句は列を変更する制限ではなく**列にマッチする制限**です。実際に変更されたのかに関わらず``WHERE``句を満たす``record_count``の列が見つかるとステートメントはすぐに停止します。
// test.php

// ... $q = Doctrine_Query::create() ->update(‘Account’) ->set(‘amount’, ‘amount + 200’) ->where(‘id > 200’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

UPDATE account SET amount = amount + 200 WHERE id > 200

更新の実行はシンプルです。次のクエリを実行するだけです:

// test.php

// ... $rows = $q->execute();

echo $rows;

DELETEクエリ

DELETE FROM WHERE ORDER BY LIMIT
  • ``DELETE``文は``component_name``からレコードを削除し削除されるレコードの数を返します。
  • オプションの``WHERE``句は削除するレコードを特定する条件を指定します。``WHERE``句なしでは、すべてのレコードが削除されます。
  • ``ORDER BY``句が指定されると、指定された順序でレコードが削除されます。
  • ``LIMIT``句は削除される列の数に制限を置きます。``record_count``の数のレコードが削除されると同時にステートメントは停止します。
// test.php

// ... $q = Doctrine_Query::create() ->delete(‘Account a’) ->where(‘a.id > 3’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

DELETE FROM account WHERE id > 3

``DELETE``クエリの実行は次の通りです:

// test.php

// ... $rows = $q->execute();

echo $rows;

NOTE DQLのUPDATEとDELETEクエリを実行すると影響を受けた列の数が返されます。

FROM句

構文:

FROM [[LEFT | INNER] JOIN ] ...

``FROM``句はレコードを読み取るコンポーネントを示します。複数のコンポーネントを名付けると、joinを実行することになります。指定されたそれぞれのテーブルに対して、オプションとしてエイリアスを指定できます。

次のDQLクエリを考えます:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u

``User``はクラス(コンポーネント)の名前で``u``はエイリアスです。常に短いエイリアスを使うべきです。大抵の場合これらによってクエリははるかに短くなるのと例えばキャッシュを利用するときに短いエイリアスが使われていればクエリのキャッシュされたフォームの取るスペースが少なくなるからです。

JOINの構文

DQL JOINの構文:

[[LEFT | INNER] JOIN ] [ON | WITH] [INDEXBY] , [[LEFT | INNER] JOIN

] [ON | WITH] [INDEXBY] , ... [[LEFT | INNER] JOIN ] [ON | WITH] [INDEXBY]

DQLはINNER JOINとLEFT JOINをサポートします。それぞれのjoinされたコンポーネントに対して、オプションとしてエイリアスを指定できます。

デフォルトのjoinの形式は``LEFT JOIN``です。このjoinは``LEFT JOIN``句もしくはシンプルな’,‘のどちらかを使うことで示せます。なので次のクエリは等しいです:

// test.php

// ... $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->getSqlQuery();

Tip

推奨される形式は前者です。より冗長で読みやすく何が行われているのか理解しやすいからです。

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, p.id AS p**id FROM user u LEFT JOIN phonenumber p

ON u.id = p.user_id

NOTE JOINの条件が自動的に追加されることに注意してください。Doctrineは``User``と``Phonenumber``は関連していることを知っているのであなたに代わって追加できるからです。

``INNER JOIN``は共通集合を生み出します(すなわち、最初のコンポーネントのありとあらゆるレコードが2番目のコンポーネントのありとあらゆるレコードにjoinされます)。ですので例えば電話番号を1つかそれ以上持つすべてのユーザーを効率的に取得したい場合、基本的に``INNER JOIN``が使われます。

デフォルトではDQLは主キーのjoin条件を自動追加します:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, p.id’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, p.id AS p**id FROM User u LEFT JOIN Phonenumbers

p ON u.id = p.user_id

ONキーワード

このビヘイビアをオーバーライドして独自のカスタムjoin条件を追加したい場合``ON``キーワードで実現できます。次のDQLクエリを考えてみましょう:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, p.id’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p ON u.id = 2’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, p.id AS p**id FROM User u LEFT JOIN Phonenumbers

p ON u.id = 2

NOTE 通常追加される``ON``条件が現れず代わりにユーザーが指定した条件が使われていることに注目してください。
WITHキーワード

大体の場合最初のjoin条件をオーバーライドする必要はありません。むしろカスタム条件を追加したいことがあります。これは``WITH``キーワードで実現できます。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, p.id’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p WITH u.id = 2’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

NOTE ``ON``条件が完全には置き換えられていないことに注意してください。代わりに指定する条件が自動条件に追加されます。

Doctrine_Query APIはJOINを追加するための2つのコンビニエンスメソッドを提供します。これらは``innerJoin()``と``leftJoin()``と呼ばれ、これらの使い方は次のようにとても直感的です:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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キーワード

``INDEXBY``キーワードはコレクション/配列のキーなどの特定のカラムをマッピングする方法を提供します。デフォルトではDoctrineは数値のインデックス付きの配列/コレクションに複数の要素のインデックスを作成します。マッピングはゼロから始まります。このビヘイビアをオーバーライドするには下記で示されるように``INDEXBY``キーワードを使う必要があります:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u INDEXBY u.username’);

$users = $q->execute();

NOTE ``INDEXBY``キーワードは生成されるSQLを変えません。コレクションのそれぞれのレコードのキーとして指定されたカラムでデータをハイドレイトするために``Doctrine_Query``によって内部で使われます。

これで``$users``コレクションのユーザーは自身の名前を通してアクセスできます:

// test.php

// ... echo $user[‘jack daniels’]->id;

``INDEXBY``キーワードは任意のJOINに適用できます。これは任意のコンポーネントがそれぞれ独自のインデックス作成のビヘイビアを持つことができることを意味します。次のコードにおいて``Users``と``Groups``の両方に対して異なるインデックス作成機能を使用しています。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u INDEXBY u.username’) ->innerJoin(‘u.Groups g INDEXBY g.name’);

$users = $q->execute();

drinkers clubの作成日を出力してみましょう。

// test.php

// ... echo $users[‘jack daniels’]->Groups[‘drinkers club’]->createdAt;

WHERE句

構文:

WHERE
  • ``WHERE``句は、与えられた場合、選択されるためにレコードが満たさなければならない条件を示します。
  • ``where_condition``は選択されるそれぞれの列に対してtrueに評価される式です。
  • ``WHERE``句が存在しない場合ステートメントはすべての列を選択します。
  • 集約関数の値で結果を絞るとき``WHERE``句の代わりに``HAVING``句が使われます。

Doctrine_Query``オブジェクトを使用する複雑なwhere条件を構築するために``addWhere()andWhere()orWhere()whereIn()andWhereIn()orWhereIn()whereNotIn(), andWhereNotIn()``orWhereNotIn()``メソッドを使うことができます。

すべてのアクティブな登録ユーザーもしくは管理者を読み取る例は次の通りです:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.type = ? AND u.is_active =

? OR u.is_super_admin = ?

条件式

リテラル

文字列

文字列リテラルはシングルクォートで囲まれます; 例: ‘literal’。シングルクォートを含む文字列リテラルは2つのシングルクォートで表現されます; 例: ‘literal’’s’。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, u.username’) ->from(‘User u’) ->where(‘u.username = ?’, ‘Vincent’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, u.username AS u**username FROM user u WHERE

u.username = ?

NOTE ``where()``メソッドに``username``の値をパラメータとして渡したので生成SQLに含まれません。クエリを実行するときにPDOが置き換え処理をします。``Doctrine_Query``インスタンス上でパラメータをチェックするには``getParams()``メソッドを使うことができます。

整数

整数リテラルはPHPの整数リテラル構文の使用をサポートします。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.id’) ->from(‘User u’) ->where(‘u.id = 4’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.id = 4

浮動小数

浮動小数はPHPの浮動小数リテラルの構文の使用をサポートします。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.id’) ->from(‘Account a’) ->where(‘a.amount = 432.123’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT a.id AS a__id FROM account a WHERE a.amount = 432.123

論理値

論理値リテラルはtrueとfalseです。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.id’) ->from(‘User u’) ->where(‘u.is_super_admin = true’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.is_super_admin = 1

列挙値

列挙値は文字リテラルと同じ方法で動作します。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘a.id’) ->from(‘User u’) ->where(“u.type = ‘admin’”);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.type = ‘admin’

予め定義され予約済みのリテラルは大文字と小文字を区別しますが、これらを大文字で書くのが良い標準です。

入力パラメータ

位置パラメータの使用の例は次の通りです:

単独の位置パラメータ:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’) ->where(‘u.username = ?’, array(‘Arnold’));

echo $q->getSqlQuery();

NOTE 位置パラメータ用に渡されたパラメータが1つの値しか格納しないとき1つの値を含む配列の代わりに単独のスカラー値を渡すことができます。

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.username = ?

複数の位置パラメータ:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->where(‘u.id > ? AND u.username LIKE ?’, array(50, ‘A%’));

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE (u.id > ? AND u.username LIKE

?)

名前付きパラメータの使い方の例は次の通りです:

単独の名前付きパラメータ:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’) ->where(‘u.username = :name’, array(‘:name’ => ‘Arnold’));

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.username = :name

LIKEステートメントを伴う名前付きパラメータ:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.id > :id AND u.username LIKE

:name

演算子と優先順位

演算子の一覧は優先順位が低い順です。

||~ 演算子 ||~ 説明 || || . || ナビゲーション演算子|| || || //算術演算子: // || || +, - || 単項式 || || *, / || 乗法と除法 || || +, - || 加法と減法 || || =, >, >=, <, <=, <> (not equal), || 比較演算子 || || [NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY || || || || //論理演算子: // || || NOT || || || AND || || || OR || ||

IN式

構文:

IN (|)

//サブクエリ//の結果から//オペランド//が見つかるもしくは指定しされたカンマで区切られた//値リスト//にある場合``IN``の条件式はtrueを返します。サブクエリの結果が空の場合``IN``の式は常にfalseです。

//値リスト//が使われているときそのリストには少なくとも1つの要素がなければなりません。

``IN``に対してサブクエリを使う例は次の通りです:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’) ->whereIn(‘u.id’, array(1, 3, 4, 5));

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.id IN (?, ?, ?, ?)
LIKE式

構文:

string_expression [NOT] LIKE pattern_value [ESCAPE escape_character]

string_expressionは文字列の値でなければなりません。pattern_valueは文字列リテラルもしくは文字列の値を持つ入力パラメータです。アンダースコア(\_)は任意の単独の文字を表し、パーセント(%)の文字は文字のシーケンス(空のシーケンスを含む)を表し、そして他のすべての文字はそれら自身を表します。オプションのescape_characterは単独文字の文字列リテラルもしくは文字の値を持つ入力パラメータ(すなわちcharもしくはCharacter)で``pattern_value``で特別な意味を持つアンダースコアとパーセントの文字をエスケープします。

例:

  • address.phone LIKE ‘12%3’は‘123’、‘12993’に対してtrueで‘1234’に対してfalseです。
  • asentence.word LIKE ‘l_se’は’lose’に対してtrueで’loose’に対してfalseです。
  • aword.underscored LIKE ‘_%’ ESCAPE ‘’は’_foo’に対してtrueで’bar’に対してfalseです。
  • address.phone NOT LIKE ‘12%3’は‘123’と‘12993’に対してfalseで‘1234’に対してtrueです。

string_expressionもしくはpattern_valueの値はNULLもしくはunknownで、LIKE式の値はunknownです。escape_characterが指定されNULLである場合、LIKE式の値はunknownです。

'@gmail.com‘で終わるEメールを持つユーザーを見つける:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’) ->leftJoin(‘u.Email e’) ->where(‘e.address LIKE ?’, '%@gmail.com‘);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u LEFT JOIN email e ON u.id =

e.user_id WHERE e.address LIKE ?

‘A’で始まる名前を持つすべてのユーザーを見つける:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’) ->where(‘u.username LIKE ?’, ‘A%’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u WHERE u.username LIKE ?
EXISTS式

構文:

[NOT ]EXISTS ()

``EXISTS``演算子はサブクエリが1つもしくは複数の列を返す場合は``TRUE``を返しそうでなければ``FALSE``を返します。

``NOT EXISTS``演算子はサブクエリが0を返す場合``TRUE``を返しそうでなければ``FALSE``を返します。

NOTE 次の例では``ReaderLog``モデルを追加する必要があります。

// models/ReaderLog.php

class ReaderLog extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘article_id’, ‘integer’, null, array( ‘primary’ => true ) );

    $this->hasColumn('user_id', 'integer', null, array(
            'primary' => true
        )
    );
}

} YAMLフォーマットでの同じは次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

ReaderLog: columns: article_id: type: integer primary: true user_id: type: integer primary: true

NOTE ``ReaderLog``モデルを追加した後で``generate.php``スクリプトを実行することをお忘れなく!

$ php generate.php

これでテストを実行できます!最初に、読者を持つすべての記事を見つけます:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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)

AllとAnyの式

構文:

operand comparison_operator ANY (subquery) operand

comparison_operator SOME (subquery) operand comparison_operator ALL (subquery)

サブクエリの結果のすべての値に対して比較演算子がtrueである場合もしくはサブクエリの結果が空の場合、ALLの条件式はtrueを返します。すべての条件式のALLは少なくとも1つの列に対して比較の結果がfalseである場合はfalseで、trueもしくはfalseのどちらでもない場合はunknownです。

$q = Doctrine_Query::create() ->from(‘C’) ->where(‘C.col1 < ALL (FROM

C2(col1))’);

サブクエリの結果の値に対して比較演算子がtrueの場合条件式のANYはtrueを返します。サブクエリの結果が空の場合もしくはサブクエリの結果のすべての値に対して比較式がfalseの場合、ANY条件式はfalseで、trueでもfalseでもなければunknownです。

$q = Doctrine_Query::create() ->from(‘C’) ->where(‘C.col1 > ANY (FROM

C2(col1))’);

SOMEキーワードはANY用のエイリアスです。

$q = Doctrine_Query::create() ->from(‘C’) ->where(‘C.col1 > SOME (FROM

C2(col1))’);

ALLもしくはANY条件式で使うことができる比較演算子は=、<、<=、>、>=、<>です。サブクエリの結果は条件式で同じ型を持たなければなりません。

NOT INは<> ALL用のエイリアスです。これら2つのステートメントは等しいです:

FROM C WHERE C.col1 <> ALL (FROM C2(col1)); FROM C WHERE C.col1 NOT IN

(FROM C2(col1));

$q = Doctrine_Query::create() ->from(‘C’) ->where(‘C.col1 <> ALL (FROM

C2(col1))’);

$q = Doctrine_Query::create() ->from(‘C’) ->where(‘C.col1 NOT IN (FROM C2(col1))’);

サブクエリ

サブクエリは通常のSELECTクエリが含むことができる任意のキーワードもしくは句を含むことができます。

サブクエリの利点です:

  • これらはクエリを構造化するのでそれぞれの部分のステートメントを分離することが可能です。
  • これらは複雑なjoinとunionを必要とするオペレーションを実行する代替方法を提供します。
  • 多くの人の意見によればこれらは読みやすいです。本当に、人々に初期のSQL “Structured Query Language.”と呼ばれるオリジナルのアイディアを与えたサブクエリのイノベーションでした。

idが1であるグループに所属しないすべてのユーザーを見つける例は次の通りです:

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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 = ?)

グループに所属していないすべてのユーザーを見つける例は次の通りです。

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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)

関数式

文字列関数

//CONCAT//関数は引数を連結した文字列を返します。上記の例においてユーザーの``first_name``と``last_name``を連結して``name``という値にマッピングします。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘CONCAT(u.first_name, u.last_name) AS name’) ->from(‘User u’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, CONCAT(u.first_name, u.last_name) AS u**0 FROM

user u

これでクエリを実行してマッピングされた関数値を取得できます:

$users = $q->execute();

foreach($users as user) { // 'name'はuserのプロパティではなく、 // マッピングされた関数値である echo $user->name; }

//SUBSTRING//関数の2番目と3番目の引数は開始位置と返される部分文字列の長さを表します。これらの引数は整数です。文字列の最初の位置は1によって表されます。//SUBSTRING//関数は文字列を返します。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->where(“SUBSTRING(u.username, 0, 1) = ‘z’”);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, u.username AS u**username FROM user u WHERE

SUBSTRING(u.username FROM 0 FOR 1) = ‘z’

NOTE SQLは使用しているDBMSに対して適切な``SUBSTRING``構文で生成されることに注目してください!

//TRIM//関数は文字列から指定された文字をトリムします。トリムされる文字が指定されていない場合、スペース(もしくは空白)が想定されます。オプションのtrim_characterは単独文字の文字列リテラルもしくは文字の値を持つ入力パラメータです(すなわちcharもしくはCharacter)[30]。トリムの仕様が提供されていない場合、BOTHが想定されます。//TRIM//関数はトリムされた文字列を返します。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->where(‘TRIM(u.username) = ?’, ‘Someone’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, u.username AS u**username FROM user u WHERE

TRIM(u.username) = ?

//LOWER//と//UPPER//関数はそれぞれ文字列を小文字と大文字に変換します。これらは文字列を返します。

// test.php

// ... $q = Doctrine_Query::create(); ->select(‘u.username’) ->from(‘User u’) ->where(“LOWER(u.username) = ‘jon wage’”);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, u.username AS u**username FROM user u WHERE

LOWER(u.username) = ‘someone’

//LOCATE//関数は文字列の範囲内で任意の文字列の位置を返します。検索は指定された位置で始められます。文字列が整数として見つかった位置で、これは最初の位置を返します。最初の引数は検索される文字列です; 2番目の引数は検索文字列です; 3番目のオプション引数は検索が始まる文字列の位置を表す整数です(デフォルトでは、検索文字列の始め)。文字列の最初の位置は1によって表現されます。文字列が見つからない場合、0が返されます。

//LENGTH//関数は文字の文字列の長さを整数として返します。

算術関数

利用可能なDQLの算術関数です:

ABS(simple_arithmetic_expression)

SQRT(simple_arithmetic_expression) MOD(simple_arithmetic_expression, simple_arithmetic_expression)

  • //ABS//関数は与えられた数の絶対値を返します。
  • //SQRT//関数は与えられた数の平方根を返します。
  • //MOD//関数は2番目の引数で最初の引数を割ったときの余りを返します。

サブクエリ

はじめに

DoctrineではFROM、SELECTとWHERE文でDQLのサブクエリを使うことができます。下記のコードではDoctrineが提供する異なる型のすべてのサブクエリの例が見つかります。

サブクエリを利用する比較

指定されたグループに所属しないすべてのユーザーを見つける。

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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 = ?)

サブクエリでユーザーの電話番号を読み取りユーザー情報の結果セットに格納します。

// test.php

// ... $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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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、HAVING句

DQLのGROUP BY構文:

GROUP BY groupby_item {, groupby_item}*

DQL HAVINGの構文:

HAVING conditional_expression

GROUP BY``と``HAVING``句は集約関数を扱うために使われます。次の集約関数がDQLで利用可能です: ``COUNTMAXMINAVGSUM

アルファベット順で最初のユーザーを名前で選択する。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘MIN(a.amount)’) ->from(‘Account a’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT MIN(a.amount) AS a__0 FROM account a

すべてのアカウントの合計数を選択する。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘SUM(a.amount)’) ->from(‘Account a’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT SUM(a.amount) AS a__0 FROM account a

GROUP BY句を含まないステートメントで集約関数を使うと、すべての列でグルーピングすることになります。下記の例ではすべてのユーザーと彼らが持つ電話番号の合計数を取得します。

// 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->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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``句は集約値を使用する結果を狭めるために使われます。次の例では少なくとも2つの電話番号を持つすべてのユーザーを取得します。

// 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’) ->having(‘num_phonenumbers >= 2’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

次のコードで電話番号の数にアクセスできます:

// test.php

// ... $users = $q->execute();

foreach($users as $user) { echo $user->name . ‘ has ‘ . $user->num_phonenumbers . ‘ phonenumbers’; }

ORDER BY句

はじめに

レコードのコレクションはORDER BY句を使用してデータベースレベルで効率的にソートできます。

構文:

[ORDER BY {ComponentAlias.columnName} [ASC | DESC], ...]

例:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->orderBy(‘u.username, p.phonenumber’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

逆順でソートするためにソートするORDER BY句のカラム名にDESC(降順)キーワードを追加できます。デフォルトは昇順です; これはASCキーワードを使用して明示的に指定できます。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username’) ->from(‘User u’) ->leftJoin(‘u.Email e’) ->orderBy(‘e.address DESC, u.id ASC’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

集約値でソートする

次の例ではすべてのユーザーを取得しユーザーが持つ電話番号の数でユーザーをソートします。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username, COUNT(p.id) count’) ->from(‘User u’) ->innerJoin(‘u.Phonenumbers p’) ->orderby(‘count’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

ランダム順を使う

次の例ではランダムな投稿を取得するために``ORDER BY``句でランダム機能を使います。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘t.id, RANDOM() AS rand’) ->from(‘Forum_Thread t’) ->orderby(‘rand’) ->limit(1);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT f.id AS f**id, RAND() AS f**0 FROM forum**thread f ORDER BY f**0

LIMIT 1

LIMITとOFFSET句

おそらく最も複雑機能であるDQLパーサーは``LIMIT``句パーサーです。DQL LIMIT句パーサーは``LIMIT``データベースポータビリティを考慮するだけでなく複雑なクエリ分析とサブクエリを使用することで列の代わりにレコードの数を制限できる機能を持ちます。

最初の20ユーザーと関連する電話番号を読み取ります:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.username, p.phonenumber’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->limit(20);

echo $q->getSqlQuery();

Tip

``Doctrine_Query``オブジェクトの``offset()``メソッドは実行SQLクエリで望みどおりの``LIMIT``と``OFFSET``を生み出すために``limit()``メソッドと組み合わせて使うことmできます。

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

ドライバーのポータビリティ

DQLの``LIMIT``句はサポートされるすべてのデータベース上でポータブルです。次の事実に対して特別な注意を払う必要があります:

  • Mysql、PgsqlとSqliteのみがLIMIT / OFFSET句をネイティブに実装します。
  • Oracle / Mssql / FirebirdではLIMIT / OFFSET句はドライバー専用の方法でシミュレートされる必要があります
  • limit-subquery-algorithmはmysqlで個別にサブクエリを実行する必要があります。まだmysqlがサブクエリでLIMIT句をサポートしていないからです。
  • PgsqlはSELECT句で保存するフィールドごとの順序を必要とします。limit-subquery-algorithmがpgsqlドライバーが使われるときに考慮される必要があるからです。
  • Oracleは< 30個未満のオブジェクト識別子のみを許可します(= テーブル/カラム 名前/エイリアス)、limitサブクエリは可能な限りショートエイリアスを使いメインクエリでエイリアスの衝突を回避しなければなりません。
limit-subquery-algorithm

limit-subquery-algorithmはDQLパーサーが内部で使用するアルゴリズムです。1対多/多対多のリレーショナルデータは同時に取得されているときに内部で使用されます。SQLの結果セットの列の代わりにレコードの数を制限するためにこの種の特別なアルゴリズムはLIMIT句に必要です。

このビヘイビアは設定システムを使用してオーバーライドできます(グローバル、接続もしくはテーブルレベル):

$table->setAttribute(Doctrine_Core::ATTR_QUERY_LIMIT,

Doctrine_Core::LIMIT_ROWS); $table->setAttribute(Doctrine_Core::ATTR_QUERY_LIMIT, Doctrine_Core::LIMIT_RECORDS); // リバート

次の例ではユーザーと電話番号がありこれらのリレーションは1対多です。最初の20ユーザーを取得しすべての関連する電話番号を取得することを考えてみましょう。

クエリの最後でシンプルなドライバ固有のLIMIT 20を追加すれば正しい結果が返されるとお考えの方がいらっしゃるかもしれません。これは間違っています。1から20までの任意のユーザーを20の電話番号を持つ最初のユーザーとして取得しレコードセットが20の列で構成されることがあるからです。

DQLはサブクエリと複雑だが効率的なサブクエリの解析でこの問題に打ち勝ちます。次の例では最初の20人のユーザーとそのすべての電話番号を効果的な1つのクエリで取得しようとしています。DQLパーサーがサブクエリでもカラム集約継承を使うほど賢くまたエイリアスの衝突を回避するサブクエリのテーブルに異なるテーブルを使うほど賢いことに注目してください。

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, u.username, p.*’) ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->limit(20);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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

次の例では最初の20人のユーザーとすべての電話番号かつ実際に電話番号を持つユーザーのみを1つの効率的なクエリで取得します。これは``INNER JOIN``を使います。サブクエリで``INNER JOIN``を使うほどDQLパーサーが賢いことに注目してください:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id, u.username, p.*’) ->from(‘User u’) ->innerJoin(‘u.Phonenumbers p’) ->limit(20);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT 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 INNER JOIN phonenumber p ON u.id = p.user_id

名前付きクエリ

変化する可能性があるモデルを扱うが、クエリを簡単に更新できるようにする必要があるとき、クエリを定義する簡単な方法を見つける必要があります。例えば1つのフィールドを変更して何も壊れていないことを確認するためにアプリケーションのすべてのクエリを追跡する必要がある状況を想像してください。

名前付きクエリはこの状況を解決する素晴らしく効率的な方法です。これによって``Doctrine_Queries``を作成しこれらを書き直すこと無く再利用できるようになります。

名前付きクエリのサポートは``Doctrine_Query_Registry``のサポートの上で構築されます。``Doctrine_Query_Registry``はクエリを登録して名前をつけるためのクラスです。これはアプリケーションクエリの編成を手助けしこれに沿ってとても便利な機能を提供します。

レジストリオブジェクトの``add()``メソッドを使用してこのクエリは追加されます。これは2つのパラメータ、クエリの名前と実際のDQLクエリを受け取ります。

// test.php

// ... $r = Doctrine_Manager::getInstance()->getQueryRegistry();

$r->add(‘User/all’, ‘FROM User u’);

$userTable = Doctrine_Core::getTable(‘User’);

// すべてのユーザーを見つける $users = $userTable->find(‘all’);

このサポートを簡略化するために、``Doctrine_Table``は``Doctrine_Query_Registry``へのアクセサをサポートします。

名前付きクエリを作成する

trueとして定義された``generateTableClasses``オプションでモデルをビルドするとき、それぞれのレコードクラスは``Doctrine_Table``を継承する``*Table``クラスも生成します。

それから、名前付きクエリを含めるために``construct()``メソッドを実装できます:

class UserTable extends Doctrine_Table { public function construct() {

// DQL文字列を使用して定義されたNamed Query $this->addNamedQuery(‘get.by.id’, ‘SELECT u.username FROM User u WHERE u.id = ?’);

    // Doctrine_Queryオブジェクトを使用して定義された名前付きのクエリ
    $this->addNamedQuery(
        'get.by.similar.usernames', Doctrine_Query::create()
            ->select('u.id, u.username')
            ->from('User u')
            ->where('LOWER(u.username) LIKE LOWER(?)')
    );
}

}

名前付きクエリにアクセスする

``Doctrine_Table``のサブクラスである``MyFooTable``クラスにリーチするには、次のようにできます:

$userTable = Doctrine_Core::getTable(‘User’);

名前付きクエリにアクセスするには(常に``Doctrine_Query``インスタンスを返す):

$q = $userTable->createNamedQuery(‘get.by.id’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u**id, u.username AS u**username FROM user u WHERE u.id

= ?

名前付きクエリを実行する

名前付きクエリを実行するには2つの方法があります。1つめの方法は通常のインスタンスとして``Doctrine_Query``を読み取り通常通りに実行します:

// test.php

// ... $users = Doctrine_Core::getTable(‘User’) ->createNamedQuery(‘get.by.similar.usernames’) ->execute(array(‘%jon%wage%’));

次のようにも実行を簡略化できます:

// test.php

// ... $users = Doctrine_Core::getTable(‘User’) ->find(‘get.by.similar.usernames’, array(‘%jon%wage%’));

``find()``メソッドはハイドレーションモード用の3番目の引数を受け取ります。

名前付きクエリにクロスアクセスする

それで十分でなければ、Doctrineは``Doctrine_Query_Registry``を利用しオブジェクト間の名前付きクエリにクロスアクセスできるようにする名前空間クエリを使います。Article``レコードの*Table``クラスのインスタンスがあることを想定します。``User``レコードの”get.by.id” 名前付きクエリを呼び出したいとします。名前付きクエリにアクセスするには、次のように行わなければなりません:

// test.php

// ... $articleTable = Doctrine_Core::getTable(‘Article’);

$users = $articleTable->find(‘User/get.by.id’, array(1, 2, 3));

BNF

QL_statement ::= select_statement | update_statement |

delete_statement 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_field 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 string_expression ::= string_primary | (subquery) string_primary ::= state_field_path_expression | string_literal | input_parameter | functions_returning_strings | aggregate_expression

datetime_expression ::= datetime_primary | (subquery) datetime_primary ::= state_field_path_expression | input_parameter | functions_returning_datetime | aggregate_expression boolean_expression ::= boolean_primary | (subquery) boolean_primary ::= state_field_path_expression | boolean_literal | input_parameter | enum_expression ::= enum_primary | (subquery) enum_primary ::= state_field_path_expression | enum_literal | input_parameter | entity_expression ::= single_valued_association_path_expression | simple_entity_expression simple_entity_expression ::= identification_variable | input_parameter functions_returning_numerics::= LENGTH( string_primary) | LOCATE( string_primary, string_primary[, simple_arithmetic_expression]) | ABS( simple_arithmetic_expression) | SQRT( simple_arithmetic_expression) | MOD( simple_arithmetic_expression, simple_arithmetic_expression) | SIZE( collection_valued_path_expression) functions_returning_datetime ::= CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP functions_returning_strings ::= CONCAT( string_primary, string_primary) | SUBSTRING( string_primary, simple_arithmetic_expression, simple_arithmetic_expression)| TRIM( [[trim_specification] [trim_character]FROM ] string_primary) | LOWER( string_primary) | UPPER( string_primary) trim_specification ::=LEADING | TRAILING | BOTH

マジックファインダー

Doctrineはモデルに存在する任意のカラムでレコードを見つけることを可能にするDoctrineモデル用のマジックファインダー(magic finder)を提供します。ユーザーの名前でユーザーを見つけたり、グループの名前でグループを見つけるために役立ちます。通常これは``Doctrine_Query``インスタンスを書き再利用できるようにこれをどこかに保存することが必要です。このようなシンプルな状況にはもはや必要ありません。

ファインダーメソッドの基本パターンは次の通りです: ``findBy%s(value)``もしくは``findOneBy%s(value)``です。``%s``はカラム名もしくはリレーションのエイリアスです。カラムの名前の場合探す値を提供しなければなりません。リレーションのエイリアスを指定する場合、見つけるリレーションクラスのインスタンスを渡すか、実際の主キーの値を渡すことができます。

最初に扱う``UserTable``インスタンスを読み取りましょう:

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’);

``find()``メソッドを利用して主キーで``User``レコードを簡単に見つけられます:

// test.php

// ... $user = $userTable->find(1);

ユーザー名で1人のユーザーを見つけたい場合は次のようにマジックファインダーを使うことができます:

// test.php

// ... $user = $userTable->findOneByUsername(‘jonwage’);

レコード間のリレーションを利用してレコードでユーザーを見つけることができます。``User``は複数の``Phonenumbers``を持つので``findBy**()``メソッドに``User``インスタンスを渡すことでこれらの``Phonenumber``を見つけることができます:

// test.php

// ... $phonenumberTable = Doctrine::getTable(‘Phonenumber’);

$phonenumbers = phonenumberTable->findByUser(user);

マジックファインダーはもう少し複雑な検索を可能にします。複数のプロパティによってレコードを検索するためにメソッド名で``And``と``Or``キーワードを使うことができます。

$user = $userTable->findOneByUsernameAndPassword(‘jonwage’,

md5(‘changeme’));

条件を混ぜることもできます。

$users = $userTable->findByIsAdminAndIsModeratorOrIsSuperAdmin(true,

true, true);

CAUTION これらは語句限られたマジックメソッドの用例で、つねに手書きのDQLクエリで展開することをおすすめします。これらのメソッドはリレーションシップなしの単独のレコードに素早くアクセスするための手段であり、素早くプロトタイプを書くのにもよいものです。

NOTE 上記のマジックファインダーはPHPの``[http://php.net/__call **call()]``のオーバーロード機能を使うことで作成されます。内在する関数は``Doctrine_Query``オブジェクトがビルドされる``Doctrine_Table::**call()``に転送され、実行されてユーザーに返されます。

クエリをデバッグする

``Doctrine_Query``オブジェクトはクエリの問題をデバッグするための手助けになる機能を少々提供します:

ときに``Doctrine_Query``オブジェクトに対して完全なSQL文字列を見たいことがあります:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’) ->orderBy(‘u.username’);

echo $q->getSqlQuery();

``getSql()``への呼び出しは次のSQLクエリを出力します:

SELECT u.id AS u__id FROM user u ORDER BY u.username

NOTE 上記の``Doctrine_Query::getSql()``メソッドによって返されるSQLはトークンをパラメータに置き換えません。これはPDOのジョブでクエリを実行するとき置き換えが実行されるPDOにパラメータを渡します。``Doctrine_Query::getParams()``メソッドでパラメータの配列を読み取ることができます。

``Doctrine_Query``インスタンス用のパラメータの配列を取得します:

// test.php

// ... print_r($q->getParams());

まとめ

Doctrine Query Languageはこれまでのところ最も高度で役に立つDoctrineの機能です。これによってRDBMSのリレーションからとても複雑なデータを簡単にかつ効率的に選択できます!

これでDoctrineの主要なコンポーネントの使い方を見たので[doc component-overview :name]の章に移りすべてを鳥の目で見ることにします。

この章はDoctrineを構成するすべてのメインコンポーネントとそれらの連携方法を鳥の目から見ることを目的としています。前の章で既に大半のコンポーネントを検討しましたがこの章ではすべてのコンポーネントとそれらのジョブの理解が進みます。

マネージャー

``Doctrine_Manager``クラスはSingletonで構成階層のrootでありDoctrineのいくつかの面をコントロールするFacadeです。次のコードでSingletonインスタンスを読み取ることができます。

// test.php

// ... $manager = Doctrine_Manager::getInstance();

接続を読み取る
// test.php

// ... $connections = manager->getConnections(); foreach (connections as $connection) { echo $connection->getName() . “”; }

``Doctrine_Manager``はイテレータを実装するので接続をループするために変数$managerをループできます。

// test.php

// ... foreach ($manager as $connection) { echo $connection->getName() . “”; }

接続

``Doctrine_Connection``はデータベース用のラッパーです。接続は典型的なPDOのインスタンスですが、Doctrineの設計のおおかげで、PDOが提供する機能を模倣する独自アダプタを設計することが可能です。

``Doctrine_Connection``クラスは次のことを対処します:

  • PDOから見つからないデータベースのポータビリティ機能(例えばLIMIT/OFFSETのエミュレーション)を処理する
  • ``Doctrine_Table``オブジェクトの経過を追跡する
  • レコードの経過を追跡する
  • update/insert/deleteする必要のあるレコードの経過を追跡する
  • トランザクションと入れ子構造のトランザクションを処理する
  • INSERT / UPDATE / DELETEオペレーションの場合の実際のデータベースクエリを処理する
  • DQLを使用データベースクエリを行う。DQLは[doc dql-doctrine-query-language :name]の章で学ぶことができる。
  • オプションとしてDoctrine_Validatorを使用してトランザクションをバリデートしてあり得るエラーの全情報を示す
利用できるドライバ

DoctrineはPDOがサポートするデータベース用のすべてのドライバを持ちます。サポートされるデータベースは次の通りです:

  • FreeTDS / Microsoft SQL Server / Sybase
  • Firebird/Interbase 6
  • Informix
  • Mysql
  • Oracle
  • Odbc
  • PostgreSQL
  • Sqlite
接続を作成する
// bootstrap.php

// ... $conn = Doctrine_Manager::connection(‘mysql://username:password@localhost/test’, ‘connection 1’);

NOTE 前の章で既に新しい接続を作成しました。上記のステップをスキップして既に作成した接続を使うことができます。``Doctrine_Manager::connection()``メソッドを使用して読み取ることができます。
接続をflushする

新しい``User``レコードを作成するときレコードは接続をflushしてその接続に対して保存されていないすべてのオブジェクトを保存します。下記は例です:

// test.php

// ... $conn = Doctrine_Manager::connection();

$user1 = new User(); $user1->username = ‘Jack’;

$user2 = new User(); $user2->username = ‘jwage’;

$conn->flush();

``Doctrine_Connection::flush()``を呼び出せばその接続に対する未保存のレコードインスタンスが保存されます。もちろんオプションとしてそれぞれのレコードごとに``save()``を呼び出して同じことができます。

// test.php

// ... $user1->save(); $user2->save();

テーブル

``Doctrine_Table``はコンポーネント(レコード)によって指定されるスキーマ情報を保有します。例えば``Doctrine_Record``を継承する``User``クラスがある場合、それぞれのスキーマ定義の呼び出しは後で使う情報を保有するユニークなテーブルオブジェクトにデリゲートされます。

それぞれの``Doctrine_Table``は``Doctrine_Connection``によって登録されます。下記に示されるそれぞれのコンポーネント用のテーブルオブジェクトを簡単に取得できます。

例えば、Userクラス用のテーブルオブジェクトを読み取りたい場合を考えます。これは``User``を``Doctrine_Core::getTable()``メソッドの第一引数として渡すことで可能です。

テーブルオブジェクトを取得する

指定するレコードのテーブルオブジェクトを取得するには、``Doctrine_Record::getTable()``を呼び出すだけです。

// test.php

// ... $accountTable = Doctrine_Core::getTable(‘Account’);

カラム情報を取得する

適切な``Doctrine_Table``メソッドを使用することで``Doctrine_Record``のカラム定義セットを読み取ることができます。すべてのカラムのすべての情報が必要な場合は次のように行います:

// test.php

// ... $columns = $accountTable->getColumns();

$columns = accountTable->getColumns(); foreach (columns as column) { print_r(column); }

上記の例が実行されるときに次の内容が出力されます:

$ php test.php Array ( [type] => integer [length] => 20 [autoincrement]

=> 1 [primary] => 1 ) Array ( [type] => string [length] => 255 ) Array ( [type] => decimal [length] => 18 )

ときにこれがやりすぎであることがあります。次の例はカラムの名前を配列として読み取る方法を示しています:

// test.php

// ... $names = accountTable->getColumnNames(); print_r(names);

上記の例が実行されるとき次の内容が出力されます:

$ php test.php Array ( [0] => id [1] => name [2] => amount )
リレーションの情報を取得する

次のように``Doctrine_Table::getRelations()``を呼び出すことですべての``Doctrine_Relation``オブジェクトの配列を取得できます:

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’);

$relations = $userTable->getRelations();

foreach ($relations as $name => $relation) { echo $name . ”:”; echo “Local - ” . $relation->getLocal() . “”; echo “Foreign - ” . $relation->getForeign() . “”; }

上記の例が実行されるとき次の内容が出力されます:

$ php test.php Email: Local - id Foreign - user_id

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

``Doctrine_Table::getRelation()``メソッドを使用することで個別のリレーション用の``Doctrine_Relation``オブジェクトを取得できます。

// test.php

// ... $relation = $userTable->getRelation(‘Phonenumbers’);

echo ‘Name: ‘ . $relation[‘alias’] . “”; echo ‘Local - ‘ . $relation[‘local’] . “”; echo ‘Foreign - ‘ . relation['foreign'] . "\n"; echo 'Relation Class - ' . get_class(relation);

上記の例が実行されるとき次の内容が出力されます:

$ php test.php Name: Phonenumbers Local - id Foreign - user_id

Relation Class - Doctrine_Relation_ForeignKey

NOTE 上記の例において変数``$relation}は}配列としてアクセスできる``Doctrine_Relation_ForeignKey``のインスタンスを格納していることに注目してください。多くのDoctrineのクラスのように、これが``ArrayAccess``を実装するからです。

``toArray()``メソッドと``print_r()``を使用することでリレーションのすべての情報を検査してデバッグすることができます。

// test.php

// ... $array = relation->toArray(); print_r(array);

ファインダーメソッド

``Doctrine_Table``は基本的なファインダーメソッドを提供します。これらのファインダーメソッドはとても速く書けるので1つのデータベーステーブルからデータを取得する場合に使われます。いくつかのコンポーネント(データベーステーブル)を使用するクエリが必要な場合 ``Doctrine_Connection::query()``を使います。

主キーで個別のユーザーを簡単に見つけるには``find()``メソッドを使用します:

// test.php

// ... $user = userTable->find(2); print_r(user->toArray());

上記の例が実行されるとき次の内容が出力されます:

$ php test.php Array ( [id] => 2 [is_active] => 1 [is_super_admin]

=> 0 [first_name] => [last_name] => [username] => jwage [password] => [type] => [created_at] => 2009-01-21 13:29:12 [updated_at] => 2009-01-21 13:29:12 )

データベースのすべての``User``レコードのコレクションを読み取るために``findAll()``メソッドを使うこともできます:

// test.php

// ... foreach ($userTable->findAll() as $user) { echo $user->username . “”; }

上記の例が実行されるとき次の内容が出力されます:

$ php test.php Jack jwage

CAUTION ``findAll()``メソッドは推奨されません。このメソッドがデータベースのすべてのレコードを返しリレーションから情報を読み取る場合高いクエリカウントを引き起こしながらそのデータを遅延ロードするからです。[doc dql-doctrine-query-language :name]の章を読めばレコードと関連レコードを効率的に読み取る方法を学べます。

``findByDql()``メソッドを使用して DQLでレコードのセットを読み取ることもできます:

// test.php

// ... $users = $userTable->findByDql(‘username LIKE ?’, ‘%jw%’);

foreach($users as $user) { echo $user->username . “”; }

上記の例が実行されるときに次の内容が出力されます:

$ php test.php jwage

Doctrineは追加のマジックファインダーメソッドも提供します。この内容はDQLの章の[doc dql-doctrine-query-language:magic-finders :name]セクションで読むことができます。

NOTE ``Doctrine_Table``によって提供される下記のすべてのファインダーメソッドはクエリを実行するために``Doctrine_Query``のインスタンスを使用します。オブジェクトは内部で動的に構築され実行されます。

リレーションを通して複数のオブジェクトにアクセスするときは``Doctrine_Query``インスタンスを使用することが多いに推奨されます。そうでなければデータが遅延ロードされるので高いクエリカウントを得ることになります。[doc dql-doctrine-query-language :name]の章で詳細を学ぶことができます。

カスタムのテーブルクラス

カスタムのテーブルクラスを追加するのはとても楽です。行う必要のあるのはクラスを[componentName]Tableとして名付けこれらに``Doctrine_Table``を継承させます。``User``モデルに関して次のようなクラスを作ることになります:

// models/UserTable.php

class UserTable extends Doctrine_Table { }

カスタムのファインダー

カスタムのテーブルオブジェクトにカスタムのファインダーメソッドを追加できます。これらのファインダーメソッドは速い``Doctrine_Table``ファインダーメソッドもしくは[doc dql-doctrine-query-language DQL API] (Doctrine_Query::create())を使用できます。

// models/UserTable.php

class UserTable extends Doctrine_Table { public function findByName(name) { return Doctrine_Query::create() ->from('User u') ->where('u.name LIKE ?', "%name%”) ->execute(); } }

Doctrineは``getTable()``を呼び出すときに``Doctrine_Table``の子クラスである``UserTable``が存在するかチェックしそうである場合、デフォルトの``Doctrine_Table``の代わりにそのクラスのインスタンスを返します。

NOTE カスタムの``Doctrine_Table``クラスをロードするには、下記のように``bootstrap.php``ファイルで``autoload_table_classes``属性を有効にしなければなりません。

// boostrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);

これで``User``テーブルオブジェクトに問い合わせるとき次の内容が得られます:

$userTable = Doctrine_Core::getTable(‘User’);

echo get_class($userTable); // UserTable

$users = $userTable->findByName(“Jack”);

NOTE ``findByName()``メソッドを追加する上記の例はマジックファインダーメソッドによって自動的に利用可能になります。DQLの章の[doc dql-doctrine-query-language:magic-finders :name]セクションで読むことができます。

レコード

Doctrineは``Doctrine_Record``子クラスを用いてRDBMSのテーブルを表します。これらのクラスはスキーマ情報、お婦四、属性などを定義する場所です。これらの子クラスのインスタンスはデータベースのレコードを表しこれらのオブジェクトでプロパティの取得と設定ができます。

プロパティ

``Doctrine_Record``のそれぞれ割り当てられたカラムプロパティはデータベースのテーブルカラムを表します。[doc defining-models :name]の章でモデルの定義方法の詳細を学ぶことになります。

カラムへのアクセスは簡単です:

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’);

$user = $userTable->find(1);

オーバーロードを通してプロパティにアクセスする

// test.php

// ... echo $user->username;

get()でプロパティにアクセスする

// test.php

// ... echo $user->get(‘username);

ArrayAccessでプロパティにアクセスする

// test.php

// ... echo $user[‘username’];

Tip

カラムの値にアクセスする推奨方法はArrayAccessを使うことです。これによって必要に応じてレコードと配列取得を切り替えるのが簡単になるからです。

レコードのプロパティのイテレーションは配列のやり方と似ています。``foreach``コンストラクトを使用します。``Doctrine_Record``は``IteratorAggregate``インターフェイスを実装するのでこれは実現可能です。

// test.php

// ... foreach ($user as $field => $value) { echo $field . ‘: ‘ . $value . “”; }

配列に関してプロパティの存在のチェックには``isset()``を、プロパティをnullに設定するには``unset()``が利用できます。

if文で’name’という名前のプロパティが存在するか簡単にチェックできます:

// test.php

// ... if (isset($user[‘username’])) {

}

nameプロパティの割り当てを解除したい場合PHPの``unset()``関数を使うことができます:

// test.php

// ... unset($user[‘username’]);

レコードプロパティ用に値を設定するとき``Doctrine_Record::getModified()``を使用して修正されたフィールドと値の配列を取得できます。

// test.php

// ... $user[‘username’] = ‘Jack Daniels’;

print_r($user->getModified());

上記のコードが実行されるとき次の内容が出力されます:

$ php test.php Array ( [username] => Jack Daniels )

``Doctrine_Record::isModified()``メソッドを使用してレコードが修正されることをチェックすることもできます:

// test.php

// ... echo $user->isModified() ? ‘Modified’:’Not Modified’;

ときどき任意のレコードのカラムカウントを読み取りたいことがあります。これを行うには``count()``関数にレコードを引数として渡します。``Doctrine_Record``が``Countable``インターフェイスを実装するのでこれは可能です。他には``count()``メソッドを呼び出す方法があります。

// test.php

// ... echo record->count(); echo count(record);

``Doctrine_Record``は任意のレコードの識別子にアクセスするための特別なメソッドを提供します。このメソッドは``identifier()``と呼ばれキーが識別子のフィールド名であり、値が、関連プロパティの値である配列を返します。

// test.php

// ... $user[‘username’] = ‘Jack Daniels’; $user->save();

print_r($user->identifier()); // array(‘id’ => 1)

よくあるのは配列の値を任意のレコードに割り当てることです。これらの値を個別に設定するのはやりずらいと思うかもしれません。しかし悩む必要はありません。``Doctrine_Record``は任意の配列もしくはレコードを別のものにマージする方法を提供します。

``merge()``メソッドはレコードもしくは配列のプロパティをイテレートしてオブジェクトに値を割り当てます。

// test.php

// ... $values = array( ‘username’ => ‘someone’, ‘age’ => 11, );

user->merge(values);

echo $user->username; // someone echo $user->age; // 11

次のように1つのレコードの値を別のものにマージすることもできます:

// test.php

// ... $user1 = new User(); $user1->username = ‘jwage’;

$user2 = new User(); user2->merge(user1);

echo $user2->username; // jwage

NOTE ``Doctrine_Record``は``fromArray()``メソッドを持ちます。このメソッドは``merge()``に理想的なもので``toArray()``メソッドとの一貫性を保つためだけに存在します。
レコードを更新する

オブジェクトの更新は非常に簡単で、``Doctrine_Record::save()``メソッドを呼び出すだけです。他の方法は``Doctrine_Connection::flush()``を呼び出す方法でこの場合すべてのオブジェクトが保存されます。flushはsaveメソッドを呼び出すだけよりも重たいオペレーションであることに注意してください。

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’);

$user = $userTable->find(2);

if ($user !== false) { $user->username = ‘Jack Daniels’;

$user->save();

}

ときどき直接更新を行いたいことがあります。直接の更新においてオブジェクトはデータベースからロードされません。むしろデータベースの状態が直接更新されます。次の例においてすべてのユーザーを更新するためにDQL UPDATE文を使います。

すべてのユーザー名を小文字にするクエリを実行します:

// test.php

// ... $q = Doctrine_Query::create() ->update(‘User u’) ->set(‘u.username’, ‘LOWER(u.name)’);

$q->execute();

レコードの識別子が既知であればオブジェクトを利用して更新を実行することもできます。``Doctrine_Record::assignIdentifier()``メソッドを使うときこれはレコード識別子を設定し状態を変更するので``Doctrine_Record::save()``の呼び出しはinsertの代わりにupdateを実行します。

// test.php

// ... $user = new User(); $user->assignIdentifer(1); $user->username = ‘jwage’; $user->save();

レコードを置き換える

レコードを置き換えるのはシンプルです。まずは新しいオブジェクトをインスタンス化して保存します。次にデータベースに既に存在する同じ主キーもしくはユニークキーの値で新しいオブジェクトをインスタンス化すればデータベースで新しい列をinsertする代わりに列を置き換え/更新が行われます。下記は例です。

最初に、ユーザー名がユニークインデックスである``User``モデルを想像してみましょう。

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->password = ‘changeme’; $user->save();

次のクエリを発行します。

INSERT INTO user (username, password) VALUES (?,?) (‘jwage’,

‘changeme’)

別の新しいオブジェクトを作り同じユーザー名と異なるパスワードを設定します。

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->password = ‘newpassword’; $user->replace();

次のクエリが発行されます

REPLACE INTO user (id,username,password) VALUES (?,?,?) (null, ‘jwage’,

‘newpassword’)

新しいレコードがinsertされる代わりにレコードが置き換え/更新されます。

レコードをリフレッシュする

ときにデータベースからのデータでレコードをリフレッシュしたいことがあります。``Doctrine_Record::refresh()``を使います。

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->find(2); $user->username = ‘New name’;

``Doctrine_Record::refresh()``メソッドを使う場合データベースからデータが再度選択されインスタンスのプロパティが更新されます。

// test.php

// ... $user->refresh();

リレーションをリフレッシュする

``Doctrine_Record::refresh()``メソッドは既にロードされたレコードのリレーションをリフレッシュすることもできますが、オリジナルのクエリでこれらを指定する必要があります。

最初に関連``Groups``で``User``を読み取りましょう:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Groups’) ->where(‘id = ?’);

$user = $q->fetchOne(array(1));

関連``Users``で``Group``を読み取りましょう:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘Group g’) ->leftJoin(‘g.Users’) ->where(‘id = ?’);

$group = $q->fetchOne(array(1));

``UserGroup``インスタンスで読み取られた``User``と``Group``をリンクしましょう:

// test.php

// ... $userGroup = new UserGroup(); $userGroup->user_id = $user->id; $userGroup->group_id = $group->id; $userGroup->save();

``Group``を``User``に追加するだけで``User``を``Group``にリンクすることもできます。Doctrineは``UserGroup``インスタンスの作成を自動的に引き受けます:

// test.php

// ... $user->Groups[] = $group; $user->save()

``Doctrine_Record::refresh(true)``を呼び出す場合新しく作成された参照をロードするレコードとリレーションがリフレッシュされます:

// test.php

// ... $user->refresh(true); $group->refresh(true);

``Doctrine_Record::refreshRelated()``を使用してモデルの定義されたすべてのリレーションを遅延リフレッシュすることもできます:

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->findOneByName(‘jon’); $user->refreshRelated();

リレーションを個別に指定してリフレッシュしたい場合リレーションの名前を``refreshRelated()``メソッドに渡せばリレーションは遅延ロードされます:

// test.php

// ... $user->refreshRelated(‘Phonenumber’);

レコードを削除する

Doctrineでのレコード削除は``Doctrine_Record::delete()``、``Doctrine_Collection::delete()``と``Doctrine_Connection::delete()``メソッドによって処理されます。

// test.php

// ... $userTable = Doctrine_Core::getTable(“User”);

$user = $userTable->find(2);

// ユーザーと関連コンポジットオブジェクトすべてを削除する if($user !== false) { $user->delete(); }

``User``レコードの``Doctrine_Collection``がある場合``delete()``を呼び出すと``Doctrine_Record::delete()``が呼び出されてすべてのレコードがループされます。

// test.php

// ... $users = $userTable->findAll();

``Doctrine_Collection::delete()``を呼び出すことですべてのユーザーと関連コンポジットオブジェクトを削除できます。deleteを1つずつ呼び出すことでコレクションのすべての``Users``がループされます:

// test.php

// ... $users->delete();

式の値を使う

SQLの式をカラムの値として使う必要のある状況があります。これはポータブルなDQL式をネイティブなSQL式に変換する``Doctrine_Expression``を使用することで実現できます。

``timepoint(datetime)``と``name(string)``のカラムを持つeventという名前のクラスがある場合を考えてみましょう。現在のタイムスタンプによるレコードの保存は次のように実現されます:

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->updated_at = new Doctrine_Expression(‘NOW()’); $user->save();

上記のコードは次のSQLクエリを発行します:

INSERT INTO user (username, updated_at_) VALUES (‘jwage’, NOW())

Tip

更新された値を取得するためにオブジェクトで``Doctrine_Expression``を使うとき``refresh()``を呼び出さなければなりません。

// test.php

// ... $user->refresh();

レコードの状態を取得する

それぞれの``Doctrine_Record``は状態を持ちます。最初のすべてレコードは一時的もしくは永続的になります。データベースから読み取られたすべてのレコードは永続的に新しく作成されたすべてのレコードは一時的なものと見なされます。``Doctrine_Record``がデータベースから読み取られるが唯一ロードされたプロパティが主キーである場合、このレコードはプロキシと呼ばれる状態を持ちます。

一時的もしくは永続的なすべての``Doctrine_Record``はcleanもしくはdirtyのどちらかです。``Doctrine_Record``はプロパティが変更されていないときはcleanで少なくともプロパティの1つが変更されたときはdirtyです。

レコードはlockedと呼ばれる状態を持つこともできます。まれに起きる循環参照の場合に無限反復を避けるためにDoctrineは現在レコードで操作オペレーションが行われていることを示すこの状態を内部で使用します。

レコードがなり得るすべての異なる状態と手短な説明を含むテーブルは下記の通りです:

||~ 名前 ||~ 説明 || || Doctrine\_Record::STATE_PROXY || レコードがproxyの状態にある一方で、永続性とすべてではないプロパティがデータベースからロードされる。 || || Doctrine\_Record::STATE_TCLEAN || レコードが一時的にcleanである一方で、一時性が変更されプロパティは変更されない。|| || Doctrine\_Record::STATE_TDIRTY || レコードが一時的にdirtyである一方で、一時性とプロパティの一部が変更される。|| || Doctrine\_Record::STATE_DIRTY || レコードがdirtyである一方で永続性とプロパティの一部が変更される。|| || Doctrine\_Record::STATE_CLEAN || レコードがcleanである一方で、永続性は変更されプロパティは変更されない。|| || Doctrine\_Record::STATE_LOCKED || レコードがロックされる。||

``Doctrine_Record::state()``メソッドを使用してレコードの状態を簡単に取得できます:

// test.php

// ... $user = new User();

if ($user->state() == Doctrine_Record::STATE_TDIRTY) { echo ‘Record is transient dirty’; }

NOTE 上記のオブジェクトは``TDIRTY``です。これがスキーマで指定されたデフォルトの値をいくつか持つからです。デフォルトの値を持たないオブジェクトを使い新しいインスタンスを作成すると``TCLEAN``が返されます。

// test.php

// ... $account = new Account();

if ($account->state() == Doctrine_Record::STATE_TCLEAN) { echo ‘Record is transient clean’; }

オブジェクトのコピーを取得する

ときにオブジェクトのコピーを手に入れたいことがあります(コピーされたすべてのプロパティを持つオブジェクト)。Doctrineはこのためのシンプルなメソッド: ``Doctrine_Record::copy()``を提供します。

// test.php

// ... $copy = $user->copy();

``copy()``でレコードをコピーすると古いレコードの値を持つ新しいレコード(``TDIRTY``の状態)が返され、そのレコードのリレーションがコピーされることに注意してください。リレーションもコピーしたくなければ、``copy(false)``を使う必要があります。

リレーション無しのユーザーのコピーを入手する

// test.php

// ... $copy = $user->copy(false);

PHPの``clone``キーワードを使えばこの``copy()``メソッドが内部で使用されます:

// test.php

// ... $copy = clone $user;

空白のレコードを保存する

デフォルトでは未修整のレコードで``save()``メソッドが呼び出されているときDoctrineは実行しません。レコードが修正されていなくてもレコードを強制的にINSERTしたい状況があります。これはレコードの状態を``Doctrine_Record::STATE_TDIRTY``を割り当てることで実現できます。

// test.php

// ... $user = new User(); $user->state(‘TDIRTY’); $user->save();

カスタムの値をマッピングする

カスタムの値をレコードにマッピングしたい状況があります。例えば値が外部のリソースに依存しておりこれらの値をデータベースにシリアライズして保存せずに実行時に利用可能にすることだけを行いたい場合があります。これは次のように実現できます:

// test.php

// ... $user->mapValue(‘isRegistered’, true);

$user->isRegistered; // true

シリアライズ

ときにレコードオブジェクトをシリアライズしたいことがあります(例えばキャッシュを保存するため):

// test.php

// ... string = serialize(user);

user = unserialize(string);

存在をチェックする

レコードがデータベースに存在するか知りたいことがとてもよくあります。任意のレコードがデータベースの列の同等の内容を持つかを確認するために``exists()``メソッドを使うことができます:

// test.php

// ... $record = new User();

echo $record->exists() ? ‘Exists’:’Does Not Exist’; // Does Not Exist

$record->username = ‘someone’; $record->save();

echo $record->exists() ? ‘Exists’:’Does Not Exist’; // Exists

カラム用のコールバック関数

``Doctrine_Record``はカラムを呼び出すコールバックを添付する方法を提供します。例えば特定のカラムをトリムしたい場合、次のメソッドを使うことができます:

// test.php

// ... $record->call(‘trim’, ‘username’);

コレクション

``Doctrine_Collection``はレコードのコレクションです(Doctrine_Recordを参照)。レコードに関してコレクションは``Doctrine_Collection::delete()``と``Doctrine_Collection::save()``をそれぞれ使用して削除と保存ができます。

DQL API(``Doctrine_Query``を参照)もしくはrawSql API(``Doctrine_RawSql``を参照)のどちらかでデータベースからデータを取得するとき、デフォルトではメソッドは``Doctrine_Collection``のインスタンスを返します。

次の例では新しいコレクションを初期化する方法を示しています:

// test.php

// ... $users = new Doctrine_Collection(‘User’);

コレクションにデータを追加します:

// test.php

// ... $users[0]->username = ‘Arnold’; $users[1]->username = ‘Somebody’;

コレクションの削除と同じように保存もできます:

$users->save();
要素にアクセスする

``set()``と``get()``メソッドもしくはArrayAccessインターフェイスで``Doctrine_Collection``の要素にアクセスできます。

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’); $users = $userTable->findAll();

ArrayAccessインターフェイスで要素にアクセスする

// test.php

// ... $users[0]->username = “Jack Daniels”; $users[1]->username = “John Locke”;

get()で要素にアクセスする

echo $users->get(1)->username;
新しい要素を追加する

存在しないコレクションの単独の要素とこれらの要素(レコード)にアクセスするときDoctrineはこれらを自動的に追加します。

次の例ではデータベースからすべてのユーザー(5人)を取得しコレクションにユーザーの組を追加します。

PHP配列に関してインデックスはゼロから始まります。

// test.php

// ... $users = $userTable->findAll();

echo count($users); // 5

$users[5]->username = “new user 1”; $users[6]->username = “new user 2”;

オプションとして配列インデックスから5と6を省略可能でその場合通常のPHP配列と同じように自動的にインクリメントされます:

// test.php

// ... $users[]->username = ‘new user 3’; // キーは7 $users[]->username = ‘new user 4’; // キーは8

コレクションのカウントを取得する

``Doctrine_Collection::count()``メソッドはコレクションの現在の要素の数を返します。

// test.php

// ... $users = $userTable->findAll();

echo $users->count();

``Doctrine_Collection``はCountableインターフェイスを実装するの以前の例に対する妥当な代替方法はcount()メソッドにコレクションを引数として渡すことです。

// test.php

// ... echo count($users);

コレクションを保存する

``Doctrine_Record``と同じようにコレクションは``save()``メソッドを呼び出すことで保存できます。``save()``が呼び出されるときDoctrineはすべてのレコードに対して``save()``オペレーションを実行しトランザクション全体のプロシージャをラップします。

// test.php

// ... $users = $userTable->findAll();

$users[0]->username = ‘Jack Daniels’;

$users[1]->username = ‘John Locke’;

$users->save();

コレクションを削除する

Doctrine Recordsとまったく同じように``delete()``メソッドを呼び出すだけでDoctrine Collectionsは削除できます。すべてのコレクションに関してDoctrineはsingle-shot-deleteを実行する方法を知っています。これはそれぞれのコレクションに対して1つのデータベースクエリのみが実行されることを意味します。

例えば複数のコレクションがある場合を考えます。ユーザーのコレクションを削除するときDoctrineはトランザクション全体に対して1つのクエリのみを実行します。クエリは次のようになります:

DELETE FROM user WHERE id IN (1,2,3, ... ,N)
キーのマッピング

ときにコレクションの要素用の通常のインデックス作成をしたくないことがあります。その場合例えば主キーをコレクションとしてマッピングすることが役に立つことがあります。次の例はこれを実現する方法を実演しています。

``id``カラムをマッピングします。

// test.php

// .... $userTable = Doctrine_Core::getTable(‘User’);

$userTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, ‘id’);

これで``user``コレクションは``id``カラムの値を要素インデックスとして使用します:

// test.php

// ... $users = $userTable->findAll();

foreach($users as $id => $user) { echo $id . $user->username; }

``name``カラムをマッピングするとよいでしょう:

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’);

$userTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, ‘username’);

これでユーザーコレクションは``name``カラムの値を要素インデックスとして使用します:

// test.php

// ... $users = $userTable->findAll();

foreach($users as $username => $user) { echo $username . ‘ - ‘ . $user->created_at . “”; }

CAUTION スキーマで``username``カラムがuniqueとして指定された場合のみこれは利用可能であることに注意してください。そうでなければ重複するコレクションのキーのためにデータは適切にハイドレイトされない事態に遭遇することになります。
関連レコードをロードする

Doctrineはすべてのレコード要素用のすべての関連レコードを効率的い読み取る方法を提供します。これは例えばユーザーのコレクションがある場合``loadRelated()``メソッドを呼び出すだけですべてのユーザーのすべての電話番号をロードできることを意味します。

しかしながら、大抵の場合関連要素を明示的にロードする必要はなく、むしろ行うべきはDQL APIとJOINを使用して一度にすべてをロードすることを試みることです。

次の例ではユーザー、電話番号とユーザーが所属するグループを読み取るために3つのクエリを使用します。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’);

$users = $q->execute();

すべてのユーザーの電話番号をロードしてみましょう:

// test.php

// ... $users->loadRelated(‘Phonenumbers’);

foreach($users as $user) { echo $user->Phonenumbers[0]->phonenumber; // ここでは追加のDBクエリは不要 }

``loadRelated()``はリレーション、アソシエーションに対しても動作します:

// test.php

// ... $users->loadRelated(‘Groups’);

foreach($users as $user) { echo $user->Groups[0]->name; }

下記の例はDQL APIを使用してより効率的にこれを行う方法を示します。

1つのクエリですべてをロードする``Doctrine_Query``を書きます:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Phonenumbers p’) ->leftJoin(‘u.Groups g’);

$users = $q->execute();

``Phonenumbers``と``Groups``を使うとき追加のデータベースクエリは必要ありません:

// test.php

// ... foreach($users as $user) { echo $user->Phonenumbers[0]->phonenumber; echo $user->Groups[0]->name; }

バリデータ

DoctrineのバリデーションはMVCアーキテクチャのモデル部分でビジネスルールを強制する方法です。このバリデーションを永続的なデータ保存が行われる直前に渡される必要のあるゲートウェイとみなすことができます。これらのビジネスルールの定義はレコードレベル、すなわちactive recordモデルクラスにおいて行われます(``Doctrine_Record``を継承するクラス)。この種のバリデーションを使うために最初に行う必要のあることはこれをグローバルに設定することです。これは``Doctrine_Manager``を通して行われます。

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);

バリデーションを有効にすると、一連のバリデーションが自動的に使えるようになります:

  • データ型のバリデーション: カラムに割り当てられるすべての値は正しい型であるかチェックされます。すなわち次のよに指定した場合

レコードのカラムが’integer’型である場合、Doctrineはそのカラムに割り当てられた値がその型であるかをバリデートします。PHPはゆるい型の言語なのでこの種の型バリデーションはできる限りスマートであるように試みます。例えば2は”7”と同じように有効な整数型である一方で”3f”はそうではありません。型バリデーションはすべてのカラムで行われます(すべてのカラム定義は型を必要とするからです)。

  • 長さのバリデーション: 名前がほのめかす通り、カラムに割り当てられたすべての値が最大長を越えないことを確認するためにバリデートされます。

次の定数: VALIDATE\_ALLVALIDATE\_TYPESVALIDATE\_LENGTHSVALIDATE\_CONSTRAINTS``VALIDATE_NONE``をビット演算子で結びつけることができます。

例えば長さバリデーション以外のすべてのバリデーションを有効にするには次のように行います:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, VALIDATE_ALL & ~VALIDATE_LENGTHS);

[doc data-validation :name]の章でこのトピックの詳細を読むことができます。

さらにバリデーション

型と長さバリデーションは手軽ですが大抵の場合これらだけでは十分ではありません。それゆえDoctrineはデータをより詳しくバリデートするために利用できるメカニズムを提供します。

バリデータはさらにバリデーションを指定するための簡単な手段です。Doctrineは``email``、countryip``range``と``regexp``バリデータなど頻繁に必要とされるたくさんのバリデータを事前に定義しています。[doc data-validation :name]の章で利用可能なバリデータの全リストが見つかります。``hasColumn()``メソッドの4番目の引数を通してどのバリデータをどのカラムに適用するのかを指定できます。これが十分ではなく事前に定義されたバリデータとして利用できない特別なバリデータが必要な場合、3つの選択肢があります:

  • 独自のバリデータを書けます。
  • Doctrineの開発者に新しいバリデータのニーズを提案できます。
  • バリデータフックが使えます。

最初の2つのオプションが推奨されます。バリデーションが一般的に利用可能で多くの状況に適用できるからです。このケースにおいて新しいバリデータを実装するのは良い考えです。しかしながら、バリデーションが特別なものでなければDoctrineが提供するフックを使う方がベターです:

  • validate() (レコードがバリデートされるたびに実行される)
  • validateOnInsert() (レコードが新しくバリデートされるときに実行される)
  • validateOnUpdate() (レコードが新しくなくバリデートされるときに実行される)

active recordで特殊なバリデーションが必要な場合active recordクラス(``Doctrine_Record``の子孫)でこれらのメソッドの1つをオーバーライドできます。フィールドをバリデートするためにこれらのメソッドの範囲内でPHPのすべての力を使うことができます。フィールドがバリデーションを渡さないときエラーをレコードのエラーに追加できます。次のコードスニペットはカスタムバリデーションと一緒にバリデータを定義する例を示しています:

// models/User.php

class User extends BaseUser { protected function validate() { if ($this->username == ‘God’) { // Blasphemy! Stop that! ;-) // syntax: add(, ) $errorStack = $this->getErrorStack(); $errorStack->add(‘name’, ‘You cannot use this username!’); } } }

// models/Email.php

class Email extends BaseEmail { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    // 使われる'email'と'unique'バリデータ
    $this->hasColumn('address','string', 150, array('email', 'unique'));
}

}

YAMLフォーマットでの同じ例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Email: columns: address: type: string(150) email: true unique: true

ValidもしくはNot Valid

モデルでビジネスルールを指定する方法を理解したので、アプリケーションの残りの部分でこれらのルールを扱う方法を見てみましょう。

暗黙のバリデーション

(``$record->save()``の呼び出しを通して)レコードが永続的データとして保存されているときバリデーションの全手続きが実行されます。そのプロセスの間にエラーが起きると``Doctrine_Validator_Exception``型のエラーが投げられます。例外を補足して``Doctrine_Validator_Exception::getInvalidRecords()``インスタンスメソッドを使用してエラーを解析できます。このメソッドはバリデーションをパスしなかったすべてのレコードへの参照を持つ通常の配列を返します。それぞれのレコードのエラースタックを解析することでそれぞれのレコードのエラーを詳しく調査することができます。レコードのエラースタックは``Doctrine_Record::getErrorStack()``インスタンスメソッドで取得できます。それぞれのエラースタックは``Doctrine_Validator_ErrorStack``クラスのインスタンスです。エラースタックはエラーを検査するためのインターフェイスを簡単に使う方法を提供します。

明示的なバリデーション

任意のときに任意のレコードに対してバリデーションを明示的に実行できます。この目的のために``Doctrine_Record``は``Doctrine_Record::isValid()``インスタンスメソッドを提供します。このメソッドはバリデーションの結果を示す論理型を返します。このメソッドがfalseを返す場合、例外が投げられないこと以外は上記と同じ方法でエラースタックを検査できるので、``Doctrine_Record::getErrorStack()``を通したバリデーションがパスしなかったレコードのエラースタックを得られます。

次のコードスニペットは``Doctrine_Validator_Exception``によって引き起こされる明示的なバリデーションの処理方法の例です。

// test.php

// ... $user = new User();

try { $user->username = str_repeat(‘t’, 256); $user->Email->address = “drink@@notvalid..”; $user->save(); } catch(Doctrine_Validator_Exception $e) { $userErrors = $user->getErrorStack(); $emailErrors = $user->Email->getErrorStack();

foreach($userErrors as $fieldName => $errorCodes) {
    echo $fieldName . " - " . implode(', ', $errorCodes) . "\n";
}

foreach($emailErrors as $fieldName => $errorCodes) {
    echo $fieldName . " - " . implode(', ', $errorCodes) . "\n";
}

}

Tip

``$e->getInvalidRecords()``を使うことができます。扱っているレコードを知っているときは上記の内容を直接使う方がシンプルです。

アプリケーションで簡単に使えるように読みやすく整形されたエラースタックを読み取ることもできます:

// test.php

// ... echo $user->getErrorStackAsString();

次のようにエラー文字列が出力されます:

Validation failed in class User

1 field had validation error:

* 1 validator failed on username (length)

プロファイラー

``Doctrine_Connection_Profiler``は``Doctrine_Connection``用のイベントリスナーです。これは柔軟なクエリプロファイリングを提供します。SQL文字列に加えクエリプロファイルはクエリを実行するための経過時間を含みます。これによってモデルクラスにデバッグコードを追加せずにクエリのインスペクションの実行が可能になります。

``Doctrine_Connection_Profiler``はDoctrine_Connection用のイベントリスナーとして追加されることで有効になります。

// test.php

// ... $profiler = new Doctrine_Connection_Profiler();

$conn = Doctrine_Manager::connection(); conn->setListener(profiler);

基本的な使い方

ページの中にはロードが遅いものがあるでしょう。次のコードは接続から完全なプロファイラーレポートを構築する方法を示しています:

// test.php

// ... time = 0; foreach (profiler as $event) { $time += $event->getElapsedSecs(); echo $event->getName() . ” ” . sprintf(“%f”, $event->getElapsedSecs()) . “”; echo $event->getQuery() . “”; $params = event->getParams(); if( ! empty(params)) { print_r($params); } } echo “Total time: ” . $time . “”;

Tip

[http://www.symfony-project.com symfony]、[http://framework.zend.com Zend]などのフレームワークはウェブデバッグツールバーを提供します。Doctrineはそれぞれのクエリにかかる時間と同様にすべてのページで実行されるクエリの回数をレポートする機能を提供します。

マネージャーをロックする

NOTE ‘トランザクション(Transaction)’という用語はデータベースのトランザクションではなく一般的な意味を示します。

ロックは並行処理をコントロールするメカニズムです。最もよく知られるロック戦略は楽観的と悲観的ロックです。次のセクションでこれら2つの戦略の手短な説明を行います。現在Doctrineがサポートしているのは悲観的ロックです。

楽観的ロック

トランザクションが開始するときオブジェクトの状態/バージョンに注目されます。トランザクションが終了するとき注目された状態/バージョンの参与しているオブジェクトが現在の状態/バージョンと比較されます。状態/バージョンが異なる場合オブジェクトは他のトランザクションによって修正され現在のトランザクションは失敗します。このアプローチは’楽観的’(optimistic)と呼ばれます。複数のユーザーが同時に同じオブジェクト上のトランザクションに参加しないことを前提としているからです。

悲観的ロック

トランザクションに参加する必要のあるオブジェクトはユーザーがトランザクションを開始した瞬間にロックされます。ロックが有効な間、他のユーザーがこれらのオブジェクトで作動するトランザクションを始めることはありません。これによってトランザクションを始めるユーザー以外のユーザーが同じオブジェクトを修正しないことが保証されます。

Doctrineの悲観的オフラインロック機能はHTTPリクエストとレスポンスサイクルと/もしくは完了させるためにたくさんの時間がかかるアクションもしくはプロシージャの並行処理をコントロールするために使うことができます。

次のコードスニペットはDoctrineの悲観的オフラインロック機能の使い方を実演しています。

ロックがリクエストされたページでロックマネージャーインスタンスを取得します:

// test.php

// ... $lockingManager = new Doctrine_Locking_Manager_Pessimistic();

Tip

300秒 = 5分のタイムアウトをロックしようとする前に、タイムアウトした古いロックを必ず解放してください。これは``releaseAgedLocks()``メソッドを使用することで可能です。

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->find(1);

try { $lockingManager->releaseAgedLocks(300);

$gotLock = $lockingManager->getLock($user, 'jwage');

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 }

トランザクションが終了するページでロックマネジャーのインスタンスを取得します:

// test.php

// ... $user = Doctrine_Core::getTable(‘User’)->find(1);

$lockingManager = new Doctrine_Locking_Manager_Pessimistic();

try { if (lockingManager->releaseLock(user, ‘jwage’)) { echo “Lock released”; } else { echo “Record was not locked. No locks released.”; } } catch(Doctrine_Locking_Exception $dle) { echo $dle->getMessage(); // handle the error }

技術的な詳細

悲観的オフラインロックマネージャーはロックをデータベースで保存します(それゆえ’オフライン’です)。マネージャーをインスタンス化して``ATTR_CREATE_TABLES``がTRUEに設定されているときに必要なロックテーブルは自動的に作成されます。インストール用の集中化と一貫したテーブル作成のプロシージャを提供するために将来この振る舞いが変更される可能性があります。

ビュー

データベースビューは複雑なクエリのパフォーマンスを多いに増大できます。これらをキャッシュされたクエリとして見なすことができます。``Doctrine_View``はデータベースビューとDQLクエリの統合を提供します。

ビューを使う

データベースでビューを使うのは簡単です。``Doctrine_View``クラスは既存のビューの作成と削除をする機能を提供します。

``Doctrine_Query``によって実行されるSQLを保存することで``Doctrine_View``クラスは``Doctrine_Query``クラスを統合します。

最初に新しい``Doctrine_Query``インスタンスを作成しましょう:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘User u’) ->leftJoin(‘u.Phonenumber p’) ->limit(20);

データベースビューを指定するための``name``と同じように``Doctrine_View``インスタンスを作成し``Doctrine_Query``インスタンスにこれを渡しましょう:

// test.php

// ... view = new Doctrine_View(q, ‘RetrieveUsersAndPhonenumbers’);

``Doctrine_View::create()``メソッドを使用してビューを簡単に作成できます:

// test.php

// ... try { $view->create(); } catch (Exception $e) {}

代わりにデータベースビューを削除したい場合``Doctrine_View::drop()``メソッドを使います:

// test.php

// ... try { $view->drop(); } catch (Exception $e) {}

ビューの使用はとても簡単です。``Doctrine_Query``オブジェクトと同じようにビューの実行と結果の取得には``Doctrine_View::execute()``を使います:

// test.php

// ... $users = $view->execute();

foreach ($users as user) { print_r(us->toArray()); }

まとめ

Doctrineが提供するコア機能の大部分を見てきました。この本の次の章では日常生活を楽にするオプション機能の一部をカバーします。

[doc native-sql 次の章]ではDoctrine Query Languageの代わりに配列とオブジェクトの間でデータをハイドレイトするネイティブなSQLの使い方を学びます。

はじめに

``Doctrine_RawSql``は生のSQLクエリを構築するための便利なインターフェイスを提供します。``Doctrine_Query``と同じように、``Doctrine_RawSql``は配列とオブジェクト取得のための手段を提供します。

Oracleでクエリヒントもしくは``CONNECT``キーワードのようなデータベース固有の機能を活用したいときに生のSQLを使う方法は便利です。

``Doctrine_RawSql``オブジェクトの作成は簡単です:

// test.php

// ... $q = new Doctrine_RawSql();

オプションとして接続パラメータが与えられた場合``Doctrine_Connection``のインスタンスが受け取られます。[doc connections :name]の章で接続の作成方法を学びます。

// test.php

// ... $conn = Doctrine_Manager::connection(); q = new Doctrine_RawSql(conn);

コンポーネントクエリ

``Doctrine_RawSql``を使う際に最初に注意しなければならないことは波かっこ({})で選択するフィールドを置かなければならないことです。またすべての選択されたコンポーネントに対して``addComponent()``を呼び出さなければなりません。

次の例はこれらの使い方を明確にします:

// test.php

// ... $q->select(‘{u.*}’) ->from(‘user u’) ->addComponent(‘u’, ‘User’);

$users = q->execute(); print_r(users->toArray());

NOTE ``addComponent()``メソッドを使用して``user``テーブルは``User``クラスにバインドしていることに注目してください。

次のことに注意を払ってください:

  • フィールドは波かっこで囲まなければならない。
  • それぞれの選択されたテーブルに対して``addComponent()``コールが1つ存在しなければならない。

複数のコンポーネントから取得する

複数のコンポーネントから取得するとき``addComponent()``コールは少し複雑になります。どのテーブルがどのコンポーネントにバインドされるのか伝えるだけでなく、どのコンポーネントがどれに所属するのかパーサーに伝えなければならないからです。

次の例においてすべての``users``と``phonenumbers``を取得します。最初に新しい``Doctrine_RawSql``オブジェクトを作成し選択する部分を追加します:

// test.php

// ... $q = new Doctrine_RawSql(); $q->select(‘{u.*}, {p.*}’);

``FROM``の部分を``user``テーブルからphonenumberテーブルへのJOINクエリに追加してすべてを一緒にマッピングする必要があります:

// test.php

// ... $q->from(‘user u LEFT JOIN phonenumber p ON u.id = p.user_id’)

``user``テーブルを``User``クラスにバインドし``User``クラスのエイリアスとして``u``も追加します。``User``クラスを参照するときにこのエイリアスが使われます。

// test.php

// ... $q->addComponent(‘u’, ‘User u’);

``phonenumber``テーブルにバインドされる別のテーブルを追加します:

// test.php

// ... $q->addComponent(‘p’, ‘u.Phonenumbers p’);

NOTE ``Phonenumber``クラスはUserの電話番号を指し示していることに注意してください。

あたかも``Doctrine_Query``オブジェクトを実行するように``Doctrine_RawSql``クエリを実行できます:

// test.php

// ... $users = q->execute(); echo get_class(users) . “”; echo get_class(users[0]) . "\n"; echo get_class(users[0][‘Phonenumbers’][0]) . “”;

上記の例が実行されるときに次の内容が出力されます:

$ php test.php Doctrine_Collection User Phonenumber

まとめ

この章はすぐに役に立つかもしれませんしそうでないかもしれません。多くの場合Doctrine Query Languageは複雑なデータセットを読み取るために十分です。しかし``Doctrine_Query``ができる範囲を超えるものが必要であれば``Doctrine_RawSql``が役立ちます。

以前の章でたくさんのYAMLスキーマファイルとその例を見てきましたが独自のものを書く練習は十分ではありません。次の章ではモデルを[doc yaml-schema-files YAMLスキーマファイル]として維持する詳細な方法を説明します。

はじめに

スキーマファイルの目的はPHPコードの編集よりもYAMLファイルでモデルの定義を直接管理できるようにすることです。すべてのモデルの定義/クラスを生成するためにYAMLスキーマファイルは解析され使われます。これによってDoctrineモデルの定義がはるかにポータブルなものになります。

スキーマファイルはPHPコードで書く通常のすべての内容をサポートします。接続バインディング用のコンポーネント、リレーション、属性、テンプレート/ビヘイビア、インデックスなどがあります。

省略構文

Doctrineは省略記法でスキーマを指定する機能を提供します。多くのスキーマパラメータはデフォルトの値を持ち、これによって構文をできるのでDoctrineはデフォルトを利用できます。すべての省略記法を利用したスキーマの例は下記の通りです。

NOTE ``detect_relations``オプションによってカラムの名前からリレーションの推測が行われます。下記の例ではDoctrine は``User``が1つの``Contact``を持つことを知っておりモデルの間のリレーションを自動的に定義します。

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

冗長な構文

上記のスキーマを100%冗長にしたものは次の通りです:

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

上記の例では``detect_relations``オプションを定義せず、代わりにローカル/外部キー、型、とそれぞれの側のリレーションのエイリアスの設定を通して完全にコントロールできるように手動でリレーションを定義します。

リレーション

リレーションを指定するとき外部キーが存在している方のリレーションを指定することだけが必要です。スキーマファイルが解析されるとき、Doctrineはリレーションを反映し反対側を自動的にビルドします。もう一方のリレーションを指定する場合、自動生成は行われません。

リレーションを検出する

前に見たようにDoctrineは``detect_relations``オプションを指定する機能を提供します。この機能はカラムの名前に基づいてリレーションを自動的に構築します。``contact_id``を持つ``User``モデルと``Contact``という名前を持つクラスが存在する場合、2つの間のリレーションが自動的に作成されます。

リレーションをカスタマイズする

Doctrineは外部キーが存在している側のリレーションを指定することのみを要求します。リレーションの反対側は反映されもう一方側に基づいてビルドされます。スキーマ構文はリレーションのエイリアスと反対側の型をカスタマイズする機能を提供します。すべての関連するリレーションを一箇所で維持できるのでこれはよいニュースです。下記の内容はエイリアスと反対側のリレーションの型をカスタマイズする方法です。これは``User``が1つの``Contact``を持ち``Contact``は1つの``User``を``UserModel``として持つというリレーションを示しています。通常は自動生成された``User``は1つの``Contact``を持ち``Contact``は複数の``User``を持ちます。``foreignType``と``foreignAlias``オプションによって反対側のリレーションをカスタマイズできます。

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)

次のようにthe detect_relationsオプションを持つ2つのモデルの間のリレーションを見つけて作成できます。

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)

結果のリレーションは``User``は1つの``Avatar``を持ち``Avatar``は複数の``User``を持ちます。

一対一
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)

一対多
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

多対多
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

この場合``User``は複数の``Groups``を持ち、``Group``は複数の``Users``を持ち、``GroupUser``は1つの``User``を持ち``GroupUser``は1つの``Group``を持つモデルのセットが作られます。

機能と例

接続バインディング

モデルを管理するためにスキーマファイルを使わないのであれば、通常は次のコードのようにコンポーネントを接続名にバインドするために使います:

下記のように接続を作成します:

Doctrine_Manager::connection(‘mysql://jwage:pass@localhost/connection1’, ‘connection1’);

Doctrineのブートストラップスクリプトでモデルをその接続にバインドします:

Doctrine_Manager::connection()->bindComponent(‘User’, ‘conn1’);

スキーマファイルは接続パラメータを指定することでこれを特定の接続にバインドする機能を提供します。接続を指定しなければモデルは``Doctrine_Manager``インスタンスにセットあれた現在の接続を使います。

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)

属性

Doctrine_Record子クラスを手作業で書いたのと同じようにDoctrineはスキーマファイルで生成モデル用の属性を直接設定する手段を提供します。

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

列挙型

スキーマファイルでenumカラムを使うために型をenumとして指定し可能なenumの値として値の配列を指定しなければなりません。

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ビヘイビア

``actAs``オプションでモデルにビヘイビアを取り付けることができます:

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

NOTE 何も指定しない場合デフォルトの値が使われるのでSluggableビヘイビアで指定されたオプションはオプションです。これらはデフォルトなので毎回入力する必要はありません。

User: connection: connection1 columns: # ... actAs: [Timestampable,

Sluggable]

リスナー

モデルに取り付けたいリスナーがある場合、同じようにYAMLファイルで直接これらを指定できます。

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)

上記の構文で次のような基底クラスが生成されます:

class BaseUser extends Doctrine_Record { // ...

public setUp() { // ... $this->addListener(new MyCustomListener()); } }

オプション

テーブル用のオプションを指定するとDoctrineがモデルからテーブルを作成するときにオプションはcreate tableステートメントに設定されます。

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

インデックス

インデックスとオプションの詳細情報は[doc defining-models chapter]の[doc defining-models:indexes :name]セクションを参照してくださるようお願いします。

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

インデックスの定義用のモデルクラスの``setTableDefinition()``で自動生成されたPHPコードは次の通りです:

$this->index(‘name_index’, array( ‘fields’ => array( ‘first_name’ =>
array( ‘sorting’ => ‘ASC’,
‘length’ => ‘10’, ‘primary’ => true ), ‘last_name’ => array()), ‘type’

=> ‘unique’ ) );

継承

下記のコードはYAMLスキーマファイルを使用して異なるタイプの継承をセットアップする方法を示しています。

単一継承
Entity: columns: name: string(255) username: string(255) password:

string(255)

User: inheritance: extends: Entity type: simple

Group: inheritance: extends: Entity type: simple

NOTE 単一継承するモデルで定義されたカラムもしくはリレーションはPHPクラスが生成されたときに親に移動します。

[doc inheritance:simple :fullname]の章でこのトピックの詳細を読むことができます。

具象継承
TextItem: columns: topic: string(255)

Comment: inheritance: extends: TextItem type: concrete columns: content: string(300)

[doc inheritance:concrete :fullname]の章でこのトピックの詳細を読むことができます。

カラム集約継承
NOTE 単一継承のように、PHPクラスが生成されたとき子に追加されるカラムもしくはリレーションは自動的に削除され親に移動します。

他のモデルが継承する``Entity``という名前のモデルを定義しましょう:

Entity: columns: name: string(255) type: string(255)

NOTE typeカラムはオプションです。このカラムは子クラスで指定された場合自動的に追加されます。

``Entity``モデルを継承する``User``モデルを作りましょう:

User: inheritance: extends: Entity type: column_aggregation keyField:

type keyValue: User columns: username: string(255) password: string(255)

NOTE ``inheritance``定義の下の``type``オプションは``keyField``もしくは``keyValue``を指定する場合暗示されるのでオプションです。``keyField``が指定されない場合デフォルトでは``type``という名前のカラムが追加されます。何も指定しない場合デフォルトで``keyValue``がモデルの名前になります。

再度``Entity``を継承する``Group``という名前の別のモデルを作りましょう:

Group: inheritance: extends: Entity type: column_aggregation keyField:

type keyValue: Group columns: description: string(255)

NOTE ``User``の``username``と``password``と``Group``の``description``カラムは自動的に親の``Entity``に移動します。

[doc inheritance:column-aggregation :fullname]で詳細トピックを読むことができます。

カスタムのエイリアス

データベースのカラム名以外のカラム名のエイリアスを作成したい場合、Doctrineでこれを実現するのは簡単です。カラムの名前で”column\_name as field\_name“の構文を使います:

User: columns: login: name: login as username type: string(255)

password: type: string(255)

上記の例では``username``エイリアスからカラム名の``login``にアクセスできます。

パッケージ

Doctrineはサブフォルダでモデルを生成する”package”パラメータを提供します。大きなスキーマファイルによってフォルダの内外でスキーマをよりよく編成できます。

User: package: User columns: username: string(255)

このスキーマファイルからのモデルファイルはUserという名前のフォルダに設置されます。”package: User.Models”とさらにサブフォルダを指定すればモデルはUser/Modelsになります。

カスタムパスのパッケージ

パッケージファイルを生成する完全なカスタムパスを指定することで適切なパスでパッケージを自動生成することもできます:

User: package: User package_custom_path: /path/to/generate/package

columns: username: string(255)

グローバルスキーマの情報

Doctrineスキーマによってスキーマファイルで定義されたすべてのモデルに適用する特定のパラメータを指定できます。スキーマファイル用に設定できるグローバルパラメータの例が見つかります。

グローバルパラメータのリストは次の通りです:

||~ 名前 ||~ 説明 || || connection || モデルをバインドする接続名。 || || attributes || モデル用の属性の配列 || || actAs || モデル用のビヘイビアの配列 || || options || モデル用のテーブルオプションの配列 || || package || モデルを設置するパッケージ || || inheritance || モデル用の継承情報の配列 || || detect_relations || 外部キーのリレーションを検出するかどうか ||

上記のグローバルパラメータをいつか使ったスキーマの例は次の通りです:

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)

トップレベルのすべての設定はYAMLで定義されたすべてのモデルに適用されます。

スキーマファイルを使う

一旦スキーマファイルを定義したらYAMLの定義からモデルをビルドするコードが必要です。

$options = array( ‘packagesPrefix’ => ‘Plugin’, ‘baseClassName’ =>

‘MyDoctrineRecord’, ‘suffix’ => ‘.php’ );

Doctrine_Core::generateModelsFromYaml(‘/path/to/yaml’, ‘/path/to/model’, $options);

上記のコードは``/path/to/generate/models``の``schema.yml``用のモデルを生成します。

モデルのビルド方法をカスタマイズするために利用できる異なるオプションの表は次の通りです。packagesPrefix``baseClassName``と``suffix``オプションを使用していることに注目してください。

||~ 名前 ||~ デフォルト ||~ 説明 || || packagesPrefix || Package || ミドルパッケージモデルのプレフィックス || || packagesPath || #models_path#/packages || パッケージファイルを書き込むパス || || packagesFolderName || packages || パッケージパス内部で、パッケージを置くフォルダーの名前 || || generateBaseClasses || true || 定義と空の基底モデルを継承するトップレベルのクラスを含めて抽象基底モデルを生成するかどうか || || generateTableClasses || true || モデルごとにテーブルを生成するか || || baseClassPrefix || Base || 生成既定モデルに使うプレフィックス || || baseClassesDirectory || generated || 基底クラスの定義を生成するフォルダーの名前 || || baseTableClassName || Doctrine_Table || ほかの生成テーブルクラス名が継承する基底テーブルクラス || || baseClassName || Doctrine_Record || Doctrine_Record既定クラスの名前 || || classPrefix || || すべての生成クラスで使うプレフィックス || || classPrefixFiles || true || 生成ファイルの名前にもクラスのプレフィックスを使うかどうか || || pearStyle || false || PEARスタイルのクラス名とファイルを生成するか。このオプションがtrueにセットされている場合。生成クラスファイルにおいて``underscores(_)``は``DIRECTORY_SEPARATOR``に置き換えられます。|| || suffix || .php || 生成モデルに使う拡張子 || || phpDocSubpackage || || docブロックで生成するphpDocのサブパッケージ名 || || phpDocName || || docブロックで生成するphpDocの著者名 || || phpDocEmail || || docブロックで生成するphpDocのメール ||

まとめ

YAMLスキーマファイルのすべてを学んだので[doc data-validation :name]に関する大きなトピックに移ります。これは重要なトピックです。ユーザーが入力したデータをあなた自身でバリデートしたくない場合データベースに永続的に保存する前にDoctrineにデータをバリデートさせます。

はじめに

Doctrineによってカラムとテーブルで*ポータブルな*制約を定義できます。制約によってテーブルのデータを望むままにコントロールできます。ユーザーがカラムの制約に違反するデータを保存しようとすると、エラーが起動します。値がデフォルトの値の定義から来る場合でもこれが適用されます。

アプリケーションレベルのバリデータと同じようにDoctrineの制約はデータベースレベルの制約として振る舞います。このことは二重のセキュリティを意味します: 間違った種類の値とアプリケーションを許容しません。

Doctrineの範囲内で利用可能なバリデーションの全リストは次の通りです:

||~ バリデータ(引数) ||~ 制約 ||~ 説明 || || notnull || NOT NULL || アプリケーションとデータベースの両方のレベルで’not null’制約を確認する || || email || || 値が有効なEメールであるかチェックする || || notblank || NOT NULL || not blankであることをチェックする || || nospace || || スペースがないことをチェックする || || past || CHECK``制約 \|\| 値が過去の日付がチェックする \|\| \|\| ``future || || 値が未来の日付かチェックする || || minlength(length) || || 値が最小長を満たすかチェックする || || country || || 値が有効な国コードであるかチェックする || || ip || || 値が有効なIP(internet protocol)アドレスかチェックする || || htmlcolor || || 値が妥当なhtmlの色であるかチェックする || || range(min, max) || CHECK``制約 \|\| 値が引数で指定された範囲に収まるかチェックする \|\| \|\| ``unique || UNIQUE``制約 \|\| 値がデータベーステーブルでユニークかチェックする \|\| \|\| ``regexp(expression)|| || 値が正規表現にマッチするかチェックする || || creditcard || || 文字列が適正なクレジットカード番号であるかチェックする || || digits(int, frac) || 精度とスケール || 値が整数の//int//の桁数を持ち分数の//frac//の桁数を持つかチェックする || || date || || 値が有効な日付であるかチェックする || readonly || || フィールドが修正され読み込み限定のフィールドに強制するようにfalseを返すかチェックする || || unsigned || || 整数値が符号無しであるかチェックする || || usstate || || 値が有効なUSの州コードであるかチェックする ||

下記のコードはバリデータの使い方とカラムでバリデータ用の引数を指定する方法の例です。

``minlength``バリデータを使う例です。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('username', 'string', 255, array(
            'minlength' => 12
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: username: type: string(255) minlength: 12 # ...

= 例 =

Not Null

``not-null``制約はカラムがnullの値を想定してはならないことを指定します。``not-null``制約は常にカラムの制約として記述されます。

次の定義はカラム名用に``notnull``制約を使います。このことは指定されたカラムはnullの値を受け取らないことを意味します。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('username', 'string', 255, array(
            'notnull' => true,
            'primary' => true,
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます。:

# schema.yml

User: columns: username: type: string(255) notnull: true primary: true # ...

このクラスがデータベースにエクスポートされるとき次のSQL文が実行されます(MySQLにて):

CREATE TABLE user (username VARCHAR(255) NOT NULL, PRIMARY

KEY(username))

not-null制約はアプリケーションレベルのバリデータとして振る舞います。このことはDoctrineのバリデータが有効な場合、Doctrineは指定されたカラムを保存するときにnullの値が含まれないことを自動的にチェックします。

これらのカラムがnullの値を含む場合``Doctrine_Validator_Exception``が起動します。

Eメール

Eメールバリデータは入力された値が本当に有効なEメールアドレスでありアドレスドメイン用のMXレコードがEメールアドレスとして解決することをチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('email', 'string', 255, array(
            'email'   => true
        )
    );
}

}

YAMLフォーマットでの同じサンプルは次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... email: type: string(255) email: true # ...

無効なEメールアドレスを持つユーザーを作成しようとするとバリデートは行われません:

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->email = ‘jonwage’;

if ( ! $user->isValid()) { echo ‘User is invalid!’; }

``jonwage``は有効なEメールアドレスではないので上記のコードは例外を投げます。これをさらに推し進めてEメールアドレスは有効であるがドメイン名が無効な例は次の通りです:

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->email = 'jonwage@somefakedomainiknowdoesntexist.com‘;

if ( ! $user->isValid()) { echo ‘User is invalid!’; }

ドメインの``somefakedomainiknowdoesntexist.com``が存在せずPHPの``[http://www.php.net/checkdnsrr checkdnsrr()]``関数はfalseを返すので上記のコードはエラーになります。

Not Blank

not blankバリデータはnot nullバリデートと似ていますが空の文字列もしくは空白文字が含まれる場合はエラーになります。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('username', 'string', 255, array(
            'notblank'   => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: username: type: string(255) notblank: true # ...

1つの空白スペースを含むusernameを持つ``User``レコードを保存しようとすると、バリデーションはエラーになります:

// test.php

// ... $user = new User(); $user->username = ‘ ‘;

if ( ! $user->isValid()) { echo ‘User is invalid!’; }

No Space

no spaceバリデータは単純です。値にスペースが含まれないことをチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('username', 'string', 255, array(
            'nospace'   => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: username: type: string(255) nospace: true # ...

スペースを含む``username``を持つ``User``を保存しようとするとバリデーションが失敗します:

$user = new User(); $user->username = ‘jon wage’;

if ( ! $user->isValid()) { echo ‘User is invalid!’; }

Past

pastバリデータは値が過去の有効な日付であるかをチェックします。この例では``birthday``カラムを持つ``User``モデルがあり日付が過去のものであることバリデートします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('birthday', 'timestamp', null, array(
            'past' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... birthday: type: timestamp past: true # ...

過去にはない誕生日を設定しようとするとバリデーションエラーになります。

Future

futureバリデータはpastバリデータの反対でデータが未来の有効な日付であることをチェックします。この例では``next_appointment_date``カラムを持つ``User``モデルがあり日付が未来のものであることをバリデートします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('next_appointment_date', 'timestamp', null, array(
            'future' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... next_appointment_date: type: timestamp future: true # ...

予約日が未来のものでなければ、バリデーションエラーになります。

— 最小長 —

最小長は正確な表現ではありません。文字列の長さが最小の長さよりも大きいことをチェックします。この例では``password``カラムを持つ``User``モデルがあり``password``の長さが少なくとも5文字であることを確認します。

// models/User.php

class User extends BaseUser { public function setTableDefinition() { parent::setTableDefinition();

    // ...

    $this->hasColumn('password', 'timestamp', null, array(
            'minlength' => 5
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... password: type: timestamp minlength: 5 # ...

5文字より短い``password``を持つ``User``を保存しようとすると、バリデーションはエラーになります。

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->password = ‘test’;

if ( ! $user->isValid()) { echo ‘User is invalid because “test” is only 4 characters long!’; }

Country

countryバリデータは値が有効なcountryコードであるかチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('country', 'string', 2, array(
            'country' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... country: type: string(2) country: true # ...

無効な国コードを持つ``User``を保存しようとするとバリデーションがエラーになります。

// test.php

// ... $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アドレス

IPアドレスバリデータは値が有効なIPアドレスであることをチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('ip_address', 'string', 15, array(
            'ip' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... ip_address: type: string(15) ip: true # ...

無効なIPアドレスを持つ``User``を保存しようとするとバリデーションはエラーになります。

$user = new User(); $user->username = ‘jwage’; $user->ip_address =

‘123.123’;

if ( ! $user->isValid()) { echo ‘User is invalid because “123.123” is not a valid ip address }

HTML Color

htmlcolorバリデータは値が有効な16進法のhtmlカラーであることをチェックします。

// models/User.php

class User extends BaseUser { public function setTableDefinition() { parent::setTableDefinition();

    // ...

    $this->hasColumn('favorite_color', 'string', 7, array(
            'htmlcolor' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... favorite_color: type: string(7) htmlcolor: true # ...

``favorite_color``カラム用の無効なhtmlカラーの値を持つ``User``を保存しようとするとバリデーションはエラーになります。

// test.php

// ... $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

rangeバリデータは値が与えられた数の範囲にあることをチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('age', 'integer', 3, array(
            'range' => array(10, 100)
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... age: type: integer(3) range: [10, 100] # ...

10才未満もしくは100才を越える``User``を保存しようとすると、バリデーションはエラーになります。

// test.php

// ... $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”’; }

範囲配列の``0``もしくは``1``キーのどちらかを省略することで最大と最小の値をバリデートするために``range``バリデータを使うことができます:

// models/User.php

class User extends BaseUser { public function setTableDefinition() { parent::setTableDefinition();

    // ...

    $this->hasColumn('age', 'integer', 3, array(
            'range' => array(1 => 100)
        )
    );
}

}

上記の例では最大年齢は100才になります。最小値を指定するには、範囲配列で``1``の代わりに``0``を指定します。

YAML構文の例は次のようになります:

# schema.yml

User: columns: # ... age: type: integer(3) range: 1: 100 # ...

Unique

unique制約は1つのカラムもしくはカラムのグループに含まれるデータがテーブルのすべての列に関してユニークであること保証します。

一般的に、制約に含まれるカラムのすべての値が等しい複数の列が存在するときにunique制約は破られます。しかしながら、この比較では2つのnull値は等しいとはみなされません。このことはunique制約の下で制約の課された少なくとも1つのカラムでnull値を含む重複列を保存することが可能であることを意味します。このビヘイビアはSQL標準に準拠しますが、一部のデータベースはこのルールに従いません。ですのでポータルなアプリケーションを開発するときは注意してください。

次の定義はカラム名に対して``unique``制約を使います。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('username', 'string', 255, array(
            'unique' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: username: type: string(255) unique: true # ....

NOTE 主キーは既にuniqueなので主キー以外のカラムに対してのみunique制約を使うべきです。
正規表現

正規表現バリデータは独自の正規表現に対してカラムの値をバリデートするシンプルな方法です。この例ではユーザー名は有効な文字もしくは数字だけを含むことを確認します。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('username', 'string', 255, array(
            'regexp' => '/[a-zA-Z0-9]/'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: username: type: string(255) regexp: ‘/ [1]_+$/’ # ...

文字か数字以外の文字を含む``username``を持つ``User``を保存しようとすると、バリデーションはエラーになります:

// test.php

// ... $user = new User(); $user->username = ‘[jwage’;

if ( ! $user->isValid()) { echo ‘User is invalid because the username contains a [ character’; }

クレジットカード

creditcardバリデータは値が本当に有効なクレジットカード番号であることをチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('cc_number', 'integer', 16, array(
            'creditcard' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... cc_number: type: integer(16) creditcard: true # ...

Read Only

``readonly``バリデータが有効なカラムを修正しようとするとバリデーションに失敗します。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('readonly_value', 'string', 255, array(
            'readonly' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... readonly_value: type: integer(16) readonly: true # ...

``User``オブジェクトインスタンスから``readonly_value``という名前のカラムを修正しようとすると、バリデーションはエラーになります。

Unsigned

unsignedバリデータは整数が符号無しであることをチェックします。

// models/User.php

class User extends BaseUser { // ...

public function setTableDefinition()
{
    parent::setTableDefinition();

    // ...

    $this->hasColumn('age', 'integer', 3, array(
            'unsigned' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

User: columns: # ... age: type: integer(3) unsigned: true # ...

マイナス年齢の``User``を保存しようとするとバリデーションはエラーになります:

// test.php

// ... $user = new User(); $user->username = ‘jwage’; $user->age = ‘-100’;

if ( ! $user->isValid()) { echo ‘User is invalid because -100 is signed’; }

US State

usstateバリデータは文字列が有効なUSの州コードであることをチェックします。

// models/State.php

class State extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); $this->hasColumn(‘code’, ‘string’, 2, array( ‘usstate’ => true ) ); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

State: columns: name: string(255) code: type: string(2) usstate: true

無効な州コードで``State``を保存しようとするとバリデーションがエラーになります。

$state = new State(); $state->name = ‘Tennessee’; $state->code = ‘ZZ’;

if ( ! $state->isValid()) { echo ‘State is invalid because “ZZ” is not a valid state code’; }

=== まとめ ===

データを永続的にデータベースに保存する前にDoctrineにデータのバリデーションを行わせる方法を理解しDoctrineコアが提供する共通のバリデータを使うことができます。

[doc inheritance 次の章]では[doc inheritance :name]を検討するので重要です!継承は最小のコードで複雑な機能を実現するための偉大な方法です。継承を検討した後で[doc behaviors :name]と呼ばれる継承よりも優れた機能を提供するカスタム戦略に移ります。

[1]a-zA-Z0-9

Doctrineは``Doctrine_Query``インスタンスを ユーザーが利用できるPHPのデータに変換するためのデータハイドレーターの概念を持ちます。データをハイドレイトするもっとも明らかな方法はこれをオブジェクトグラフに入れるとモデル/クラスインスタンスが返されることです。ときにはデータを配列にハイドレイトしたい場合、ハイドレーションを使わないと単独のスカラーの値が買えされます。この章は異なるハイドレーションのタイプおよび独自のハイドレーションのタイプの書き方もデモンストレートします。

コアハイドレーションメソッド

Doctrineはもっとも共通のハイドレーションのニーズによってあなたを助けてくれる少数のコアのハイドレーションメソッドを提供します。

レコード

最初のタイプは``HYDRATE_RECORD``でこれはデフォルトのハイドレーションのタイプです。これはクエリからデータをとりこれはオブジェクトグラフにハイドレイトします。このタイプによった次のようなことが可能になります。

$q = Doctrine_Core::getTable(‘User’) ->createQuery(‘u’)

->leftJoin(‘u.Email e’) ->where(‘u.username = ?’, ‘jwage’);

$user = $q->fetchOne(array(), Doctrine_Core::HYDRATE_RECORD);

echo $user->Email->email;

上記のクエリのデータは1つのクエリによって検索されレコードハイドレーターによってオブジェクトグラフにハイドレイトされました。データベースからの結果セットではなくレコードは扱うのが難しいので、これによってデータを扱うのがはるかに簡単になります。

配列

配列ハイドレーションタイプは``HYDRATE_ARRAY``定数によって表現されます。PHPオブジェクトを使ってデータをオブジェクトグラフにハイドレイトする代わりにPHP配列を使うこと以外、上記のレコードハイドレーションは理想的です。オブジェクトの代わりに配列を使う利点はこれらがずっと速くハイドレーションは時間がかからないことです。

同じ結果を実行したい場合、同じデータにアクセスできますが、PHP配列経由になります。

$q = Doctrine_Core::getTable(‘User’) ->createQuery(‘u’)

->leftJoin(‘u.Email e’) ->where(‘u.username = ?’, ‘jwage’);

$user = $q->fetchOne(array(), Doctrine_Core::HYDRATE_ARRAY);

echo $user[‘Email’][‘email’];

スカラー

スカラーハイドレーションタイプは``HYDRATE_SCALAR``定数によって表現されデータをハイドレイトするためにとても速くて効率的な方法です。この方法の欠点はこれはデータをオブジェクトグラフにハイドレイトしないことで、これはたくさんのレコードを扱うときに扱いにくいフラットな長方形の結果セットを返します。

$q = Doctrine_Core::getTable(‘User’) ->createQuery(‘u’)

->where(‘u.username = ?’, ‘jwage’);

$user = $q->fetchOne(array(), Doctrine_Core::HYDRATE_SCALAR);

echo $user[‘u_username’];

上記のクエリは次のようなデータ構造を生み出します:

$user = array( ‘u_username’ => ‘jwage’, ‘u_password’ => ‘changeme’,

// ... );

クエリがデータよりもJOINされた多くのリレーションシップを持つ場合 ユーザーが存在するすべてのレコードでユーザーが重複することになります。これはたくさんのレコードを扱うときに扱いが難しいので欠点です。

シングルスカラー

単独のスカラー値だけを返したいことがよくあります。これはシングルスカラーハイドレーションの方法で可能で``HYDRATE_SINGLE_SCALAR``属性によって表現されます。

このハイドレーションタイプによって次のように1人のユーザーが持つ電話番号の数を簡単に数えることができます:

$q = Doctrine_Core::getTable(‘User’) ->createQuery(‘u’)

->select(‘COUNT(p.id)’) ->leftJoin(‘u.Phonenumber p’) ->where(‘u.username = ?’, ‘jwage’);

$numPhonenumbers = $q->fetchOne(array(), Doctrine_Core::HYDRATE_SINGLE_SCALAR);

echo $numPhonenumbers;

これはより複雑な方法でデータをハイドレイトしてこれらの結果から値を得るよりもはるかによい方法です。これによって本当に欲しいデータをとても速く効率的に得ることができます。

オンデマンド

よりメモリーの使用量が少ないハイドレーションの方法を使いたいのであれば``HYDRATE_ON_DEMAND``定数によって表現されるオンデマンドハイドレーションを使うことができます。これは一度の1つのレコードグラフのみをハイドレイトするので使われるメモリーがより少なくて済むことを意味します。

// Doctrine_Collection_OnDemandのインスタンスを返す $result =

q->execute(array(), Doctrine_Core::HYDRATE_ON_DEMAND); foreach (result as $obj) { // ... }

``Doctrine_Collection_OnDemand``はイテレートするときに一度にそれぞれのオブジェクトをハイドレイトするのでこの結果はより少ないメモリーで済みます。これは最初にデータベースからすべてのデータをPHPにロードし、返すデータ構造全体を変換する必要がないからです。

入れ子集合のレコード階層

入れ子集合のビヘイビアを使うモデルのために、入れ子集合のツリーを入れ子オブジェクトの実際の階層にハイドレイトするレコード階層ハイドレーションの方法を使うことができます。

$categories = Doctrine_Core::getTable(‘Category’) ->createQuery(‘c’)

->execute(array(), Doctrine_Core::HYDRATE_RECORD_HIERARCHY);

これで``__children``という名前のマッピングされた値のプロパティにアクセスすることでレコードの子にアクセスできます。名前の衝突を避けるためにこの名前にはプレフィックスとしてアンダースコアがつけられています。

foreach ($categories->getFirst()->get(‘__children’) as $child) { //

... }

入れ子集合の配列階層

入れ子集合階層をオブジェクトではなく配列にハイドレイトしたい場合``HYDRATE_ARRAY_HIERARCHY``定数を使ってこれを実現できます。これはオブジェクトの代わりにPHP配列を使っている以外は``HYDRATE_RECORD_HIERARCHY``と同じです。

$categories = Doctrine_Core::getTable(‘Category’) ->createQuery(‘c’)

->execute(array(), Doctrine_Core::HYDRATE_ARRAY_HIERARCHY);

次のことができるようになります:

foreach ($categories[0][‘__children’] as $child) { // ... }

ハイドレーションメソッドを書く

Doctrineは独自のハイドレーション方法を書きこれらを登録する機能を提供します。必要なのは``Doctrine_Hydrator_Abstract``を継承するクラスを書きこれを``Doctrine_Manager``で登録することです。

最初にサンプルのハイドレイターのクラスを書いてみましょう:

class Doctrine_Hydrator_MyHydrator extends

Doctrine_Hydrator_Abstract { public function hydrateResultSet($stmt) { $data = $stmt->fetchAll(PDO::FETCH_ASSOC); // $dataで何かを行う return $data; } }

これを使うためには``Doctrine_Manager``で登録します:

// bootstrap.php

// ... $manager->registerHydrator(‘my_hydrator’, ‘Doctrine_Hydrator_MyHydrator’);

クエリを実行するとき、``my_hydrator``を渡せばデータをハイドレイトするクラスが使われます。

$q->execute(array(), ‘my_hydrator’);

Doctrineは3種類の継承戦略をサポートします。これらの戦略は混ぜることができます。三種類は単一、具象とカラム集約です。これらの異なる継承の使い方をこの章で学びます。

この章では前の章で利用してきたテスト環境の既存のすべてのスキーマとモデルを削除してください。

$ rm schema.yml $ touch schema.yml $ rm -rf models/*

単一継承

単一継承は最も簡単でシンプルです。単一継承においてすべての子クラスは同じカラムを親として共有しすべての情報は親テーブルに保存されます。

// models/Entity.php

class Entity extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 30); $this->hasColumn(‘username’, ‘string’, 20); $this->hasColumn(‘password’, ‘string’, 16); $this->hasColumn(‘created_at’, ‘timestamp’); $this->hasColumn(‘update_at’, ‘timestamp’); } }

``Entity``を継承する``User``モデルを作りましょう:

// models/User.php

class User extends Entity { }

``Group``モデルに対しても同じことを行いましょう:

// models/Group.php

class Group extends Entity { }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# 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

上記のコードによって生成されたSQLを確認してみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘Entity’, ‘User’, ‘Group’)); echo $sql[0];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE entity (id BIGINT AUTO_INCREMENT, username VARCHAR(20),

password VARCHAR(16), created_at DATETIME, updated_at DATETIME, type VARCHAR(255), name VARCHAR(30), PRIMARY KEY(id)) ENGINE = INNODB

NOTE YAMLスキーマファイルを使うとき子クラスでカラムを定義できますがYAMLが解析されるときカラムは自動的に親に移動します。これはカラムを簡単に組織できるようにするためだけです。

具象継承

具象継承は子クラス用の独立したテーブルを作成します。しかしながら具象継承ではそれぞれのクラスはすべてのカラムを含むテーブルを生成します(継承されたカラムを含む)。具象継承を使うには下記で示されるように子クラスへの明示的な``parent::setTableDefinition()``を追加する必要があります。

// models/TextItem.php

class TextItem extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘topic’, ‘string’, 100); } }

``TextItem``を継承する``Comment``という名前のモデルを作り``content``という名前のカラムを追加してみましょう:

// models/Comment.php

class Comment extends TextItem { public function setTableDefinition() { parent::setTableDefinition();

    $this->hasColumn('content', 'string', 300);
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

TextItem: columns: topic: string(100)

Comment: inheritance: extends: TextItem type: concrete columns: content: string(300)

上記のモデルによって生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine::generateSqlFromArray(array(‘TextItem’, ‘Comment’)); echo $sql[0] . “”; echo $sql[1];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE text_item (id BIGINT AUTO_INCREMENT, topic VARCHAR(100),

PRIMARY KEY(id)) ENGINE = INNODB CREATE TABLE comment (id BIGINT AUTO_INCREMENT, topic VARCHAR(100), content TEXT, PRIMARY KEY(id)) ENGINE = INNODB

具象クラスにおいて追加のカラムを定義する必要はありませんが、それぞれのクラス用に個別のテーブルを作るには``setTableDefinition()``の呼び出しを繰り返し書かなければなりません。

次の例では``entity``、user``と``group``と呼ばれるデータベーステーブルがあります。``Users``と``groups``は両方とも``entities``です。行わなければならないことは3つのクラス(``EntityGroup``と``User)を書き``setTableDefinition()``メソッドの呼び出しを繰り返し記述することです。

// models/Entity.php

class Entity extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 30); $this->hasColumn(‘username’, ‘string’, 20); $this->hasColumn(‘password’, ‘string’, 16); $this->hasColumn(‘created’, ‘integer’, 11); } }

// models/User.php

class User extends Entity { public function setTableDefinition() { // 次のメソッド呼び出しは // 具象継承で必要 parent::setTableDefinition(); } }

// models/Group.php class Group extends Entity { public function setTableDefinition() { // 次のメソッド呼び出しは // 具象継承で必要 parent::setTableDefinition(); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

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

上記のモデルによって生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine::generateSqlFromArray(array(‘Entity’, ‘User’, ‘Group’)); echo $sql[0] . “”; echo $sql[1] . “”; echo $sql[2] . “”;

上記のコードは次のSQLクエリを出力します:

CREATE TABLE user (id BIGINT AUTO_INCREMENT, name VARCHAR(30),

username VARCHAR(20), password VARCHAR(16), created BIGINT, PRIMARY KEY(id)) ENGINE = INNODB CREATE TABLE groups (id BIGINT AUTO_INCREMENT, name VARCHAR(30), username VARCHAR(20), password VARCHAR(16), created BIGINT, PRIMARY KEY(id)) ENGINE = INNODB CREATE TABLE entity (id BIGINT AUTO_INCREMENT, name VARCHAR(30), username VARCHAR(20), password VARCHAR(16), created BIGINT, PRIMARY KEY(id)) ENGINE = INNODB

カラム集約

次の例において``entity``という名前の1つのデータベーステーブルがあります。``Users``と``groups``は両方とも``entities``でこれらは同じデータベーステーブルを共有します。

``entity``テーブルは``type``と呼ばれる1つのカラムを持ちます。このカラムは``group``もしくは``user``であることを伝えます。``users``はタイプ1でグループはタイプ2であると決めます。

行わなければならない唯一の作業は3のレコード(以前と同じ)を作成し親クラスからの``Doctrine_Table::setSubclasses()``メソッド呼び出しを追加することです。

// models/Entity.php

class Entity extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 30); $this->hasColumn(‘username’, ‘string’, 20); $this->hasColumn(‘password’, ‘string’, 16); $this->hasColumn(‘created_at’, ‘timestamp’); $this->hasColumn(‘update_at’, ‘timestamp’);

    $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 { }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

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

上記のモデルによって生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine::generateSqlFromArray(array(‘Entity’, ‘User’, ‘Group’)); echo $sql[0];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE entity (id BIGINT AUTO_INCREMENT, username VARCHAR(20),

password VARCHAR(16), created_at DATETIME, updated_at DATETIME, type VARCHAR(255), PRIMARY KEY(id)) ENGINE = INNODB

NOTE ``type``カラムが自動的に追加されたことに注目してください。データベースのそれぞれのレコードが所属するモデルを知っているカラム集約継承です。

この機能によって``Entity``テーブルにクエリを行い変えされたオブジェクトが親クラスで設定された制約にマッチする場合``User``もしくは``Group``オブジェクトを戻します。

具体的な内容は下記のコードで見てみましょう。最初に新しい``User``オブジェクトを保存しましょう:

// test.php

// ... $user = new User(); $user->name = ‘Bjarte S. Karlsen’; $user->username = ‘meus’; $user->password = ‘rat’; $user->save();

新しい``Group``オブジェクトを保存しましょう:

// test.php

// ... $group = new Group(); $group->name = ‘Users’; $group->username = ‘users’; $group->password = ‘password’; $group->save();

作成した``User``のid用の``Entity``モデルにクエリを行うと、``Doctrine_Query``は``User``のインスタンスを返します。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘Entity e’) ->where(‘e.id = ?’);

$user = q->fetchOne(array(user->id));

echo get_class($user); // User

``Group``レコードに対して同じようなことを行うと、``Group``のインスタンスが戻されます。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘Entity e’) ->where(‘e.id = ?’);

$group = q->fetchOne(array(group->id));

echo get_class($group); // Group

NOTE 上記の内容は``type``カラムであるから可能です。Doctrineはどのクラスによってそれぞれのレコードが作成されたのか知っているので、データは適切なサブクラスにハイドレイトされます。

個別の``User``もしくは``Group``モデルにクエリを行うこともできます:

$q = Doctrine_Query::create() ->select(‘u.id’) ->from(‘User u’);

echo $q->getSqlQuery();

上記の``getSql()``の呼び出しは次のSQLクエリを出力します:

SELECT e.id AS e__id FROM entity e WHERE (e.type = ‘1’)

NOTE ``User``型であるレコードのみが返されるように``type``の条件が自動的に追加されたことに注目してください。

まとめ

モデルでPHPの継承機能を利用する方法を学んだので[doc behaviors :name]の章に移動します。これは複雑なモデルを小さくて簡単なコードで維持するためのもっとも洗練された便利な機能の1つです。

Behaviours

はじめに

モデルの中で似た内容を持つクラスを見つけることはよくあります。これらの内容はコンポーネント自身のスキーマ(リレーション、カラムの定義、インデックスの定義など)に関連することがあります。このコードをリファクタリングする明らかな方法は基底クラスとそれを継承するクラスを用意することです。

しかしながら継承はこれらの一部しか解決しません。次のセクションで``Doctrine_Template``が継承よりもはるかに強力で柔軟であることを示します。

``Doctrine_Template``はクラステンプレートシステムです。基本的にテンプレートはRecordクラスがロードできるすぐ使える小さなコンポーネントです。テンプレートがロードされるとき``setTableDefinition()``と``setUp()``メソッドが起動されこれらの内部でメソッドが呼び出され渦中のクラスに導かれます。

この章ではDoctrineで利用できる様々なビヘイビアの使い方を説明します。独自ビヘイビアの作り方も説明します。この章のコンセプトを掴むために``Doctrine_Template``と``Doctrine_Record_Generator``の背景にある理論に慣れなければなりません。これらのクラスがどんなものであるのか手短に説明します。

ビヘイビアを言及するときテンプレート、ジェネレータとリスナーを広範囲に渡って使用するクラスパッケージを意味します。この章で紹介されるすべてのコンポーネントは``core``ビヘイビアとしてみなされています。これはDoctrineのメインリポジトリに保管されていることを意味します。

通常ビヘイビアはテンプレートクラス(``Doctrine_Template``を継承するクラス)でジェネレータを使用します。共通のワークフローは次の通りです:

  • 新しいテンプレートが初期化される
  • テンプレートはジェネレータを作成し``initialize()``メソッドを呼び出す
  • テンプレートはクラスに添付される

ご存知かもしれませんがテンプレートは共通の定義とオプションをレコードクラスに追加するために使われます。ジェネレータの目的はとても複雑です。通常これらはジェネリックレコードクラスを動的に作成するために使われます。これらのジェネリッククラスの定義はオーナーのクラスによります。例えばクラスをバージョニングする``AuditLog``カラムの定義はすべてのsequenceとautoincrementの定義が削除された親クラスのカラムです。

シンプルなテンプレート

次の例において``TimestampBehavior``と呼ばれるテンプレートを定義します。基本的にテンプレートの目的はこのテンプレートをロードするレコードクラスに日付カラムの’created’と’updated’を追加することです。加えてこのテンプレートはレコードのアクションに基づいてこれらのカラムを更新するTimestampリスナーを使用します。

// models/TimestampListener.php

class TimestampListener extends Doctrine_Record_Listener { public function preInsert(Doctrine_Event $event) { $event->getInvoker()->created = date(‘Y-m-d’, time()); $event->getInvoker()->updated = date(‘Y-m-d’, time()); }

public function preUpdate(Doctrine_Event $event)
{
    $event->getInvoker()->updated = date('Y-m-d', time());
}

}

``actAs()``メソッドでモデルに添付できるように``TimestampTemplate``という名前の子の``Doctrine_Template``を作りましょう:

// models/TimestampBehavior.php

class TimestampTemplate extends Doctrine_Template { public function setTableDefinition() { $this->hasColumn(‘created’, ‘date’); $this->hasColumn(‘updated’, ‘date’);

    $this->setListener(new TimestampListener());
}

}

タイムスタンプの機能を必要とする``BlogPost``クラスを考えてみましょう。行う必要があるのはクラスの定義に``actAs()``の呼び出しを追加することです。

class BlogPost extends Doctrine_Record
public function setTableDefinition()
{
    $this->hasColumn('title', 'string', 200);
    $this->hasColumn('body', 'clob');
}

public function setUp()
{
    $this->actAs('TimestampBehavior');
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

BlogPost: actAs: [TimestampBehavior] columns: title: string(200) body:

clob

``BlogPost``モデルを活用しようとするとき``created``と``updated``カラムが追加され保存されるときに自動的に設定されたことがわかります:

$blogPost = new BlogPost(); $blogPost->title = ‘Test’; $blogPost->body

= ‘test’; $blogPost->save();

print_r($blogPost->toArray());

上記の例は次の出力を表示します:

$ php test.php Array ( [id] => 1 [title] => Test [body] => test

[created] => 2009-01-22 [updated] => 2009-01-22 )

NOTE 上記で説明した機能は既にお話した``Timestampable``ビヘイビアを通して利用できます。この章の[doc behaviors:core-behaviors:timestampable :name]セクションに戻って詳細内容を読むことができます。
リレーション付きのテンプレート

以前の章よりも状況は複雑になりがちです。他のモデルクラスへのリレーションを持つクラスがあり任意のクラスを格調されたクラスで置き換えたいことがあります。

次の定義を持つ``User``と``Email``の2つのクラスを考えてみましょう:

class User extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp()
{
    $this->hasMany('Email', array(
            'local' => 'id',
            'foreign' => 'user_id'
        )
    );
}

}

class Email extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘address’, ‘string’); $this->hasColumn(‘user_id’, ‘integer’); }

public function setUp()
{
    $this->hasOne('User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

User: columns: username: string(255) password: string(255)

Email: columns: address: string user_id: integer relations: User:

``User``と``Email``クラスを拡張し、例えば``ExtendedUser``と``ExtendedEmail``クラスを作る場合、``ExtendedUser``は``Email``クラスへのリレーションを保存しますが``ExtendedEmail``クラスへのリレーションは保存しません。もちろん``User``クラスの``setUp()``メソッドをオーバーライドして``ExtendedEmail``クラスへのリレーションを定義することはできますが、継承の本質を失います。``Doctrine_Template``はこの問題を依存オブジェクトの注入(dependency injection)の方法でエレガントに解決します。

次の例では2つのテンプレート、``UserTemplate``と``EmailTemplate``を``User``と``Email``クラスが持つほぼ理想的な定義で定義します。

// models/UserTemplate.php

class UserTemplate extends Doctrine_Template { public function setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255); $this->hasColumn(‘password’, ‘string’, 255); }

public function setUp()
{
    $this->hasMany('EmailTemplate as Emails', array(
            'local' => 'id',
            'foreign' => 'user_id'
        )
    );
}

}

``EmailTemplate``を定義しましょう:

// models/EmailTemplate.php

class EmailTemplate extends Doctrine_Template { public function setTableDefinition() { $this->hasColumn(‘address’, ‘string’); $this->hasColumn(‘user_id’, ‘integer’); }

public function setUp()
{
    $this->hasOne('UserTemplate as User', array(
            'local' => 'user_id',
            'foreign' => 'id'
        )
    );
}

}

リレーションの設定方法に注目してください。Record具象クラスを指し示すのではなく、テンプレートへのリレーションを設定しています。これはDoctrineにこれらのテンプレート用のRecord具象クラスを探すように伝えています。Doctrineがこれらの具象継承を見つけられない場合リレーションパーサーは例外を投げますが、前に進む前に、実際のレコードクラスは次の通りです:

class User extends Doctrine_Record { public function setUp() {

$this->actAs(‘UserTemplate’); } }

class Email extends Doctrine_Record { public function setUp() { $this->actAs(‘EmailTemplate’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

User: actAs: [UserTemplate]

Email: actAs: [EmailTemplate]

次のコードスニペットを考えてみましょう。テンプレート用の具象実装を設定していないのでこのコードスニペットは動きません。

// test.php

// ... $user = new User(); $user->Emails; // throws an exception

次のバージョンが動作します。``Doctrine_Manager``を使用してグローバルにテンプレート用の具象実装の設定をする方法を注目してください:

// bootstrap.php

// ... $manager->setImpl(‘UserTemplate’, ‘User’) ->setImpl(‘EmailTemplate’, ‘Email’);

このコードは動作しますが以前のように例外を投げません:

$user = new User(); $user->Emails[0]->address = 'jonwage@gmail.com‘;

$user->save();

print_r($user->toArray(true));

上記の例は次の内容を出力します:

$ php test.php Array ( [id] => 1 [username] => [password] => [Emails]

=> Array ( [0] => Array ( [id] => 1 [address] => jonwage@gmail.com [user_id] => 1 )

)

)

Tip

テンプレート用の実装はマネージャー、接続とテーブルレベルでも設定できます。

デリゲートメソッド

フルテーブル定義のデリゲートシステムとして振る舞うことに加えて、Doctrine\_Template``はメソッドの呼び出しのデリゲートを可能にします。これはロードされたテンプレート内のすべてのメソッドはテンプレートをロードしたレコードの中で利用できることを意味します。この機能を実現するために内部では__call()``と呼ばれるマジックメソッドが使用されます。

以前の例に``UserTemplate``にカスタムメソッドを追加してみましょう:

// models/UserTemplate.php

class UserTemplate extends Doctrine_Template { // ...

public function authenticate($username, $password)
{
    $invoker = $this->getInvoker();
    if ($invoker->username == $username && $invoker->password == $password) {
        return true;
    } else {
        return false;
    }
}

}

次のコードで使い方を見ましょう:

$user = new User(); $user->username = ‘jwage’; $user->password =

‘changeme’;

if ($user->authenticate(‘jwage’, ‘changemte’)) { echo ‘Authenticated successfully!’; } else { echo ‘Could not authenticate user!’; }

``Doctrine_Table``クラスにメソッドをデリゲートすることも簡単にできます。しかし名前衝突を避けるために、テーブルクラス用のメソッドはメソッド名の最後に追加される``TableProxy``の文字列を持たなければなりません。

新しいファインダーメソッドを追加する例は次の通りです:

// models/UserTemplate.php

class UserTemplate extends Doctrine_Template { // ...

public function findUsersWithEmailTableProxy()
{
    return Doctrine_Query::create()
        ->select('u.username')
        ->from('User u')
        ->innerJoin('u.Emails e')
        ->execute();
}

}

``User``モデル用の``Doctrine_Table``オブジェクトからのメソッドにアクセスできます:

$userTable = Doctrine_Core::getTable(‘User’);

$users = $userTable->findUsersWithEmail();

Tip

それぞれのクラスは複数のテンプレートから構成されます。テンプレートが似たような定義を格納する場合最新のロードされたテンプレートは 前のものを常にオーバーライドします。

ビヘイビアを作成する

この節では独自ビヘイビア作成用の方法を説明します。一対多のEメールが必要な様々なRecordクラスを考えてみましょう。Emailクラスを即座に作成する一般的なビヘイビアを作成することでこの機能を実現します。

``EmailBehavior``と呼ばれるビヘイビアを``setTableDefinition()``メソッドで作成することからこのタスクを始めます。``setTableDefinition()``メソッドの内部では動的なレコードの定義に様々なヘルパーメソッドが使われます。次のメソッドが共通で使われています:

public function initOptions() public function buildLocalRelation()

public function buildForeignKeys(Doctrine_Table table) public function buildForeignRelation(alias = null) public function buildRelation() // buildForeignRelation()とbuildLocalRelation()を呼び出す

class EmailBehavior extends Doctrine_Record_Generator { public

function initOptions() { $this->setOption(‘className’, ‘%CLASS%Email’);

    // ほかのオプション
    // $this->setOption('appLevelDelete', true);
    // $this->setOption('cascadeDelete', false);
}

public function buildRelation()
{
    $this->buildForeignRelation('Emails');
    $this->buildLocalRelation();
}

public function setTableDefinition()
{
    $this->hasColumn('address', 'string', 255, array(
            'email'  => true,
            'primary' => true
        )
    );
}

}

コアビヘイビア

コアビヘイビアを使う次のいくつかの例のために以前の章で作成したテスト環境から既存のスキーマとモデルをすべて削除しましょう。

$ rm schema.yml $ touch schema.yml $ rm -rf models/*

– 紹介 –

Doctrineにはモデルにそのまま使える機能を提供するテンプレートが搭載されています。モデルでこれらのテンプレートを簡単に有効にできます。``Doctrine_Records``で直接行うもしくはYAMLでモデルを管理しているのであればこれらをYAMLスキーマで指定できます。

次の例ではDoctrineに搭載されているビヘイビアの一部を実演します。

Versionable

バージョン管理の機能を持たせるために``BlogPost``モデルを作成しましょう:

// models/BlogPost.php

class BlogPost extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255); $this->hasColumn(‘body’, ‘clob’); }

public function setUp()
{
    $this->actAs('Versionable', array(
            'versionColumn' => 'version',
            'className' => '%CLASS%Version',
            'auditLog' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

BlogPost: actAs: Versionable: versionColumn: version className:

%CLASS%Version auditLog: true columns: title: string(255) body: clob

NOTE ``auditLog``オプションはauditのログ履歴を無効にするために使われます。これはバージョン番号を維持したいがそれぞれのバージョンでのデータを維持したくない場合に使います。

上記のモデルで生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘BlogPost’)); echo $sql[0] . “”; echo $sql[1];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE blog_post_version (id BIGINT, title VARCHAR(255), body

LONGTEXT, version BIGINT, PRIMARY KEY(id, version)) ENGINE = INNODB CREATE TABLE blog_post (id BIGINT AUTO_INCREMENT, title VARCHAR(255), body LONGTEXT, version BIGINT, PRIMARY KEY(id)) ENGINE = INNODB ALTER TABLE blog_post_version ADD FOREIGN KEY (id) REFERENCES blog_post(id) ON UPDATE CASCADE ON DELETE CASCADE

NOTE おそらく予期していなかったであろう2の追加ステートメントがあることに注目してください。ビヘイビアは自動的に``blog_post_version``テーブルを作成しこれを``blog_post``に関連付けます。

``BlogPost``を挿入もしくは更新するときバージョンテーブルは古いバージョンのレコードをすべて保存していつでも差し戻しできるようにします。最初に``NewsItem``をインスタンス化するとき内部で起きていることは次の通りです:

  • ``BlogPostVersion``という名前のクラスが即座に作成される。レコードが指し示すテーブルは``blog_post_version``である
  • ``BlogPost``オブジェクトが削除/更新されるたびに以前のバージョンは``blog_post_version``に保存される
  • ``BlogPost``オブジェクトが更新されるたびにバージョン番号が増える。

``BlogPost``モデルで遊びましょう:

$blogPost = new BlogPost(); $blogPost->title = ‘Test blog post’;

$blogPost->body = ‘test’; $blogPost->save();

$blogPost->title = ‘Modified blog post title’; $blogPost->save();

print_r($blogPost->toArray());

上記の例では次の内容が出力されます:

$ php test.php Array ( [id] => 1 [title] => Modified blog post title

[body] => test [version] => 2 )

NOTE ``version``カラムの値が``2``であることに注目してください。2つのバージョンの``BlogPost``モデルを保存したからです。ビヘイビアが格納する``revert()``メソッドを使用することで別のバージョンに差し戻すことができます。

最初のバージョンに差し戻してみましょう:

blogPost->revert(1); print_r(blogPost->toArray());

上記の例は次の内容を出力する:

$ php test.php Array ( [id] => 2 [title] => Test blog post [body] =>

test [version] => 1 )

NOTE ``version``カラムの値が1に設定され``title``は``BlogPost``を作成するときに設定されたオリジナルの値に戻ります。
Timestampable

Timestampableビヘイビアは``created_at``と``updated_at``カラムを追加しレコードが挿入と更新されたときに値を自動的に設定します。

日付を知ることは共通のニーズなので``BlogPost``モデルを展開してこれらの日付を自動的に設定するために``Timestampable``ビヘイビアを追加します。

// models/BlogPost.php

class BlogPost extends Doctrine_Record { // ...

public function setUp()
{
    $this->actAs('Timestampable');
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

BlogPost: actAs: # ... Timestampable: # ...

``updated_at``フィールドではなく``created_at``タイムスタンプといったカラムの1つだけを使うことに興味があるのであれば、下記の例のようにフィールドのどちらかに対して``disabled``をtrueに設定します。

BlogPost: actAs: # ... Timestampable: created: name: created_at type:

timestamp format: Y-m-d H:i:s updated: disabled: true # ...

新しい投稿を作成するときに何が起きるのか見てみましょう:

$blogPost = new BlogPost(); $blogPost->title = ‘Test blog post’;

$blogPost->body = ‘test’; $blogPost->save();

print_r($blogPost->toArray());

上記の例は次の内容を出力します:

$ php test.php Array ( [id] => 1 [title] => Test blog post [body] =>

test [version] => 1 [created_at] => 2009-01-21 17:54:23 [updated_at] => 2009-01-21 17:54:23 )

NOTE ``created_at``と``updated_at``の値が自動的に設定されることに注目してください!

ビヘイビアの作成側の``Timestampable``ビヘイビアで使うことができるすべてのオプションのリストです:

||~ 名前 ||~ デフォルト ||~ 説明 || || name || created_at || カラムの名前 || || type || timestamp || カラムの型 || || options || array() || カラム用の追加オプション || || format || Y-m-d H:i:s || timestampカラム型を使いたくない場合のタイムスタンプのフォーマット。日付はPHPの[http://www.php.net/date date()]関数で生成される || || disabled || false || 作成日を無効にするか || || expression || NOW() || カラムの値を設定するために使用する式 ||

作成側では不可能な更新側のビヘイビアで``Timestampable``ビヘイビアで使うことができるすべてのオプションのリストは次の通りです:

||~ 名前 ||~ デフォルト||~ 説明 || || onInsert ||``true`` || レコードが最初に挿入されるときに更新日付を設定するか ||

Sluggable

``Sluggable``ビヘイビアは素晴らしい機能の1つでタイトル、題目などのカラムから作成できる人間が読解できるユニークな識別子を保存するためにモデルにカラムを自動的に追加します。これらの値は検索エンジンにフレンドリーなURLに使うことができます。

投稿記事用のわかりやすいURLが欲しいので``Sluggable``ビヘイビアを使うように``BlogPost``モデルを拡張してみましょう:

// models/BlogPost.php

class BlogPost extends Doctrine_Record { // ...

public function setUp()
{
    // ...

    $this->actAs('Sluggable', array(
            'unique'    => true,
            'fields'    => array('title'),
            'canUpdate' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

BlogPost: actAs: # ... Sluggable: unique: true fields: [title] canUpdate: true # ...

新しい投稿を作成する際に何が起きるのか見てみましょう。slugカラムは自動的に設定されます:

$blogPost = new BlogPost(); $blogPost->title = ‘Test blog post’;

$blogPost->body = ‘test’; $blogPost->save();

print_r($blogPost->toArray());

上記の例は次の内容を出力します:

$ php test.php Array ( [id] => 1 [title] => Test blog post [body] =>

test [version] => 1 [created_at] => 2009-01-21 17:57:05 [updated_at] => 2009-01-21 17:57:05 [slug] => test-blog-post )

NOTE ``title``カラムの値に基づいて``slug``カラムの値が自動的に設定されることに注目してください。スラッグが作成されるとき、デフォルトでは``urlized``が使われます。これはURLにフレンドリーではない文字は削除されホワイトスペースはハイフン(-)に置き換えられます。

uniqueフラグは作成されたスラッグがユニークであることを強制します。ユニークではない場合データベースに保存される前にauto incrementな整数がスラッグに自動的に追加されます。

``canUpdate``フラグはurlフレンドリーなスラッグを生成する際にユーザーが使用するスラッグを自動的に設定することを許可します。

``Sluggable``ビヘイビアで使うことができるすべてのオプションのリストは次の通りです:

||~ 名前 ||~ デフォルト||~ 説明 || || name || slug || スラッグカラムの名前 || || alias || null || スラッグカラムのエイリアス || || type || string || スラッグカラムの型 || || length || 255 || スラッグカラムの長さ || || unique || true || ユニークスラッグの値が強制されるかどうか || || options || array() || スラッグカラム用の他のオプション || || fields || array() || スラッグの値をビルドするために使用するフィールド || || uniqueBy || array() || ユニークスラッグを決定するフィールド || || uniqueIndex|| true || ユニークインデックスを作成するかどうか || || canUpdate || false || スラッグが更新できるかどうか || || builder || array('Doctrine_Inflector', 'urlize') || スラッグをビルドするために使う``Class::method()`` || || indexName || sluggable || 作成するインデックスの名前 ||

I18n

``Doctrine_I18n``パッケージはレコードクラス用の国際化サポートを提供するビヘイビアです。次の例では``title``と``content``の2つのフィールドを持つ``NewsItem``クラスがあります。異なる言語サポートを持つ``title``フィールドを用意したい場合を考えます。これは次のように実現できます:

class NewsItem extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255); $this->hasColumn(‘body’, ‘blog’); }

public function setUp()
{
    $this->actAs('I18n', array(
            'fields' => array('title', 'body')
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

NewsItem: actAs: I18n: fields: [title, body] columns: title:

string(255) body: clob

``I18n``ビヘイビアで使うことができるすべてのオプションのリストは次の通りです:

||~ 名前 ||~ デフォルト ||~ 説明 || || className || %CLASS%Translation || 生成クラスに使う名前のパターン || || fields || array() || 国際化するフィールド || || type || string || lang``カラムの型 \|\| \|\| ``length || 2 || lang``カラムの長さ \|\| \|\| ``options || array() || ``lang``カラム用の他のオプション ||

上記のモデルで生成されるSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘NewsItem’)); echo $sql[0] . “”; echo $sql[1];

上記のコードは次のSQLを出力します:

CREATE TABLE news_item_translation (id BIGINT, title VARCHAR(255),

body LONGTEXT, lang CHAR(2), PRIMARY KEY(id, lang)) ENGINE = INNODB CREATE TABLE news_item (id BIGINT AUTO_INCREMENT, PRIMARY KEY(id)) ENGINE = INNODB

NOTE ``title``フィールドが``news_item``テーブルに存在しないことに注目してください。翻訳テーブルにあるとメインテーブルで同じフィールドが存在してリソースの無駄遣いになるからです。基本的にDoctrineは常にメインテーブルから翻訳されたフィールドをすべて削除します。

初めて新しい``NewsItem``レコードを初期化するときDoctrineは次の内容をビルドするビヘイビアを初期化します:

  1. ``NewsItemTranslation``と呼ばれるRecordクラス
  2. ``NewsItemTranslation``と``NewsItem``の双方向なリレーション

``NewsItem``の翻訳を操作する方法を見てみましょう:

// test.php

// ... $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());

上記の例は次の内容を出力します:

$ php test.php Array ( [id] => 1 [Translation] => Array ( [en] => Array

( [id] => 1 [title] => some title [body] => test [lang] => en ) [fi] => Array ( [id] => 1 [title] => joku otsikko [body] => test [lang] => fi )

)

)

翻訳データをどのように読み取るのでしょうか?これは簡単です!すべての項目を見つけて翻訳を終わらせましょう:

// test.php

// ... $newsItems = Doctrine_Query::create() ->from(‘NewsItem n’) ->leftJoin(‘n.Translation t’) ->where(‘t.lang = ?’) ->execute(array(‘fi’));

echo $newsItems[0]->Translation[‘fi’]->title;

上記のコードは次の内容を出力します:

$ php test.php joku otsikko
NestedSet

``NestedSet``ビヘイビアによってモデルを入れ子集合ツリー構造(nested set tree structure)に変換できます。ツリー構造全体を1つのクエリで効率的に読み取ることができます。このビヘイビアはツリーのデータを操作するための素晴らしいインターフェイスも提供します。

例として``Category``モデルを考えてみましょう。カテゴリを階層ツリー構造で編成する必要がある場合は次のようになります:

// models/Category.php

class Category extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); }

public function setUp()
{
    $this->actAs('NestedSet', array(
            'hasManyRoots' => true,
            'rootColumnName' => 'root_id'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Category: actAs: NestedSet: hasManyRoots: true rootColumnName: root_id columns: name: string(255)

上記のモデルで生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘Category’)); echo $sql[0];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE category (id BIGINT AUTO_INCREMENT, name VARCHAR(255),

root_id INT, lft INT, rgt INT, level SMALLINT, PRIMARY KEY(id)) ENGINE = INNODB

NOTE root_idlft``rgt``と``level``カラムが自動的に追加されることに注目してください。これらのカラムはツリー構造を編成して内部の自動処理に使われます。

ここでは``NestedSet``ビヘイビアの100%を検討しません。とても大きなビヘイビアなので[doc hierarchical-data 専用の章]があります。

Searchable

``Searchable``ビヘイビアは全文インデックス作成と検索機能を提供します。データベースとファイルの両方のインデックスと検索に使われます。

求人投稿用の``Job``モデルがあり簡単に検索できるようにすることを考えてみましょう:

// models/Job.php

class Job extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255); $this->hasColumn(‘description’, ‘clob’); }

public function setUp()
{
    $this->actAs('Searchable', array(
            'fields' => array('title', 'content')
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

Job: actAs: Searchable: fields: [title, description] columns: title:

string(255) description: clob

上記のモデルで生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘Job’)); echo $sql[0] . “”; echo $sql[1] . “”; echo $sql[2];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE job_index (id BIGINT, keyword VARCHAR(200), field

VARCHAR(50), position BIGINT, PRIMARY KEY(id, keyword, field, position)) ENGINE = INNODB CREATE TABLE job (id BIGINT AUTO_INCREMENT, title VARCHAR(255), description LONGTEXT, PRIMARY KEY(id)) ENGINE = INNODB ALTER TABLE job_index ADD FOREIGN KEY (id) REFERENCES job(id) ON UPDATE CASCADE ON DELETE CASCADE

NOTE ``job_index``テーブルおよび``job``と``job_index``の間の外部キーが自動的に生成されることに注目してください。

``Searchable``ビヘイビアは非常に大きなトピックなので、詳細は[doc searching :name]の章で見つかります。

Geographical

下記のコードはデモのみです。Geographicalビヘイビアは2つのレコードの間のマイルもしくはキロメータの数値を決定するためのレコードデータで使うことができます。

// models/Zipcode.php

class Zipcode extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘zipcode’, ‘string’, 255); $this->hasColumn(‘city’, ‘string’, 255); $this->hasColumn(‘state’, ‘string’, 2); $this->hasColumn(‘county’, ‘string’, 255); $this->hasColumn(‘zip_class’, ‘string’, 255); }

public function setUp()
{
    $this->actAs('Geographical');
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Zipcode: actAs: [Geographical] columns: zipcode: string(255) city: string(255) state: string(2) county: string(255) zip_class: string(255)

上記のモデルで生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘Zipcode’)); echo $sql[0];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE zipcode (id BIGINT AUTO_INCREMENT, zipcode VARCHAR(255),

city VARCHAR(255), state VARCHAR(2), county VARCHAR(255), zip_class VARCHAR(255), latitude DOUBLE, longitude DOUBLE, PRIMARY KEY(id)) ENGINE = INNODB

NOTE Geographicalビヘイビアが2つのレコードの距離を算出するために使われるレコードに``latitude``と``longitude``カラムを自動的に追加することに注目してください。使い方の例は下記の通りです。

最初に2つの異なるzipcodeレコードを読み取りましょう:

// test.php

// ... $zipcode1 = Doctrine_Core::getTable(‘Zipcode’)->findOneByZipcode(‘37209’); $zipcode2 = Doctrine_Core::getTable(‘Zipcode’)->findOneByZipcode(‘37388’);

ビヘイビアが提供する``getDistance()``メソッドを使用してこれら2つのレコードの間の距離を取得できます:

// test.php

// ... echo zipcode1->getDistance(zipcode2, $kilometers = false);

NOTE ``getDistance()``メソッドの2番目の引数はキロメーターで距離を返すかどうかです。デフォルトはfalseです。

同じ市にはない50の近いzipcodeを取得してみましょう:

// test.php

// ... $q = $zipcode1->getDistanceQuery();

q->orderby('miles asc') ->addWhere(q->getRootAlias() . ‘.city != ?’, $zipcode1->city) ->limit(50);

echo $q->getSqlQuery();

``getSql()``への上記の呼び出しは次のSQLクエリを出力します:

SELECT 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

NOTE 上記のSQLクエリが書かなかったSQLの束を含んでいることに注目してください。これはレコードの間のマイル数を計算するためにビヘイビアによって自動的に追加されます。

クエリを実行して算出されたマイル数の値を使用します:

// test.php

// ... $result = $q->execute();

foreach ($result as $zipcode) { echo $zipcode->city . ” - ” . $zipcode->miles . “”; // You could also access $zipcode->kilometers }

これをテストするためにサンプルのzipcodeを取得します

http://www.populardata.com/zip_codes.zip

csvファイルをダウンロードして次の関数でインポートしてください:

// test.php

// ... function parseCsvFile($file, $columnheadings = false, $delimiter = ‘,’, $enclosure = “””) { $row = 1; $rows = array(); handle = fopen(file, ‘r’);

while (($data = fgetcsv($handle, 1000, $delimiter, $enclosure)) !== FALSE) {

    if (!($columnheadings == false) && ($row == 1)) {
        $headingTexts = $data;
    } elseif (!($columnheadings == false)) {
        foreach ($data as $key => $value) {
            unset($data[$key]);
            $data[$headingTexts[$key]] = $value;
        }
        $rows[] = $data;
    } else {
        $rows[] = $data;
    }
    $row++;
}

fclose($handle);
return $rows;

}

$array = parseCsvFile(‘zipcodes.csv’, false);

foreach ($array as $key => $value) { $zipcode = new Zipcode(); zipcode->fromArray(value); $zipcode->save(); }

SoftDelete

``SoftDelete``ビヘイビアは``delete()``機能をオーバーライドし``deleted``カラムを追加するとてもシンプルだが大いにおすすめできるモデルビヘイビアです。``delete()``が呼び出されるとき、データベースからレコードを削除する代わりに、削除フラグを1にセットします。下記のコードは``SoftDelete``ビヘイビアでモデルを作る方法です。

// models/SoftDeleteTest.php

class SoftDeleteTest extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, null, array( ‘primary’ => true ) ); }

public function setUp()
{
    $this->actAs('SoftDelete');
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

SoftDeleteTest: actAs: [SoftDelete] columns: name: type: string(255) primary: true

上記のモデルによって生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘SoftDeleteTest’)); echo $sql[0];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE soft_delete_test (name VARCHAR(255), deleted TINYINT(1)

DEFAULT ‘0’ NOT NULL, PRIMARY KEY(name)) ENGINE = INNODB

ビヘイビアを動かしてみましょう。

NOTE すべての実行されるクエリのためにDQLコールバックを有功にする必要があります。``SoftDelete``ビヘイビアにおいて追加のWHERE条件で``deleted_at``フラグが設定されているすべてのレコードを除外するSELECT文をフィルタリングするために使われます。

DQLコールバックを有効にする

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);

``SoftDelete``の機能を実行できるように新しいレコードを保存します:

// test.php

// ... $record = new SoftDeleteTest(); $record->name = ‘new record’; $record->save();

``delete()``を呼び出すとき``deleted``フラグが``true``にセットされます:

// test.php

// ... $record->delete();

print_r($record->toArray());

上記の例は次の内容を出力します:

$ php test.php Array ( [name] => new record [deleted] => 1 )

また、クエリを行うとき、``deleted``がnullではないレコードは結果から除外されます:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘SoftDeleteTest t’);

echo $q->getSqlQuery();

``getSql()``の呼び出しは次のSQLクエリを出力します:

SELECT s.name AS s**name, s.deleted AS s**deleted FROM

soft_delete_test s WHERE (s.deleted = ? OR s.deleted IS NULL)

NOTE 削除されていないレコードだけを返すためにwhere条件が自動的に追加されたことに注目してください。

クエリを実行する場合:

// test.php

// ... $count = $q->count(); echo $count;

上記のコード0をechoします。deleteフラグが設定されたので保存されたレコードが除外されます。

入れ子のビヘイビア

versionable、searchable、sluggable、と完全なI18nである完全なwikiデータベースを与える複数のビヘイビアの例です。

class Wiki extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255); $this->hasColumn(‘content’, ‘string’); }

public function setUp()
{
    $options = array('fields' => array('title', 'content'));
    $auditLog = new Doctrine_Template_Versionable($options);
    $search = new Doctrine_Template_Searchable($options);
    $slug = new Doctrine_Template_Sluggable(array(
            'fields' => array('title')
        )
    );
    $i18n = new Doctrine_Template_I18n($options);

    $i18n->addChild($auditLog)
         ->addChild($search)
         ->addChild($slug);

    $this->actAs($i18n);

    $this->actAs('Timestampable');
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

WikiTest: actAs: I18n: fields: [title, content] actAs: Versionable:

fields: [title, content] Searchable: fields: [title, content] Sluggable: fields: [title] columns: title: string(255) content: string

Note

現在上記の入れ子のビヘイビアは壊れています。開発者は後方互換性を修正するために懸命に取り組んでいます。修正ができたときにアナウンスを行いドキュメントを更新します。

ファイルを生成する

デフォルトではビヘイビアによって生成されるクラスは実行時に評価されクラスを格納するファイルはディスクに書き込まれません。これは設定オプションで変更できます。下記のコードは実行時にクラスを評価する代わりにクラスを生成してファイルに書き込むためのI18nビヘイビアを設定する方法の例です。

class NewsArticle extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255); $this->hasColumn(‘body’, ‘string’, 255); $this->hasColumn(‘author’, ‘string’, 255); }

public function setUp()
{
    $this->actAs('I18n', array(
            'fields'          => array('title', 'body'),
            'generateFiles'   => true,
            'generatePath'    => '/path/to/generate'
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

NewsArticle: actAs: I18n: fields: [title, body] generateFiles: true

generatePath: /path/to/generate columns: title: string(255) body: string(255) author: string(255)

コードを生成して実行時に評価するために``[http://www.php.net/eval eval()]``を使用する代わりにこれでビヘイビアはファイルを生成します。

生成クラスをクエリする

自動生成モデルをクエリしたい場合添付されたモデルを持つモデルがロードされ初期化されることを確認する必要があります。``Doctrine_Core::initializeModels()``スタティックメソッドを使用することでこれをできます。例えば``BlogPost``モデル用の翻訳テーブルにクエリをしたい場合、次のコードを実行する必要があります:

Doctrine_Core::initializeModels(array(‘BlogPost’));

$q = Doctrine_Query::create() ->from(‘BlogPostTranslation t’) ->where(‘t.id = ? AND t.lang = ?’, array(1, ‘en’));

$translations = $q->execute();

Note

モデルが最初にインスタンス化されるまでビヘイビアはインスタンス化されないのでこれは必須です。上記の``initializeModels()``メソッドは渡されたモデルをインスタンス化して情報がロードされたモデルの配列に適切にロードされることを確認します。

=== まとめ ===

Doctrineビヘイビアについて多くのことを学びます。Doctrineに搭載されている素晴らしいビヘイビアの使い方と同じようにモデル用の独自ビヘイビアの書き方を学びます。

[doc searching Searchable]ビヘイビアを詳しく検討するために移動する準備ができています。これは大きなトピックなので専門の章が用意されています。

はじめに

検索は巨大なトピックなので、この章全体では``Searchable``と呼ばれるビヘイビアを専門に扱います。これは全文インデックス作成と検索ツールでデータベースとファイルの両方で使うことができます。

次の定義を持つ``NewsItem``クラスを考えてみましょう:

// models/NewsItem.php

class NewsItem extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255); $this->hasColumn(‘body’, ‘clob’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

NewsItem: columns: title: string(255) body: clob

ユーザーが異なるニュースの項目を検索できるアプリケーションを考えてみましょう。これを実装する明らかな方法はフォームを構築し投稿された値に基づいて次のようなDQLクエリを構築することです:

// test.php

// ... $q = Doctrine_Query::create() ->from(‘NewsItem i’) ->where(‘n.title LIKE ? OR n.content LIKE ?’);

アプリケーションが成長するにつれてこの種のクエリはとても遅くなります。例えば``%framework%``パラメータで以前のクエリを使うとき、(``framework``という単語を含むタイトルもしくは内容を持つすべてのニュースの項目を見つけることと同等です)データベースはテーブルのそれぞれの列をトラバースしなければなりません。当然ながらこれは非常に遅くなります。

Doctrineはこの問題を検索コンポーネントとインバースインデックスで解決します。最初に定義を少し変えてみましょう:

// models/NewsItem.php

class NewsItem extends Doctrine_Record { // ...

public function setUp()
{
    $this->actAs('Searchable', array(
            'fields' => array('title', 'content')
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

NewsItem: actAs: Searchable: fields: [title, content] # ...

上記のモデルで生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘NewsItem’)); echo $sql[0] . “”; echo $sql[1] . “”; echo $sql[2];

上記のコードは次のSQLクエリを出力します:

CREATE TABLE news_item_index (id BIGINT, keyword VARCHAR(200), field

VARCHAR(50), position BIGINT, PRIMARY KEY(id, keyword, field, position)) ENGINE = INNODB CREATE TABLE news_item (id BIGINT AUTO_INCREMENT, 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``クラスがsearchable(内部ではDoctrineが``Doctrine_Template_Searchable``をロードする)として振る舞い``title``と``content``フィールドは全文検索用のインデックス付きフィールドとしてマークされます。これは``NewsItem``が追加もしくは更新されるたびにDoctrineは次のことを行うことを意味します:

  1. インバース検索インデックスを更新するもしくは
  2. インバース検索インデックスに新しいエントリを追加する(バッチでインバース検索インデックスを更新するのが効率的であることがあります)

インデックス構造

Doctrineが使用するインバースインデックスの構造は次の通りです:

[ (string) keyword] [ (string) field ] [ (integer) position ] [ (mixed) [foreign_keys] ]

||~ カラム ||~ 説明 || || keyword || 検索できるテキストのキーワード || || field || キーワードが見つかるフィールド || || position || キーワードが見つかる位置 || || [foreign_keys] || インデックスが作成されるレコードの外部キー ||

``NewsItem``の例において``[foreign_keys]``は``NewsItem(id)``への外部キー参照と``onDelete => CASCADE``制約を持つ1つの``id``フィールドを格納します。

このテーブルの列のようになりますの例は次のようになります:

||~ キーワード ||~ フィールド ||~ 位置 ||~ id || || database || title || 3 || 1||

この例において単語の``database``は``1``の``id``を持つ``NewsItem``の``title``フィールドの3番目の単語です。

インデックスのビルド

検索可能なレコードがデータベースにinsertされるときDoctrineはインデックスビルドのプロシージャを実行します。プロシージャが検索リスナーによって起動されているときこれはバックグラウンドで行われます。このプロシージャのフェーズは次の通りです:

  1. ``Doctrine_Search_Analyzer``基底クラスを使用してテキストを分析する
  2. 分析されたすべてのキーワード用に新しい列をインデックステーブルに挿入する

新しい検索可能なエントリが追加されるときインデックステーブルを更新したくなく、むしろ特定の間隔でインデックステーブルをバッチ更新したい場合があります。直接の更新機能を無効にするにはビヘイビアを添付する際にbatchUpdatesオプションをtrueに設定する必要があります:

// models/NewsItem.php

class NewsItem extends Doctrine_Record { // ...

public function setUp()
{
    $this->actAs('Searchable', array(
            'fields' => array('title', 'content')
            'batchUpdates' => true
        )
    );
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

NewsItem: actAs: Searchable: fields: [title, content] batchUpdates: true # ...

更新プロシージャの実際のバッチは``batchUpdateIndex()``メソッドによって起動します。これは2つのオプション引数: ``limit``と``offset``を受けとります。バッチでインデックス化されるエントリ数を制限するためにlimitが使用できoffsetはインデックス作成を始める最初のエントリを設定するために使用できます。

最初に新しい``NewsItem``レコードを挿入してみましょう:

// test.php

// ... $newsItem = new NewsItem(); $newsItem->title = ‘Test’; $newsItem->body = ‘test’; $newsItem->save();

NOTE バッチ更新を有効にしない場合``NewsItem``レコードを挿入もしくは更新するときにインデックスは自動的に更新されます。バッチ更新を有功にする場合次のコードでバッチ更新を実行できます:

// test.php

// ... $newsItemTable = Doctrine_Core::getTable(‘NewsItem’); $newsItemTable->batchUpdateIndex();

テキストアナライザー

デフォルトではDoctrineはテキスト分析のために``Doctrine_Search_Analyzer_Standard``を使用します。このクラスは次のことを実行します:

  • ‘and’、’if’などのストップワードをはぎとる。よく使われ検索には関係ないのと、インデックスのサイズを適切なものにするため。
  • すべてのキーワードを小文字にする。標準アナライザーはすべてのキーワードを小文字にするので単語を検索するとき’database’と’DataBase’は等しいものとしてみなされる。
  • アルファベットと数字ではないすべての文字はホワイトスペースに置き換える。通常のテキストでは例えば’database.’などアルファベットと数字ではない文字がキーワードに含まれるからである。標準のアナライザーはこれらをはぎとるので’database’は’database.’にマッチします
  • すべてのクォテーション記号を空の文字列に置き換えるので”O’Connor”は”oconnor”にマッチします

``Doctrine_Search_Analyzer_Interface``を実装することで独自のアナライザークラスを書くことができます。``MyAnalyzer``という名前のアナライザーを作成する例は次の通りです:

// models/MyAnalyzer.php

class MyAnalyzer implements Doctrine_Search_Analyzer_Interface { public function analyze($text) { text = trim(text); return $text; } }

NOTE 検索アナライザーは``analyze()``という名前の1つのメソッドを持たなければなりません。このメソッドはインデックス化される入力テキストの修正版を返します。

このアナライザーは検索オブジェクトに次のように適用されます:

// test.php

// ... $newsItemTable = Doctrine_Core::getTable(‘NewsItem’); $search = $newsItemTable ->getTemplate(‘Doctrine_Template_Searchable’) ->getPlugin();

$search->setOption(‘analyzer’, new MyAnalyzer());

クエリ言語

``Doctrine_Search``はApache Luceneに似たクエリ言語を提供します。``Doctrine_Search_Query``は人間が読解でき、構築が簡単なクエリ言語を同等の複雑なDQLに変換します。そしてこのDQLは通常のSQLに変換されます。

検索を実行する

次のコードはレコードのidと関連データを読み取るシンプルな例です。

// test.php

// ... $newsItemTable = Doctrine_Core::getTable(‘NewsItem’);

$results = newsItemTable->search('test'); print_r(results);

上記のコードは次のクエリを実行します:

SELECT 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

コードの出力は次の通りです:

$ php test.php Array ( [0] => Array ( [relevance] => 1 [id] => 1 )

)

実際の``NewsItem``オブジェクトを読み取るために別のクエリでこれらの結果を使うことができます:

// test.php

// ... 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());

上記の例は次の出力を生み出します:

$ php test.php Array ( [0] => Array ( [id] => 1 [title] => Test [body]

=> test )

)

オプションとして検索インデックスを使用して結果を制限するwhere条件サブクエリで修正するために``search()``メソッドにクエリオブジェクトを渡すことができます。

// test.php

// ... $q = Doctrine_Query::create() ->from(‘NewsItem i’);

$q = Doctrine_Core::getTable(‘Article’) ->search(‘test’, $q);

echo $q->getSqlQuery();

上記の``getSql()``の呼び出しは次のSQLクエリを出力します:

SELECT 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)

クエリを実行して``NewsItem``オブジェクトを取得できます:

// test.php

// ... $newsItems = $q->execute();

print_r($newsItems->toArray());

上記の例は次の出力を生み出します:

$ php test.php Array ( [0] => Array ( [id] => 1 [title] => Test [body]

=> test )

)

ファイル検索

前に述べたように``Doctrine_Search``はファイル検索にも使うことができます。検索可能なディレクトリを用意したい場合を考えてみましょう。最初に``Doctrine_Search_File``のインスタンスを作る必要があります。これは``Doctrine_Search``の子クラスでファイル検索に必要な機能を提供します。

// test.php

// ... $search = new Doctrine_Search_File();

2番目に行うことはインデックステーブルを生成することです。デフォルトではDoctrineはデータベースのインデックスクラスを``FileIndex`` と名づけます。

上記のモデルによって生成されたSQLをチェックしてみましょう:

// test.php

// ... $sql = Doctrine_Core::generateSqlFromArray(array(‘FileIndex’));

上記のコードは次のSQLクエリを出力します:

CREATE TABLE file_index (url VARCHAR(255), keyword VARCHAR(200), field

VARCHAR(50), position BIGINT, PRIMARY KEY(url, keyword, field, position)) ENGINE = INNODB

``Doctrine_Core::createTablesFromArray()``メソッドを使用することでデータベースで実際のテーブルを作ることができます:

// test.php

// ... Doctrine_Core::createTablesFromArray(array(‘FileIndex’));

ファイルサーチャーを使い始めることができます。この例では``models``ディレクトリのインデックスを作りましょう:

// test.php

// ... $search->indexDirectory(‘models’);

``indexDirectory()``はディレクトリを再帰的にイテレートしインデックステーブルを更新しながらその範囲のすべてのファイルを分析します。

最後にインデックス化されたファイルの範囲内でテキストのピースの検索を始めることができます:

// test.php

// ... $results = search->search('hasColumn'); print_r(results);

上記の例は次の出力を生み出します:

$ php test.php Array ( [0] => Array ( [relevance] => 2 [url] =>

models/generated/BaseNewsItem.php )

)

まとめ

``Searchable``ビヘイビアのすべてを学んだので[doc hierarchical-data :name]の章で``NestedSet``ビヘイビアの詳細を学ぶ準備ができています。``NestedSet``は``Searchable``ビヘイビアのように大きなトピックなので1つの章全体で扱います。

はじめに

大抵のユーザーは一度はSQLデータベースで階層データを扱います。階層データの管理はリレーショナルデータベースが意図していることはではないことは疑いの余地はありません。リレーショナルデータベースのテーブルは(XMLのように)階層的ではなく、シンプルでフラットなリストです。階層データは親子のリレーションを持ちリレーショナルデータベーステーブルで自然に表現されません。

我々の目的のために、階層データはデータのコレクションとしてそれぞれのアイテムは単独の親とゼロもしくはそれ以上の子を持ちます(例外はrootアイテムで、これは親を持ちません)。階層データはフォーラムとメーリングリストのスレッド、ビジネス組織のチャート、コンテンツマネジメントのカテゴリ、製品カテゴリを含む様々なデータベースアプリケーションで見つかります。 階層データモデルにおいて、データは木のような構造に編成されます。木構造は親/子のリレーションを使用する情報の反復を可能にします。木構造のデータの説明に関しては、[http://en.wikipedia.org/wiki/Tree_data_structure ここ]を参照してください。

リレーショナルデータベースでツリー構造を管理する方法は主に3つあります:

  • 隣接リストモデル
  • 入れ子集合モデル(もしくは修正版先行順木走査アルゴリズムとも知られる)
  • 経路実体化モデル

次のリンク先で詳細な説明があります:

入れ子集合

はじめに

入れ子集合はとても早い読み込みアクセス方法を提供する階層データを保存するための解決方法です。しかしながら、入れ子集合の更新はコストがかかります。それゆえこの解決方法は書き込みよりも読み込みがはるかに多い階層に最適です。ウェブの性質から、この方法は大抵のウェブアプリケーションに当てはまります。

入れ子集合の詳細に関しては、次の記事をご覧ください:

セットアップする

モデルを入れ子集合としてセットアップするには、モデルの``setUp()``メソッドにコードを追加しなければなりません。例として下記の``Category``モデルを考えてみましょう:

// models/Category.php

class Category extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); }

public function setUp()
{
    $this->actAs('NestedSet');
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Category: actAs: [NestedSet] columns: name: string(255)

Doctrineのテンプレートモデルの詳細情報は[doc behaviors :index :name]の章で見つかります。これらのテンプレートはモデルに機能を追加します。入れ子集合の例において、3の追加フィールド: lft``rgt``と``level``が得られます。``lft``と``rgt``フィールドを気にする必要はありません。これらは内部で木構造を管理するために使われます。しかしながら``level``フィールドは関係があります。整数の値が木の範囲内のノードの深さを表すからです。レベル0はrootノードを意味します。レベル1はrootノードの直接の子であることを意味します。ノードから``level``フィールドを読み込むことで適切なインデントで木を簡単に表示できます。

CAUTION lftrgt``level``に値を割り当ててはなりません。これらは入れ子集合で透過的に管理されているからです。
マルチプルツリー

入れ子集合の実装によってテーブルが複数のrootノードを持つ、すなわち同じテーブルで複数の木を持つことが可能になります。

下記の例は``Category``モデルで複数のrootをセットアップして使う方法を示しています:

// models/Category.php

class Category extends Doctrine_Record { // ...

public function setUp()
{
    $options = array(
        'hasManyRoots'     => true,
        'rootColumnName'   => 'root_id'
    );
    $this->actAs('NestedSet', $options);
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# schema.yml

Category: actAs: NestedSet: hasManyRoots: true rootColumnName: root_id columns: name: string(255)

``rootColumnName``は木を区別するために使われるカラムです。新しいrootノードを作成するとき``root_id``を手動で設定する選択肢があります。さもなければDoctrineが値を割り当てます。

一般的に``root_id``を直接扱う必要はありません。例えば、新しいノードを既存の木に差し込むもしくはツリーの間でノードを移動させるときDoctrineは関連する``root_id``の変更を透過的に処理します。

Working with Trees

モデルを入れ子集合としてセットアップが成功したら作業を始めることができます。Doctrineの入れ子集合を実装する作業は2つのクラス: ``Doctrine_Tree_NestedSet``と``Doctrine_Node_NestedSet``で行われます。これらのクラスは``Doctrine_Tree_Interface``と``Doctrine_Node_Interface``インターフェイスの実装です。ツリーオブジェクトはテーブルオブジェクトにバインドされノードオブジェクトはレコードオブジェクトにバインドされます。これらの内容は次の通りです:

次のコードを使うことですべてのツリーインターフェイスが利用できます:

// test.php

// ... $treeObject = Doctrine_Core::getTable(‘Category’)->getTree();

次の例では``$category``は``Category``のインスタンスです:

// test.php

// ... $nodeObject = $category->getNode();

上記のコードによって全ノードインターフェイスは``$nodeObject``で利用できます。

次のセクションでノードとツリークラスでもっともよく使われるオペレーションを実演するコードスニペットを見ます。

rootノードを作成する
// test.php

// ... $category = new Category(); $category->name = ‘Root Category 1’; $category->save();

$treeObject = Doctrine_Core::getTable(‘Category’)->getTree(); treeObject->createRoot(category);

ノードを挿入する

次の例では新しい``Category``インスタンスを``Category``のrootの子として追加しています:

// test.php

// ... $child1 = new Category(); $child1->name = ‘Child Category 1’;

$child2 = new Category(); $child2->name = ‘Child Category 1’;

child1->getNode()->insertAsLastChildOf(category); child2->getNode()->insertAsLastChildOf(category);

ノードを削除する

ツリーからノードを削除するのは簡単でノードオブジェクトで``delete()``メソッドを呼び出します:

// test.php

// ... $category = Doctrine_Core::getTable(‘Category’)->findOneByName(‘Child Category 1’); $category->getNode()->delete();

CAUTION 上記のコードは``$category->delete()``を内部で呼び出しています。レコードではなくノードの上で削除を行うことが重要です。さもなければツリーが壊れることがあります。

ノードを削除するとそのノードのすべての子孫も削除されます。ですのでこれらの子孫を削除したくなければノードを削除するまえにどこか別の場所に移動させてください。

ノードを移動させる

ノードの移動は簡単です。Doctrineはツリーの間でノードを移動させるためのいくつかのメソッドを提供します:

// test.php

// ... $category = new Category(); $category->name = ‘Root Category 2’; $category->save();

$categoryTable = Doctrine_Core::getTable(‘Category’);

$treeObject = $categoryTable->getTree(); treeObject->createRoot(category);

$childCategory = $categoryTable->findOneByName(‘Child Category 1’); childCategory->getNode()->moveAsLastChildOf(category); ...

ノードを移動させるために利用可能なメソッドのリストは次の通りです:

  • moveAsLastChildOf($other)
  • moveAsFirstChildOf($other)
  • moveAsPrevSiblingOf($other)
  • moveAsNextSiblingOf($other).

メソッドの名前はその名の通りでなけれればなりません。

ノードを検査する

次のメソッドを使うことでノードとその型を検査することができます:

// test.php

// ... $isLeaf = $category->getNode()->isLeaf(); $isRoot = $category->getNode()->isRoot();

NOTE 上記のメソッドは葉ノードであるかrootノードであるかによってtrue/falseを返します。
兄弟の検査と読み込み

次のメソッドを使うことでノードが次もしくは前の兄弟を持つのか簡単にチェックできます:

// test.php

// ... $hasNextSib = $category->getNode()->hasNextSibling(); $hasPrevSib = $category->getNode()->hasPrevSibling();

次のメソッドで存在する次もしくは前の兄弟を読み取ることができます:

// test.php

// ... $nextSib = $category->getNode()->getNextSibling(); $prevSib = $category->getNode()->getPrevSibling();

NOTE 上記のメソッドは次もしくは前の兄弟が存在しない場合falseを返します。

すべての兄弟の配列を読み取るには``getSiblings()``メソッドを使います:

// test.php

// ... $siblings = $category->getNode()->getSiblings();

子孫の検査と読み取り

次のメソッドを使用することでノードが親もしくは子を持つことをチェックできます:

// test.php

// ... $hasChildren = $category->getNode()->hasChildren(); $hasParent = $category->getNode()->hasParent();

次のメソッドで最初と最後の子ノードを読み取ることができます:

// test.php

// ... $firstChild = $category->getNode()->getFirstChild(); $lastChild = $category->getNode()->getLastChild();

もしくはノードの親を読み取りたい場合:

// test.php

// ... $parent = $category->getNode()->getParent();

次のメソッドを使用してノードの子を取得できます:

// test.php

// ... $children = $category->getNode()->getChildren();

CAUTION ``getChildren()``メソッドは直接の子孫のみを返します。すべての子孫を取得したい場合、``getDescendants()``メソッドを使います。

次のメソッドでノードの祖先もしくは子孫を取得できます:

// test.php

// ... $descendants = $category->getNode()->getDescendants(); $ancestors = $category->getNode()->getAncestors();

ときに子もしくは子孫の数だけ取得したいことがあります。これは次のメソッドで実現できます:

// test.php

// ... $numChildren = $category->getNode()->getNumberChildren(); $numDescendants = $category->getNode()->getNumberDescendants();

``getDescendants()``と``getAncestors()``は結果ブランチの``depth``を指定するために使用できるパラメータを受けとります。例えば``getDescendants(1)``は直接の子孫のみを読み取ります(1レベル下の子孫で、これは``getChildren()``と同じです)。同じ流儀で ``getAncestors(1)``は直接の祖先(親など)のみを読み取ります。rootノードもしくは特定の祖先までのこのノードのパスを効率的に決定するために``getAncestors()``はとても便利です(すなわちパンくずナビゲーションを構築するため).

単純木をレンダリングする
NOTE 次の例では``hasManyRoots``をfalseに設定することを前提とします。下記の例を適切に動作させるためにこのオプションをfalsenに設定しなければなりません。前のセクションでは値をtrueに設定しました。

// test.php

// ... $treeObject = Doctrine_Core::getTable(‘Category’)->getTree(); $tree = $treeObject->fetchTree();

foreach ($tree as $node) { echo str_repeat(‘  ’, $node[‘level’]) . $node[‘name’] . “”; }

高度な使い方

以前のセクションでは入れ子集合の基本的な使い方を説明しました。このセクションは高度な内容に進みます。

リレーションでツリーを取得する

ソフトウェア開発者に要求している場合すでにこの質問が念頭にあるかもしれません: “関連データを持つツリー/ブランチを取得するには?”. Simple example: カテゴリのツリーを表示したいが、それぞれのカテゴリの関連データの一部も表示したい場合、そのカテゴリのもっとも詳細な製品の商品を考えてみましょう。以前のセクションのようにツリーを取得しツリーをイテレートする合間にリレーションにアクセスするのは可能ですが、必要のないデータベースクエリをたくさん生み出します。幸いにして、``Doctrine_Query``と入れ子集合の実装の柔軟性が手助けしてくれます。入れ子集合の実装は``Doctrine_Query``オブジェクトを使用します。入れ子集合実装の基本クエリオブジェクトにアクセスすることで入れ子集合を使いながら``Doctrine_Query``のフルパワーを解き放つことができます。

最初にツリーデータを読み取るために使うクエリを作りましょう:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘c.name, p.name, m.name’) ->from(‘Category c’) ->leftJoin(‘c.HottestProduct p’) ->leftJoin(‘p.Manufacturer m’);

ツリー用の基本クエリとして上記のクエリを設定する必要があります:

$treeObject = Doctrine_Core::getTable(‘Category’)->getTree();

treeObject->setBaseQuery(q); $tree = $treeObject->fetchTree();

必要なすべてのデータを持つツリーは1つのクエリで取得できます。

NOTE 独自の基本クエリを設定しない場合内部で自動的に作成されます。

終えたら基本クエリを通常のものに戻すのは良い考えです:

// test.php

// ... $treeObject->resetBaseQuery();

さらに踏み込むことができます。[doc improving-performance :name]の章で述べたように必要なときのみにオブジェクトを取得すべきです。ですので表示(読み込みのみ)目的のみにツリーを表示する場合少し加速するために配列のハイドレーションを使うことができます:

// test.php

// ... $q = Doctrine_Query::create() ->select(‘c.name, p.name, m.name’) ->from(‘Category c’) ->leftJoin(‘c.HottestProduct p’) ->leftJoin(‘p.Manufacturer m’) ->setHydrationMode(Doctrine_Core::HYDRATE_ARRAY);

$treeObject = Doctrine_Core::getTable(‘Category’)->getTree(); treeObject->setBaseQuery(q); $tree = $treeObject->fetchTree(); treeObject->resetBaseQuery(); </code> ``tree``で素晴らしく構造化された配列が手に入ります。ともかくレコードにアクセスする配列を使う場合、このような変更はコードの他の部分に影響を与えません。クエリを修正するこのメソッドはすべてのノードとツリーメソッド(getAncestors(), getDescendants()getChildren()getParent())に対して使うことができます。クエリを作り、ツリーオブジェクトの基本クエリとして設定し適切なメソッドとして起動させます。

インデントでレンダリングする

下記の例ではすべてのツリーが適切なインデントでレンダリングされます。``fetchRoots()``メソッドを使用してrootを読み取り``fetchTree()``メソッドを使用して個別のツリーを読み取ることができます。

// test.php

// ... $treeObject = Doctrine_Core::getTable(‘Category’)->getTree(); $rootColumnName = $treeObject->getAttribute(‘rootColumnName’);

foreach ($treeObject->fetchRoots() as $root) { $options = array( ‘root_id’ => root->rootColumnName ); foreach(treeObject->fetchTree(options) as $node) { echo str_repeat(‘ ‘, $node[‘level’]) . $node[‘name’] . “”; } }

すべての作業を終えた後で上記のコードは次のようにレンダリングされます:

$ php test.php Root Category 1 Root Category 2 Child Category 1

=== まとめ ===

``NestedSet``ビヘイビアに関するすべての内容と階層データを管理する方法を学んだので[doc data-fixtures :name]を学ぶ準備ができています。データフィクスチャはアプリケーションの小さなテストデータをロードするための偉大なツールでユニットテストと機能テストを行うもしくは初期データをアプリケーションに投入するために使われます。

データフィクスチャはテストするデータをデータベースに投入するためにモデルを通してテストデータの小さなセットをロードするための手段です。データフィクスチャはユニット/機能テストスィートでよく使われます。

インポートする

データフィクスチャのインポートはダンプと同じ簡単です。``loadData()``メソッドを使うことができます:

Doctrine_Core::loadData(‘/path/to/data.yml’);

上記のように個別のYAMLファイルを指定するか、もしくはディレクトリ全体を指定できます:

Doctrine_Core::loadData(‘/path/to/directory’);

インポートしたデータを既存のデータに追加したい場合``loadData()``メソッドの2番目の引数を使う必要があります。2番目の引数をtrueとして指定しない場合データはインポートされる前にパージされます。

パージする代わりに追加する方法は次の通りです:

Doctrine_Core::loadData(‘/path/to/data.yml’, true);

ダンプする

データフィクスチャを書き始めるのを手助けするために多くの異なるフォーマットでフィクスチャファイルにデータをダンプできます。次のようんデータフィクスチャを1つの大きなYAMLファイルにダンプできます:

Doctrine_Core::dumpData(‘/path/to/data.yml’);

もしくはオプションとしてすべてのデータを個別のファイルにダンプできます。モデルごとに1つのYAMLファイルをダンプするには次のようになります:

Doctrine_Core::dumpData(‘/path/to/directory’, true);

実装

データフィクスチャを知ったので次のセクションで使われるフィクスチャの例をテストできるように、以前の章で利用してきたテスト環境で実装してみましょう。

最初に``doctrine_test``ディレクトリの中で``fixtures``という名前のディレクトリを作成し内部で``data.yml``という名前のファイルを作成します:

$ mkdir fixtures $ touch fixtures/data.yml

データフィクスチャをロードするコードを含めるために``generate.php``スクリプトを修正する必要があります。``generate.php``の一番下に次のコードを追加します:

// generate.php

// ... Doctrine_Core::loadData(‘fixtures’);

書く

手作業でフィクスチャファイルを書いてアプリケーションでロードできます。下記のコードはサンプルの``data.yml``フィクスチャファイルです。データフィクスチャを複数のファイルに分割することもできます。Doctrineはすべてのフィクスチャファイルを読み込み解析し、すべてのデータをロードします。

次のいくつかの例に対して次のモデルを使用します:

// models/Resouce.php

class Resource extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); $this->hasColumn(‘resource_type_id’, ‘integer’); }

public function setUp()
{
    $this->hasOne('ResourceType as Type', array(
            'local' => 'resource_type_id',
            'foreign' => 'id'
        )
    );

    $this->hasMany('Tag as Tags', array(
            'local' => 'resource_id',
            'foreign' => 'tag_id',
            'refClass' => 'ResourceTag'
        )
    );
}

}

// models/ResourceType.php

class ResourceType extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); }

public function setUp()
{
    $this->hasMany('Resource as Resouces', array(
            'local' => 'id',
            'foreign' => 'resource_type_id'
        )
    );
}

}

// models/Tag.php

class Tag extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); }

public function setUp()
{
    $this->hasMany('Resource as Resources', array(
            'local' => 'tag_id',
            'foreign' => 'resource_id',
            'refClass' => 'ResourceTag'
        )
    );
}

}

// models/ResourceTag.php

class ResourceTag extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘resource_id’, ‘integer’); $this->hasColumn(‘tag_id’, ‘integer’); } }

// models/Category.php

class BaseCategory extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255, array( ‘type’ => ‘string’, ‘length’ => ‘255’ ) ); }

public function setUp()
{
    $this->actAs('NestedSet');
}

}

class BaseArticle extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 255, array( ‘type’ => ‘string’, ‘length’ => ‘255’ ) );

    $this->hasColumn('body', 'clob', null, array(
            'type' => 'clob'
        )
    );
}

public function setUp()
{
    $this->actAs('I18n', array('fields' => array('title', 'body')));
}

}

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

# 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

NOTE すべての列のキーはすべてのYAMLデータフィクスチャにまたがってユニークでなければなりません。下記のtutorial、doctrine、help、cheatはすべてユニークです。

# 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

Resourceが持つTagsを指定する代わりにそれぞれのタグが関係するResourcesを指定できます。

# 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]

入れ子集合用のフィクスチャ

入れ子集合のツリー用のフィクスチャファイルの書き方は通常のフィクスチャファイルの書き方と少し異なります。ツリーの構造は次のように定義されます:

# 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

Tip

NestedSet用のデータフィクスチャを書くとき少なくとも最初のデータブロックの``children``要素を指定するかNestedSetのAPIを使用してデータフィクスチャをインポートするためにNestedSetであるモデルの下で``NestedSet: true``を指定しなければなりません。

# fixtures/data.yml

Category: NestedSet: true Category_1: name: Categories # ...

もしくはchildrenキーワードを指定することでNestedSetのAPIを使用してデータをインポートします。

# fixtures/data.yml

Category: Category_1: name: Categories children: [] # ...

上記の方法を使わない場合入れ子集合レコード用にlft、rgtとレベルの値を手動で指定するのはあなた次第です。

国際化用のフィクスチャ

``I18n``用のフィクスチャはカスタマイズできません。``I18n``は動的に構築されるリレーションの通常の集合にすぎないからです:

# 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

まとめ

データフィクスチャを書いてアプリケーションにロードできるようになりました。内在する[doc database-abstraction-layer :name]を学ぶために次の章に移動します。このレイヤーは以前検討したすべての機能に関係します。このレイヤーをORMから独立したものとして使うことができます。次の章ではDBALそのものの使い方を説明します。

Doctrine Database Abstraction Layerは内臓されるフレームワークで、使用しているデータベースとコミュニケーションを行いデータベースタイプに応じて適切なSQLを送信するためにORMが使用しています。このフレームワークはデータベースが持つテーブルもしくはテーブルが持つフィールドのような情報をデータベースに問い合わせる機能も持ち、ORMが既存のデータベースからモデルを簡単に生成できる手段です。

このレイヤーはORMから独立して使うことができます。例えば既存のアプリケーションがPDOを直接使用しこれをDoctrine ConnectionsとDBALを使うためにこれを移植したい場合に役立ちます。後のフェーズで新しいことのためにORMを使い始めたりORMで使えるように古いピースを書き直したりします。

DBALは少数の異なるモジュールで構成されます。この章では異なるモジュールとそれらが担っている仕事を検討します

エクスポート

Exportモジュールはデータベース構造を管理するためのメソッドを提供します。メソッドはそれぞれの責務に基づいて分類できます。例えばデータベースの要素をcreate、edit(alterもしくはupdate)、listもしくはdelete (drop)するなどです。以下のドキュメントでは利用可能なメソッドの一覧と使い方の例を示します。

はじめに

Exportモジュールでメソッドを変更するすべてのスキーマはalterオペレーションのために使われるSQLを返す同等物を持ちます。例えば``createTable()``は``createTableSql()``によって返されるクエリを実行します。

この章では``events_db``という名前のデータベースで、次のテーブルが作成され、変更され最後には削除されます:

events

||~ 名前 ||~ 型 ||~ Primary ||~ Auto Increment || || id || integer || true || true || || name || string(255) || false || false || || datetime || timestamp || false || false ||

people

||~ 名前 ||~ 型 ||~ Primary ||~ Auto Increment || || id || integer || true || true || || name || string(255) || false|| false ||

event_participants

||~ 名前 ||~ 型 ||~ Primary ||~ Auto Increment || || event_id || integer || true || false || || person_id || string(255) || true || false ||

データベースを作成する

Doctrineで新しいデータベースを作成するのはシンプルです。作成するデータベースの名前を引数にして``createDatabase()``メソッドを呼び出すだけです。

// test.php

// ... $conn->export->createDatabase(‘events_db’);

新しい``events_db``に接続するために``bootstrap.php``ファイルの接続を変更してみましょう:

// bootstrap.php

/** * Bootstrap Doctrine.php, register autoloader and specify * configuration attributes */

// ...

$conn = Doctrine_Manager::connection(‘mysql://root:@localhost/events_db’, ‘doctrine’);

// ...

テーブルを作成する

データベースは作成され接続を再設定しました。テーブルに追加に移ります。``createTable()``メソッドは3つのパラメータ: テーブルの名前、フィールド定義の配列、と追加オプション(オプションでRDBMS固有)を受けとります。

``events``テーブルを作成しましょう:

// test.php

// $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);

定義配列のキーはテーブルのフィールド名です。値は他のキーと同じように必須キーの``type``を格納する配列で、``type``の値によって、``type``キーの値はDoctrineのデータ型と同じものが可能です。データ型によって、他のオプションが変わることがあります。

||~ データ型 ||~ 長さ ||~ デフォルト ||~ not null ||~ unsigned ||~ autoincrement || || string || x || x || x || || || || boolean || || x || x || || || || integer || x || x || x || x || x || || decimal || || x || x || || || || float || || x || x || || || || timestamp || || x || x || || || || time || || x || x || || || || date || || x || x || || || || clob || x || || x || || || || blob || x || || x || || ||

``people``テーブルを作ることができます:

// test.php

// ... $definition = array( ‘id’ => array( ‘type’ => ‘integer’, ‘primary’ => true, ‘autoincrement’ => true ), ‘name’ => array( ‘type’ => ‘string’, ‘length’ => 255 ) );

$conn->export->createTable(‘people’, $definition);

``createTable()``メソッドの3番目の引数としてオプションの配列を指定することもできます:

// test.php

// ... $options = array( ‘comment’ => ‘Repository of people’, ‘character_set’ => ‘utf8’, ‘collate’ => ‘utf8_unicode_ci’, ‘type’ => ‘innodb’, );

// ...

$conn->export->createTable(‘people’, $definition, $options);

外部キーを作成する

外部キーで``event_participants``テーブルを作成します:

// test.php

// ... $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 ), );

$conn->export->createTable(‘event_participants’, $definition, $options);

TIP 上記の例で``person_id``に対して外部キーを省略していることに注目してください。この例では次の例で個別の外部キーをテーブルに追加する方法を示すために省略しました。通常は``foreignKeys``で定義された両方の外部キーがあることがベストです。

``person_id``カラムの``event_participants``テーブルに見つからない外部キーを追加してみましょう:

// test.php

// ... $definition = array(‘local’ => ‘person_id’, ‘foreign’ => ‘id’, ‘foreignTable’ => ‘people’, ‘onDelete’ => ‘CASCADE’);

$conn->export->createForeignKey(‘event_participants’, $definition);

テーブルを変更する

``Doctrine_Export``ドライバはデータベースがポータブルでありながら既存のデータベーステーブルを簡単に変更する方法を提供します。

// test.php

// ... $alter = array( ‘add’ => array( ‘new_column’ => array( ‘type’ => ‘string’, ‘length’ => 255 ), ‘new_column2’ => array( ‘type’ => ‘string’, ‘length’ => 255 ) ) );

echo $conn->export->alterTableSql(‘events’, $alter);

``alterTableSql()``への呼び出しは次のSQLクエリを出力します:

ALTER TABLE events ADD new_column VARCHAR(255), ADD new_column2

VARCHAR(255)

NOTE 生成SQLのみを実行しこれを返したくない場合、``alterTable()``メソッドを使います。

// test.php

// ...

$conn->export->alterTable(‘events’, $alter);

``alterTable()``メソッドは2つのパラメータを必須とし3番目のパラメータはオプションです:

||~ 名前 ||~ 型 ||~ 説明 || || //name// || ``string` || 変更が想定されるテーブルの名前。 || || //changes// || ``array` || 実行を前提とされる変更のそれぞれのタイプの詳細を含む連想配列。||

オプションの3番目のパラメータ(デフォルト: false):

||~ 名前 ||~ 型 ||~ 説明 || || //$check// || boolean || 実行前にDBMSが実際にオペレーションを実行できるかチェックする ||

現在サポートされる変更のタイプは次のように定義されます:

||~ 変更 ||~ 説明 || || //name// || テーブル用の新しい名前 || || //add// || 配列のインデックスとして追加されるフィールドの名前を格納する連想配列。配列のそれぞれのエントリの値は追加されるフィールドのプロパティを格納する別の連想配列に設定されます。フィールドのプロパティはDoctrineパーサーによって定義されたものと同じです。|| || // remove// || 配列のインデックスとして削除されるフィールドの名前を格納する連想配列。現在それぞれのエントリに割り当てられた値は無視されます。空の配列は将来の互換性のために使われます。|| || //rename// || 配列のインデックスとしてリネームされるフィールドの名前を格納する連想配列。配列のそれぞれのエントリの値は別の連想配列に設定されます。この別の連想配列は新しいフィールド名と``CREATE TABLE``文として使われるDBM固有のSQLコードで既にあるフィールドの宣言の一部を格納するものとして設定されるDeclarationという名前のエントリを持ちます。|| || //change// || 配列のインデックスとして変更されるフィールドの名前を格納する連想配列。フィールドと他のプロパティを変更するか、change配列エントリは配列インデックスとしてフィールドの新しい名前を格納するかを念頭においてください。||

配列のそれぞれのエントリの値はフィールドのプロパティを格納する別の連想配列に設定されます。これは配列エントリとして変更されることを意味します。これらのエントリはそれぞれのプロパティの新しい値に割り当てられます。フィールドのプロパティはDoctrineパーサーが定義するものと同じです。

// test.php

// ... $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);

NOTE テーブルを``event``にリネームしたことに注目してください。テーブルを``events``にリネームし直しましょう。機能を示すためだけにテーブルをリネームしたので次の例のためにテーブルを``events``と名づける必要があります。

// test.php

// ... $alter = array( ‘name’ => ‘events’ );

$conn->export->alterTable(‘event’, $alter);

インデックスを作成する

インデックスを作成するために、``createIndex()``メソッドが使われます。このメソッドは``createConstraint()``と似たシグニチャを持ち、テーブルの名前、インデックスの名前と定義配列を受け取ります。定義配列は``fields``という名前の1つのキーを持ち、その値はインデックスの一部であるフィールドを格納する別の連想配列です。フィールドは次のキーを持つ配列として定義されます: ソート、昇順と降順の長さを持つ値、整数値

すべてのRDBMSはインデックスソートもしくは長さをサポートしないので、これらの場合ドライバはこれらを無視します。テストのeventデータベースでは、アプリケーションが固有のtimeframeで起きるイベントを表示することを前提とすることができます。selectは``WHERE``条件でdatatimeフィールドを使います。このフィールドにインデックスが存在する場合に手助けになります。

// test.php

// ... $definition = array( ‘fields’ => array( ‘datetime’ => array() ) );

$conn->export->createIndex(‘events’, ‘datetime’, $definition);

データベースの要素を削除する

上記で示されたそれぞれの``create*()``メソッドに対して、データベース、テーブル、フィールド、インデックスもしくは制約を削除するために対応する``drop*()``メソッドが存在します。``drop*()``メソッドは削除されるアイテムの存在をチェックしません。try-catchブロックを使用して例外をチェックするのは開発者の責務です:

// test.php

// ... try { $conn->export->dropSequence(‘nonexisting’); } catch(Doctrine_Exception $e) {

}

次のコードで制約を簡単に削除できます:

// test.php

// ... $conn->export->dropConstraint(‘events’, ‘PRIMARY’, true);

NOTE 3番目の引数はこれが主キーであることのヒントを与えます。

// test.php

// ... $conn->export->dropConstraint(‘event_participants’, ‘event_id’);

次のコードでインデックスを簡単に削除できます:

$conn->export->dropIndex(‘events’, ‘event_timestamp’);

TIP 次の2つの例を実際に実行するのは推奨されません。次のセクションで我々の例が無傷で動作できるように``events_db``が必要です。

次のコードでデータベースからテーブルを削除します:

// test.php

// ... $conn->export->dropTable(‘events’);

次のコードでデータベースを削除できます:

// test.php

// ... $conn->export->dropDatabase(‘events_db’);

Import

importモジュールによってデータベース接続の内容を検証できます。それぞれのデータベースとそれぞれのデータベースのスキーマを学びます。

紹介

データベースに何があるのか見るために、Importモジュールの``list*()``ファミリーのメソッドを使うことができます。

||~ 名前 ||~ 説明 || || listDatabases() || データベースの一覧を表示する。|| || listFunctions() || 利用可能なメソッドの一覧を表示する。|| || listSequences(:code:`dbName) || 利用可能なシーケンスの一覧を表示する。オプションパラメータとしてデータベースの名前を受け取る。帝京されない場合、選択されたデータベースが想定されます。|| || listTableConstraints(`\ tableName) || 利用可能なテーブルの一覧を表示する。テーブルの名前を受け取る。|| || listTableColumns(:code:`tableName) || テーブルで利用可能なカラムの一覧を表示する。|| || listTableIndexes(`\ tableName) || テーブルで定義されているインデックスの一覧を表示する。|| || listTables(:code:`dbName) || データベースのテーブルの一覧を表示する。 || || listTableTriggers(`\ tableName) || テーブルのトリッガーの一覧を表示する。|| || listTableViews(:code:`tableName) || テーブルで利用可能なビューの一覧を表示する。|| || listUsers() || データベース用のユーザーの一覧を表示する。|| || listViews(`\ dbName) || データベース用のビューの一覧を表示する。||

下記において上記のメソッドの使い方の例が見つかります:

データベースの一覧を表示する
// test.php

// ... $databases = conn->import->listDatabases(); print_r(databases);

シーケンスの一覧を表示する
// test.php

// ... $sequences = conn->import->listSequences('events_db'); print_r(sequences);

制約の一覧を表示する
// test.php

// ... $constraints = conn->import->listTableConstraints('event_participants'); print_r(constraints);

テーブルカラムの一覧を表示する
// test.php

// ... $columns = conn->import->listTableColumns('events'); print_r(columns);

テーブルインデックスの一覧を表示する
// test.php

// ... $indexes = conn->import->listTableIndexes('events'); print_r(indexes);

テーブルの一覧を表示する
$tables = conn->import->listTables(); print_r(tables);
ビューの一覧を表示する
NOTE 現在、ビューを作成するメソッドは存在しないので、手動で作成してください。

$sql = “CREATE VIEW names_only AS SELECT name FROM people”;

conn->exec(sql);

$sql = “CREATE VIEW last_ten_events AS SELECT * FROM events ORDER BY id DESC LIMIT 0,10”; conn->exec(sql);

先ほど作成したビューの一覧を表示できます:

$views = conn->import->listViews(); print_r(views);

DataDict

はじめに

ネイティブのRDBMの型をDoctrineの型に変換するもしくはその逆を行うためにDoctrineは内部で``DataDict``モジュールを使用します。``DataDict``モジュールは変換のために2つのメソッドを使用します:

  • ``getPortableDeclaration()``はネイティブなRDBMSの型宣言をポータブルなDoctrine宣言に変換するために使われる
  • ``getNativeDeclaration()``はDoctrine宣言をドライバ固有の型宣言に変換するために使われる
ポータブルな宣言を取得する
// test.php

// ... $declaration = $conn->dataDict->getPortableDeclaration(‘VARCHAR(255)’);

print_r($declaration);

上記の例は次の内容を出力します:

$ php test.php Array ( [type] => Array ( [0] => string )
[length] => 255
[unsigned] =>
[fixed] =>

)

ネイティブな宣言を取得する
// test.php

// ... $portableDeclaration = array( ‘type’ => ‘string’, ‘length’ => 20, ‘fixed’ => true );

$nativeDeclaration = conn->dataDict->getNativeDeclaration(portableDeclaration);

echo $nativeDeclaration;

上記の例は次の内容を出力します:

$ php test.php CHAR(20)

ドライバ

Mysql
テーブル型を設定する
// test.php

// ... $fields = array( ‘id’ => array( ‘type’ => ‘integer’, ‘autoincrement’ => true ), ‘name’ => array( ‘type’ => ‘string’, ‘fixed’ => true, ‘length’ => 8 ) );

NOTE 次のオプションはMySQL固有で他のドライバはスキップします。

$options = array(‘type’ => ‘INNODB’);

$sql = $conn->export->createTableSql(‘test_table’, $fields); echo $sql[0];

上記の例は次のSQLクエリを出力します:

CREATE TABLE test_table (id INT AUTO_INCREMENT, name CHAR(8)) ENGINE

= INNODB

まとめ

この章は本当に素晴らしいものです。Doctrine DBALはそれ自身が偉大なツールです。おそらく最も機能を持つものの1つでPHPデータベース抽象化レイヤーを簡単に利用できます。

[doc transactions :name]の使い方を学ぶ準備が整いました。

はじめに

データベーストランザクションはデータベースマネジメントシステムもしくは似たようなシステムとのインタラクションのユニットです。トランザクションは完結させるか停止するかどちらかでなければならず他のトランザクションから独立した整合性と信頼性のある方法で扱われます。理想的には、データベースシステムはそれぞれのトランザクションに対してACID(Atomicity、Consistency、Isolation、とDurability)プロパティのすべてを保証します。

  • //[http://en.wikipedia.org/wiki/Atomicity 原子性(Atomicity)]//はすべてのタスクのトランザクションが実行されるかそれともまったく行われないことを保証するDBMSの機能を示します。資金の転送は完全にやり通すか複数の理由から停止するかどちらかですが、原始性はあるアカウントにお金が入らない場合、他のアカウントが負債を得ることはないことを保証します。
  • //[http://en.wikipedia.org/wiki/Database_consistency Consistency(一貫性)]//はトランザクションの開始時と終了時にデータベースが正しい状態にあることを示します。これはトランザクションはデータベースのルール、もしくは//整合性制約//を破ることができないことを意味します。整合性制約がすべてのアカウントがプラス残高でなければならないことを述べる場合、このルールに違反するトランザクションは停止します。
  • //[http://en.wikipedia.org/wiki/Isolation*%28computer_science%29 隔離性(Isolation)]//は他のすべてのオペレーションからトランザクション内のオペレーションを分離させるアプリケーションの機能を示します。これはトランザクション外部のオペレーションは中間状態のデータを見ることができないことを意味します;転送がまだ処理されている間にがクエリが実行された場合でも、銀行のマネージャーはファンドが特定のアカウントもしくは他のアカウントのどちらかに転送されることがわかります。よりフォーマルには、隔離性はトランザクションの履歴(もしくは[http://en.wikipedia.org/wiki/Schedule*%28computer_science%29 スケジュール])が[http://en.wikipedia.org/wiki/Serializability serializable]であることを意味します。パフォーマンスの理由から、この機能はもっとも緩やかな制約であることが多いです。詳細は[http://en.wikipedia.org/wiki/Isolation_%28computer_science%29 隔離性]の記事を参照してください。
  • //[http://en.wikipedia.org/wiki/Durability_%28computer_science%29 持続性(Durability)]//はユーザーが成功の通知を受けると、トランザクションが永続化され、取り消しされないことが保証されることを示します。このことはシステム障害を乗り越え、[http://en.wikipedia.org/wiki/Database_system データベースシステム]が整合性制約をチェックしトランザクションを停止する必要がないことを意味します。すべてのトランザクションは[http://en.wikipedia.org/wiki/Database_log ログ]に書き込まれトランザクション前の正しい状態にシステムを再現できます。ログで安全になった後でトランザクションはコミットのみできます。

Doctrineにおいてデフォルトではすべてのオペレーションはトランザクションにラップされます。Doctrineが内部で動作する方法について注目すべきことは次の通りです:

  • Doctrineはアプリケーションレベルのトランザクションネスティングを使用する
  • (最も外側のコミットが呼び出されるとき)Doctrineは常に``INSERT`` / UPDATE / ``DELETE``クエリを実行する。オペレーションは次の順序で実行されます: すべてのinsert、すべてのupdateと最後にすべてのdelete。同じコンポーネントのdeleteオペレーションが1つのクエリに集約されるようにDoctrineはdeleteを最適化する方法を知っています。

最初に新しいトランザクションを始める必要があります:

$conn->beginTransaction();

次に実行されているクエリになるオペレーションをいくつか実行します:

$user = new User(); $user->name = ‘New user’; $user->save();

$user = Doctrine_Core::getTable(‘User’)->find(5); $user->name = ‘Modified user’; $user->save();

``commit()``メソッドを使用することですべてのクエリをコミットできます:

$conn->commit();

ネスティング

Doctrine DBALでトランザクションを簡単にネストできます。ネストされたトランザクションを実演するシンプルな例を示す下記のコードを確認しましょう。

最初に``saveUserAndGroup()``という名前のPHP関数を作ってみましょう:

function saveUserAndGroup(Doctrine_Connection $conn, User $user, Group

$group) { $conn->beginTransaction();

$user->save();

$group->save();

$conn->commit();

}

別のトランザクション内部でメソッドを利用します:

try { $conn->beginTransaction();
saveUserAndGroup($conn,$user,$group);
saveUserAndGroup($conn,$user2,$group2);
saveUserAndGroup($conn,$user3,$group3);

$conn->commit();

} catch(Doctrine_Exception $e) { $conn->rollback(); }

NOTE ``saveUserAndGroup()``への3つの呼び出しがトランザクションでラップされ、それぞれの関数呼び出しは独自のネストされたトランザクションを始めることに注目してください。

Savepoints

Doctrineはトランザクションのセーブポイントをサポートします。このことは名前有りのトランザクションを設定してこれらをネストできることを意味します。

Doctrine_Transaction::beginTransaction(:code:`savepoint)``は`savepoint``の名前で名付けられたトランザクションセーブポイントを設定し、現在のトランザクションが同じ名前のセーブポイントを持つ場合、古いセーブポイントは削除され新しいものが設定されます。

try { $conn->beginTransaction(); // 何らかのオペレーションをここで行う
// mysavepointと呼ばれる新しいセーブポイントを作成する
$conn->beginTransaction('mysavepoint');
try {
    // 何らかのオペレーションをここで行う

    $conn->commit('mysavepoint');
} catch(Exception $e) {
    $conn->rollback('mysavepoint');
}
$conn->commit();

} catch(Exception $e) { $conn->rollback(); }

``Doctrine_Transaction::rollback($savepoint)``はトランザクションを名前付きのセーブポイントにロールバックします。セーブポイントの後で現在のトランザクションが列に行った修正はロールバックで取り消しになります。

NOTE 例えばMysqlの場合、セーブポイントの後でメモリーに保存された列のロックを開放しません。

名前付きのセーブポイントの後で設定されたセーブポイントは削除されます。

``Doctrine_Transaction::commit($savepoint)``は現在のトランザクションのセーブポイントのセットから名前付きのセーブポイントを削除します。

コミットを実行するもしくはセーブポイントの名前パラメータ無しでロールバックが呼び出されている場合現在のトランザクションのすべてのセーブポイントは削除されます。

try { $conn->beginTransaction(); // ここで何らかのオペレーションを行う
// mysavepointと呼ばれる新しいセーブポイントを作成する
$conn->beginTransaction('mysavepoint');

// ここで何らかのオペレーションを行う

$conn->commit();   // すべてのセーブポイントを削除する

} catch(Exception $e) { $conn->rollback(); // すべてのセーブポイントを削除する }

Isolationレベル

トランザクションの独立性レベル(isolation level)はデフォルトのトランザクションのビヘイビアを設定します。’独立性レベル’という名前が示すように、設定がそれぞれのトランザクションの独立性の程度、もしくはトランザクション内部でどんな種類のロックがクエリに関連付けされているかを決定します。利用できるレベルは4つあります(厳密性の昇順):

: READ UNCOMMITTED : トランザクションがまれな場合で、この設定はいわゆる’ダーティリード(dirty read)’を許可します。1つのトランザクション内部のクエリは別のトランザクションのコミットされていない変更によって影響を受けます。

: READ COMMITTED : コミットされた更新は別のトランザクションの範囲内で見えます。トランザクション内の理想的なクエリは異なる結果を返すことができることを意味します。一部のDBMSではこれはデフォルトです。

: REPEATABLE READ : トランザクションの範囲内では、すべての読み込みが一貫しています。これはMysql INNODBエンジンのデフォルトです。

: SERIALIZABLE : トランザクションが通常の``SELECT``クエリを持つ場合、他のトランザクションで更新が許可されない。

transactionモジュールを取得するには、次のコードを使います:

$tx = $conn->transaction;

独立性レベルをREAD COMMITTEDに設定する:

$tx->setIsolation(‘READ COMMITTED’);

独立性レベルをSERIALIZABLEに設定する:

$tx->setIsolation(‘SERIALIZABLE’);

Tip

ドライバの中には(Mysqlのように)現在の独立性レベルの取得をサポートするものがあります。次のようにできます:

$level = $tx->getIsolation();

まとめ

トランザクションはデータベースの質と一貫性を保証する偉大な機能です。トランザクションを理解したのでイベントサブフレームワークについて学ぶ準備ができています。

イベントサブフレームワークはDoctrineのコアメソッドにフックを入れることを可能にする偉大な機能でコアコードを一行も修正せずに内部機能のオペレーションを変更します。

はじめに

Doctrineは柔軟なイベントリスナーアーキテクチャを提供します。このアークテクチャは異なるイベントのリスニングだけでなくリスニングされるメソッドの実行を変更することも可能にします。

様々なDoctrineコンポーネント用の異なるリスナーとフックがあります。リスナーは個別のクラスであるのに対してフックはリスニングされるクラスの範囲内の空のテンプレートメソッドです。

フックはイベントリスナーよりもシンプルですがこれらは異なるアスペクトの分離が欠けています。``Doctrine_Record``フックを使用する例は次の通りです:

// models/BlogPost.php

class BlogPost extends Doctrine_Record { // ...

public function preInsert($event)
{
    $invoker = $event->getInvoker();

    $invoker->created = date('Y-m-d', time());
}

}

NOTE これまでたくさんのモデルを定義してきたので、``BlogModel``に対して独自の``setTableDefinition()``を定義できます。もしくは独自のカスタムモデルを作りましょう!

次のコードで上記のモデルを使うことができます。title``body``と``created``カラムをモデルに追加することを前提とします:

// test.php

// ... $blog = new BlogPost(); $blog->title = ‘New title’; $blog->body = ‘Some content’; $blog->save();

echo $blog->created;

上記の例はPHPが理解する現在の日付を出力します。

それぞれのリスナーとフックメソッドは ``Doctrine_Event``オブジェクトを1つのパラメータとして受け取ります。``Doctrine_Event``オブジェクトは問題のイベントの情報を格納しリスニングされるメソッドの実行を変更できます。

ドキュメントの目的のために多くのメソッドテーブルは``params``という名前のカラムで提供されます。このカラムはパラメータの名前は与えられたイベント上でイベントオブジェクトが保有するパラメータの名前を示します。例えば``preCreateSavepoint``イベントは作成された``savepoint``の名前を持つ1つのパラメータを持ちます。

接続リスナー

接続リスナーは``Doctrine_Connection``とそのモジュール(``Doctrine_Transaction``など)のメソッドをリスニングするために使われます。すべてのリスナーメソッドはリスニングされるイベントの情報を格納する``Doctrine_Event``オブジェクトを1つの引数として受け取ります。

新しいリスナーを作成する

リスナーを定義する方法は3つあります。最初に``Doctrine_EventListener``を継承するクラスを作成することでリスナーを作成できます:

class MyListener extends Doctrine_EventListener { public function

preExec(Doctrine_Event $event) {

}

}

``Doctrine_EventListener``を継承するクラスを定義することで``Doctrine_EventListener_Interface``の範囲内ですべてのメソッドを定義する必要はありません。これは``Doctrine_EventListener``が既にこれらすべてのメソッド用の空のスケルトンを持つからです。

ときに``Doctrine_EventListener``を継承するリスナーを定義できないことがあります(他の基底クラスを継承するリスナーを用意できます)。この場合``Doctrine_EventListener_Interface``を実装させることができます。

class MyListener implements Doctrine_EventListener_Interface { public

function preTransactionCommit(Doctrine_Event $event) {} public function postTransactionCommit(Doctrine_Event $event) {}

public function preTransactionRollback(Doctrine_Event $event) {}
public function postTransactionRollback(Doctrine_Event $event) {}

public function preTransactionBegin(Doctrine_Event $event) {}
public function postTransactionBegin(Doctrine_Event $event) {}

public function postConnect(Doctrine_Event $event) {}
public function preConnect(Doctrine_Event $event) {}

public function preQuery(Doctrine_Event $event) {}
public function postQuery(Doctrine_Event $event) {}

public function prePrepare(Doctrine_Event $event) {}
public function postPrepare(Doctrine_Event $event) {}

public function preExec(Doctrine_Event $event) {}
public function postExec(Doctrine_Event $event) {}

public function preError(Doctrine_Event $event) {}
public function postError(Doctrine_Event $event) {}

public function preFetch(Doctrine_Event $event) {}
public function postFetch(Doctrine_Event $event) {}

public function preFetchAll(Doctrine_Event $event) {}
public function postFetchAll(Doctrine_Event $event) {}

public function preStmtExecute(Doctrine_Event $event) {}
public function postStmtExecute(Doctrine_Event $event) {}

}

CAUTION すべてのリスナーメソッドはここで定義しなければなりません。さもないとPHPは致命的エラーを投げます。

リスナーを作成する3番目の方法はとても優雅です。``Doctrine_Overloadable``を実装するクラスを作成します。インターフェイスは1つのメソッド: ``__call()``のみを持ちます。このメソッドは*すべての*イベントと補足するために使われます。

class MyDebugger implements Doctrine_Overloadable { public function

__call($methodName, $args) { echo $methodName . ‘ called !’; } }

リスナーを追加する

setListener()でリスナーを接続に追加できます。

$conn->setListener(new MyDebugger());

複数のリスナーが必要な場合はaddListener()を使います。

$conn->addListener(new MyDebugger()); $conn->addListener(new

MyLogger());

プレ接続とポスト接続

下記のリスナーのすべては``Doctrine_Connection``クラスに含まれます。これらすべては``Doctrine_Event``のインスタンスです。

||~ メソッド ||~ リスニング ||~ パラメータ || || preConnect(Doctrine_Event $event) || Doctrine_Connection::connection() || || || postConnect(Doctrine_Event $event) || Doctrine_Connection::connection() || ||

トランザクションリスナー

下記のリスナーのすべては``Doctrine_Transaction``クラスに含まれます。これらすべてに``Doctrine_Event``のインスタンスが渡されます。

||~ メソッド ||~ リスニング ||~ パラメータ || || preTransactionBegin() || beginTransaction() || || || postTransactionBegin() || beginTransaction() || || || preTransactionRollback() || rollback() || || || postTransactionRollback() || rollback() || || || preTransactionCommit() || commit() || || || postTransactionCommit() || commit() || || || preCreateSavepoint() || createSavepoint() || savepoint || || postCreateSavepoint() || createSavepoint() || savepoint || || preRollbackSavepoint() || rollbackSavepoint() || savepoint || || postRollbackSavepoint() || rollbackSavepoint() || savepoint || || preReleaseSavepoint() || releaseSavepoint() || savepoint || || postReleaseSavepoint() || releaseSavepoint() || savepoint ||

class MyTransactionListener extends Doctrine_EventListener { public

function preTransactionBegin(Doctrine_Event $event) { echo ‘beginning transaction... ‘; }

public function preTransactionRollback(Doctrine_Event $event)
{
    echo 'rolling back transaction... ';
}

}

クエリ実行リスナー

下記のリスナーのすべては``Doctrine_Connection``と``Doctrine_Connection_Statement``クラスに含まれます。そしてこれらすべては``Doctrine_Event``のインスタンスです。

||~ メソッド ||~ リスニング ||~ パラメータ || || prePrepare() || prepare() || query || || postPrepare() || prepare() || query || || preExec() || exec() || query || || postExec() || exec() || query, rows || || preStmtExecute() || execute() || query || || postStmtExecute() || execute() || query || || preExecute() || execute() * || query || || postExecute() || execute() * || query || || preFetch() || fetch() || query, data || || postFetch() || fetch() || query, data || || preFetchAll() || fetchAll() || query, data || || postFetchAll() || fetchAll() || query, data ||

NOTE Doctrine\_Connection::execute()``がプリペアードステートメントパラメータで呼び出されるときにのみ``preExecute()``と``postExecute()``は起動します。そうではない場合``Doctrine_Connection::execute()``は``prePrepare()postPrepare()``preStmtExecute()``と``postStmtExecute()``を起動します。

ハイドレーションリスナー

ハイドレーションリスナーは結果セットのハイドレーション処理をリスニングするために使われます。ハイドレーション処理をリスニングするために2つのメソッド: ``preHydrate()``と``postHydrate()``が存在します。

ハイドレーションリスナーを接続レベルで設定する場合、``preHydrate()``と``postHydrate()``ブロックの範囲内のコードは複数のコンポーネントの結果セットの範囲内ですべてのコンポーネントによって実行されます。テーブルレベルで同様のリスナーを追加する場合、テーブルのデータがハイドレイトされているときのみ起動します。

フィールド: first\_name``last_name``と``age``を持つ``User``クラスを考えてみましょう。次の例では``first_name``と``last_name``フィールドに基づいて``full__name``と呼ばれる生成フィールドを常にビルドするリスナーを作成します。

// test.php

// ... class HydrationListener extends Doctrine_Record_Listener { public function preHydrate(Doctrine_Event $event) { $data = $event->data;

    $data['full_name'] = $data['first_name'] . ' ' . $data['last_name'];
    $event->data = $data;
}

}

行う必要があるのは``User``レコードにこのリスナーを追加して複数のユーザーを取得することです:

// test.php

// ... $userTable = Doctrine_Core::getTable(‘User’); $userTable->addRecordListener(new HydrationListener());

$q = Doctrine_Query::create() ->from(‘User’);

$users = $q->execute();

foreach ($users as $user) { echo $user->full_name; }

レコードリスナー

``Doctrine_Record``は``Doctrine_Connection``とよく似たリスナーを提供します。グローバル、接続、テーブルレベルでリスナーを設定できます。

利用可能なすべてのリスナーメソッドの一覧は次の通りです: 下記のリスナーすべてが``Doctrine_Record``と``Doctrine_Validator``クラスに含まれます。そしてこれらすべてに``Doctrine_Event``のインスタンスが渡されます。

||~ メソッド ||~ リスニング || || preSave() || save() || || postSave() || save() || || preUpdate() || レコードが``DIRTY``のとき``save()`` || || postUpdate() || レコードが``DIRTY``のとき``save()`` || || preInsert() || レコードが``DIRTY``のとき``save()`` || || postInsert() || レコードが``DIRTY``のとき``save()`` || || preDelete() || delete() || || postDelete() || delete() || || preValidate() || validate() || || postValidate() || validate() ||

接続リスナーと同じようにレコードリスナーを定義する方法は3つあります: ``Doctrine_Record_Listener``を継承する、``Doctrine_Record_Listener_Interface``を実装するもしくは``Doctrine_Overloadable``を実装するです。

次の例では``Doctrine_Overloadable``を実装することでグローバルレベルのリスナーを作成します:

class Logger implements Doctrine_Overloadable { public function

__call($m, $a) { echo ‘caught event ‘ . $m;

    // do some logging here...
}

}

マネージャーにリスナーを追加するのは簡単です:

$manager->addRecordListener(new Logger());

マネージャーレベルのリスナーを追加することでこれらの接続の範囲内ですべてのテーブル/レコードに影響を及ぼします。次の例では接続レベルのリスナーを作成します:

class Debugger extends Doctrine_Record_Listener { public function

preInsert(Doctrine_Event $event) { echo ‘inserting a record ...’; }

public function preUpdate(Doctrine_Event $event)
{
    echo 'updating a record...';
}

}

接続にリスナーを追加するのも簡単です:

$conn->addRecordListener(new Debugger());

リスナーが特定のテーブルのみにアクションを適用するようにリスナーをテーブル固有のものにしたい場合がよくあります。

例は次の通りです:

class Debugger extends Doctrine_Record_Listener { public function

postDelete(Doctrine_Event $event) { echo ‘deleted ‘ . $event->getInvoker()->id; } }

このリスナーを任意のテーブルに追加するのは次のようにできます:

class MyRecord extends Doctrine_Record { // ...
public function setUp()
{
    $this->addListener(new Debugger());
}

}

レコードフック

||~ メソッド ||~ リスニング || || preSave() || save() || || postSave() || save() || || preUpdate() || レコード状態が``DIRTY``であるとき``save()`` || || postUpdate() || レコード状態が``DIRTY``であるとき``save()`` || || preInsert() || レコード状態が``DIRTY``であるとき``save()`` || || postInsert() || レコード状態が``DIRTY``であるとき``save()`` || || preDelete() || delete() || || postDelete() || delete() || || preValidate() || validate() || || postValidate() || validate() ||

``preInsert()``と``preUpdate()``メソッドを利用するシンプルな例は次の通りです:

class BlogPost extends Doctrine_Record { public function

setTableDefinition() { $this->hasColumn(‘title’, ‘string’, 200); $this->hasColumn(‘content’, ‘string’); $this->hasColumn(‘created’, ‘date’); $this->hasColumn(‘updated’, ‘date’); }

public function preInsert($event)
{
    $this->created = date('Y-m-d', time());
}

public function preUpdate($event)
{
    $this->updated = date('Y-m-d', time());
}

}

DQLフック

レコードリスナーをグローバル、それぞれの接続で、もしくは特定のレコードインスタンスで追加することができます。``Doctrine_Query``は``preDql*()``フックを実装します。これはクエリが実行されるときに、追加されたレコードリスナーもしくはモデルインスタンス自身でチェックされます。フックを起動したクエリを変更できるフックのためにクエリはクエリの``from``部分に関連するすべてのモデルをチェックします。

DQLで使うことができるフックのリストは次の通りです:

||~ メソッド ||~ リスニング || || preDqlSelect() || from() || || preDqlUpdate() || update() || || preDqlDelete() || delete() ||

下記のコードは``User``モデル用の``SoftDelete``機能を実装するモデルにレコードリスナーを直接追加する例です。

Tip

SoftDeleteの機能はDoctrineのビヘイビアとして含まれます。このコードは実行されるクエリを修正するためにDQLリスナーをselect、delete、updateする方法を実演しています。Doctrine_Record::setUp()の定義で$this->actAs(‘SoftDelete’)を指定することでSoftDeleteビヘイビアを使うことができます。

class UserListener extends Doctrine_EventListener { /** * Skip the

normal delete options so we can override it with our own * * @param Doctrine_Event $event * @return void */ public function preDelete(Doctrine_Event $event) { $event->skipOperation(); }

/**
 * 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));
    }
}

}

オプションとして上記のリスナーのメソッドは下記のユーザークラスに設置できます。Doctrineはそこでフックを同じようにチェックします:

class User extends Doctrine_Record { // ...
public function preDqlSelect()
{
    // ...
}

public function preDqlUpdate()
{
    // ...
}

public function preDqlDelete()
{
    // ...
}

}

これらのDQLコールバックがチェックされるようにするには、これらを明示的に有効にしなければなりません。これはそれぞれのクエリに対して少量のオーバーヘッドを追加するので、デフォルトでは無効です。以前の章で既にこの属性を有効にしました。

思い出すためにコードを再掲載します:

// bootstrap.php

// ... $manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);

Userモデルとやりとりをするとき削除フラグが考慮されます:

レコードインスタンスを通してユーザーを削除します:

$user = new User(); $user->username = ‘jwage’; $user->password =

‘changeme’; $user->save(); $user->delete();

NOTE ``$user->delete()``を呼び出しても実際にはレコードは削除されず代わりに削除フラグがtrueに設定されます。

$q = Doctrine_Query::create() ->from(‘User u’);

echo $q->getSql();

SELECT 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 = ?

NOTE ``“u.deleted = ?”``が//true//のパラメータの値でwhere条件に自動的に追加されたことに注目してください。

複数のリスナーを連結する

異なるイベントリスナーを連結することができます。このことは同じイベントをリスニングするために複数のリスナーを追加できることを意味します。次の例では与えられた接続用に2つのリスナーを追加します:

この例では``Debugger``と``Logger``は両方とも``Doctrine_EventListener``を継承します:

$conn->addListener(new Debugger()); $conn->addListener(new Logger());

イベントオブジェクト

インボーカーを取得する

``getInvoker()``を呼び出すことでイベントを起動したオブジェクトを取得できます:

class MyListener extends Doctrine_EventListener { public function

preExec(Doctrine_Event $event) { $event->getInvoker(); // Doctrine_Connection } }

イベントコード

``Doctrine_Event``は定数をイベントコードとして使用します。利用可能なイベントの定数の一覧は下記の通りです:

  • 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

フックの使い方と返されるコードの例は次の通りです:

class MyListener extends Doctrine_EventListener { public function

preExec(Doctrine_Event $event) { $event->getCode(); // Doctrine_Event::CONN_EXEC } }

class MyRecord extends Doctrine_Record { public function preUpdate(Doctrine_Event $event) { $event->getCode(); // Doctrine_Event::RECORD_UPDATE } }

インボーカーを取得する

``getInvoker()``メソッドは与えられたイベントを起動したオブジェクトを返します。例えばイベント用の``Doctrine_Event::CONN_QUERY``インボーカーは``Doctrine_Connection``オブジェクトです。

``Doctrine_Record``インスタンスが保存されupdateがデータベースに発行されるときに起動する``preUpdate()``という名前のレコードフックの使い方の例は次の通りです:

class MyRecord extends Doctrine_Record { public function

preUpdate(Doctrine_Event $event) { $event->getInvoker(); // Object(MyRecord) } }

次のオペレーションをスキップする

リスナーチェーンのビヘイビアの変更と同様にリスニングされているメソッドの実行の変更のために``Doctrine_Event``は多くのメソッドを提供します。

多くの理由からリスニングされているメソッドの実行をスキップしたいことがあります。これは次のように実現できます(``preExec()``は任意のリスナーメソッドにできることに注意してください):

class MyListener extends Doctrine_EventListener { public function

preExec(Doctrine_Event $event) { // some business logic, then:

    $event->skipOperation();
}

}

次のリスナーをスキップする

リスナーのチェーンを使うとき次のリスナーの実行をスキップしたいことがあります。これは次のように実現できます:

class MyListener extends Doctrine_EventListener { public function

preExec(Doctrine_Event $event) { // some business logic, then:

    $event->skipNextListener();
}

}

まとめ

イベントリスナーはDoctrineの素晴らしい機能で[doc behaviors :name]に結びつけられます。これらは最小量のコードで非常に複雑な機能を提供します。

これでパフォーマンスを改善するためのベストな機能である[doc caching :name]を検討する準備ができました。

はじめに

``Doctrine_Cache``は直感的で使いやすいキャッシュのソリューションを提供します。:

  • 複数のキャッシュのバックエンド(Memcached、APCとSqliteを服務)
  • 微調整できる高度なオプション。``Doctrine_Cache``はパフォーマンスの微調整用のたくさんのオプションを持つ。

すべてのキャッシュドライバは次のようにインスタンス化されます:

// bootstrap.php

// ... $options = array(); cacheDriver = new Doctrine_Cache_Memcache(options);

NOTE それぞれのドライバは配列``$options``用の独自の値を持ちます。

ドライバ

Memcache

Memcacheドライバはキャッシュレコードをmemcachedサーバーに保存します。Memcachedはハイパフォーマンスで、 分散型のメモリオブジェクトキャッシュシステムです。これをバックエンドに使うには、memcachedデーモンとPECLのmemcacheエクステンションが必要です。

Memcacheキャッシュドライバは次のコードでインスタンス化できます:

// bootstrap.php

// ... $servers = array( ‘host’ => ‘localhost’, ‘port’ => 11211, ‘persistent’ => true );

$cacheDriver = new Doctrine_Cache_Memcache(array( ‘servers’ => $servers, ‘compression’ => false ) );

NOTE Memcacheは複数のサーバーを許可します。

Memcacheドライバで利用できるオプション:

||~ オプション ||~ データ型 ||~ デフォルト値 ||~ 説明 || || servers || array || array(array('host' => 'localhost','port' => 11211, 'persistent' => true)) || memcachedサーバーの配列; それぞれのmemcachedサーバーは連想配列で記述される: 'host' => (string) : memcachedサーバーの名前、'port' => (int) : memcachedサーバーのポート、'persistent' => (bool) : memcachedサーバーへの永続接続を使うかどうか || || compression || boolean || false || 即座に圧縮したい場合は``true`` ||

APC

Alternative PHP Cache (APC)はフリーでPHPのためのオープンなオプコードキャッシュです。これはPHPの中間コードのキャッシュと最適化のための頑強なフレームワークとも認識されています。DoctrineのAPCキャッシュドライバはキャッシュレコードを共用メモリに保存します。

次のコードでAPCキャッシュドライバをインスタンス化できます:

// bootstrap.php

// ... $cacheDriver = new Doctrine_Cache_Apc();

Db

Dbキャッシュバックエンドはキャッシュレコードを任意のデータベースに保存します。通常は処理が速いフラットファイルベースのデータベースが使われます(sqliteなど)。

次のコードでデータベースキャッシュをインスタンス化できます:

// bootstrap.php

// ... $cacheConn = Doctrine_Manager::connection(new PDO(‘sqlite::memory:’)); $cacheDriver = new Doctrine_Cache_Db(array(‘connection’ => $cacheConn));

クエリキャッシュと結果キャッシュ

はじめに

DoctrineはDQLクエリの最終結果(データ)と同様にDQLの解析処理の結果をキャッシュする方法を提供します。これら2つのキャッシュメカニズムはパフォーマンスを大いに向上させます。DQLクエリ実行の標準ワークフローを考えてみましょう:

新しいDQLクエリを初期化する
DQLクエリを解析する
データベース固有のSQLクエリをビルドする
SQLクエリを実行する
結果セットをビルドする
結果セットを返す

これらのフェーズはとても時間がかかります。とりわけクエリをデータベースサーバーに送信するフェーズ4は時間がかかります。Doctrineクエリキャッシュが実行されているとき次のフェーズが行われます:

新しいDQLクエリを初期化する
SQLクエリを実行する(キャッシュから取得)
結果セットをビルドする
結果セットを返す

DQLクエリが有効なキャッシュエントリーを持つ場合キャッシュされたSQLクエリが使われ、さもなければフェーズ2-3が実行されこれらのステップの結果がキャッシュに保存されます。最新のクエリ結果を常に得られるのでクエリキャッシュには不都合なことはありません。

NOTE 本番環境では常にクエリキャッシュを使うべきです。すなわち開発期間も簡単に使えます。DQLクエリを変更して実行するときキャッシュが修正されたことをDoctrineが最初に確認して新しいキャッシュエントリを作ります。そのためキャッシュを取り消す必要はありません。

クエリキャッシュの効率性がプリペアードステートメントの使い方(ともかくDoctrineはデフォルトで使用)に依存するのは無意味です。動的なクエリの部分を直接埋め込む代わりに常にプレースホルダーを使うべきです。

結果キャッシュを使えば状況がよくなります。クエリ処理は次のようになります(有効なキャッシュエントリが見つかることが前提):

新しいDQLクエリを初期化する
結果セットを返す

ご覧の通り、結果キャッシュは以前示されたクエリキャッシュを暗に伝えます。クエリによって返されるデータが最新である必要がなければ結果キャッシュを使うことを常に考えるべきです。

クエリキャッシュ

``Doctrine_Core::ATTR_QUERY_CACHE``属性を使用することで接続もしくは管理レベルのクエリキャッシュドライバを設定できます。接続レベルのキャッシュドライバを設定することはこの接続で実行されるすべてのドライバは特定のキャッシュドライバを使用するのに大してマネージャーレベルのキャッシュドライバを設定することは(接続レベルでオーバーライドされない限り)すべての接続が任意のキャッシュドライバを使用することを意味します。

マネージャーレベルのクエリキャッシュドライバを設定する:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_QUERY_CACHE, $cacheDriver);

NOTE ``$cacheDriver``の値はこの章の前のセクションでインスタンス化されたドライバになります。

接続レベルのキャッシュドライバを設定する:

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_QUERY_CACHE, $cacheDriver);

以前の章でグローバルキャッシュの属性を使いました。これらの属性はクエリレベルでオーバーライドできます。``useQueryCache()``を呼び出すことでキャッシュドライバをオーバーライドしてこれにDoctrineの有効なキャッシュドライバを渡すことができます。これはクエリキャッシュにはほとんど意味ありませんが可能です:

$q = Doctrine_Query::create() ->useQueryCache(new

Doctrine_Cache_Apc());

結果キャッシュ

``Doctrine_Core::ATTR_RESULT_CACHE``を使用することで接続もしくはマネージャーレベルの結果キャッシュドライバを設定できます。接続レベルのキャッシュドライバはこの接続で実行されるすべてのクエリが指定されたキャッシュドライバを使用するのに対してマネージャーレベルのキャッシュドライバは(接続レベルでオーバーライドされない限り)すべての接続が任意のキャッシュドライバを使用することを意味します。

マネージャーレベルのキャッシュドライバを設定する:

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE, $cacheDriver);

接続レベルのキャッシュドライバを設定する:

// bootstrap.php

// ... $conn->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE, $cacheDriver);

通常キャッシュエントリは同じ時間に対してのみ有効です。``Doctrine_Core::ATTR_RESULT_CACHE_LIFESPAN``を使用することでキャッシュエントリの有効な期間のためのグローバルな値を設定できます。

寿命を1時間に設定する(60秒 * 60 = 1時間 = 3600秒):

// bootstrap.php

// ... $manager->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE_LIFESPAN, 3600);

キャッシュドライバを設定したので``userResultCache()``メソッドを呼び出すことでDQLクエリを使うことができます:

blog投稿のタイトルとコメントの数を取得する:

$q = Doctrine_Query::create(); ->select(‘b.title, COUNT(c.id) count’)

->from(‘BlogPost b’) ->leftJoin(‘b.Comments c’) ->limit(10) ->useResultCache(true);

$blogPosts = $q->execute();

以前の章でグローバルキャッシュ属性を使いました。これらの属性はクエリレベルでオーバーライドできます。``useCache()``を呼び出してキャッシュドライバをオーバーライドしこれらにDoctrineのキャッシュドライバのインスタンスを渡すことができます。

$q = Doctrine_Query::create() ->useResultCache(new

Doctrine_Cache_Apc());

``setResultCacheLifeSpan()``を呼び出すことでもlifespan属性をオーバーライドできます:

$q = Doctrine_Query::create() ->setResultCacheLifeSpan(60 * 30);

まとめ

開発と本番環境の両方でDoctrineのキャッシュ機能を使うことは大いにお勧めします。使うことで不都合な影響はなくアプリケーションのパフォーマンスの改善に役立ちます。

キャッシュ機能はこの本で検討される最後から2番目の機能です。この後のDoctrineで使われている[doc technology テクノロジー]、[doc coding-standards コーディング規約]と[doc unit-testing ユニットテスト]の章はまとめです。最後の機能である[doc migrations :name]を検討するために移動しましょう。

Doctrineのマイグレーションパッケージのプログラミングインターフェイスを通して本番のデータベースを簡単に更新できます。データベースがバージョン管理されバージョンを通して差し戻しできるように変更が行われます。

マイグレーションを実行する

マイグレーションクラスの作り方を学んだので次のセクションでDoctrinenのテスト環境でマイグレーションを実装できるようにマイグレーションの実行の仕方を見てみましょう。

最初に``Doctrine_Migration``の新しいインスタンスを作りこれにマイグレーションクラスへのパスを渡しましょう:

$migration = new Doctrine_Migration(‘/path/to/migration_classes’,

$conn);

NOTE ``Doctrine_Migration``コンストラクタの2番目の引数に注目してください。オプションの``Doctrine_Connection``インスタンスを渡すことができます。使うマイグレーションクラスの接続を渡さなければ、現在の接続が取り込まれます。

``migrate()``メソッドを呼び出すことで最新バージョンに移行できます:

$migration->migrate();

特定のバージョンにマイグレートするには``migrate()``に引数を渡します。例えばバージョン0から3にマイグレートできます:

$migration->migrate(3);

バージョン3から0に戻すことができます:

$migration->migrate(0);

データベースの現在のバージョンを取得するには``getCurrentVersion()``メソッドを使います:

echo $migration->getCurrentVersion();

Tip

``migrate()``メソッドのバージョン番号の引数を省略するとDoctrineは内部でディレクトリで見つかるクラスの最新バージョン番号にマイグレートしようとします。

NOTE マイグレーションのトランザクション 内部ではDoctrineはトランザクションのマイグレーションバージョンをラップしません。マイグレーションクラスで例外とトランザクションを処理するのは開発者しだいです。トランザクションDDLをサポートするデータベースはごくわずかであることを覚えておいてください。大抵のデータベースでは、トランザクションでマイグレーションをラップする場合でも、create、alter、drop、renameなどのステートメントは効果があります。

実装

マイグレーションの実施方法を理解したので``migrate.php``という名前でテスト環境のスクリプトを実装してみましょう。

最初にマイグレーションクラスを保存する場所が必要なので``migrations``という名前のディレクトリを作りましょう:

$ mkdir migrations

``migrate.php``スクリプトを作り次のコードを記入します:

// migrate.php

require_once(‘bootstrap.php’);

$migration = new Doctrine_Migration(‘migrations’); $migration->migrate();

マイグレーションクラスを書く

マイグレーションクラスは``Doctrine_Migration``を継承するシンプルなクラスで構成されます。``up()``と``down()``メソッドを定義します。これらのメソッドはそれぞれ指定されたマイグレーションバージョンでのデータベースの変更とその取り消しを意味します。

NOTE クラスの名前がなんであれ、正しい順序でマイグレーションをロードするために使われる数字が含まれる接頭辞をクラスが含まれるファイルの名前につけなければなりません。

バージョン1から始まるデータベースをビルドするために使うマイグレーションクラスの例です。

最初のバージョンとして``migration_test``という名前の新しいテーブルを作りましょう:

// migrations/1_add_table.php

class AddTable extends Doctrine_Migration_Base { public function up() { $this->createTable(‘migration_test’, array(‘field1’ => array(‘type’ => ‘string’))); }

public function down()
{
    $this->dropTable('migration_test');
}

}

前のバージョンで追加したテーブルに新しいカラムを追加した2番目のバージョンを作りましょう:

// migrations/2_add_column.php

class AddColumn extends Doctrine_Migration_Base { public function up() { $this->addColumn(‘migration_test’, ‘field2’, ‘string’); }

public function down()
{
    $this->removeColumn('migration_test', 'field2');
}

}

最後に、``field1``カラムの型を変更してみましょう:

// migrations/3_change_column.php

class ChangeColumn extends Doctrine_Migration_Base { public function up() { $this->changeColumn(‘migration_test’, ‘field2’, ‘integer’); }

public function down()
{
    $this->changeColumn('migration_test', 'field2', 'string');
}

}

3つのマイグレーションクラスを作成したので以前実装した``migrate.php``スクリプトを実行できます:

$ php migrate.php

データベースを見ると``migrate_test``という名前のテーブルが存在し``migration_version``のバージョン番号が3に設定されることが確認できます。

最初の状態に差し戻したい場合``migrate.php``スクリプトで``migrate()``メソッドにバージョン番号を渡します:

// migrate.php

// ... $migration = new Doctrine_Migration(‘migrations’); $migration->migrate(0);

そして``migrate.php``スクリプトを実行します:

$ php migrate.php

データベースを見ると、``up()``メソッドで行ったすべての内容が``down()``メソッドの内容によって差し戻されます。

利用可能なオペレーション

マイグレーションクラスでデータベースを変えるために利用できるメソッドの一覧は次の通りです。

テーブルを作成する
// ... 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'
    );

    $this->createTable('table_name', $columns, $options);
}

// ...

NOTE スキーマを操作するために使われるデータ構造とデータベース抽象化レイヤーで使われるデータ構造が同じであることにお気づきかもしれません。これはマイグレーションクラスで指定されているオペレーションを実行するために内部でマイグレーションパッケージがデータベース抽象化レイヤーを使用しているからです。
テーブルを削除する
// ... public function down() { $this->dropTable(‘table_name’); } //

...

テーブルをリネームする
// ... public function up() { $this->renameTable(‘old_table_name’,

‘new_table_name’); } // ...

制約を作成する
// ... public function up() { $definition = array( ‘fields’ => array(

‘username’ => array() ), ‘unique’ => true );

    $this->createConstraint('table_name', 'constraint_name', $definition);
}

// ...

制約を削除する

Now the opposite ``down()`` would look like the following:

// ... public function down() { $this->dropConstraint(‘table_name’,

‘constraint_name’); } // ...

外部キーを削除する
// ... public function up() { $definition = array( ‘local’ =>

‘email_id’, ‘foreign’ => ‘id’, ‘foreignTable’ => ‘email’, ‘onDelete’ => ‘CASCADE’ );

    $this->createForeignKey('table_name', 'email_foreign_key', $definition);
}

// ...

``$definition``用の有効なオプションは次の通りです:

||~ 名前 ||~ 説明 || || name || オプションの制約名 || || local || ローカルフィールド || || foreign || 外部参照フィールド || || foreignTable || 外部テーブルの名前 || || onDelete || 参照の削除アクション || || onUpdate || 参照の更新アクション || || deferred || 延期された制約チェック ||

外部キーを削除する
// ... public function down() { $this->dropForeignKey(‘table_name’,

‘email_foreign_key’); } // ...

カラムを追加する
// ... public function up() { $this->addColumn(‘table_name’,

‘column_name’, ‘string’, $length, $options); } // ...

カラムをリネームする
NOTE sqliteのような一部のDBMSはカラムのリネームオペレーションを実装していません。sqlite接続を使用している場合カラムをリネームしようとすると例外が投げられます。

// ... public function up() { $this->renameColumn(‘table_name’,

‘old_column_name’, ‘new_column_name’); } // ...

カラムを変更する

既存のカラムのアスペクトを変更する:

// ... public function up() { $options = array(‘length’ => 1);

$this->changeColumn(‘table_name’, ‘column_name’, ‘tinyint’, $options); } // ...

カラムを削除する
// ... public function up() { $this->removeColumn(‘table_name’,

‘column_name’); } // ...

不可逆なマイグレーション

Tip

リバースできない``up()``メソッドでオペレーションを実行することがあります。例えばテーブルからカラムを削除する場合です。この場合新しい``Doctrine_Migration_IrreversibleMigrationException``例外を投げる必要があります。

// ... public function down() { throw new

Doctrine_Migration_IrreversibleMigrationException( ‘The remove column operation cannot be undone!’ ); } // ...

インデックスを追加する
// ... public function up() { $options = array(‘fields’ => array(

‘username’ => array( ‘sorting’ => ‘ascending’ ), ‘last_login’ => array()));

    $this->addIndex('table_name', 'index_name', $options)
}

// ...

インデックスを削除する
// ... public function down() { $this->removeIndex(‘table_name’,

‘index_name’); } // ...

プレフックとポストフック

モデルでデータベースのデータを変えることが必要な場合があります。テーブルを作成もしくは変更するので``up()``もしくは``down()``メソッドが処理された後でデータを変更しなければなりません。preUp()postUp()preDown()、と``postDown()``という名前でフックを用意します。定義すればこれらのメソッドは実行されます:

// migrations/1_add_table.php

class AddTable extends Doctrine_Migration_Base { public function up() { $this->createTable(‘migration_test’, array(‘field1’ => array(‘type’ => ‘string’))); }

public function postUp()
{
    $migrationTest = new MigrationTest();
    $migrationTest->field1 = 'Initial record created by migrations';
    $migrationTest->save();
}

public function down()
{
    $this->dropTable('migration_test');
}

}

NOTE 上記の例は``MigrationTest``モデルを作成し利用可能にしたことを前提とします。``up()``メソッドが実行されると``migration_test``テーブルが作成されるので``MigrationTest``モデルが使われます。このモデルの定義は下記の通りです。

// models/MigrationTest.php

class MigrationTest extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘field1’, ‘string’); } }

YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を学びます:

# schema.yml

MigrationTest: columns: field1: string

Up/Downの自動化

Doctrineのマイグレーション機能では大抵の場合マイグレーションメソッドの反対側を自動化することが可能です。例えばマイグレーションのupで新しいカラムを作成する場合、downを簡単に自動化するのは可能で必要なのは作成されたカラムを削除することです。これは``up``と``down``の両方に対して``migrate()``メソッドを使用して実現可能です。

migrate()``メソッドは$directionの引数を受け取り``up``もしくは``down``の値を持つようになります。この値は``columntable、のようなメソッドの最初の引数に渡されます。

カラムの追加と削除を自動化する例は次の通りです。

class MigrationTest extends Doctrine_Migration_Base { public function

migrate($direction) { this->column(direction, ‘table_name’, ‘column_name’, ‘string’, ‘255’); } }

上記のマイグレーションでupを実行するときカラムが追加され、downが実行されるときカラムが削除されます。

自動化できるマイグレーションメソッドのリストは次の通りです:

||~ 自動メソッド名 ||~ 自動化 || || table() || createTable()/dropTable() || || constraint() || createConstraint()/dropConstraint() || || foreignKey() || createForeignKey()/dropForeignKey() || || column() || addColumn()/removeColumn() || || index() || addInex()/removeIndex() ||

マイグレーションを生成する

Doctrineはいくつかの異なる方法でマイグレーションクラスを生成する機能を提供します。既存のデータベースを再現するマイグレーションのセットを生成する、もしくは既存のモデルのセット用にデータベースを作成するマイグレーションクラスを生成します。2つのスキーマ情報の2つのセットの間の違いからマイグレーションを生成することもできます。

データベースから

既存のデータベース接続からマイグレーションのセットを生成するには、``Doctrine_Core::generateMigrationsFromDb()``を使います。

Doctrine_Core::generateMigrationsFromDb(‘/path/to/migration/classes’);
既存のモデルから

既存のモデルのセットからマイグレーションのセットを生成するには、``Doctrine_Core::generateMigrationsFromModels()``を使うだけです。

Doctrine_Core::generateMigrationsFromModels(‘/path/to/migration/classes’, ‘/path/to/models’);

差分ツール

ときにはモデルを変更して変更に対するマイグレーション処理を自動化できるようにしたいことがあります。以前は変更に対してマイグレーションクラスを書かなければなりませんでした。しかし差分ツールによって変更を行い変更用のマイグレーションクラスを生成できます。

差分ツールはシンプルで使いやすいです。これは”from”と”to”を受け取り、これらは次のうちのどれかになります:

  • YAMLスキーマファイルへのパス
  • 既存のデータベース接続の名前
  • モデルの既存のセットへのパス

2つのYAMLスキーマファイルを作るシンプルな例を考えます。1つは``schema1.yml``でもう1つは``schema2.yml``という名前です。

``schema1.yml``はシンプルな``User``モデルを含みます:

# schema1.yml

User: columns: username: string(255) password: string(255)

スキーマを修正して``email_address``カラムを追加する場合を考えてみましょう:

# schema1.yml

User: columns: username: string(255) password: string(255) email_address: string(255)

これでデータベースに新しいカラムを追加できるマイグレーションクラスを簡単に作ることができます:

Doctrine_Core::generateMigrationsFromDiff(‘/path/to/migration/classes’, ‘/path/to/schema1.yml’, ‘/path/to/schema2.yml’);

これによって``/path/to/migration/classes/1236199329_version1.php``のパスでファイルが生み出されます。

class Version1 extends Doctrine_Migration_Base { public function up()

{ $this->addColumn(‘user’, ‘email_address’, ‘string’, ‘255’, array ()); }

public function down()
{
    $this->removeColumn('user', 'email_address');
}

}

データベースを簡単にマイグレートして新しいカラムを追加できます!

まとめ

安全かつ簡単にスキーマを変更できるので本番のデータベーススキーマを変更するためにマイグレーション機能は大いに推奨されます。

マイグレーションはこの本で検討する最後の機能です。最後の章では日常業務で手助けになる他のトピックを検討します。最初に他の[doc utilities :name]を検討しましょう。

Extensions

Doctrineエクステンションは任意のプロジェクトに入れて有効にできる再利用可能なDoctrineエクステンションを作成する方法です。エクステンションはコードの命名や、オートロードなどDoctrineの標準に従う単なるコードです。

エクステンションを使うには最初にどこにエクステンションがあるのかDoctrineにわかるように設定しなければなりません:

Doctrine_Core::setExtensionsPath(‘/path/to/extensions’);

SVNから既存のエクステンションをチェックアウトしてみましょう。ソートの上げ下げを提供するモデルのビヘイビアを搭載する``Sortable``エクステンションを見てみましょう。

$ svn co

http://svn.doctrine-project.org/extensions/Sortable/branches/1.2-1.0/ /path/to/extensions/Sortable

``/path/to/extensions/Sortable``を見てみると次のようなディレクトリ構造を見ることになります:

Sortable/ lib/ Doctrine/ Template/ Listener/ Sortable.php Sortable.php

tests/ run.php Template/ SortableTestCase.php

このエクステンションがあなたのマシンで動くことを確認するためにエクステンションのテストスイートを実行します。必要なのは``DOCTRINE_DIR``環境変数をセットすることです。

$ export DOCTRINE_DIR=/path/to/doctrine

NOTE 上記のDoctrineへのパスはlibフォルダーではなくメインフォルダーへのパスでなければなりません。テストを実行するにはDoctrineを含めた``tests``ディレクトリにアクセスできなければなりません。

``Sortable``エクステンションのテストを実行することが可能です:

$ cd /path/to/extensions/Sortable/tests $ php run.php

次のようなテストが成功したことを示すテストの出力が表示されます:

Doctrine Unit Tests ===================

Doctrine_Template_Sortable_TestCase.............................................passed

Tested: 1 test cases. Successes: 26 passes. Failures: 0 fails. Number of new Failures: 0 Number of fixed Failures: 0

Tests ran in 1 seconds and used 13024.9414062 KB of memory

プロジェクトでエクステンションを使いたい場合Doctrineでエクステンションを登録しエクステンションのオートロードメカニズムをセットアップする必要があります。

最初にエクステンションのオートロードをセットアップしましょう。

// bootstrap.php

// ... spl_autoload_register(array(‘Doctrine’, ‘extensionsAutoload’));

これでエクステンションを登録したのでエクステンション内部のクラスがオートロードされます。

$manager->registerExtension(‘Sortable’);

NOTE 異なる場所からエクステンションを登録する必要がある場合、``registerExtension()``メソッドの2番目の引数でエクステンションディレクトリへのフルパスを指定します。

ページ分割

はじめに

実際の世界のアプリケーションでは、データベースからコンテンツを表示するのは共通のタスクです。またコンテンツが何千もの項目を含む検索結果である場合を想像してください。うたがいなく、巨大なリストになり、メモリーの消費量が多くなりユーザーは正しい項目を見つけづらくなります。この問題に対してコンテンツ表示の編成が必要でページ分割が手助けになります。

Doctrineは高度で柔軟なページャーパッケージを実装します。これによってリストを複数のページに分割できるだけでなく、ページリンクのレイアウトもコントロールできます。この章では、ページャーオブジェクトの作り方、ページャースタイルをコントロール仕方を学び、最後にページャーレイアウトオブジェクト - Doctrineの強力なページリンク表示機能の概要を見ます。

ページャーを扱う

クエリのページ分割はクエリ自身と同じぐらいシンプルで効率的にできます。``Doctrine_Pager``はクエリを処理してページ分割することを担います。次の小さなピースのコードで確認しましょう:

// 初期値を定義する $currentPage = 1; $resultsPerPage = 50;

// ページャーオブジェクトを作成する $pager = new Doctrine_Pager( Doctrine_Query::create() ->from( ‘User u’ ) ->leftJoin( ‘u.Group g’ ) ->orderby( ‘u.username ASC’ ), $currentPage, // リクエストの現在のページ $resultsPerPage // (オプション)ページごとの結果数。デフォルトは25 );

この場所までは、このコードは古い``Doctrine_Query``オブジェクトと同じです。唯一の違いは新しい2つの引数が存在することです。これら2の引数に加えて古いクエリオブジェクトは``Doctrine_Pager``オブジェクトによってカプセル化されます。この段階では、``Doctrine_Pager``はページ分割をコントロールするために必要な基本データを定義します。ページャーの実際のステータスを知りたい場合、行うべきことはこれが既に実行されたかどうかチェックすることです:

$pager->getExecuted();

``Doctrine_Pager``によって提供される任意のメソッドにアクセスしようとする場合、Pagerがまだ実行されなかったことを報告する``Doctrine_Pager_Exception``が投げられるのを経験することになります。実行されたとき、``Doctrine_Pager``は情報を検索する強力なメソッドを提供します。APIの使い方はこのトピックの最後に並べてあります。

クエリを実行するには、プロセスは現存する``Doctrine_Query``実行呼び出しと似ています。オプションのパラメータを含む構文の完全な例は次の通りです:

$items = pager->execute([args = array() [, $fetchType =

null]]);

foreach ($items as $item) { // ... }

レコードクエリがカウンタークエリと異なる特別なケースがあります。この状況に対応するために、``Doctrine_Pager``にはカウントしてから実行できるようにするメソッドがあります。最初に行わなければならないのはカウントクエリを定義することです:

pager->setCountQuery(query [, $params = null]);

// ...

$rs = $pager->execute();

``setCountQuery``の最初のパラメータは有効な``Doctrine_Query``オブジェクトかDQL文字列です。2番目の引数はカウンタークエリに送信されるオプションパラメータです。このパラメータを定義しないので、後で``setCountQueryParams``を呼び出して定義することができます:

pager->setCountQueryParams([params = array() [, $append =

false]]);

このメソッドは2つのパラメータを受けとります。最初のパラメータはカウントパラメータに送信されるもので2番目のパラメータは``:code:params``がリストに追加されるもしくはカウントクエリパラメータがオーバーライドされるかどうかです。デフォルトのビヘイビアはリストをオーバーライドします。カウントクエリに関して最後に言うことは、カウントクエリ用のパラメータを定義しない場合、``pager->execute()``の呼び出しで定義するパラメータを送り出すことができます。

カウントクエリは常にアクセスできます。これを定義して``$pager->getCountQuery()``を呼び出す場合、”取得(fetcher)”クエリが返されます。

``Doctrine_Pager``が提供する他の機能にアクセスする必要がある場合、APIを通してアクセスできます:

// Pagerが既に実行されたかのチェックを返す $pager->getExecuted();

// クエリ検索で見つかるアイテムの合計数を返す $pager->getNumResults();

// 最初のページを返す(常に1) $pager->getFirstPage();

// ページの合計数を返す $pager->getLastPage();

// 現在のページを返す $pager->getPage();

// 現在の新しいページを定義する(実行を再度呼び出してオフセットと値を調整する必要がある) pager->setPage(page);

// 次のページを返す $pager->getNextPage();

// 前のページを返す $pager->getPreviousPage();

// 現在のページの最初のインデックスを返す $pager->getFirstIndice();

// 現在のページの最後のインデックスを返す $pager->getLastIndice();

// ページ分割をする必要がある場合はtrueそうでなければfalseを返す $pager->haveToPaginate();

// ページごとの最大数を返す $pager->getMaxPerPage();

// ページごとのレコードの最大数を定義する(再度呼び出してオフセットと値を調整する必要がある) pager->setMaxPerPage(maxPerPage);

// 現在のページのアイテム数を返す $pager->getResultsInPage();

// カウント結果をページャーにするために使われるDoctrine_Queryオブジェクトを返す $pager->getCountQuery();

// ページャーによって使われるカウンタクエリを定義する pager->setCountQuery(query, $params = null);

// Doctrine_Queryカウントによって使われるパラメータを返す(パラメータが定義されていない場合$defaultParamsを返す) pager->getCountQueryParams(defaultParams = array());

// Doctrine_Queryカウンタによって使われるパラメータを定義する pager->setCountQueryParams(params = array(), $append = false);

// Doctrine_Queryオブジェクトを返す $pager->getQuery();

// 関連するDoctrine_Pager_Range_* インスタンスを返す pager->getRange(rangeStyle, $options = array());

レンジスタイルをコントロールする

シンプルなページ分割では不十分なケースがあります。1つの例はページリンクのリストを書くときです。ページャーを越えるより強力なコントロール機能を有効にするために、レンジを作ることを可能にするページャーパッケージの小さなサブセットがあります。

現在Doctrineは2種類(2つのスタイル)のレンジ: スライディング(Doctrine\_Pager\_Range\_Sliding)とジャンピング(Doctrine\_Pager\_Range_Jumping)を実装します。

スライディング

スライディングページレンジスタイルは、ページレンジは現在のページでスムーズに移動します。最初と最後のページのレンジ以外、現在のページは常に真ん中です。5つのアイテムのチャンクの長さでどのように動作するのか確認してください:

Listing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Page 1: o——-| Page 2:

|-o—–| Page 3: |—o—| Page 4: |—o—| Page 5: |—o—| Page 6: |—o—| Page 7: |—o—| Page 8: |—o—|

ジャンピング

ジャンピングページレンジスタイルでは、ページリンクのレンジは常に”フレーム”の固定長の1つです: 1-5、6-10、11-15など。

Listing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Page 1: o——-| Page 2:

|-o—–| Page 3: |—o—| Page 4: |—–o-| Page 5: |——-o Page 6: o———| Page 7: |-o——-| Page 8: |—o—–|

ページレンジのスタイルの違いがわかったので、使い方を学びましょう:

$pagerRange = new Doctrine_Pager_Range_Sliding( array( ‘chunk’ => 5

// チャンクの長さ ), $pager // 以前のトピックで作り方を学んだDoctrine_Pagerオブジェクト );

代わりに、次のコードを使うこともできます:

$pagerRange = $pager->getRange( ‘Sliding’, array( ‘chunk’ => 5 ) );

``Doctrine_Pager``の代わりにこのオブジェクトを使う利点は何でしょうか?たった1つです; 現在のページ周辺のレンジを読み取ることができることです。

次の例を見てみましょう:

// 現在のページ周辺のレンジを読み取る //

この例では、スライディングスタイルを使用しページ1にいる $pages = $pager_range->rangeAroundPage();

// Outputs: [1][2][3][4][5] echo ‘[‘. implode(‘][‘, $pages) .’]’;

レンジオブジェクトの範囲内で``Doctrine_Pager``をビルドする場合、APIによって``Doctrine_Pager_Range``サブクラスのインスタンスに関連する情報を読み取ることができます:

// このPager_Rangeに関連するページャーを返す

$pager_range->getPager();

// 新しいDoctrine_Pagerを定義する(自動的なprotectされたcall _initializedメソッド) pager_range->setPager(pager);

// 現在のPager_Rangeに割り当てられたオプションを返す $pager_range->getOptions();

// カスタムのDoctrine_Pager_Range実装のオフセットオプションを返す pager_range->getOption(option);

// 渡されたページがレンジの中にあるかチェックする pager_range->isInRange(page);

// 現在のページ周辺のレンジを返す // ($pager_rangeインスタンスに関連するDoctrine_Pagerから取得) $pager_range->rangeAroundPage();

ページャーによる高度なレイアウト

これまで、ページ分割と現在のページ周辺のレンジを読み取る方法を学びました。ページリンク生成を含むビジネスロジックを抽象化するために、``Doctrine_Pager_Layout``と呼ばれる強力なコンポーネントがあります。このコンポーネントのメインのアイディアはPHPロジックを抽象化してHTMLをDoctrineの開発者に定義させることです。

``Doctrine_Pager_Layout``は必須の引数を3つ受け取ります: a ``Doctrine_Pager``インスタンス、``Doctrine_Pager_Range``サブクラスインスタンスとテンプレートの{%url}マスクとして割り当てられるURLを含む文字列です。ご覧の通り、``Doctrine_Pager_Layout``の”変数”が2種類あります:

マスク

マスクはテンプレート内部で置き換えるものとして定義される文字列のピースです。これらは**{%mask_name}**として定義されオプションで定義するものもしくは``Doctrine_Pager_Layout``コンポーネントによって内部で定義されたものによって置き換えられます。現在、これらは内部マスクとして利用可能です:

  • **{%page}**はページ番号、すなわち、page_numberを保有しますが、別のマスクもしくは値のように振る舞う``addMaskReplacement()``で上書きできます。
  • **{%page_number}**は現在のページ番号を保存しますが、上書き可能ではありません
  • **{%url}**は``setTemplate()``と``setSelectedTemplate()``メソッドでのみ利用可能です。コンストラクタで定義され、処理されたURLを保有します
テンプレート

その名の通り、これはHTMLのスケルトンもしくはその他のスケルトンで``Doctrine_Pager_Range::rangeAroundPage()``サブクラスによって返されるそれぞれのページに適用されるその他のリソースです。定義できるテンプレートは3種類あります:

  • ``setTemplate()``は``Doctrine_Pager_Range::rangeAroundPage()``によって返されるすべてのページで使われるテンプレートを定義します。
  • 処理されるページが現在のページであるときに``setSelectedTemplate()``テンプレートは適用されます。何も定義されていない場合(空白文字もしくは定義無し)、``setTemplate()``で定義したテンプレートが使われます
  • ``setSeparatorTemplate()``セパレータテンプレートはそれぞれの処理されたページの間で適用される文字列です。最初のコールの前と最後のコールの後では含まれません。このメソッドの定義されたテンプレートはオプションによって影響を受けますが、マスクは処理できません。

``Doctrine_Pager_Layout``とこのコンポーネント周囲のタイプの作り方を理解したので、基本的な使い方を見てみましょう:

ページャーレイアウトの作り方は簡単です:

$pagerLayout = new Doctrine_Pager_Layout( new Doctrine_Pager(

Doctrine_Query::create() ->from( ‘User u’ ) ->leftJoin( ‘u.Group g’ ) ->orderby( ‘u.username ASC’ ), $currentPage, $resultsPerPage ), new Doctrine_Pager_Range_Sliding(array( ‘chunk’ => 5 )), ‘http://wwww.domain.com/app/User/list/page,{%page_number}’ );

ページリンク作成のためにテンプレートを割り当てます:

$pagerLayout->setTemplate(‘[{%page}]’);

$pagerLayout->setSelectedTemplate(‘[{%page}]’);

// Doctrine_Pagerインスタンスを読み取る $pager = $pagerLayout->getPager();

// ユーザーを取得する $users = $pager->execute(); // これも可能!

// ページリンクを表示する // 表示: [1][2][3][4][5] // $currentPageを除いて、すべてのページでリンクがつく(この例では、ページ1) $pagerLayout->display();

このソースを説明すると、最初の部分はページャーレイアウトのインスタンスを作成します。2番目に、すべてのページと現在のページ用のテンプレートを定義します。最後の部分では、``Doctrine_Pager``オブジェクトを読み取りクエリを実行し、変数``$users``を返します。最後のっ部分はオプションのマスク無しでディスプレイヤーを呼び出します。これは``Doctrine_Pager_Range::rangeAroundPage()``サブクラスで見つかるすべてのページにテンプレートを適用します。

ご覧の通り、内部マスク以外に他のマスクを使う必要はありません。既存のアプリケーションでUsersを検索機能を実装することを考えてみましょう。またページャーレイアウトでこの機能をサポートする必要があるとします。我々のケースを簡略化するために、検索パラメータは”search”と名付け、スーパーグローバル配列``$_GET``を通して受け取ります。他のページに送信できるようにするために、最初に行う必要のある変更は``Doctrine_Query``オブジェクトとURLを調整することです。

ページャーレイアウトを作成する:

pagerLayout = new Doctrine_Pager_Layout( new Doctrine_Pager( Doctrine_Query::create() ->from( 'User u' ) ->leftJoin( 'u.Group g' ) ->where('LOWER(u.username) LIKE LOWER(?)', array( '%'._GET[‘search’].’%’ ) ) ->orderby( ‘u.username ASC’ ), $currentPage, $resultsPerPage ), new Doctrine_Pager_Range_Sliding(array( ‘chunk’ => 5 )), ‘http://wwww.domain.com/app/User/list/page,{%page_number}?search={%search}’ );

コードを確認して``{%search}``と呼ばれる新しいマスクを追加したことに注目してください。後の段階で処理するテンプレートにこのマスクを送る必要があります。変更せずに、以前定義したように、テンプレートを割り当てます。そして、クエリの実行を変更する必要もありません。

ページリンク作成のためにテンプレートを割り当てます:

$pagerLayout->setTemplate(‘[{%page}]’);

$pagerLayout->setSelectedTemplate(‘[{%page}]’);

// Fetching users $users = $pagerLayout->execute();

foreach ($users as $user) { // ... }

display()``メソッドは作成したカスタムのマスクを定義する場所ですこのメソッドは2つのオプション引数を受け取ります: オプションマスクの1つの配列でスクリーンに出力される代わりに返される出力です。我々の場合、新しいマスクである``{%search}を定義する必要があります。このマスクはスーパーグローバル配列``$_GET``のsearchオフセットです。このマスクはURLとして送られるので、エンコードする必要があります。カスタムのマスクは「キー => 値」のペアで定義されます。ですので必要なコードはオフセットと置き換える値で配列を定義することです:

// Displaying page links

pagerLayout->display( array( 'search' => urlencode(_GET[‘search’]) ) );

``Doctrine_Pager_Layout``コンポーネントは定義されたリソースへのアクセサを提供します。ページャーとページャレンジを変数として定義してページャーレイアウトを送る必要はありません。これらのインスタンスは次のアクセサによって読み取られます:

// Pager_Layoutに関連するPagerを返す $pagerLayout->getPager();

// Pager_Layoutに関連するPager_Rangeを返す $pagerLayout->getPagerRange();

// Pager_Layoutに関連するURLマスクを返す $pagerLayout->getUrlMask();

// Pager_Layoutに関連するテンプレートを返す $pagerLayout->getTemplate();

// Pager_Layoutに関連する現在のページテンプレートを返す $pagerLayout->getSelectedTemplate();

// それぞれのページに適用されるSeparatorテンプレートを定義する pagerLayout->setSeparatorTemplate(separatorTemplate);

// Pager_Layoutに関連する現在のページテンプレートを返す $pagerLayout->getSeparatorTemplate();

// Pagerインスタンスを読み取らずにクエリを実行するハンディメソッド pagerLayout->execute(params = array(), $hydrationMode = null);

カスタムのレイアウト作成機能を作るために``Doctrine_Pager_Layout``を継承したい場合、利用可能な他のメソッドはたくさんあります。次のセクションでこれらのメソッドを見ます。

ページャーレイアウトをカスタマイズする

``Doctrine_Pager_Layout``は本当に良い仕事をしますが、ときに十分ではないことがあります。次のようなページ分割のレイアウトを作らなければならない状況を考えてみましょう:

<< < 1 2 3 4 5 > >>

現在、生の``Doctrine_Pager_Layout``では不可能ですが、このクラスを継承して利用可能なメソッドを使えば実現可能です。基底レイアウトクラスは独自の実装を作成するために使われるメソッドを提供します。内容は次の通りです:

// $thisはDoctrine_Pager_Layoutのインスタンスを参照する

// マスクの置き換えを定義する。テンプレートを解析するとき、置き換えマスクを // 新しいもの(もしくは値)に変換する。即座にマスクを変更できます this->addMaskReplacement(oldMask, $newMask, $asValue = false);

// マスク置き換えを削除する this->removeMaskReplacement(oldMask);

// すべてのマスク置き換えを削除する $this->cleanMaskReplacements();

// テンプレートを解析し処理されたページの文字列を返す this->processPage(options = array()); // 少なくとも配列$optionsのpage_numberで必要

// Protectされたメソッドであるが、とても便利

// 渡されたページのテンプレートを解析し処理されたテンプレートを返す this->_parseTemplate(options = array());

// 送られたオプションによって正しいテンプレートを返すようにURLマスクを解析する // 既に割り当てられたマスク置き換えを処理する this->_parseUrlTemplate(options = array());

// 与えられたページのマスク置き換えを解析する this->_parseReplacementsTemplate(options = array());

// 与えられたページのURLマスクを解析し処理されたURLを返す this->_parseUrl(options = array());

// 置き換え予定のマスクを新しいマスク/値に変更して、マスク置き換えを解析する this->_parseMaskReplacements(str);

``Doctrine_Pager_Layout``を継承するとき便利で小さなメソッドがあるので、実装されたクラスを見てみましょう:

class PagerLayoutWithArrows extends Doctrine_Pager_Layout { public

function display($options = array(), $return = false) { $pager = $this->getPager(); $str = ‘’;

    // 最初のページ
    $this->addMaskReplacement('page', '&laquo;', true);
    $options['page_number'] = $pager->getFirstPage();
    $str .= $this->processPage($options);

    // 以前のページ
    $this->addMaskReplacement('page', '&lsaquo;', true);
    $options['page_number'] = $pager->getPreviousPage();
    $str .= $this->processPage($options);

    // ページの一覧
    $this->removeMaskReplacement('page');
    $str .= parent::display($options, true);

    // 次のページ
    $this->addMaskReplacement('page', '&rsaquo;', true);
    $options['page_number'] = $pager->getNextPage();
    $str .= $this->processPage($options);

    // 最後のページ
    $this->addMaskReplacement('page', '&raquo;', true);
    $options['page_number'] = $pager->getLastPage();
    $str .= $this->processPage($options);

    // スクリーンに表示する代わりに値を返すことが可能
    if ($return) {
        return $str;
    }

    echo $str;
}

}

ご覧の通り、<<、<、>と>>のアイテムを手動で処理しなければなりません。生の値を設定することで**{%page}**マスクをオーバーライドします(生の値は3番目のパラメータをtrueとして設定します)。それからページを処理する必須情報のみを定義しこれを呼び出します。戻り値は文字列として処理されたテンプレートです。これをカスタムボタンにします。

これで全体的に異なる状況をサポートでいます。Doctrineは透過的なフレームワークですが、多くのユーザーはsymfonyと一緒に使います。``Doctrine_Pager``とサブクラスはsymfonyと100%互換性がありますが、``Doctrine_Pager_Layout``はsymfonyの``link_to``ヘルパー関数と連携するために調整が必要です。``Doctrine_Pager_Layout``でこれを使うことができるようにするにはこのクラスを継承しカスタムプロセッサーを追加しなければなりません。例として(symfonyと連携させる場合)、**{link_to}...{/link_to}**をテンプレートプロセッサーとして使います。継承クラスとsymfonyでの使い方は次の通りです:

class sfDoctrinePagerLayout extends Doctrine_Pager_Layout { public

function __construct($pager, $pagerRange, urlMask) { sfLoader::loadHelpers(array('Url', 'Tag')); parent::__construct(pager, $pagerRange, $urlMask); }

protected function _parseTemplate($options = array())
{
    $str = parent::_parseTemplate($options);

    return preg_replace(
        '/\{link_to\}(.*?)\{\/link_to\}/', link_to('$1', $this->_parseUrl($options)), $str
    );
}

}

使い方:

$pagerLayout = new sfDoctrinePagerLayout( $pager, new

Doctrine_Pager_Range_Sliding(array(‘chunk’ => 5)), '@hostHistoryList?page={%page_number}’ );

$pagerLayout->setTemplate(‘[{link_to}{%page}{/link_to}]’);

Facade

データベースの作成と削除

Doctrineは接続からデータベースを作成したり削除する機能を提供します。これを使うためのしかけはDoctrineの接続名がデータベースの名前でなければならないことです。これが必須なのはPDOは接続するデータベースの名前を読み取るメソッドを提供しないことによります。データベースの作成と削除をできるようにするにはDoctrine自身がデータベースの名前を認識できなければなりません。

コンビニエンスメソッド

Doctrineはメインクラスで利用可能なスタティックなコンビニエンスメソッドを提供します。これらのメソッドはDoctrineの最もよく使われる複数の機能を1つのメソッドで実行します。これらのメソッドの大半は``Doctrine_Task``システムを使用します。これらのタスクは``Doctrine_Cli``からも実行されます。

// デバッグモードをon/offに切り替えこれがon/offであるかチェックする

Doctrine_Core::debug(true);

if (Doctrine_Core::debug() { echo ‘debugging is on’; } else { echo ‘debugging is off’; }

// Doctrineライブラリへのパスを取得する $path = Doctrine_Core::getPath();

// Doctrineライブラリへのパスがデフォルトの位置ではない場合パスをセットする Doctrine_Core::setPath(‘/path/to/doctrine/libs’);

// Doctrineと連携させるためにモデルをロードする // 発見されロードされたDoctrine_Recordsの配列を返す models = Doctrine_Core::loadModels('/path/to/models', Doctrine_CoreMODEL_LOADING_CONSERVATIVE); // or Doctrine_Core::MODEL_LOADING_AGGRESSIVE print_r(models);

// ロードされたすべてのモデルの配列を取得する $models = Doctrine_Core::getLoadedModels();

// クラスの配列を上記のメソッドに渡しDoctrine_Recordsではないものを除去する models = Doctrine_Core::filterInvalidModels(array('User', 'Formatter', 'Doctrine_Record')); print_r(models); // FormatterとDoctrine_Recordが有効ではないのでarray(‘User’)を返す

// 実際のテーブル名用のDoctrine_Connectionオブジェクトを取得する $conn = Doctrine_Core::getConnectionByTableName(‘user’); // テーブル名が関連する接続オブジェクトを返す with.

// 既存のデータベースからYAMLスキーマを生成する Doctrine_Core::generateYamlFromDb(‘/path/to/dump/schema.yml’, array(‘connection_name’), $options);

// 既存のデータベースからモデルを生成する Doctrine_Core::generateModelsFromDb(‘/path/to/generate/models’, array(‘connection_name’), $options);

// オプションとデフォルト値の配列 $options = array(‘packagesPrefix’ => ‘Package’, ‘packagesPath’ => ‘’, ‘packagesFolderName’ => ‘packages’, ‘suffix’ => ‘.php’, ‘generateBaseClasses’ => true, ‘baseClassesPrefix’ => ‘Base’, ‘baseClassesDirectory’ => ‘generated’, ‘baseClassName’ => ‘Doctrine_Record’);

// YAMLスキーマからモデルを生成する Doctrine_Core::generateModelsFromYaml(‘/path/to/schema.yml’, ‘/path/to/generate/models’, $options);

// 配列で提供されるテーブルを作成する Doctrine_Core::createTablesFromArray(array(‘User’, ‘Phoneumber’));

// 既存のモデルセットからすべてのテーブルを作成する // ディレクトリが渡されなければロードされたすべてのモデル用のSQLを生成する Doctrine_Core::createTablesFromModels(‘/path/to/models’);

// 既存のモデルのセットからSQLコマンドの文字列を生成する // ディレクトリが渡されなければロードされたすべてのモデル用のSQLを生成する Doctrine_Core::generateSqlFromModels(‘/path/to/models’);

// 渡されたモデルの配列を作成するSQL文の配列を生成する Doctrine_Core::generateSqlFromArray(array(‘User’, ‘Phonenumber’));

// 既存のモデルセットからYAMLスキーマを生成する Doctrine_Core::generateYamlFromModels(‘/path/to/schema.yml’, ‘/path/to/models’);

// 接続用のすべてのデータベースを作成する // 接続名の配列はオプション Doctrine_Core::createDatabases(array(‘connection_name’));

// 接続に対するすべてのデータベースを削除する // 接続名の配列はオプション Doctrine_Core::dropDatabases(array(‘connection_name’));

// モデル用のすべてのデータをYAMLフィクスチャファイルにダンプする // 2番目の引数はbool値でそれぞれのモデルに大して個別のフィクスチャファイルを生成するかどうか // trueの場合ファイルの代わりにフォルダを指定する必要がある Doctrine_Core::dumpData(‘/path/to/dump/data.yml’, true);

// YAMLフィクスチャファイルからデータをロードする // 2番目の引数はブール値でロードするときにデータを追加するかロードする前にすべてのデータを最初に削除するか Doctrine_Core::loadData(‘/path/to/fixture/files’, true);

// マイグレーションクラスのセット用のマイグレーション処理を実行する $num = 5; // バージョン #5にマイグレートする Doctrine::migration(‘/path/to/migrations’, $num);

// 空白のマイグレーションクラスのテンプレートを生成する Doctrine_Core::generateMigrationClass(‘ClassName’, ‘/path/to/migrations’);

// 既存のデータベース用のすべてのマイグレーションクラスを生成する Doctrine_Core::generateMigrationsFromDb(‘/path/to/migrations’);

// 既存のモデルのセット用のすべてのマイグレーションクラスを生成する // 2番目の引数はloadModels()を使用して既にモデルをロードしている場合のオプション Doctrine_Core::generateMigrationsFromModels(‘/path/to/migrations’, ‘/path/to/models’);

// モデル用のDoctrine_Tableインスタンスを取得する $userTable = Doctrine_Core::getTable(‘User’);

// Doctrineを単独のPHPファイルにコンパイルする $drivers = array(‘mysql’); //コンパイルされたバージョンに含めたいドライバの配列を指定する Doctrine_Core::compile(‘/path/to/write/compiled/doctrine’, $drivers);

// デバッグ用にDoctrineオブジェクトをダンプする conn = Doctrine_Manager::connection(); Doctrine_Core::dump(conn);

タスク

タスクはコアのコンビニエンスメソッドを搭載するクラスです。必須の引数を設定することでタスクを簡単に実行できます。これらのタスクはDoctrineコマンドラインインターフェイスで直接使われます。

BuildAll BuildAllLoad BuildAllReload Compile CreateDb CreateTables Dql

DropDb DumpData Exception GenerateMigration GenerateMigrationsDb GenerateMigrationsModels GenerateModelsDb GenerateModelsYaml GenerateSql GenerateYamlDb GenerateYamlModels LoadData Migrate RebuildDb

独自スクリプトでDoctrine Tasksを単独で実行する方法は下記の通りです。

コマンドラインインターフェイス

はじめに

``Doctrine_Cli``はタスクのコレクションで開発とテストの手助けをしてくれます。このマニュアルの典型例に関して、必要なタスクを実行するためにPHPスクリプトをセットアップします。このcliツールはこれらのタスクのためにそのまま使えることを目的としています。

タスク

Doctrineの実装を管理するために利用できるタスクの一覧は下記の通りです。

$ ./doctrine 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

CLI用のタスクは独立しており単独で使うことができます。下記のコードは例です。

$task = new Doctrine_Task_GenerateModelsFromYaml();

$args = array(‘yaml_schema_path’ => ‘/path/to/schema’, ‘models_path’ => ‘/path/to/models’);

task->setArguments(args);

try { if ($task->validate()) { $task->execute(); } } catch (Exception e) { throw new Doctrine_Exception(e->getMessage()); }

使い方

“doctrine”という名前のファイルを実行可能にします。

#!/usr/bin/env php

``Doctrine_Cli``を実装する実際の”doctrine.php”という名前のPHPファイルは次の通りです。

// Doctrineの設定/セットアップ、接続、モデルなどを含める

// Doctrine Cliを設定する // 通常cliタスクの引数がありますがここで設定すれば引数は自動的に入力されスクリプト実行時に入力する必要がなくなる

$config = array(‘data_fixtures_path’ => ‘/path/to/data/fixtures’, ‘models_path’ => ‘/path/to/models’, ‘migrations_path’ => ‘/path/to/migrations’, ‘sql_path’ => ‘/path/to/data/sql’, ‘yaml_schema_path’ => ‘/path/to/schema’);

cli = new Doctrine_Cli(config); cli->run(_SERVER[‘argv’]);

これで次のようにコマンドを実行できます。

./doctrine generate-models-yaml ./doctrine create-tables

サンドボックス

インストール方法

http://www.doctrine-project.org/download からもしくはsvnリポジトリから特別なサンドボックスをインストールできます。

doctrine/tools/sandbox chmod 0777 doctrine

./doctrine

上記のステップによってサンドボックスのcliが実行できるようになります。引数無しで./doctrineコマンドを実行すると利用可能なすべてのcliタスクのインデックスが表示されます。

まとめ

この章で検討したこれらのユーティリティが役に立つことを願います。[doc unit-testing :name]を使用することでDoctrineの安定性を維持し回帰を避ける方法を検討します。

Doctrineはユニットテストを使用するプログラムでテストされます。Wikipediaでユニットテストの[http://en.wikipedia.org/wiki/Unit_testing 記事]を読むことができます。

テストを実施する

Doctrineに搭載されるテストを実施するにはライブラリフォルダだけでなくプロジェクト全体をチェックアウトする必要があります。

/path/to/co/doctrine

Doctrineのチェックアウトのディレクトリに移動します。

$ cd /path/to/co/doctrine

次のファイルとディレクトリが見えます。

CHANGELOG COPYRIGHT lib/ LICENSE package.xml tests/ tools/ vendor/

Tip

認識できるテストの失敗ケースが用意されるのはめったにありません。もしくはDoctrineには後のバージョンまでコミットできないバグ修正もしくは強化リクエスト用のテストケースが存在します。もしくは単に問題の修正コードが存在しないので失敗のままのテストもあります。Doctrineのそれぞれのバージョンでどれだけの数のテストの失敗ケースがあるのかメーリングリストもしくはIRCでたずねることができます。

CLI

コマンドラインでテストを実施するには、php-cliをインストールしなければなりません。

``/path/to/co/doctrine/tests``フォルダに移動して``run.php``スクリプトを実行します:

$ cd /path/to/co/doctrine/tests $ php run.php

これによってすべてのユニットテストを実行しているときに進行バーが出力されます。テストが終了したとき何が成功し何が失敗したのか報告されます。

CLIには特定のテスト、テストのグループを実施するもしくはテストスィートの名前でテストをフィルタリングするためのオプションがありあす。これらのオプションを確認するために次のコマンドを実行してください。

$ php run.php -help

次のように個別のテストグループを実行できます:

$ php run.php –group data_dict
ブラウザ

``doctrine/tests/run.php``に移動すればブラウザでユニットテストを実施できます。オプションは変数``_GET``を通して設定できます。

例:

  • http://localhost/doctrine/tests/run.php

  • http://localhost/doctrine/tests/run.php?filter=Limit&group[]=query&group[]=record

    CAUTION テストの結果が環境に大きく左右されることがあることにご注意ください。例えば``php.ini``の``apc.enable_cli``ディレクティブが0に設定されている場合追加テストが失敗することがあります。

テストを書く

テストスィートを書き始めるとき、``TemplateTestCase.php``をコピーすることから始めます。サンプルのテストケースは次の通りです:

class Doctrine_Sample_TestCase extends Doctrine_UnitTestCase {

public function prepareTables() { $this->tables[] = “MyModel1”; $this->tables[] = “MyModel2”; parent::prepareTables(); }

public function prepareData()
{
  $this->myModel = new MyModel1();
  //$this->myModel->save();
}

public function testInit()
{

}

// This produces a failing test
public function testTest()
{
    $this->assertTrue($this->myModel->exists());
    $this->assertEqual(0, 1);
    $this->assertIdentical(0, '0');
    $this->assertNotEqual(1, 2);
    $this->assertTrue((5 < 1));
    $this->assertFalse((1 > 2));
}

}

class Model1 extends Doctrine_Record { }

class Model2 extends Doctrine_Record { }

NOTE モデルの定義はテストケースファイルに直接含まれるもしくはこれらは``/path/to/co/doctrine/tests/models``に設置可能です。そうすればこれらはオートロードされます。

テストを書く作業を終えたら必ず``run.php``に次のコードを追加してください。

$test->addTestCase(new Doctrine_Sample_TestCase());

run.phpを実行するとき新しい失敗ケースが報告されます。

チケットテスト

Doctrineにおいてtracに報告される個別のチケット用のテストの失敗ケースをコミットするのが慣行になっています。``/path/to/co/doctrine/tests/Ticket/``フォルダで見つかるすべてのテストケースを読むことでこれらの手数とケースは自動的にrun.phpに追加されます。

CLIから新しいテストケースのチケットを作成できます:

$ php run.php –ticket 9999

チケット番号9999がまだ存在しない場合空白のテストケースクラスが``/path/to/co/doctrine/tests/Ticket/9999TestCase.php``に生成されます。

class Doctrine_Ticket_9999_TestCase extends Doctrine_UnitTestCase {

}

テスト用のメソッド
Equalをアサートする
// ... public function test1Equals1() { $this->assertEqual(1, 1); } //

...

Not Equalをアサートする
// ... public function test1DoesNotEqual2() { $this->assertNotEqual(1,

2); } // ...

Identicalをアサートする

ロジックがより厳密で2つの値の比較に``===``を使用すること以外``assertIdentical()``メソッドは``assertEqual()``と同じです。

// ... public function testAssertIdentical() {

$this->assertIdentical(1, ‘1’); } // ...

NOTE 1番目の引数の数字の1がPHPの整数型としてキャストされるのに対して2番目の引数の数字の1はPHPの文字列型としてキャストされるので明らかに失敗します。
Trueをアサートする
// ... public function testAssertTrue() { $this->assertTrue(5 > 2); }

// ...

Falseをアサートする
// ... public function testAssertFalse() { $this->assertFalse(5 < 2); }

// ...

モックドライバ

Doctrineはsqlite以外のすべてのドライバ用のモックドライバを使用します。次のコードスニペットはモックドライバの使い方を示します:

class Doctrine_Sample_TestCase extends Doctrine_UnitTestCase {

public function testInit() { $this->dbh = new Doctrine_Adapter_Mock(‘oracle’); this->conn = Doctrine_Manager::getInstance()->openConnection(this->dbh); } }

クエリを実行するときこれらは本当のデータベースに対して実行されません。代わりにこれらは配列に収集され実行されたクエリとそれらに対するテストのアサーションを分析できます。

class Doctrine_Sample_TestCase extends Doctrine_UnitTestCase { //

...

public function testMockDriver()
{
    $user = new User();
    $user->username = 'jwage';
    $user->password = 'changeme';
    $user->save();

    $sql = $this->dbh->getAll();

    // 探しているクエリを見つけるためにSQL配列を出力する
    // print_r($sql);

    $this->assertEqual($sql[0], 'INSERT INTO user (username, password) VALUES (?, ?)');
}

}

テストクラスのガイドライン

すべてのクラスはTestCaseと同等のものを少なくとも1つ持ち``Doctrine_UnitTestCase``を継承します。テストクラスはクラスもしくはクラスのアスペクトを参照し、それに応じて命名されます。

例:

  • ``Doctrine_Record_TestCase``は``Doctrine_Record``クラスを指し示すので良い名前です。
  • ``Doctrine_Record_State_TestCase``は``Doctrine_Record``クラスの状態を指し示すのでこれも良い名前です。
  • ``Doctrine_PrimaryKey_TestCase``は一般的すぎるので悪い名前です。
テストメソッドのガイドライン

メソッドはアジャイルなドキュメントをサポートし何が失敗したのか明確にわかるように名付けられます。これらはテストするシステムの情報も提供します。

例えばメソッドのテスト名として``Doctrine_Export_Pgsql_TestCase::testCreateTableSupportsAutoincPks()``は良い名前です。

テストメソッドの名前は長くなる可能性がありますが、メソッドの内容は長くならないようにすべきです。複数のアサート呼び出しが必要なら、メソッドを複数のより小さなメソッドに分割します。ロープもしくは関数の中にアサーションがあってはなりません。

NOTE 共通の命名規約として使われる``TestCase::test[methodName]``はDoctrineでは**されません**。ですのでこのケースでは``Doctrine_Export_Pgsql_TestCase::testCreateTable()``は許可されません!

まとめ

Doctrineのようなソフトウェアのピースにとってユニットテストは非常に重要です。これ無しでは、変更によって既存の作業ユースケースに悪影響があるのかを知るのは不可能です。ユニットテストのコレクションによって変更が既存の機能を壊さないことを確認できます。

次に[doc improving-performance パフォーマンスを改善する]方法を学ぶために移動します。

はじめに

大きなアプリケーションにとってパフォーマンスはとても重要な側面です。Doctrineはオブジェクトリレーショナルマッピングと同様に大きなデータベース抽象化レイヤーを提供する抽象化ライブラリです。これがポータビリティと開発のしやすさを提供する一方でパフォーマンスの観点から欠点になるのは避けられません。この章では読者がDoctrineのベストなパフォーマンスを得られるようにするためのお手伝いを試みます。

コンパイル

Doctrineはとても大きなフレームワークなのでそれぞれのリクエストごとにたくさんのファイルがインクルードされます。これは大きなオーバーヘッドをもたらします。実際これらのファイルオペレーションはデータベースに複数のクエリを送信するのと同じくらい時間がかかります。開発環境ではファイルごとにクラスを明確に分離する方法はうまくゆきますが、プロジェクトが商用ディストリビューションになるとき動作速度がファイルごとのクラス分離の原則よりも優先されます。

Doctrineはこの問題を解決するために``compile()``と呼ばれるメソッドを提供します。compileメソッドは最もよく使われるDoctrineコンポーネントの単独のファイルを作成します。デフォルトではこのファイルは``Doctrine.compiled.php``という名前でDoctrineのrootに作成されます。

コンパイルは複数のファイル(最悪の場合数十のファイル)の代わりにコンパイルされたファイルを含む最も使われるDoctrineのランタイムコンポーネントの単独ファイルを作成するための手段で桁違いのパフォーマンスを改善できます。失敗ケースでは、``Doctrine_Exception``が詳細なエラーを投げます。

Doctrineのコンパイルを処理する``compile.php``という名前のコンパイルスクリプトを作りましょう:

// compile.php

require_once(‘/path/to/doctrine/lib/Doctrine.php’); spl_autoload_register(array(‘Doctrine’, ‘autoload’));

Doctrine_Core::compile(‘Doctrine.compiled.php’);

``compile.php``を実行すると``Doctrine.compiled.php``ファイルが``doctrine_test``フォルダのrootに生成されます:

$ php compile.php

使用しているデータベースドライバのみをコンパイルしたいのであれば``compile()``に2番目の引数としてドライバの配列を渡すことができます。この例ではMySQLのみを使用しているのであればDoctrineに``mysql``ドライバのみをコンパイルするように伝えましょう:

// compile.php

// ...

Doctrine_Core::compile(‘Doctrine.compiled.php’, array(‘mysql’));

コンパイルされたDoctrineを含めるために``bootstrap.php``スクリプトを変更できます:

// bootstrap.php

// ...

require_once(‘Doctrine.compiled.php’);

// ...

コンサーバティブな取得

おそらく最も重要なルールはコンサーバティブなモードで実際に必要なデータのみを取得することです。ささいなことに聞こえるかもしれませんができることに対する無精や知識の欠如は必要のないオーバーヘッドにつながることがよくあります。

例を見てみましょう:

$record = table->find(id);

このようにコードを書くのはどのくらいの頻度でしょうか?便利ですが望むものではありません。上記の例はデータベースからレコードのすべてのカラムを引き出し新しく作成されたオブジェクトにこのデータを投入します。これは不必要なネットワークトラフィックだけでなくDoctrineデータを決して使われないオブジェクトに投入しなければならないことも意味します。

次のようなクエリが理想的ではない理由は読者のみなさんはみんなご存知だと思います:

SELECT * FROM my_table

上記のコードはどのアプリケーションでも悪いものでDoctrineを使う際にも当てはまります。オブジェクトに必要のないデータを投入するのは時間の無駄使いになるのでDoctrineを使うとより悪くなります。

このカテゴリに所属する別の重要なルールは: **本当に必要なときだけオブジェクトを取得する**ことです。Doctrineはオブジェクトグラフの代わりに”配列グラフ”を取得する機能を持ちます。最初は奇妙なことに聞こえるかもしれません。ではなぜオブジェクトリレーショナルマッパーを使うのかもう一度考えてみましょう。PHPは優れたOOPのために多くの機能で強化された手続き型の言語です。それでも配列はPHPで使うことのできる最も効率的なデータ構造です。複雑なビジネスロジックを実現する際にオブジェクトは最も価値があります。データをオブジェクト構造にラッピングしても恩恵がない場合はリソースの無駄使いです。記事用の関連データを持つすべてのコメントを取得する次のコードを見てみましょう。後で表示するためにこれらをビューに渡します:

$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));

最新のblog投稿をレンダリングするビューもしくはテンプレートを想像してください:

    • Posted on by .

      ()

    ビューの中で配列の代わりにオブジェクトを使う利点を想像できますか?ビューの中でビジネスロジックを実行しているわけではないですよね?1つのパラメータによってたくさんの不必要な処理をしなくて済みます:

    // ...

    $blogPosts = $q->execute(array(1), Doctrine_Core::HYDRATE_ARRAY);

    望むのであれば``setHydrationMethod()``メソッドを使うこともできます:

    // ...

    $q->setHydrationMode(Doctrine_Core::HYDRATE_ARRAY);

    $blogPosts = $q->execute(array(1));

    上記のコードはデータをオブジェクトではなくはるかに負荷が低い配列にハイドレイトします。

    NOTE 配列のハイドレーションに関して1つの素晴らしいことはオブジェクトで``ArrayAccess``を使用する場合配列のハイドレーションを使用するようにクエリを切り替えばコードはまったく同じように動作します。例えば最新のblog投稿をレンダリングするために書いた上記のコードはその背後のクエリを配列のハイドレーションに切り替えるときに動作します。

    ときに、オブジェクトや配列ではなくPDOから直接出力したいことがあります。これを行うには、ハイドレーションモードを **``Doctrine_Core::HYDRATE_NONE``**に切り替えます。例は次の通りです:

    $q = Doctrine_Query::create() ->select(‘SUM(d.amount)’)

    ->from(‘Donation d’);

    $results = $q->execute(array(), Doctrine_Core::HYDRATE_NONE);

    結果を出力するとDQLクエリに依存する配列形式の値が見つかります:

    print_r($results);

    この例では結果は次のコードでアクセスできます:

    $total = $results[0][1];
  • クラスファイルをバンドルする

    Doctrineもしくは他の大きなOOライブラリもしくはフレームワークを使うとき通常のHTTPリクエストでインクルードされるファイルの数は秘女に大きくなります。1つのリクエストで50-100のファイルがインクルードされるのも珍しいことではありません。これはたくさんのディスクオペレーションにつながるのでパフォーマンスに大きな影響を及ぼします。これは一般的に開発環境では問題ありませんが、本番環境には適していません。この問題に対処するための推奨方法はもっともよく使われるライブラリのクラスを本番環境用に1つのファイルにまとめ、不要なホワイトスペース、改行とコメントを剥ぎ取ります。この方法によってバイトコードキャッシュ無しでも大きなパフォーマンスの改善ができます(次のセクションをご覧ください。このようなバンドルを作成するベストな方法は自動化ビルド処理、例えばPhingによる方法です。

    バイトコードキャッシュを使用する

    APCのようなバイトコードキャッシュは実行に先駆けてPHPによって生成されます。このことはファイルの解析とバイトコードの作成は毎回のリクエストごとではなく一度だけ行われることを意味します。これはとりわけ大きなライブラリ/フレームワークを利用しているときに役立ちます。本番環境用のファイルを利用することで大きなパフォーマンスの改善ができます。キャッシュを最適化するための設定オプションがたくさんあるのでバイトコードを最大限活用するにはマニュアルページを調べる必要があります。

    オブジェクトを開放する

    バージョン5.2.5に関して、PHPは循環参照を持つオブジェクトグラフのガーベッジコレクションを行うことができません。例えば親が子への参照を持ち子が親に参照を持つ場合です。多くのDoctrineオブジェクトモデルがこのようなリレーションを持つので、オブジェクトがスコープの外側にゆくときでさえPHPはメモリーを解放しません。

    大抵のPHPアプリケーションに関して、この問題はほとんど関係しません。PHPスクリプトの実行時間が短い傾向にあるからです。長い実行時間のスクリプト、例えば、バルクデータインポーターやエクスポーターなどの実行時間の長いスクリプトは循環参照のチェーンを破棄しない限りメモリを使い果たしてしまうことがあります。Doctrineは``Doctrine_Record``、Doctrine\_Collection、と``Doctrine_Query``で``free()``メソッドを提供します。このメソッドはこれらのオブジェクト上の循環参照を削除します。ガベージコレクション用にこれらを開放します。使い方は次の通りです:

    大量のレコードを挿入するときにオブジェクトを解放します:

    for ($i = 0; $i < 1000; $i++) { $object = createBigObject();

    $object->save(); $object->free(true); }

    同じ方法でクエリオブジェクトを解放することもできます:

    for ($i = 0; $i < 1000; $i++) { $q = Doctrine_Query::create()

    ->from(‘User u’);

    $results = $q->fetchArray(); $q->free(); }

    もしくはループの中のそれぞれのクエリに対して同じクエリオブジェクトを使う場合よりベターです:

    $q = Doctrine_Query::create() ->from(‘User u’);

    for ($i = 0; $i < 1000; $i++) { $results = $q->fetchArray(); $q->free(); }

    他のティップス

    DQLパーサーを手助けする

    DQLを使用する際に可能な方法は2つあります。最初の方法はプレーンなDQLクエリを書きこれらを``Doctrine_Connection::query($dql)``に渡すことです。2番目の方法は``Doctrine_Query``オブジェクトと流れるようなインターフェイスを使うことです。とてもシンプルなクエリ以外は後者の方が望ましいです。これは``Doctrine_Query``オブジェクトとそのメソッドを利用することでDQLパーサーの負担が少し減るからです。これは解析する必要のあるクエリの量を減らすのでそれゆえ速くなります。

    効率的なリレーションのハンドリング

    2つのコンポーネントの間のリレーションを追加したい場合次のようなことを行うべきでは**ありません**:

    NOTE 次の例は``Role``と``User``の多対多のリレーションを想定します。

    $role = new Role(); $role->name = ‘New Role Name’;

    $user->Roles[] = $newRole;

    CAUTION 上記のコードはロールがまだロードされていない場合データベースからすべてのロールをロードします!1つの新しいリンクを追加するためだけに!

    代わりに次の方法が推奨されます:

    $userRole = new UserRole(); $userRole->role_id = $role_id;

    $userRole->user_id = $user_id; $userRole->save();

    まとめ

    Doctrineのパフォーマンスを改善するメソッドはたくさん存在します。この章で説明されたメソッドを検討することを多いに推奨します。

    Doctrineで使われている[doc technology テクノロジー]を学ぶために次の章に移動します。

    はじめに

    Doctrineは多くの人の作業の成果物です。他の言語のORMは開発者の学習のための主要なリソースです。

    NOTE Doctrineは車輪の再発明をする代わりに他のオープンソースのプロジェクトからコードのピースも借りました。コードを借りた2つのプロジェクトは[http://www.symfony-project.com symfony]と[http://framework.zend.com Zend Framework]です。Doctrineのライセンス情報は``LICENSE``という名前のファイルで見つかります。

    アーキテクチャ

    Doctrineは3つのパッケージ: CORE、ORMとDBALに分割されます。下記のリストはそれぞれのパッケージを構成するメインクラスの一部のリストは下記の通りです。

    Doctrine CORE
    • Doctrine
    • [doc component-overview:manager Doctrine_Manager]
    • [doc component-overview:connection Doctrine_Connection]
    • [doc improving-performance:compile Doctrine_Compiler]
    • [doc exceptions-and-warnings Doctrine_Exception]
    • Doctrine_Formatter
    • Doctrine_Object
    • Doctrine_Null
    • [doc event-listeners Doctrine_Event]
    • Doctrine_Overloadable
    • Doctrine_Configurable
    • [doc event-listeners Doctrine_EventListener]
    Doctrine DBAL
    • [doc component-overview:record:using-expression-values Doctrine_Expression_Driver]
    • [doc database-abstraction-layer:export Doctrine_Export]
    • [doc database-abstraction-layer:import Doctrine_Import]
    • Doctrine_Sequence
    • [doc transactions Doctrine_Transaction]
    • [doc database-abstraction-layer:datadict Doctrine_DataDict]

    Doctrine DBALはドライバパッケージにも分割されます。

    Doctrine ORM
    • [doc component-overview:record Doctrine_Record]
    • [doc component-overview:table Doctrine_Table]
    • [doc defining-models:relationships Doctrine_Relation]
    • [doc component-overview:record:using-expression-values Doctrine_Expression]
    • [doc dql-doctrine-query-language Doctrine_Query]
    • [doc native-sql Doctrine_RawSql]
    • [doc component-overview:collection Doctrine_Collection]
    • Doctrine_Tokenizer

    その他のパッケージ。

    • [doc data-validation Doctrine_Validator]
    • Doctrine_Hook
    • [doc component-overview:views Doctrine_View]

    Doctrine用のビヘイビアもあります:

    • [doc behaviors:core-behaviors:geographical :name]
    • [doc behaviors:core-behaviors:i18n :name]
    • [doc behaviors:core-behaviors:nestedset :name]
    • [doc behaviors:core-behaviors:searchable :name]
    • [doc behaviors:core-behaviors:sluggable :name]
    • [doc behaviors:core-behaviors:softdelete :name]
    • [doc behaviors:core-behaviors:timestampable :name]
    • [doc behaviors:core-behaviors:versionable :name]

    デザインパターン

    使用されている``GoF (Gang of Four)``デザインパターン:

    使用されているエンタープライズアプリケーションデザインパターン:

    動作速度

    • 遅延初期化 - コレクション要素
    • Subselectの取得 - Doctrineはsubselectを使用してコレクションを効率的に取得する方法を知っている。
    • 必要なときに、SQLステートメントの遅延実行 : 実際に必要になるまで接続はINSERTもしくはUPDATEを発行しません。ですので例外が起きてトランザクションを停止させる必要がある場合、一部のステートメントは実際に発行されることはありません。さらに、これによってデータベースのロック時間をできるかぎり短く保ちます(遅延UPDATEからトランザクションの終了まで)。
    • Joinの取得 - Doctrineはjoinとsubselectを使用して複雑なオブジェクトグラフを取得する方法を知っている
    • 複数のコレクション取得戦略 - Doctrineはパフォーマンスチューニングのための複数のコレクション取得戦略を持ちます。
    • 取得戦略の動的なミックス - 取得戦略は組み合わせ可能で例えばユーザーがバッチコレクションで取得可能である一方でユーザーの電話番号が1つのクエリのみを使用してオフセットコレクションでロードできます。
    • ドライバ固有の最適化 - Doctrineはmysqlのbulk-insertを知っています。
    • トランザクションの単発削除 - Doctrineは削除リストの追加オブジェクトのすべての主キーを集めテーブルごとに1つのdelete文のみを実行する方法を知っています。
    • 修正されたカラムのみを更新する - Doctrineはどのカラムが変更されたのか常に知っています。
    • 未修正オブジェクトを挿入/更新しない - Doctrineはレコードの状態が変更されたか知っています。
    • データベース抽象化のためのPDO - PDOはPHPの最速のデータベース抽象化レイヤーです。

    まとめ

    この章ではDoctrineのコンポーネントの完全な鳥瞰図と編成の情報を提供します。これまでこれらを個別の部分として見てきましたが3つのメインパッケージの個別のリストによってこれまでわからなかったことが明らかになります。

    次に例外の扱い方を学ぶために[doc exceptions-and-warnings :name]の章に移動します。

    マネージャーの例外

    接続管理で何かがエラーになると``Doctrine_Manager_Exception``が投げられます。

    try { $manager->getConnection(‘unknown’); } catch

    (Doctrine_Manager_Exception) { // エラーを補足する }

    リレーションの例外

    リレーションの解析の間にエラーになるとリレーションの例外が投げられます。

    接続の例外

    データベースレベルで何かがエラーになると接続例外が投げられます。Doctrineは完全にデータベースにポータルなエラーハンドリングを提供します。このことはsqliteやその他のデータベースを使っていようが起きたエラーに関するポータブルなエラーとメッセージを常に得られることを意味します。

    try { $conn->execute(‘SELECT * FROM unknowntable’); } catch

    (Doctrine_Connection_Exception $e) { echo ‘Code : ‘ . $e->getPortableCode(); echo ‘Message : ‘ . $e->getPortableMessage(); }

    クエリの例外

    DQLクエリが無効な場合にクエリが実行されるときに例外が投げられます。

    まとめ

    Doctrineの例外を扱い方を学んだので[doc real-world-examples 実際の世界のスキーマ]の章に移動して今日のウェブで見つかる共通のウェブアプリケーションで使われている例を見ます。

    ユーザー管理システム

    ほとんどすべてのアプリケーションではユーザー、ロール、パーミッションなど何らかのセキュリティもしくは認証システムを提供する必要があります。基本的なユーザー管理とセキュリティシステムを提供するいくつかのモデルの例です。

    class User extends Doctrine_Record { public function

    setTableDefinition() { $this->hasColumn(‘username’, ‘string’, 255, array( ‘unique’ => true ) ); $this->hasColumn(‘password’, ‘string’, 255); } }

    class Role extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); } }

    class Permission extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘name’, ‘string’, 255); } }

    class RolePermission extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘role_id’, ‘integer’, null, array( ‘primary’ => true ) ); $this->hasColumn(‘permission_id’, ‘integer’, null, array( ‘primary’ => true ) ); }

    public function setUp()
    {
        $this->hasOne('Role', array(
                'local' => 'role_id',
                'foreign' => 'id'
            )
        );
        $this->hasOne('Permission', array(
                'local' => 'permission_id',
                'foreign' => 'id'
            )
        );
    }
    

    }

    class UserRole extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’, null, array( ‘primary’ => true ) ); $this->hasColumn(‘role_id’, ‘integer’, null, array( ‘primary’ => true ) ); }

    public function setUp()
    {
        $this->hasOne('User', array(
                'local' => 'user_id',
                'foreign' => 'id'
            )
        );
        $this->hasOne('Role', array(
                'local' => 'role_id',
                'foreign' => 'id'
            )
        );
    }
    

    }

    class UserPermission extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’, null, array( ‘primary’ => true ) ); $this->hasColumn(‘permission_id’, ‘integer’, null, array( ‘primary’ => true ) ); }

    public function setUp()
    {
        $this->hasOne('User', array(
                'local' => 'user_id',
                'foreign' => 'id'
            )
        );
        $this->hasOne('Permission', array(
                'local' => 'permission_id',
                'foreign' => 'id'
            )
        );
    }
    

    }

    YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

    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:

    フォーラムアプリケーション

    カテゴリ、ボードと、スレッドと投稿機能を持つフォーラムアプリケーションの例は次の通りです:

    class Forum_Category extends Doctrine_Record { public function

    setTableDefinition() { $this->hasColumn(‘root_category_id’, ‘integer’, 10); $this->hasColumn(‘parent_category_id’, ‘integer’, 10); $this->hasColumn(‘name’, ‘string’, 50); $this->hasColumn(‘description’, ‘string’, 99999); }

    public function setUp()
    {
        $this->hasMany('Forum_Category as Subcategory', array(
                'local' => 'parent_category_id',
                'foreign' => 'id'
            )
        );
        $this->hasOne('Forum_Category as Rootcategory', array(
                'local' => 'root_category_id',
                'foreign' => 'id'
            )
        );
    }
    

    }

    class Forum_Board extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘category_id’, ‘integer’, 10); $this->hasColumn(‘name’, ‘string’, 100); $this->hasColumn(‘description’, ‘string’, 5000); }

    public function setUp()
    {
        $this->hasOne('Forum_Category as Category', array(
                'local' => 'category_id',
                'foreign' => 'id'
            )
        );
        $this->hasMany('Forum_Thread as Threads',  array(
                'local' => 'id',
                'foreign' => 'board_id'
            )
        );
    }
    

    }

    class Forum_Entry extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘author’, ‘string’, 50); $this->hasColumn(‘topic’, ‘string’, 100); $this->hasColumn(‘message’, ‘string’, 99999); $this->hasColumn(‘parent_entry_id’, ‘integer’, 10); $this->hasColumn(‘thread_id’, ‘integer’, 10); $this->hasColumn(‘date’, ‘integer’, 10); }

    public function setUp()
    {
        $this->hasOne('Forum_Entry as Parent',  array(
                'local' => 'parent_entry_id',
                'foreign' => 'id'
            )
        );
        $this->hasOne('Forum_Thread as Thread', array(
                'local' => 'thread_id',
                'foreign' => 'id'
            )
        );
    }
    

    }

    class Forum_Thread extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn(‘board_id’, ‘integer’, 10); $this->hasColumn(‘updated’, ‘integer’, 10); $this->hasColumn(‘closed’, ‘integer’, 1); }

    public function setUp()
    {
        $this->hasOne('Forum_Board as Board', array(
                'local' => 'board_id',
                'foreign' => 'id'
            )
        );
    
        $this->ownsMany('Forum_Entry as Entries', array(
                'local' => 'id',
                'foreign' => thread_id'
            )
        );
    }
    

    }

    YAMLフォーマットでの例は次の通りです。[doc yaml-schema-files :name]の章でYAMLの詳細を読むことができます:

    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

    まとめ

    これらの実際の世界のスキーマの例によってDoctrineの実際のアプリケーションを使うことに役立つことを願っております。この本の最後の章では[doc coding-standards コーディング規約]を検討します。コーディング規約はあなたのアプリケーションにもお勧めします。コードの一貫性が大切であることを覚えておいてください!

    PHPファイルのフォーマット

    一般

    PHPコードのみのファイルには、閉じタグ(“?>”)は使ってはなりません。PHPには必須ではないからです。これを含めないことで意図しない末尾の空白が出力に混入されることが防止されます。

    NOTE ``__HALT_COMPILER()``による任意のバイナリデータのインクルードはDoctrine Frameworkによって禁止されています。この機能の使用は特別なインストールスクリプトでのみ許可されます。
    インデント

    4スペースのインデントを使います。タブは使いません。

    最大の長さ

    ターゲットの行の長さは80文字です。すなわち慣習として開発者は80カラム以内にコードを維持すべきです。しかしながら、より長い行も許容されます。PHPコードの最大長は120文字です。

    改行

    改行は行の終わりを表現するためのUnixテキストファイルの標準的な方法です。行はラインフィード(LF - linefeed)のみで終わらなければなりません。ラインフィードは通常の10もしくは16進法の0x0Aとして表現されます。

    Macintoshコンピュータ(0x0D)のようにキャリッジリターン(CR - carriage returns)を使わないでください。 またWindowsコンピュータ(0x0D, 0x0A)のようにキャリッジリターンとラインフィードの組み合わせ(CRLF)をつかわないでください。

    命名慣習

    クラス

    Doctrine ORM FrameworkはPEARとZendフレームワークと同じクラスの命名慣習を使います。クラスの名前はそれらを保存するディレクトリを直接指し示します。Doctrine Frameworkのrootレベルディレクトリは”Doctrine/”ディレクトリで、この下ですべてのクラスが階層状に保存されます。

    クラスの名前はアルファベットの文字のみ含むことが許可されます。数字も許可されますが非推奨です。アンダースコアはパス区切りの位置のみ許可されます。例えば”Doctrine/Table/Exception.php”というファイルの名前は”Doctrine\_Table_Exception“に対応します。

    クラスの名前が複数の単語で構成される場合、それぞれの新しい単語の最初の文字は大文字にしなければなりません。連続する大文字は許可されません例えば”XML_Reader”クラスは許可されませんが”Xml_Reader”は受け入れられます。

    インターフェイス

    インターフェイスは他のクラスと同じ慣習に従わなければなりません(上記を参照)。

    これらの名前は”Interface”という単語で終わらなければなりません(``Doctrine_Overloadable``といったものは認められません)。例です:

    • Doctrine\_Adapter_Interface
    • Doctrine\_EventListener_Interface
    ファイルの名前

    他のすべてのファイルに関して、アルファベット、アンダースコア、とダッシュ(“-”)のみが許可されます。スペースは禁止です。PHPコードを含むファイルは”.php”の拡張子で終わらなければなりません。次の例は上記のセクションの例からクラスの名前を含めるために受け入れられるファイルの名前を示します:

    • Doctrine/Adapter/Interface.php
    • Doctrine/EventListener/Interface

    ファイルの名前は先ほど説明したクラスの名前のマッピング方法に従わなければなりません。

    関数とメソッド

    関数名はアルファベットのみで構成されアンダースコアは認められません。数字も認められますが非推奨です。関数の名前は常に小文字で始まらなければならず複数の単語で構成される場合、それぞれの単語の最初の文字は大文字で始まらなければなりません。この表記は”studlyCaps”もしくは”camelCaps”メソッドと呼ばれます。冗長性が推奨されコードの可読性を強化するために関数の名前は実用的に冗長でなければなりません。

    オブジェクト指向のプログラミングに関して、オブジェクトのアクセサには”get”もしくは”set”の接頭辞をつけなければなりません。’obtain’と’assign’を接頭辞とするアクセサを持つ``Doctrine_Record``以外のすべてのクラスに対してこの原則は適用されます。この理由はすべてのユーザーが``Doctrine_Record``を継承するActiveRecordを定義したので、できる限り最小のget/setの名前空間が投入されるからです。

    NOTE グローバルスコープの関数は許可されません。すべてのスタティック関数はスタティッククラスにラップされます。
    変数

    変数の名前にはアルファベットのみが許可されます。アンダースコアは許可されません。数字は許可されますが非推奨です。これらは小文字で始まり”camelCaps”のルールに従わなければなりません。冗長性は推奨されます。変数は常に冗長で実用的であるべきです。最小のループコンテキスト以外では”i"と"n”のような簡単な変数名は非推奨です。ループの箇所が20行以上のコードである場合、インデックス用の変数は説明のような名前をより多く持つことが必要です。フレームワークの範囲内で特定のジェネリックオブジェクト変数は次の名前を使います:

    ||~ オブジェクトの型 ||~ 変数名 || || Doctrine_Connection || $conn || || Doctrine_Collection || $coll || || Doctrine\_Manager || $manager || || Doctrine_Query || $q ||

    より説明のような名前が適切であることがあります(例えば同じクラスん複数のオブジェクトが同じコンテキストで使われるとき)。このケースでは異なる名前を使うことが許可されます。

    定数

    定数にはアルファベットとアンダースコアの両方が許可されます。これらは常にすべてが大文字です。可読性のために、定数の単語はアンダースコアで区切らなければなりません。例えば、``ATTR_EXC_LOGGING``は許可されますが``ATTR_EXCLOGGING``は許可されません。定数は”const”コンストラクトを使用してクラスメンバーとして定義しなければなりません。グローバルスコープで”define”を使用して定数を定義するのは許可されていません。

    class Doctrine_SomeClass { const MY_CONSTANT = ‘something’; }

    echo $Doctrine_SomeClass::MY_CONSTANT;

    レコードのカラム

    すべてのレコードのカラムは小文字でカラムが複数の単語で構成される場合はアンダースコア(_)の使用が推奨されます。

    class User { public function setTableDefinition() {

    $this->hasColumn(‘home_address’, ‘string’); } }

    外部キーフィールドは``[table_name]_[column]``の形式でなければなりません。次の例は``user(id)``を指し示す外部キーです:

    class Phonenumber extends Doctrine_Record { public function

    setTableDefinition() { $this->hasColumn(‘user_id’, ‘integer’); } }

    コーディングスタイル

    PHPコードの境界

    PHPコードはフルフォームの標準のPHPタグで常に区切らなければならずショートタグは許可されません。PHPコードのみのファイルでは、閉じタグを常に省略しなければなりません。

    文字列

    文字列がリテラルであるとき(変数の置き換えを含まない)、文字列を区切るためにアポストロフィもしくは”シングルクォート”を使わなければなりません:

    リテラル文字列
    $string = ‘something’;

    リテラル文字列自身がアポストロフィを含むとき、クォテーション記号もしくは”ダブルクォート”で文字列を区切ることが許可されます。これはとりわけSQL文で推奨されます:

    アポストフィを含む文字列
    $sql = “SELECT id, name FROM people WHERE name = ‘Fred’ OR name =

    ‘Susan’”;

    変数の置き換え

    変数の置き換えは次の形式で許可されます:

    // variable substitution $greeting = “Hello $name, welcome back!”;
    文字列の連結

    複数の文字列は”.”演算子を使用して連結できます。可読性を向上させるために”.”演算子の前後でスペースを常に追加します:

    $framework = ‘Doctrine’ . ‘ ORM ‘ . ‘Framework’;
    改行の連結

    文字列を”.”演算子で連結するとき、可読性を向上させるためにステートメントを複数行に分割することが許可されます。このケースでは、それぞれの連続行はホワイトスペースで詰められます。”.”演算子は”=”演算子の下に並べられます:

    $sql = “SELECT id, name FROM user ” . “WHERE name = ? ” . “ORDER BY

    name ASC”;

    配列

    インデックスには負の数は許可されず負ではない数で始まらなければなりません。しかしながらこれは非推奨ですべての配列に0を起点とするインデックスを持たせることが推奨されます。arrayコンストラクトでインデックス付きの配列を宣言するとき、可読性を向上させるために行末のスペースをそれぞれのコンマ区切り文字の後に追加しなえkればなりません。”array”コンストラクトで複数行のインデックス付きの配列を宣言することも許可されます。このケースでは、それぞれの連続する行はスペースで詰められます。arrayコンストラクタで連想配列を宣言するとき、ステートメントを複数行に分割することが推奨されます。この場合、キーと値が並ぶように、連続するそれぞれの行はホワイトスペースで埋めなければなりません:

    $sampleArray = array(‘Doctrine’, ‘ORM’, 1, 2, 3);
    $sampleArray = array(1, 2, 3, $a, $b, $c,
    56.44, $d, 500);

    $sampleArray = array(‘first’ => ‘firstValue’, ‘second’ => ‘secondValue’);

    クラス

    クラスは次の命名慣習に従って名付けなければなりません。クラスの名前(もしくはinterface宣言)の後の次の行にかっこを常に書かなければなりません。すべてのクラスはPHPDocumentor標準に従うドキュメントブロックを持たなければなりません。クラスの範囲内のコードは4つのスペースでインデントして1つのPHPファイルにつき1つのクラスのみ許可されます。クラスファイルにコードを追加することは許可されません。

    受け入れられるクラス宣言の例は次の通りです:

    /** * Documentation here */ class Doctrine_SampleClass { // entire

    content of class // must be indented four spaces }

    関数とメソッド

    メソッドは以下の命名慣習に従いprivate、protected、もしくはpublicコンストラクトの1つを使用して可視性を常に宣言しなければなりません。クラスのように、かっこはメソッドの名前の後の次の行に書きます。関数名と引数用の開き波かっこの間にはスペースは入れません。グローバルスコープの関数は非推奨です。クラス内の関数宣言の許容される例は次の通りです:

    /** * Documentation Block Here / class Foo { /* * Documentation

    Block Here */ public function bar() { // entire content of function // must be indented four spaces }

    public function bar2()
    {
    
    }
    

    }

    NOTE メソッドの間の改行は上記の``bar()``と``bar2()``メソッドのように行われます。

    参照渡しは関数の宣言でのみ許可されます:

    /** * Documentation Block Here / class Foo { /* * Documentation

    Block Here */ public function bar(&$baz) { } }

    呼び出し時の参照渡しは禁止です。戻り値は丸かっこで囲んではなりません。これは可読性を下げるだけでなく後でメソッドが参照渡しに変更された場合にコードを壊す可能性があります。

    /** * Documentation Block Here / class Foo { /* * WRONG */ public function bar() { return($this->bar); }
    /**
     * RIGHT
     */
    public function bar()
    {
        return $this->bar;
    }
    

    }

    関数の引数は区切り文字のコンマの後の単独スペースで区切られます。3つの引数を受け取る関数の呼び出し例は次の通りです:

    threeArguments(1, 2, 3);

    呼び出し時の参照渡しは禁止です。関数の引数を参照渡しする適切な方法は上記の内容をご覧ください。引数に配列を許可する関数に関しては、可読性を向上させるためにarrayコンストラクトを含む関数呼び出しは複数行に分割されます。これらのケースでは、配列の適切な書き方は次のようになります:

    threeArguments(array(1, 2, 3), 2, 3);

    threeArguments(array(1, 2, 3, ‘Framework’, ‘Doctrine’, 56.44, 500), 2, 3);

    制御文

    ifとelseifコンストラクトに基づく制御文では開き丸かっこと閉じ丸かっこの前後に単独のスペースを置かなければなりません。丸かっこの間の条件文の範囲では、可読性のために演算子はスペースで区切らなければなりません。大きな条件の論理的なグルーピングを改善するために内側の丸かっこは推奨されます。開き波かっこは条件文と同じ行で書きます。閉じ波かっこは専用の行で書きます。かっこ内の内容は4つのスペースでインデントしなければなりません。

    if ($foo != 2) { $foo = 2; }

    elseifもしくはelseを含むif文に対して、形式は次のようにならなければなりません:

    if ($foo != 1) { $foo = 1; } else { $foo = 3; }

    if ($foo != 2) { foo = 2; } elseif (foo == 1) { $foo = 3; } else {

    $foo = 11; }

    !オペランドを使うときは次の形式に従わなければなりません:

    if ( ! $foo) {

    }

    switchコンストラクトで書く制御文では開き丸かっこの前と閉じ丸かっこの後でそれぞれの単独のスペースを置かなければなりません。switch文の範囲内のすべての内容は4つのスペースでインデントしなければなりません。それぞれのcase文の下の内容は追加の4つの追加スペースでインデントしなければなりませんがbreak文はcase文と同じインデントレベルでなければなりません。

    switch ($case) { case 1: case 2: break; case 3: break; default: break;

    }

    defaultコンストラクトはswitch文から省略してはなりません。

    インラインドキュメント

    ドキュメントのフォーマット:

    すべてのドキュメントブロック(“docblocks”)はphpDocumentorのフォーマットと互換性がなければなりません。phpDocumentorのフォーマットの説明はこのドキュメントの範囲を越えるので詳細は、http://phpdoc.org/のサイトを訪問してください

    すべてのメソッドは、最小限のdocblockを持たなければなりません:

    • 関数の説明
    • すべての引数
    • 可能なすべての戻り値
    • 関数を宣言するのに使われるpublic、privateもしくはprotectedからアクセスレベルが既知なので@accessタグを使う必要がない場合

    関数/メソッドが例外を投げる場合、@throwsを使います:

    /* * Test function * * @throws Doctrine_Exception */ public

    function test() { throw new Doctrine_Exception(‘This function did not work’); }

    まとめ

    この章は//Doctrine ORM for PHP - Guide to Doctrine for PHP//の最後の章です。この本が本当に役立ちDoctrineを快適に使い必要なときに調べるために戻ってくださることを筆者は心より願っております。

    常に、Doctrineと共にありますように :)

    Thanks, Jon

    About Sensio Labs

    Sensio Labs is a French web agency well known for its innovative ideas on web development. Founded in 1998 by Fabien Potencier, Gregory Pascal, and Samuel Potencier, Sensio benefited from the Internet growth of the late 1990s and situated itself as a major player for building complex web applications. It survived the Internet bubble burst by applying professional and industrial methods to a business where most players seemed to reinvent the wheel for each project. Most of Sensio’s clients are large French corporations, who hire its teams to deal with small- to middle-scale projects with strong time-to-market and innovation constraints.

    Sensio Labs develops interactive web applications, both for dot-com and traditional companies. Sensio Labs also provides auditing, consulting, and training on Internet technologies and complex application deployment. It helps define the global Internet strategy of large-scale industrial players. Sensio Labs has projects in France and abroad.

    For its own needs, Sensio Labs develops the symfony framework and sponsors its deployment as an Open-Source project. This means that symfony is built from experience and is employed in many web applications, including those of large corporations.

    Since its beginnings ten years ago, Sensio has always based its strategy on strong technical expertise. The company focuses on Open-Source technologies, and as for dynamic scripting languages, Sensio offers developments in all LAMP platforms. Sensio acquired strong experience on the best frameworks using these languages, and often develops web applications in Django, Rails, and, of course, symfony.

    Sensio Labs is always open to new business opportunities, so if you ever need help developing a web application, learning symfony, or evaluating a project using Doctrine, feel free to contact us at fabien.potencier@sensio.com. The consultants, project managers, web designers, and developers of Sensio can handle projects from A to Z.

    About the Author

    Jonathan is a software developer who resides in the rolling hills of Nashville, Tennessee. He joined the Doctrine project as a contributor in early 2007 and has been involved in one way or another since then. Jonathan currently works full-time as a software developer at Sensio Labs, a French web agency with 10 years of experience developing web applications.

    It all began at the age of 12 when he first got his hands on the tools to build his first website. From then on he hopped from one thing to another learning the various aspects of programming in several different languages. He enjoys working on Doctrine as it is a daily challenge and allows him to explore uncharted territories in PHP.

    About the Authors

    Contributors
    Roman S. Borschel

    Roman is a software developer from Berlin, Germany who joined the project as a user in its early stages in 2006. Though being mainly a Java and .NET developer who currently works in the field of medical information systems, Roman is also a long-time PHP user and likes to push PHP to its limits and beyond.

    Having a special interest in Object-Relational Mapping, he finds it challenging to try to apply ideas and concepts of object persistence solutions of other languages and tools in the PHP world where this territory is still in its infancy.

    Guilherme Blanco

    Guilherme Blanco was born in 09/19/1983 and lives in Brazil. He has a bachelors degree in Computer Science from the Federal University of São Carlos and works in the web development industry. He is currently employed by his company named Bisna and holds a blog at http://blog.bisna.com.

    He has worked with Java, .NET and the majority of server/client side languages and decided to pick PHP as his default development language. Instead of reinventing the wheel after he planned an entire ORM tool, he decided to jump on board the Doctrine project in November of 2007 and put his efforts in to help it move forward.

    Konsta Vesterinen

    Konsta Vesterinen is the founder of the Doctrine project and is responsible for getting things started for this great project. He is the creator of the first versions of all code and documentation for Doctrine. A majority of the content in this book has been evolved and transformed from his original version that was created years ago.

    Companies

    As Doctrine is an open source project, the names mentioned above are only the people who organize and manage the contributions of the community behind the project. Though the people above get most of the credit for the work, none of this would be possible without the companies that have gotten behind Doctrine and supported it in any way they can. Below are some acknowledgements to the companies that have helped get the project to where it is today.

    centre{source}

    Before working for Sensio Labs, Jonathan Wage was one of the original employees at centre{source}. The great people at this company saw the need for a powerful ORM in the PHP world and got behind the project right away. Jonathan was permitted to spend countless hours of company time on the project to help move things forward.

    Without these contributions, the project would not have been able to make the jump from a small starting open source project to a large and trusted project. Now Doctrine is known across the globe and companies rely on the project to assist with the building of their high end web applications.

    Note

    centre{source} is a full-service interactive firm assisting organizations that view the web as a strategic asset. They provide their clients four essential services: strategy, planning, execution, and on-going management.

    Sensio Labs

    In September of 2008, Sensio Labs officially got behind the Doctrine project and hired Jonathan to work on the project full-time. This was a major turning point for the project as it showed that corporate companies were willing to sponsor not just existing resources, but real funds.

    With this change, lots of new things became possible such as Doctrine trainings, certifications, this book and much more.

    Note

    Sensio created symfony, a PHP Web application framework. Sensio has over 10 years of experience developing high value web applications and is a major player in the open source world.

    Other Contributors

    Below are some other contributors that have played a role in the success of the project that deserve a mention here.

    • `Ian P. Christian <http://pookey.co.uk/blog/>`_ - For hosting the Doctrine infrastructure on his own dime.
    • Werner Mollentze - For providing the original concepts of the Doctrine logo.
    • `Phu Son Nguyen <http://www.phuson.com/>`_ - For providing a final copy of the Doctrine logo and the web design.
    • `Fabien Potencier <http://www.aide-de-camp.org/>`_ - For lending us code from the symfony project to help with building the Doctrine website.

    Lots of other people have contributed a long the way, as I cannot mention everyone here, you can always read more about the contributors on the Doctrine about page.

    Conclusion

    As you can see Doctrine is possible because of a lot of people and companies. Big thanks to all these people as none of it would be possible without them. Now we are ready to dive in to Doctrine in the 要件を確認する chapter.

    Attribution-Share Alike 3.0 Unported License

    THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE (“CCPL” OR “LICENSE”). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.

    BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.

    1. Definitions
    1. “Adaptation” means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image (“synching”) will be considered an Adaptation for the purpose of this License.
    2. “Collection” means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License.
    3. “Creative Commons Compatible License” means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License.
    4. “Distribute” means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership.
    5. “License Elements” means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike.
    6. “Licensor” means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License.
    7. “Original Author” means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast.
    8. “Work” means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work.
    9. “You” means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
    10. “Publicly Perform” means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images.
    11. “Reproduce” means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium.
    1. Fair Dealing Rights

    Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws.

    1. License Grant

    Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:

    1. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections;
    2. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked “The original work was translated from English to Spanish,” or a modification could indicate “The original work has been modified.”;
    3. to Distribute and Publicly Perform the Work including as incorporated in Collections; and,
    4. to Distribute and Publicly Perform Adaptations.
    5. For the avoidance of doubt:
      1. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License;
      2. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and,
      3. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License.

    The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved.

    1. Restrictions

    The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:

    1. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested.
    2. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the “Applicable License”), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License.
    3. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution (“Attribution Parties”) in Licensor’s copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., “French translation of the Work by Original Author,” or “Screenplay based on original Work by Original Author”). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
    4. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author’s honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author’s honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise.
    1. Representations, Warranties and Disclaimer

    UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.

    1. Limitation on Liability

    EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

    1. Termination
    1. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
    2. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
    1. Miscellaneous
    1. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.

    2. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.

    3. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.

    4. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.

    5. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.

    6. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law.

      SIDEBAR Creative Commons Notice

      Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor.

      Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark “Creative Commons” or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons’ then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License.

      Creative Commons may be contacted at http://creativecommons.org/.