Commit 5329c382 authored by jepso's avatar jepso

* Converted most of the docs to the new format.

* Fixed a few layout bugs in new documentation
* Fixed documentation table of contents indentation bug in IE6 (fixes #344)
* Fixed a parser bug in Sensei_Doc_Section
* Restructrured a bit some files of the documentation.
parent ca6a1387
<code type="php">
class Customer extends Doctrine_Record {
public function setUp() {
// setup code goes here
}
public function setTableDefinition() {
// table definition code goes here
}
public function getAvailibleProducts() {
// some code
}
public function setName($name) {
if($this->isValidName($name))
$this->set("name",$name);
}
public function getName() {
return $this->get("name");
}
}
</code>
<code type="php">
// custom primary key column name
class Group extends Doctrine_Record {
public function setUp() {
$this->setPrimaryKeyColumn("group_id");
}
}
</code>
* Doctrine::ATTR_LISTENER
* Doctrine::ATTR_FETCHMODE = 2;
* Doctrine::ATTR_CACHE_DIR = 3;
* Doctrine::ATTR_CACHE_TTL = 4;
* Doctrine::ATTR_CACHE_SIZE = 5;
* Doctrine::ATTR_CACHE_SLAM = 6;
* Doctrine::ATTR_CACHE = 7;
* Doctrine::ATTR_BATCH_SIZE = 8;
* Doctrine::ATTR_PK_COLUMNS = 9;
/**
* primary key type attribute
*/
* Doctrine::ATTR_PK_TYPE = 10;
/**
* locking attribute
*/
* Doctrine::ATTR_LOCKMODE = 11;
/**
* validatate attribute
*/
* Doctrine::ATTR_VLD = 12;
/**
* name prefix attribute
*/
* Doctrine::ATTR_NAME_PREFIX = 13;
/**
* create tables attribute
*/
* Doctrine::ATTR_CREATE_TABLES = 14;
/**
* collection key attribute
*/
* Doctrine::ATTR_COLL_KEY = 15;
/**
* collection limit attribute
*/
* Doctrine::ATTR_COLL_LIMIT = 16;
<code type="php">
class Email extends Doctrine_Record {
public function setUp() {
$this->setAttribute(Doctrine::ATTR_LISTENER,new MyListener());
}
public function setTableDefinition() {
$this->hasColumn("address","string",150,"email|unique");
}
}
</code>
<code type="php">
// setting default fetchmode
// availible fetchmodes are Doctrine::FETCH_LAZY, Doctrine::FETCH_IMMEDIATE and Doctrine::FETCH_BATCH
// the default fetchmode is Doctrine::FETCH_LAZY
class Address extends Doctrine_Record {
public function setUp() {
$this->setAttribute(Doctrine::ATTR_FETCHMODE,Doctrine::FETCH_IMMEDIATE);
}
}
</code>
<code type="php">
// using sequences
class User extends Doctrine_Record {
public function setUp() {
$this->setSequenceName("user_seq");
}
}
</code>
<code type="php">
$q = new Doctrine_Query();
$q->from('User')->where('User.Phonenumber.phonenumber.contains(?,?,?)');
$users = $q->execute(array('123 123 123', '0400 999 999', '+358 100 100'));
</code>
<code type="php">
$q = new Doctrine_Query();
$q->from('User')->where('User.Phonenumber.phonenumber.like(?,?)');
$users = $q->execute(array('%123%', '456%'));
</code>
<code type="php">
$q = new Doctrine_Query();
$q->from('User')->where('User.Phonenumber.phonenumber.regexp(?,?)');
$users = $q->execute(array('[123]', '^[3-5]'));
</code>
*
NOT, !
Logical NOT. Evaluates to 1 if the
operand is 0, to 0 if
the operand is non-zero, and NOT NULL
returns NULL.
<b class='title'>DQL condition :** NOT 10
-&gt; 0
<b class='title'>DQL condition :** NOT 0
-&gt; 1
<b class='title'>DQL condition :** NOT NULL
-&gt; NULL
<b class='title'>DQL condition :** ! (1+1)
-&gt; 0
<b class='title'>DQL condition :** ! 1+1
-&gt; 1
</pre>
The last example produces 1 because the
expression evaluates the same way as
(!1)+1.
*
<a name="function_and"></a>
<a class="indexterm" name="id2965271"></a>
<a class="indexterm" name="id2965283"></a>
AND
Logical AND. Evaluates to 1 if all
operands are non-zero and not NULL, to
0 if one or more operands are
0, otherwise NULL is
returned.
<b class='title'>DQL condition :** 1 AND 1
-&gt; 1
<b class='title'>DQL condition :** 1 AND 0
-&gt; 0
<b class='title'>DQL condition :** 1 AND NULL
-&gt; NULL
<b class='title'>DQL condition :** 0 AND NULL
-&gt; 0
<b class='title'>DQL condition :** NULL AND 0
-&gt; 0
</pre>
*
OR
Logical OR. When both operands are
non-NULL, the result is
1 if any operand is non-zero, and
0 otherwise. With a
NULL operand, the result is
1 if the other operand is non-zero, and
NULL otherwise. If both operands are
NULL, the result is
NULL.
<b class='title'>DQL condition :** 1 OR 1
-&gt; 1
<b class='title'>DQL condition :** 1 OR 0
-&gt; 1
<b class='title'>DQL condition :** 0 OR 0
-&gt; 0
<b class='title'>DQL condition :** 0 OR NULL
-&gt; NULL
<b class='title'>DQL condition :** 1 OR NULL
-&gt; 1
</pre>
*
<a name="function_xor"></a>
<a class="indexterm" name="id2965520"></a>
XOR
Logical XOR. Returns NULL if either
operand is NULL. For
non-NULL operands, evaluates to
1 if an odd number of operands is
non-zero, otherwise 0 is returned.
<b class='title'>DQL condition :** 1 XOR 1
-&gt; 0
<b class='title'>DQL condition :** 1 XOR 0
-&gt; 1
<b class='title'>DQL condition :** 1 XOR NULL
-&gt; NULL
<b class='title'>DQL condition :** 1 XOR 1 XOR 1
-&gt; 1
</pre>
a XOR b is mathematically equal to
(a AND (NOT b)) OR ((NOT a) and b).
<code type="php">
$sess = Doctrine_Manager::getInstance()->openConnection(new PDO("dsn","username","password"));
// select first ten rows starting from the row 20
$sess->select("select * from user",10,20);
</code>
<code type="php">
try {
$conn->beginTransaction();
$user->save();
$conn->beginTransaction();
$group->save();
$email->save();
$conn->commit();
$conn->commit();
} catch(Exception $e) {
$conn->rollback();
}
</code>
<code type="php">
// works only if you use doctrine database handler
$dbh = $conn->getDBH();
$times = $dbh->getExecTimes();
// print all executed queries and their execution times
foreach($dbh->getQueries() as $index => $query) {
print $query." ".$times[$index];
}
</code>
<code type="php">
$sess = Doctrine_Manager::getInstance()->openConnection(new PDO("dsn","username","password"));
// gets the next ID from a sequence
$sess->getNextID($sequence);
</code>
<code type="php">
$sess = Doctrine_Manager::getInstance()->openConnection(new PDO("dsn","username","password"));
try {
$sess->beginTransaction();
// some database operations
$sess->commit();
} catch(Exception $e) {
$sess->rollback();
}
</code>
Doctrine supports aggregates and composites. When binding composites you can use methods Doctrine_Record::ownsOne() and Doctrine_Record::ownsMany(). When binding
aggregates you can use methods Doctrine_Record::hasOne() and Doctrine_Record::hasMany(). Basically using the owns* methods is like adding a database level ON CASCADE DELETE
constraint on related component with an exception that doctrine handles the deletion in application level.
In Doctrine if you bind an Email to a User using ownsOne or ownsMany methods, everytime User record calls delete the associated
Email record is also deleted.
Then again if you bind an Email to a User using hasOne or hasMany methods, everytime User record calls delete the associated
Email record is NOT deleted.
Doctrine_Association represents a many-to-many association between database tables.
Doctrine_Collection is a collection of Data Access Objects. Doctrine_Collection represents a record set.
Doctrine_Collection_Batch is a Doctrine_Collection with batch fetching strategy.
Doctrine_Collection_Immediate is a Doctrine_Collection with immediate fetching strategy.
Doctrine_Collection_Lazy is a Doctrine_Collection with lazy fetching strategy.
Doctrine_ForeignKey represents a one-to-many association or one-to-one association between two database tables.
Doctrine_Manager is the base component of Doctrine ORM framework. Doctrine_Manager is a colletion of Doctrine_Connections. All the new connections are being
opened by Doctrine_Manager::openConnection().
Doctrine_Record is a wrapper for database row.
<code type="php">
$user = $table->find(2);
// get state
$state = $user->getState();
print $user->name;
print $user["name"];
print $user->get("name");
$user->name = "Jack Daniels";
$user->set("name","Jack Daniels");
// serialize record
$serialized = serialize($user);
$user = unserialize($serialized);
// create a copy
$copy = $user->copy();
// get primary key
$id = $user->getID();
// print lots of useful info
print $user;
// save all the properties and composites
$user->save();
// delete this data access object and related objects
$user->delete();
</code>
Doctrine_Connection is a wrapper for database connection. It creates Doctrine_Tables and keeps track of all the created tables.
Doctrine_Connection provides things that are missing from PDO like sequence support and limit/offset emulation.
<code type="php">
$sess = $manager->openConnection(Doctrine_Db::getConnection("schema://username:password@hostname/database"));
// get connection state:
switch($sess):
case Doctrine_Connection::STATE_BUSY:
// multiple open transactions
break;
case Doctrine_Connection::STATE_ACTIVE:
// one open transaction
break;
case Doctrine_Connection::STATE_CLOSED:
// closed state
break;
case Doctrine_Connection::STATE_OPEN:
// open state and zero open transactions
break;
endswitch;
// getting database handler
$dbh = $sess->getDBH();
// flushing the connection
$sess->flush();
// print lots of useful info about connection:
print $sess;
</code>
Doctrine_Table creates records and holds info of all foreign keys and associations. Doctrine_Table
represents a database table.
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.
<code type="php">
$q = new Doctrine_Query();
$q->from('User(COUNT(id))');
// returns an array
$a = $q->execute();
// selecting multiple aggregate values:
$q = new Doctrine_Query();
$q->from('User(COUNT(id)).Phonenumber(MAX(phonenumber))');
$a = $q->execute();
</code>
<code type="php">
$query->from("User")
->where("User.name = ?");
$query->execute(array('Jack Daniels'));
</code>
<?php
/**
$str = "
The following examples should give a hint of how DQL is converted into SQL.
The classes used in here are the same as in chapter 14.1 (Users and groups are both entities etc).
DQL QUERY: FROM Email WHERE Email.address LIKE '%@example%'
SQL QUERY: SELECT email.id AS Email__id FROM email WHERE (email.address LIKE '%@example%')
DQL QUERY: FROM User(id) WHERE User.Phonenumber.phonenumber LIKE '%123%'
SQL QUERY: SELECT entity.id AS entity__id FROM entity LEFT JOIN phonenumber ON entity.id = phonenumber.entity_id WHERE phonenumber.phonenumber LIKE '%123%' AND (entity.type = 0)
DQL QUERY: FROM Forum_Board(id).Threads(id).Entries(id)
SQL QUERY: SELECT forum_board.id AS forum_board__id, forum_thread.id AS forum_thread__id, forum_entry.id AS forum_entry__id FROM forum_board LEFT JOIN forum_thread ON forum_board.id = forum_thread.board_id LEFT JOIN forum_entry ON forum_thread.id = forum_entry.thread_id
DQL QUERY: FROM User(id) WHERE User.Group.name = 'Action Actors'
SQL QUERY: SELECT entity.id AS entity__id FROM entity LEFT JOIN groupuser ON entity.id = groupuser.user_id LEFT JOIN entity AS entity2 ON entity2.id = groupuser.group_id WHERE entity2.name = 'Action Actors' AND (entity.type = 0 AND (entity2.type = 1 OR entity2.type IS NULL))
DQL QUERY: FROM User(id) WHERE User.Group.Phonenumber.phonenumber LIKE '123 123'
SQL QUERY: SELECT entity.id AS entity__id FROM entity LEFT JOIN groupuser ON entity.id = groupuser.user_id LEFT JOIN entity AS entity2 ON entity2.id = groupuser.group_id LEFT JOIN phonenumber ON entity2.id = phonenumber.entity_id WHERE phonenumber.phonenumber LIKE '123 123' AND (entity.type = 0 AND (entity2.type = 1 OR entity2.type IS NULL))
";
function renderQueries($str) {
$e = explode("\n",$str);
$color = "367FAC";
foreach($e as $line) {
if(strpos($line, "SQL") !== false)
$color = "A50A3D";
elseif(strpos($line, "DQL") !== false)
$color = "367FAC";
$l = str_replace("SELECT","
<font color='$color'>**SELECT**</font>",$line);
$l = str_replace("FROM","
<font color='$color'>**FROM**</font>",$l);
$l = str_replace("LEFT JOIN","
<font color='$color'>**LEFT JOIN**</font>",$l);
$l = str_replace("INNER JOIN","
<font color='$color'>**INNER JOIN**</font>",$l);
$l = str_replace("WHERE","
<font color='$color'>**WHERE**</font>",$l);
$l = str_replace("AS","<font color='$color'>**AS**</font>",$l);
$l = str_replace("ON","<font color='$color'>**ON**</font>",$l);
$l = str_replace("ORDER BY","<font color='$color'>**ORDER BY**</font>",$l);
$l = str_replace("LIMIT","<font color='$color'>**LIMIT**</font>",$l);
$l = str_replace("OFFSET","<font color='$color'>**OFFSET**</font>",$l);
$l = str_replace("DISTINCT","<font color='$color'>**DISTINCT**</font>",$l);
$l = str_replace(" ","<dd>",$l);
print $l."<br>";
if(substr($l,0,3) == "SQL") print "<hr valign='left' class='small'>";
}
}
renderQueries($str);
*/
?>
<code type="php">
// select all users and load the data directly (Immediate fetching strategy)
$coll = $conn->query("FROM User-I");
// or
$coll = $conn->query("FROM User-IMMEDIATE");
// select all users and load the data in batches
$coll = $conn->query("FROM User-B");
// or
$coll = $conn->query("FROM User-BATCH");
// select all user and use lazy fetching
$coll = $conn->query("FROM User-L");
// or
$coll = $conn->query("FROM User-LAZY");
</code>
<code type="php">
// retrieve all users with only their properties id and name loaded
$users = $conn->query("FROM User(id, name)");
</code>
You can overload the query object by calling the dql query parts as methods.
<code type="php">
$conn = Doctrine_Manager::getInstance()->openConnection(new PDO("dsn","username","password"));
$query = new Doctrine_Query($conn);
$query->from("User-b")
->where("User.name LIKE 'Jack%'")
->orderby("User.created")
->limit(5);
$users = $query->execute();
$query->from("User.Group.Phonenumber")
->where("User.Group.name LIKE 'Actors%'")
->orderby("User.name")
->limit(10)
->offset(5);
$users = $query->execute();
</code>
Doctrine provides two relation operators: '.' aka dot and ':' aka colon.
The dot-operator is used for SQL LEFT JOINs and the colon-operator is used
for SQL INNER JOINs. Basically you should use dot operator if you want for example
to select all users and their phonenumbers AND it doesn't matter if the users actually have any phonenumbers.
On the other hand if you want to select only the users which actually have phonenumbers you should use the colon-operator.
<code type="php">
$query->from('User u')->innerJoin('u.Email e');
$query->execute();
// executed SQL query:
// SELECT ... FROM user INNER JOIN email ON ...
$query->from('User u')->leftJoin('u.Email e');
$query->execute();
// executed SQL query:
// SELECT ... FROM user LEFT JOIN email ON ...
</code>
++ Eventlisteners
++ Validators
++ View
++ Cache
++ Locking Manager
++ Db_Profiler
++ Hook
++ Query
++ RawSql
++ Db
++ Exceptions
+++ Introduction
Caching is one of the most influental things when it comes to performance tuning. {{Doctrine_Cache}} provides means for caching queries and for managing the cached queries.
+++ Query cache
+++ Introduction
{{Doctrine_Db}} is a wrapper for PDO database object. Why should you consider using {{Doctrine_Db}} instead of PDO?
# It provides efficient eventlistener architecture, hence its easy to add new aspects to existing methods like on-demand-caching
# {{Doctrine_Db}} lazy-connects database. Creating an instance of {{Doctrine_Db}} doesn't directly connect database, hence {{Doctrine_Db}} fits perfectly for application using for example page caching.
# It has many short cuts for commonly used fetching methods like {{Doctrine_Db::fetchOne()}}.
# Supports PEAR-like data source names as well as PDO data source names.
+++ Connecting to a database
{{Doctrine_Db}} allows both PEAR-like DSN (data source name) as well as PDO like DSN as constructor parameters.
Getting an instance of {{Doctrine_Db}} using PEAR-like DSN:
<code type="php">
// using PEAR like dsn for connecting pgsql database
$dbh = new Doctrine_Db('pgsql://root:password@localhost/mydb');
// using PEAR like dsn for connecting mysql database
$dbh = new Doctrine_Db('mysql://root:password@localhost/test');
</code>
Getting an instance of {{Doctrine_Db}} using PDO-like DSN (PDO mysql driver):
<code type="php">
$dbh = new Doctrine_Db('mysql:host=localhost;dbname=test',
$user, $pass);
</code>
Getting an instance of {{Doctrine_Db}} using PDO-like DSN (PDO sqlite with memory tables):
<code type="php">
$dbh = new Doctrine_Db('sqlite::memory:');
</code>
Handling connection errors:
<code type="php">
try {
$dbh = new Doctrine_Db('mysql:host=localhost;dbname=test',
$user, $pass);
foreach ($dbh->query('SELECT * FROM foo') as $row) {
print_r($row);
}
$dbh = null;
} catch (PDOException $e) {
print 'Error!: ' . $e->getMessage() . '
';
die();
}
</code>
+++ Using event listeners
{{Doctrine_Db}} has a pluggable event listener architecture. It provides before and after listeners for all relevant methods. Every listener method takes one parameter: a {{Doctrine_Db_Event}} object, which holds info about the occurred event.
Every listener object must either implement the {{Doctrine_Db_EventListener_Interface}} or {{Doctrine_Overloadable}} interface. Using {{Doctrine_Overloadable}} interface only requires you to implement {{__call()}} which is then used for listening all the events.
<code type="php">
class OutputLogger extends Doctrine_Overloadable {
public function __call($m, $a) {
print $m . ' called!';
}
}
</code>
For convience you may want to make your listener class extend {{Doctrine_Db_EventListener}} which has empty listener methods, hence allowing you not to define all the listener methods by hand. The following listener, 'MyLogger', is used for listening only {{onPreQuery}} and {{onQuery}} methods.
<code type="php">
class MyLogger extends Doctrine_Db_EventListener {
public function onPreQuery(Doctrine_Db_Event $event) {
print 'database is going to be queried!';
}
public function onQuery(Doctrine_Db_Event $event) {
print 'executed: ' . $event->getQuery();
}
}
</code>
Now the next thing we need to do is bind the eventlistener objects to our database handler.
<code type="php">
// using PDO dsn for connecting sqlite memory table
$dbh = Doctrine_Db::getConnection('sqlite::memory:');
class MyLogger extends Doctrine_Db_EventListener {
public function onPreQuery(Doctrine_Db_Event $event) {
print "database is going to be queried!";
}
public function onQuery(Doctrine_Db_Event $event) {
print "executed: " . $event->getQuery();
}
}
$dbh->setListener(new MyLogger());
$dbh->query("SELECT * FROM foo");
// prints:
// database is going to be queried
// executed: SELECT * FROM foo
class MyLogger2 extends Doctrine_Overloadable {
public function __call($m, $a) {
print $m." called!";
}
}
$dbh->setListener(new MyLogger2());
$dbh->exec("DELETE FROM foo");
// prints:
// onPreExec called!
// onExec called!
</code>
+++ Chaining listeners
{{Doctrine_Db}} supports event listener chaining. It means multiple listeners can be attached for
listening the events of a single instance of {{Doctrine_Db}}.
For example you might want to add different aspects to your {{Doctrine_Db}} instance on-demand. These aspects may include caching, query profiling etc.
<code type="php">
// using PDO dsn for connecting sqlite memory table
$dbh = Doctrine_Db::getConnection('sqlite::memory:');
class Counter extends Doctrine_Db_EventListener {
private $queries = 0;
public function onQuery(Doctrine_Db_Event $event) {
$this->queries++;
}
public function count() {
return count($this->queries);
}
}
class OutputLogger extends Doctrine_Overloadable {
public function __call($m, $a) {
print $m." called!";
}
}
$counter = new Counter();
$dbh->addListener($counter);
$dbh->addListener(new OutputLogger());
$dbh->query("SELECT * FROM foo");
// prints:
// onPreQuery called!
// onQuery called!
print $counter->count(); // 1
</code>
+++ Introduction
{{Doctrine_Db_Profiler}} is an eventlistener for {{Doctrine_Db}}. 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_Db_Profiler}} can be enabled by adding it as an eventlistener for {{Doctrine_Db}}.
+++ Basic usage
+++ Advanced usage
+++ Introduction
+++ Creating new listener
Creating a new listener is very easy. You can set the listener in global, connection or factory level.
<code type="php">
class MyListener extends Doctrine_EventListener {
public function onLoad(Doctrine_Record $record) {
print $record->getTable()->getComponentName()." just got loaded!";
}
public function onSave(Doctrine_Record $record) {
print "saved data access object!";
}
}
class MyListener2 extends Doctrine_EventListener {
public function onPreUpdate() {
try {
$record->set("updated",time());
} catch(InvalidKeyException $e) {
}
}
}
// setting global listener
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_LISTENER,new MyListener());
// setting connection level listener
$conn = $manager->openConnection($dbh);
$conn->setAttribute(Doctrine::ATTR_LISTENER,new MyListener2());
// setting factory level listener
$table = $conn->getTable("User");
$table->setAttribute(Doctrine::ATTR_LISTENER,new MyListener());
</code>
+++ List of events
Here is a list of availible events and their parameters:
<code type="php">
interface Doctrine_EventListener_Interface {
public function onLoad(Doctrine_Record $record);
public function onPreLoad(Doctrine_Record $record);
public function onUpdate(Doctrine_Record $record);
public function onPreUpdate(Doctrine_Record $record);
public function onCreate(Doctrine_Record $record);
public function onPreCreate(Doctrine_Record $record);
public function onSave(Doctrine_Record $record);
public function onPreSave(Doctrine_Record $record);
public function onInsert(Doctrine_Record $record);
public function onPreInsert(Doctrine_Record $record);
public function onDelete(Doctrine_Record $record);
public function onPreDelete(Doctrine_Record $record);
public function onEvict(Doctrine_Record $record);
public function onPreEvict(Doctrine_Record $record);
public function onSleep(Doctrine_Record $record);
public function onWakeUp(Doctrine_Record $record);
public function onClose(Doctrine_Connection $connection);
public function onPreClose(Doctrine_Connection $connection);
public function onOpen(Doctrine_Connection $connection);
public function onTransactionCommit(Doctrine_Connection $connection);
public function onPreTransactionCommit(Doctrine_Connection $connection);
public function onTransactionRollback(Doctrine_Connection $connection);
public function onPreTransactionRollback(Doctrine_Connection $connection);
public function onTransactionBegin(Doctrine_Connection $connection);
public function onPreTransactionBegin(Doctrine_Connection $connection);
public function onCollectionDelete(Doctrine_Collection $collection);
public function onPreCollectionDelete(Doctrine_Collection $collection);
}
</code>
+++ Listening events
<code type="php">
$table = $conn->getTable("User");
$table->setEventListener(new MyListener2());
// retrieve user whose primary key is 2
$user = $table->find(2);
$user->name = "John Locke";
// update event will be listened and current time will be assigned to the field 'updated'
$user->save();
</code>
+++ Chaining
+++ AccessorInvoker
<code type="php">
class User {
public function setTableDefinition() {
$this->hasColumn("name", "string", 200);
$this->hasColumn("password", "string", 32);
}
public function setPassword($password) {
return md5($password);
}
public function getName($name) {
return strtoupper($name);
}
}
$user = new User();
$user->name = 'someone';
print $user->name; // someone
$user->password = '123';
print $user->password; // 123
$user->setAttribute(Doctrine::ATTR_LISTENER, new Doctrine_EventListener_AccessorInvoker());
print $user->name; // SOMEONE
$user->password = '123';
print $user->password; // 202cb962ac59075b964b07152d234b70
</code>
+++ Creating a logger
+++ Overview
+++ List of exceptions
: {{InvalidKeyException}} :
: {{Doctrine_Exception}} :
: {{DQLException}} :
: {{Doctrine_PrimaryKey_Exception}} : thrown when {{Doctrine_Record}} is loaded and there is no primary key field
: {{Doctrine_Refresh_Exception}} : thrown when {{Doctrine_Record}} is refreshed and the refreshed primary key doens't match the old one
: {{Doctrine_Find_Exception}} : thrown when user tries to find a {{Doctrine_Record}} for given primary key and that object is not found
: {{Doctrine_Naming_Exception}} : thrown when user defined {{Doctrine_Table}} is badly named
: {{Doctrine_Connection_Exception}} : thrown when user tries to get the current connection and there are no open connections
: {{Doctrine_Table_Exception}} : thrown when user tries to initialize a new instance of {{Doctrine_Table}}, while there already exists an instance of that factory
: {{Doctrine_Mapping_Exception}} : thrown when user tries to get a foreign key object but the mapping is not done right
+++ Introduction
Many web applications have different kinds of lists. The lists may contain data from multiple components (= database tables) and they may have actions such as paging, sorting and setting conditions. {{Doctrine_Hook}} helps building these lists. It has a simple API for building search criteria forms as well as building a DQL query from the 'hooked' parameters.
+++ Building queries
+++ List of parsers
+++ Introduction
[**Note**: The term 'Transaction' doesnt refer to database transactions here but to the general meaning of this term]
[**Note**: This component is in **Alpha State**]
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 noone 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...
<code type="php">
// Get a locking manager instance
$lockingMngr = new Doctrine_Locking_Manager_Pessimistic();
try
{
// Ensure that old locks which timed out are released
// before we try to acquire our lock
// 300 seconds = 5 minutes timeout
$lockingMngr->releaseAgedLocks(300);
// Try to get the lock on a record
$gotLock = $lockingMngr->getLock(
// The record to lock. This can be any Doctrine_Record
$myRecordToLock,
// The unique identifier of the user who is trying to get the lock
'Bart Simpson'
);
if($gotLock)
{
echo "Got lock!";
// ... proceed
}
else
{
echo "Sorry, someone else is currently working on this record";
}
}
catch(Doctrine_Locking_Exception $dle)
{
echo $dle->getMessage();
// handle the error
}
</code>
At the page where the transaction finishes...
<code type="php">
// Get a locking manager instance
$lockingMngr = new Doctrine_Locking_Manager_Pessimistic();
try
{
if($lockingMngr->releaseLock($myRecordToUnlock, 'Bart Simpson'))
{
echo "Lock released";
}
else
{
echo "Record was not locked. No locks released.";
}
}
catch(Doctrine_Locking_Exception $dle)
{
echo $dle->getMessage();
// handle the error
}
</code>
+++ Planned
* Possibility to release locks of a specific Record type (i.e. releasing all locks on 'User' objects).
+++ 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 behaviour may change in the future to provide a centralised and consistent table creation procedure for installation purposes.
+++ Maintainer
Roman Borschel - romanb at #doctrine (freenode)
Don't hesitate to contact me if you have questions, ideas, ect.
+++ Introduction
DQL (Doctrine Query Language) is a object query language which allows you to find objects. DQL understands things like object relationships, polymorphism and inheritance (including column aggregation inheritance). For more info about DQL see the actual DQL chapter.
{{Doctrine_Query}} along with {{Doctrine_Expression}} provide an easy-to-use wrapper for writing DQL queries. Creating a new query object can be done by either using the new operator or by calling create method. The create method exists for allowing easy method call chaining.
<code type="php">
// initalizing a new Doctrine_Query (using the current connection)
$q = new Doctrine_Query();
// initalizing a new Doctrine_Query (using custom connection parameter)
// here $conn is an instance of Doctrine_Connection
$q = new Doctrine_Query($conn);
// an example using the create method
// here we simple fetch all users
$users = new Doctrine_Query::create()->from('User')->execute();
</code>
+++ Selecting tables
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. {{Doctrine_Query}} offers easy to use methods such as {{from()}}, {{addFrom()}}, {{leftJoin()}} and {{innerJoin()}} for managing the {{FROM}} part of your DQL query.
<code type="php">
// find all users
$q = new Doctrine_Query();
$coll = $q->from('User')->execute();
// find all users with only their names (and primary keys) fetched
$coll = $q->select('u.name')->('User u');
</code>
The following example shows how to use leftJoin and innerJoin methods:
<code type="php">
// find all groups
$coll = $q->from("FROM Group");
// find all users and user emails
$coll = $q->from("FROM User u LEFT JOIN u.Email e");
// find all users and user emails with only user name and
// age + email address loaded
$coll = $q->select('u.name, u.age, e.address')
->from('FROM User u')
->leftJoin('u.Email e')
->execute();
// find all users, user email and user phonenumbers
$coll = $q->from('FROM User u')
->innerJoin('u.Email e')
->innerJoin('u.Phonenumber p')
->execute();
</code>
+++ Limiting the query results
<code type="php">
// find the first ten users and associated emails
$q = new Doctrine_Query();
$coll = $q->from('User u LEFT JOIN u.Email e')->limit(10);
// find the first ten users starting from the user number 5
$coll = $q->from('User u')->limit(10)->offset(5);
</code>
+++ Setting query conditions
The {{WHERE}} clause, if given, indicates the condition or conditions that the records must satisfy to be selected.
{{Doctrine_Query}} provides easy to use {{WHERE}} -part management methods {{where}} and {{addWhere}}. The {{where}} methods always overrides the query {{WHERE}} -part whereas {{addWhere}} adds new condition to the {{WHERE}} -part stack.
<code type="php">
// find all groups where the group primary key is bigger than 10
$coll = $q->from('Group')->where('Group.id > 10');
// the same query using Doctrine_Expression component
$e = $q->expr;
$coll = $q->from('Group')->where($e->gt('Group.id', 10));
</code>
Using regular expression operator:
<code type="php">
// find all users where users where user name matches
// a regular expression, regular expressions must be
// supported by the underlying database
$coll = $conn->query("FROM User WHERE User.name REGEXP '[ad]'");
</code>
DQL has support for portable {{LIKE}} operator:
<code type="php">
// find all users and their associated emails
// where SOME of the users phonenumbers
// (the association between user and phonenumber
// tables is One-To-Many) starts with 123
$coll = $q->select('u.*, e.*')
->from('User u LEFT JOIN u.Email e LEFT JOIN u.Phonenumber p')
->where(\"p.phonenumber LIKE '123%'");
</code>
Using multiple conditions and condition nesting are also possible:
<code type="php">
// multiple conditions
$coll = $q->select('u.*')
->from('User u LEFT JOIN u.Email e')
->where(\"u.name LIKE '%Jack%' AND e.address LIKE '%@drinkmore.info'\");
// nesting conditions
$coll = $q->select('u.*')
->from('User u LEFT JOIN u.Email e')
->where(\"u.name LIKE '%Jack%' OR u.name LIKE '%John%') AND e.address LIKE '%@drinkmore.info'");
</code>
+++ HAVING conditions
{{Doctrine_Query}} provides {{having()}} method for adding {{HAVING}} conditions to the DQL query. This method is identical in function to the {{Doctrine_Query::where()}} method.
If you call {{having()}} multiple times, the conditions are ANDed together; if you want to OR a condition, use {{orHaving()}}.
<code type="php">
$q = new Doctrine_Query();
$users = $q->select('u.name')
->from('User u')
->leftJoin('u.Phonenumber p');
->having('COUNT(p.id) > 3');
</code>
+++ Sorting query results
{{ORDER BY}} - part works in much same way as SQL {{ORDER BY}}.
<code type="php">
$q = new Doctrine_Query();
// find all users, sort by name descending
$users = $q->from('User u')->orderby('u.name DESC');
// find all users sort by name ascending
$users = $q->from('User u')->orderby('u.name ASC');
// find all users and their emails, sort by email address in ascending order
$users = $q->from('User u')->leftJoin('u.Email e')->orderby('e.address');
// find all users and their emails, sort by user name and email address
$users = $q->from('User u')->leftJoin('u.Email e')
->addOrderby('u.name')->addOrderby('e.address');
// grab randomly 10 users
$users = $q->select('u.*, RAND() rand')->from('User u')->limit(10)->orderby('rand DESC');
</code>
+++ Introduction
In Doctrine you may express your queries in the native SQL dialect of your database. This is useful if you want to use the full power of your database vendor's features (like query hints or the CONNECT keyword in Oracle).
It should be noted that not all the sql is portable. So when you make database portable applications you might want to use the DQL API instead.
+++ Using SQL
The {{rawSql}} component works in much same way as {{Zend_Db_Select}}. You may use method overloading like {{$q->from()->where()}} or just use {{$q->parseQuery()}}. There are some differences though:
# In {{Doctrine_RawSql}} component you need to specify all the mapped table columns in curly brackets {} this is used for smart column aliasing.
# When joining multiple tables you need to specify the component paths with {{addComponent()}} method
The following example represents a very simple case where no {{addComponent()}} calls are needed.
Here we select all entities from table entity with all the columns loaded in the records.
<code type="php">
$query = new Doctrine_RawSql($conn);
$query->parseQuery("SELECT {entity.name} FROM entity");
$entities = $query->execute();
</code>
+++ Adding components
The following example represents a bit harder case where we select all entities and their associated phonenumbers using a left join. Again we wrap all the columns in curly brackets but we also specify what tables associate to which components.
First we specify that table entity maps to record class {{Entity}}.
Then we specify that table phonenumber maps to {{Entity.Phonenumber}} (meaning phonenumber associated with an entity).
<code type="php">
$query = new Doctrine_RawSql($conn);
$query->parseQuery("SELECT {entity.*}, {phonenumber.*}
FROM entity
LEFT JOIN phonenumber
ON phonenumber.entity_id = entity.id");
$query->addComponent("entity", "Entity");
$query->addComponent("phonenumber", "Entity.Phonenumber");
$entities = $query->execute();
</code>
+++ Method overloading
<code type="php">
$query = new Doctrine_RawSql($conn);
$query->select('{entity.name}')
->from('entity');
$query->addComponent("entity", "User");
$coll = $query->execute();
</code>
+++ Introduction
Validation in Doctrine is a way to enforce your business rules in the model part of the MVC architecture. You can think of this validation as a gateway that needs to be passed right before data gets into the persistent data store. The definition of these business rules takes place at the record level, that means in your active record model classes (classes derived from {{Doctrine_Record}}). The first thing you need to do to be able to use this kind of validation is to enable it globally. This is done through the {{Doctrine_Manager}} (see the code below).
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.
<code type="php">
// turning on validation
Doctrine_Manager::getInstance()->setAttribute(Doctrine::ATTR_VLD, true);
</code>
+++ 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 at the bottom of this page. You can specify which validators apply to which column through the 4th argument of the {{hasColumn()}} method. If that is still not enough and you need some specialized validation that is not yet available as a predefined validator you have three options:
* 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:
* {{validate()}} (Executed every time the record gets validated)
* {{validateOnInsert()}} (Executed when the record is new and gets validated)
* {{validateOnUpdate()}} (Executed when the record is not new and gets validated)
If you need a special validation in your active record you can simply override one of these methods in your active record class (a descendant of {{Doctrine_Record}}). Within thess methods you can use all the power of PHP to validate your fields. When a field doesnt 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:
<code type="php">
class User extends Doctrine_Record {
public function setUp() {
$this->ownsOne("Email","User.email_id");
}
public function setTableDefinition() {
// no special validators used only types
// and lengths will be validated
$this->hasColumn("name","string",15);
$this->hasColumn("email_id","integer");
$this->hasColumn("created","integer",11);
}
// Our own validation
protected function validate() {
if ($this->name == 'God') {
// Blasphemy! Stop that! ;-)
// syntax: add(<fieldName>, <error code/identifier>)
$this->getErrorStack()->add('name', 'forbiddenName');
}
}
}
class Email extends Doctrine_Record {
public function setTableDefinition() {
// validators 'email' and 'unique' used
$this->hasColumn("address","string",150, array("email", "unique"));
}
}
</code>
+++ 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 {{$record->save()}}) the full validation procedure is executed. If errors occur during that process an exception of the type {{Doctrine_Validator_Exception}} will be thrown. You can catch that exception and analyze the errors by using the instance method {{Doctine_Validator_Exception::getInvalidRecords()}}. This method returns an ordinary array with references to all records that did not pass validation. You can then further explore the errors of each record by analyzing the error stack of each record. The error stack of a record can be obtained with the instance method {{Doctrine_Record::getErrorStack()}}. Each error stack is an instance of the class {{Doctrine_Validator_ErrorStack}}. The error stack provides an easy to use interface to inspect the errors.
++++ Explicit validation
You can explicitly trigger the validation for any record at any time. For this purpose Doctrine_Record provides the instance method {{Doctrine_Record::isValid()}}. This method returns a boolean value indicating the result of the validation. If the method returns false, you can inspect the error stack in the same way as seen above except that no exception is thrown, so you simply obtain the error stack of the record that didnt pass validation through {{Doctrine_Record::getErrorStack()}}.
The following code snippet shows an example of handling implicit validation which caused a {{Doctrine_Validator_Exception}}.
<code type="php">
try {
$user->name = "this is an example of too long name";
$user->Email->address = "drink@@notvalid..";
$user->save();
} catch(Doctrine_Validator_Exception $e) {
// Note: you could also use $e->getInvalidRecords(). The direct way
// used here is just more simple when you know the records you're dealing with.
$userErrors = $user->getErrorStack();
$emailErrors = $user->Email->getErrorStack();
/* Inspect user errors */
foreach($userErrors as $fieldName => $errorCodes) {
switch ($fieldName) {
case 'name':
// $user->name is invalid. inspect the error codes if needed.
break;
}
}
/* Inspect email errors */
foreach($emailErrors as $fieldName => $errorCodes) {
switch ($fieldName) {
case 'address':
// $user->Email->address is invalid. inspect the error codes if needed.
break;
}
}
}
</code>
+++ List of predefined validators
Here is a list of predefined validators. You cannot use these names for your custom validators.
||~ name ||~ arguments ||~ task ||
|| email || || Check if value is valid email. ||
|| notblank || || Check if value is not blank. ||
|| notnull || || Check if value is not null. ||
|| country || || Check if valid is valid country code. ||
|| ip || || Checks if value is valid IP (internet protocol) address. ||
|| htmlcolor || || Checks if value is valid html color. ||
|| nospace || || Check if value has no space chars. ||
|| range || [min,max] || Checks if value is in range specified by arguments. ||
|| unique || || Checks if value is unique in its database table. ||
|| regexp || [expression] || Check if valie matches a given regexp. ||
+++ Introduction
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.
+++ Managing views
<code type="php">
$conn = Doctrine_Manager::getInstance()
->openConnection(new PDO("dsn","username","password"));
$query = new Doctrine_Query($conn);
$query->from('User.Phonenumber')->limit(20);
$view = new Doctrine_View($query, 'MyView');
// creating a database view
$view->create();
// dropping the view from the database
$view->drop();
</code>
+++ Using views
<code type="php">
$conn = Doctrine_Manager::getInstance()
->openConnection(new PDO("dsn","username","password"));
$query = new Doctrine_Query($conn);
$query->from('User.Phonenumber')->limit(20);
// hook the query into appropriate view
$view = new Doctrine_View($query, 'MyView');
// now fetch the data from the view
$coll = $view->execute();
</code>
++ Introduction
++ Available options
++ Drivers
+++ Memcache
+++ APC
+++ Sqlite
{{Doctrine_Cache}} offers many options for performance fine-tuning:
: {{savePropability}} : Option that defines the propability of which a query is getting cached.
: {{cleanPropability}} : Option that defines the propability the actual cleaning will occur when calling {{Doctrine_Cache::clean()}}
: {{statsPropability}} :
There are couple of availible Cache attributes on Doctrine:
* Doctrine::ATTR_CACHE_SIZE
* Defines which cache container Doctrine uses
* Possible values: Doctrine::CACHE_* (for example Doctrine::CACHE_FILE)
* Doctrine::ATTR_CACHE_DIR
* cache directory where .cache files are saved
* the default cache dir is %ROOT%/cachedir, where
%ROOT% is automatically converted to doctrine root dir
* Doctrine::ATTR_CACHE_SLAM
* On very busy servers whenever you start the server or modify files you can create a race of many processes all trying to cache the same file at the same time. This option sets the percentage of processes that will skip trying to cache an uncached file. Or think of it as the probability of a single process to skip caching. For example, setting apc.slam_defense to 75 would mean that there is a 75% chance that the process will not cache an uncached file. So, the higher the setting the greater the defense against cache slams. Setting this to 0 disables this feature
* Doctrine::ATTR_CACHE_SIZE
* Cache size attribute
* Doctrine::ATTR_CACHE_TTL
* How often the cache is cleaned
{{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)
* Manual tuning and/or self-optimization. {{Doctrine_Cache}} knows how to optimize itself, yet it leaves user full freedom of whether or not he/she wants to take advantage of this feature.
* Advanced options for fine-tuning. {{Doctrine_Cache}} has many options for fine-tuning performance.
* Cache hooks itself directly into {{Doctrine_Db}} eventlistener system allowing it to be easily added on-demand.
{{Doctrine_Cache}} hooks into {{Doctrine_Db}} eventlistener system allowing pluggable caching. It evaluates queries and puts {{SELECT}} statements in cache. The caching is based on propabalistics. For example if {{savePropability = 0.1}} there is a 10% chance that a query gets cached.
Now eventually the cache would grow very big, hence Doctrine uses propabalistic cache cleaning. When calling {{Doctrine_Cache::clean()}} with {{cleanPropability = 0.25}} there is a 25% chance of the clean operation being invoked. What the cleaning does is that it first reads all the queries in the stats file and sorts them by the number of times occurred. Then if the size is set to 100 it means the cleaning operation will leave 100 most issued queries in cache and delete all other cache entries.
Initializing a new cache instance:
<code type="php">
$dbh = new Doctrine_Db('mysql:host=localhost;dbname=test', $user, $pass);
$cache = new Doctrine_Cache('memcache');
// register it as a Doctrine_Db listener
$dbh->addListener($cache);
</code>
Now you know how to set up the query cache. In the next chapter you'll learn how to tweak the cache in order to get maximum performance.
Doctrine has very comprehensive and fast caching solution.
Its cache is **always up-to-date**.
In order to achieve this doctrine does the following things:
|| 1. Every Doctrine_Table has its own cache directory. The default is cache/componentname/. All the cache files are saved into that directory.
The format of each cache file is [primarykey].cache.
2. When retrieving records from the database doctrine always tries to hit the cache first.
3. If a record (Doctrine_Record) is retrieved from database or inserted into database it will be saved into cache.
4. When a Data Access Object is deleted or updated it will be deleted from the cache ||
Now one might wonder that this kind of solution won't work since eventually the cache will be a copy of database!
So doctrine does the following things to ensure the cache won't get too big:
|| 1. Every time a cache file is accessed the id of that record will be added into the $fetched property of Doctrine_Cache
2. At the end of each script the Doctrine_Cache destructor will write all these primary keys at the end of a stats.cache file
3. Doctrine does propabalistic cache cleaning. The default interval is 200 page loads (= 200 constructed Doctrine_Managers). Basically this means
that the average number of page loads between cache cleans is 200.
4. On every cache clean stats.cache files are being read and the least accessed cache files
(cache files that have the smallest id occurance in the stats file) are then deleted.
For example if the cache size is set to 200 and the number of files in cache is 300, then 100 least accessed files are being deleted.
Doctrine also clears every stats.cache file. ||
So for every 199 fast page loads there is one page load which suffers a little overhead from the cache cleaning operation.
++ Overview
++ PHP File Formatting
++ Naming Conventions
++ Coding Style
++ Testing
+++ PHP code demarcation
* PHP code must always be delimited by the full-form, standard PHP tags
* 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:
<code type="php">
// literal string
$string = 'something';
</code>
* 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:
<code type="php">
// string contains apostrophes
$sql = "SELECT id, name FROM people WHERE name = 'Fred' OR name = 'Susan'";
</code>
* Variable substitution is permitted using the following form:
<code type="php">
// variable substitution
$greeting = "Hello $name, welcome back!";
</code>
* Strings may be concatenated using the "." operator. A space must always be added before and after the "." operator to improve readability:
<code type="php">
// concatenation
$framework = 'Doctrine' . ' ORM ' . 'Framework';
</code>
* 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:
<code type="php">
// concatenation line breaking
$sql = "SELECT id, name FROM user "
. "WHERE name = ? "
. "ORDER BY name ASC";
</code>
+++ Arrays
* Negative numbers are not permitted as indices.
* An 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:
<code type="php">
$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');
</code>
+++ Classes
* Classes must be named by following the naming conventions.
* The brace is always written right 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.
* 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:
<code type="php">
/**
* Documentation here
*/
class Doctrine_SampleClass {
// entire content of class
// must be indented four spaces
}
</code>
+++ Functions and methods
* Methods must be named by following the naming conventions.
* Methods must always declare their visibility by using one of the private, protected, or public constructs.
* Like classes, the brace is always written right 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:
<code type="php">
/**
* Documentation Block Here
*/
class Foo {
/**
* Documentation Block Here
*/
public function bar() {
// entire content of function
// must be indented four spaces
}
}
</code>
* Passing by-reference is permitted in the function declaration only:
<code type="php">
/**
* Documentation Block Here
*/
class Foo {
/**
* Documentation Block Here
*/
public function bar(&$baz) {
}
}
</code>
* 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.
<code type="php">
/**
* Documentation Block Here
*/
class Foo {
/**
* WRONG
*/
public function bar() {
return($this->bar);
}
/**
* RIGHT
*/
public function bar() {
return $this->bar;
}
}
</code>
* 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:
<code type="php">
threeArguments(1, 2, 3);
</code>
* Call-time pass by-reference is prohibited. See the function declarations section 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:
<code type="php">
threeArguments(array(1, 2, 3), 2, 3);
threeArguments(array(1, 2, 3, 'Framework',
'Doctrine', 56.44, 500), 2, 3);
</code>
+++ 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.
<code type="php">
if ($foo != 2) {
$foo = 2;
}
</code>
* For {{if}} statements that include {{elseif}} or {{else}}, the formatting must be as in these examples:
<code type="php">
if ($foo != 1) {
$foo = 1;
} else {
$foo = 3;
}
if ($foo != 2) {
$foo = 2;
} elseif ($foo == 1) {
$foo = 3;
} else {
$foo = 11;
}
</code>
* PHP allows for these statements to be written without braces in some circumstances, the following format for if statements is also allowed:
<code type="php">
if ($foo != 1)
$foo = 1;
else
$foo = 3;
if ($foo != 2)
$foo = 2;
elseif ($foo == 1)
$foo = 3;
else
$foo = 11;
</code>
* 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.
<code type="php">
switch ($case) {
case 1:
case 2:
break;
case 3:
break;
default:
break;
}
</code>
* 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/
Methods:
* 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}}:
* {{@throws exceptionclass [description]}}
+++ Classes
* The Doctrine ORM Framework uses the same class naming convention as PEAR and Zend framework, where the names of the classes directly map to the directories in which they are stored. The root level directory of the Doctrine Framework is the "Doctrine/" directory, under which all classes are stored hierarchially.
* Class names may only contain alphanumeric characters. Numbers are permitted in class names but are discouraged. Underscores are only permitted in place of the path separator, eg. the filename "Doctrine/Table/Exception.php" must map to the class name "Doctrine_Table_Exception".
* If a class name is comprised of more than one word, the first letter of each new word must be capitalized. Successive capitalized letters are not allowed, e.g. a class "XML_Reader" is not allowed while "Xml_Reader" is acceptable.
+++ Interfaces
* Interface classes must follow the same conventions as other classes (see above), however must end with the word "{{Interface}}" (unless the interface is approved not to contain it such as {{Doctrine_Overloadable}}). Some examples:
* {{Doctrine_Db_EventListener_Interface}}
* {{Doctrine_EventListener_Interface}}
+++ Filenames
* For all other files, only alphanumeric characters, underscores, and the dash character ("-") are permitted. Spaces are prohibited.
* Any file that contains any PHP code must end with the extension ".php". These examples show the acceptable filenames for containing the class names from the examples in the section above:
* {{Doctrine/Db.php}}
* {{Doctrine/Connection/Transaction.php}}
* File names must follow the mapping to class names described above.
+++ Functions and methods
* Function names may only contain alphanumeric characters. Underscores are not permitted. Numbers are permitted in function names but are discouraged.
* Function names must always start with a lowercase letter. 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. 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 Doctrine_Record which has some accessor methods prefixed with 'obtain' and 'assign'. The reason for this is that since all user defined ActiveRecords inherit {{Doctrine_Record}}, it should populate the get / set namespace as little as possible.
* Functions in the global scope ("floating functions") are NOT permmitted. All static functions should be wrapped in a static class.
+++ Variables
All variables must satisfy the following conditions:
* Variable names may only contain alphanumeric characters. Underscores are not permitted. Numbers are permitted in variable names but are discouraged.
* Variable names 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}} || {{$query}} ||
|| {{Doctrine_Db}} || {{$db}} ||
* 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
Following rules must apply to all constants used within Doctrine framework:
* Constants may contain both alphanumeric characters and the underscore.
* Constants 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.
<code type="php">
class Doctrine_SomeClass {
const MY_CONSTANT = 'something';
}
print Doctrine_SomeClass::MY_CONSTANT;
</code>
+++ Record columns
+++ 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.
IMPORTANT: 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. Lines must end only with a linefeed (LF). Linefeeds are represented as ordinal 10, or hexadecimal 0x0A.
* Do not use carriage returns (CR) like Macintosh computers (0x0D).
* Do not use the carriage return/linefeed combination (CRLF) as Windows computers (0x0D, 0x0A).
+++ Writing tests
++++ Classes
* All test classes should be referring to a class or specific testing aspect of some class.
* For example {{Doctrine_Record_TestCase}} is a valid name since its referring to class named {{Doctrine_Record}}.
* {{Doctrine_Record_State_TestCase}} is also a valid name since its referring to testing the state aspect of the {{Doctrine_Record}} class.
* However something like {{Doctrine_PrimaryKey_TestCase}} is not valid since its way too generic.
* Every class should have atleast one {{TestCase}} equivalent
* All testcase classes should inherit {{Doctrine_UnitTestCase}}
++++ Methods
* All methods should support agile documentation; if some method failed it should be evident from the name of the test method what went wrong. Also the test method names should give information of the system they test.
* For example {{Doctrine_Export_Pgsql_TestCase::testCreateTableSupportsAutoincPks()}} is a valid test method name. Just by looking at it we know what it is testing. Also we can run agile documentation tool to get little up-to-date system information.
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!
* Test method names can often be long. However the content within the methods should rarely be more than dozen lines long. If you need several assert-calls divide the method into smaller methods.
++++ Assertions
* There should never be assertions within any loops and rarely within functions.
++ Introduction
++ Levels of configuration
++ Setting attributes
<code type="php">
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_LISTENER, new MyListener());
</code>
Doctrine has a three-level configuration structure. You can set configuration attributes in 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 user first sets default fetchmode in global level to {{Doctrine::FETCH_BATCH}} and then sets {{example}} table fetchmode to {{Doctrine::FETCH_LAZY}}, the lazy fetching strategy will be used whenever the records of 'example' 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.
<code type="php">
// setting a global level attribute
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_VLD, false);
// setting a connection level attribute
// (overrides the global level attribute on this connection)
$conn = $manager->openConnection(new PDO('dsn', 'username', 'pw'));
$conn->setAttribute(Doctrine::ATTR_VLD, true);
// setting a table level attribute
// (overrides the connection/global level attribute on this table)
$table = $conn->getTable('User');
$table->setAttribute(Doctrine::ATTR_LISTENER, new UserListener());
</code>
+++ 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 scripts over to another server type. PEAR Doctrine:: strives to overcome these differences so your program can switch between DBMS's without any changes.
You control which portability modes are enabled by using the portability configuration option. Configuration options are set via {{factory()}} and {{setOption()}}.
The portability modes are bitwised, so they can be combined using {{|}} and removed using {{^}}. See the examples section below on how to do this.
++++ Portability Mode Constants
: {{Doctrine::PORTABILITY_ALL}} (default) : turn on all portability features. this is the default setting.
: {{Doctrine::PORTABILITY_DELETE_COUNT}} : Force reporting the number of rows deleted. Some DBMS's don't count the number of rows deleted when performing simple {{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.
: {{Doctrine::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.
: {{Doctrine::PORTABILITY_ERRORS}} : Makes certain error messages in certain drivers compatible with those from other DBMS's
Error Code Re-mappings:
||~ Driver ||~ Description ||~ Old Constant ||~ New Constant ||
|| mysql, mysqli || unique and primary key constraints || {{Doctrine::ERROR_ALREADY_EXISTS}} || {{Doctrine::ERROR_CONSTRAINT}} ||
|| mysql, mysqli || not-null constraints || {{Doctrine::ERROR_CONSTRAINT}} || {{Doctrine::ERROR_CONSTRAINT_NOT_NULL}} ||
: {{Doctrine::PORTABILITY_FIX_ASSOC_FIELD_NAMES}} : This removes any qualifiers from keys in associative fetches. some RDBMS , like for example SQLite, will be default use the fully qualified name for a column in assoc fetches if it is qualified in a query.
: {{Doctrine::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}}
: {{Doctrine::PORTABILITY_NONE}} : Turn off all portability features
: {{Doctrine::PORTABILITY_NUMROWS}} : Enable hack that makes {{numRows()}} work in Oracle
: {{Doctrine::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
Using {{setAttribute()}} to enable portability for lowercasing and trimming
<code type="php">
$conn->setAttribute('portability',
Doctrine::PORTABILITY_FIX_CASE | Doctrine::PORTABILITY_RTRIM);
</code>
Using {{setAttribute()}} to enable all portability options except trimming
<code type="php">
$conn->setAttribute('portability',
Doctrine::PORTABILITY_ALL ^ Doctrine::PORTABILITY_RTRIM);
</code>
+++ Identifier quoting
You can quote the db identifiers (table and field names) with {{quoteIdentifier()}}. The delimiting style depends on which database driver is being used.
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:
* backtick (`) -- due to MySQL
* double quote (") -- due to Oracle
* brackets ([ or ]) -- due to Access
Delimited identifiers are known to generally work correctly under the following drivers:
* Mssql
* Mysql
* Oracle
* Pgsql
* Sqlite
* Firebird
When using the {{quote_identifiers}} option, all of the field identifiers will be automatically quoted in the resulting SQL statements:
<code type="php">
$conn->setAttribute('quote_identifiers', true);
</code>
will result in a SQL statement that all the field names are quoted with the backtick '`' operator (in MySQL).
<code type="sql">
SELECT * FROM `sometable` WHERE `id` = '123'
</code>
as opposed to:
<code type="sql">
SELECT * FROM sometable WHERE id='123'
</code>
+++ Table creation
<code type="php">
// turns automatic table creation off
$manager->setAttribute(Doctrine::ATTR_CREATE_TABLES, false);
</code>
+++ Fetching strategy
<code type="php">
// sets the default collection type (fetching strategy)
$manager->setAttribute(Doctrine::ATTR_FETCHMODE, Doctrine::FETCH_LAZY);
</code>
+++ Batch size
<code type="php">
// setting default batch size for batch collections
$manager->setAttribute(Doctrine::ATTR_BATCH_SIZE, 7);
</code>
+++ Session lockmode
<code type="php">
// setting default lockmode
$manager->setAttribute(Doctrine::ATTR_LOCKMODE, Doctrine::LOCK_PESSIMISTIC);
</code>
+++ Event listener
<code type="php">
// setting default event listener
$manager->setAttribute(Doctrine::ATTR_LISTENER, new MyListener());
</code>
+++ Validation
<code type="php">
// turns transactional validation on
$manager->setAttribute(Doctrine::ATTR_VLD, true);
</code>
+++ Offset collection limit
<code type="php">
// sets the default offset collection limit
$manager->setAttribute(Doctrine::ATTR_COLL_LIMIT, 10);
</code>
++ DSN, the Data Source Name
++ Opening a new connection
++ Lazy-connecting to database
++ Managing connections
++ Connection-component binding
......@@ -24,11 +24,8 @@ The DSN consists in the following parts:
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
......@@ -43,6 +40,7 @@ phptype
The currently supported database backends are:
||//fbsql//|| -> FrontBase ||
||//ibase//|| -> InterBase / Firebird (requires PHP 5) ||
||//mssql//|| -> Microsoft SQL Server (NOT for Sybase. Compile PHP --with-mssql) ||
......@@ -53,13 +51,10 @@ The currently supported database backends are:
||//querysim//|| -> QuerySim ||
||//sqlite//|| -> SQLite 2 ||
A second DSN format is supported 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:
``: = %3a``
``/ = %2f``
``@ = %40``
......@@ -69,9 +64,6 @@ If your database, option values, username or password contain characters used to
``? = %3f``
``= = %3d``
``& = %26``
Warning
Please note, that some features may be not supported by all database backends.
......
......@@ -42,8 +42,6 @@ $manager->getCurrentConnection(); // $conn
You can iterate over the opened connection by simple passing the manager object to foreach clause. This is possible since {{Doctrine_Manager}} implements special {{IteratorAggregate}} interface.
<code type="php">
// iterating through connections
......
Opening a new database connection in Doctrine is very easy. If you wish to use PDO (www.php.net/PDO) you can just initalize a new PDO object:
<code type="php">
$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
......@@ -15,11 +12,7 @@ try {
}
</code>
If your database extension isn't supported by PDO you can use special Doctrine_Adapter class (if availible). The following example uses db2 adapter:
If your database extension isn't supported by PDO you can use special {{Doctrine_Adapter}} class (if availible). The following example uses db2 adapter:
<code type="php">
$dsn = 'db2:dbname=testdb;host=127.0.0.1';
......@@ -33,11 +26,7 @@ try {
}
</code>
The next step is opening a new Doctrine_Connection.
The next step is opening a new {{Doctrine_Connection}}.
<code type="php">
$conn = Doctrine_Manager::connection($dbh);
......
+++ Oracle
++++ Making unsuported functions work
+++ Mysql
++++ Tips and tricks
+++ Export
++++ Introduction
++++ Creating new table
<code type="php">
$dbh = new PDO('dsn','username','pw');
$conn = Doctrine_Manager::getInstance()->openConnection($dbh);
$fields = array('id' => array(
'type' => 'integer',
'autoincrement' => true),
'name' => array(
'type' => 'string',
'fixed' => true,
'length' => 8)
);
// the following option is mysql specific and
// skipped by other drivers
$options = array('type' => 'MYISAM');
$conn->export->createTable('mytable', $fields);
// on mysql this executes query:
// CREATE TABLE mytable (id INT AUTO_INCREMENT PRIMARY KEY,
// name CHAR(8));
</code>
++++ Altering table
Doctrine_Export drivers provide an easy database portable way of altering existing database tables.
NOTE: if you only want to get the generated sql (and not execute it) use Doctrine_Export::alterTableSql()
<code type="php">
$dbh = new PDO('dsn','username','pw');
$conn = Doctrine_Manager::getInstance()
->openConnection($dbh);
$a = array('add' => array('name' => array('type' => 'string', 'length' => 255)));
$conn->export->alterTableSql('mytable', $a);
// On mysql this method returns:
// ALTER TABLE mytable ADD COLUMN name VARCHAR(255)
</code>
Doctrine_Export::alterTable() takes two parameters:
: string //$name// : name of the table that is intended to be changed.
: array //$changes// : associative array that contains the details of each type of change that is intended to be performed.
The types of changes that are currently supported are defined as follows:
* //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.
<code type="php">
$a = array('name' => 'userlist',
'add' => array(
'quota' => array(
'type' => 'integer',
'unsigned' => 1
)
),
'remove' => array(
'file_limit' => array(),
'time_limit' => array()
),
'change' => array(
'name' => array(
'length' => '20',
'definition' => array(
'type' => 'text',
'length' => 20
)
)
),
'rename' => array(
'sex' => array(
'name' => 'gender',
'definition' => array(
'type' => 'text',
'length' => 1,
'default' => 'M'
)
)
)
);
$dbh = new PDO('dsn','username','pw');
$conn = Doctrine_Manager::getInstance()->openConnection($dbh);
$conn->export->alterTable('mytable', $a);
</code>
+++ Import
++++ Introduction
++++ Getting table info
++++ Getting foreign key info
++++ Getting view info
+++ Util
++++ Using explain
+++ DataDict
++++ Getting portable type
++++ Getting database declaration
++++ Reserved keywords
++ Introduction
++ SELECT queries
++ UPDATE queries
++ DELETE queries
++ FROM clause
++ WHERE clause
++ Conditional expressions
++ Functional Expressions
++ Subqueries
++ GROUP BY, HAVING clauses
++ ORDER BY clause
++ LIMIT and OFFSET clauses
++ Examples
++ BNF
+++ Literals
**Strings**
A string literal is enclosed in single quotes; for example: 'literal'. A string literal that includes a single quote is represented by two single quotes; for example: 'literal''s'.
<code>
FROM User WHERE User.name = 'Vincent'
</code>
**Integers**
Integer literals support the use of PHP integer literal syntax.
<code>
FROM User WHERE User.id = 4
</code>
**Floats**
Float literals support the use of PHP float literal syntax.
<code>
FROM Account WHERE Account.amount = 432.123
</code>
**Booleans**
The boolean literals are true and false.
<code>
FROM User WHERE User.admin = true
FROM Session WHERE Session.is_authed = false
</code>
**Enums**
The enumerated values work in the same way as string literals.
<code>
FROM User WHERE User.type = 'admin'
</code>
Predefined reserved literals are case insensitive, although its a good standard to write them in uppercase.
+++ Input parameters
<code type="php">
// POSITIONAL PARAMETERS:
$users = $conn->query("FROM User WHERE User.name = ?", array('Arnold'));
$users = $conn->query("FROM User WHERE User.id > ? AND User.name LIKE ?", array(50, 'A%'));
// NAMED PARAMETERS:
$users = $conn->query("FROM User WHERE User.name = :name", array(':name' => 'Arnold'));
$users = $conn->query("FROM User WHERE User.id > :id AND User.name LIKE :name", array(':id' => 50, ':name' => 'A%'));
</code>
+++ 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 ||
|| =, >, >=, <, <=, <> (not equal), || Comparison operators ||
|| [NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY || ||
|| || //Logical operators: // ||
|| NOT || ||
|| AND || ||
|| OR || ||
+++ Between expressions
+++ In expressions
Syntax:
<code>
<operand> IN (<subquery>|<value list>)
</code>
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.
<code>
FROM C1 WHERE C1.col1 IN (FROM C2(col1));
FROM User WHERE User.id IN (1,3,4,5)
</code>
The keyword IN is an alias for = ANY. Thus, these two statements are equal:
<code>
FROM C1 WHERE C1.col1 = ANY (FROM C2(col1));
FROM C1 WHERE C1.col1 IN (FROM C2(col1));
</code>
+++ Like Expressions
Syntax:
<code>
string_expression [NOT] LIKE pattern_value [ESCAPE escape_character]
</code>
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.
<code type="php">
// finding all users whose email ends with '@gmail.com'
$users = $conn->query("FROM User u, u.Email e WHERE e.address LIKE '%@gmail.com'");
// finding all users whose name starts with letter 'A'
$users = $conn->query("FROM User u WHERE u.name LIKE 'A%'");
</code>
+++ Null Comparison Expressions
+++ Empty Collection Comparison Expressions
+++ Collection Member Expressions
+++ Exists Expressions
Syntax:
<code>
<operand> [NOT ]EXISTS (<subquery>)
</code>
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.
Finding all articles which have readers:
<code>
FROM Article
WHERE EXISTS (FROM ReaderLog(id)
WHERE ReaderLog.article_id = Article.id)
</code>
Finding all articles which don't have readers:
<code>
FROM Article
WHERE NOT EXISTS (FROM ReaderLog(id)
WHERE ReaderLog.article_id = Article.id)
</code>
+++ All and Any Expressions
Syntax:
<code>
operand comparison_operator ANY (subquery)
operand comparison_operator SOME (subquery)
operand comparison_operator ALL (subquery)
</code>
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.
<code>
FROM C WHERE C.col1 < ALL (FROM C2(col1))
</code>
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.
<code>
FROM C WHERE C.col1 > ANY (FROM C2(col1))
</code>
The keyword SOME is an alias for ANY.
<code>
FROM C WHERE C.col1 > SOME (FROM C2(col1))
</code>
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:
<code>
FROM C WHERE C.col1 <> ALL (FROM C2(col1));
FROM C WHERE C.col1 NOT IN (FROM C2(col1));
</code>
+++ 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."
<code type="php">
// finding all users which don't belong to any group 1
$query = "FROM User WHERE User.id NOT IN
(SELECT u.id FROM User u
INNER JOIN u.Group g WHERE g.id = ?";
$users = $conn->query($query, array(1));
// finding all users which don't belong to any groups
// Notice:
// the usage of INNER JOIN
// the usage of empty brackets preceding the Group component
$query = "FROM User WHERE User.id NOT IN
(SELECT u.id FROM User u
INNER JOIN u.Group g)";
$users = $conn->query($query);
</code>
<code>
DELETE FROM <component_name>
[WHERE <where_condition>]
[ORDER BY ...]
[LIMIT <record_count>]
</code>
* 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.
<code type="php">
$q = 'DELETE FROM Account WHERE id > ?';
$rows = $this->conn->query($q, array(3));
// the same query using the query interface
$q = new Doctrine_Query();
$rows = $q->delete('Account')
->from('Account a')
->where('a.id > ?', 3)
->execute();
print $rows; // the number of affected rows
</code>
Syntax:
<code>
FROM <component_reference> [[LEFT | INNER] JOIN <component_reference>] ...
</code>
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.
* 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:
<code>
SELECT u.*, p.* FROM User u LEFT JOIN u.Phonenumber
SELECT u.*, p.* FROM User u, u.Phonenumber p
</code>
* {{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.
<code>
SELECT u.*, p.* FROM User u INNER JOIN u.Phonenumber p
</code>
+++ 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 firstname and lastname to a value called name
<code type="php">
$q = new Doctrine_Query();
$users = $q->select('CONCAT(u.firstname, u.lastname) name')->from('User u')->execute();
foreach($users as $user) {
// here 'name' is not a property of $user,
// its a mapped function value
print $user->name;
}
</code>
* 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.
<code type="php">
$q = new Doctrine_Query();
$users = $q->select('u.name')->from('User u')->where("SUBSTRING(u.name, 0, 1) = 'z'")->execute();
foreach($users as $user) {
print $user->name;
}
</code>
* 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.
<code type="php">
$q = new Doctrine_Query();
$users = $q->select('u.name')->from('User u')->where("TRIM(u.name) = 'Someone'")->execute();
foreach($users as $user) {
print $user->name;
}
</code>
* The //LOWER// and //UPPER// functions convert a string to lower and upper case, respectively. They return a string.
<code type="php">
$q = new Doctrine_Query();
$users = $q->select('u.name')->from('User u')->where("LOWER(u.name) = 'someone'")->execute();
foreach($users as $user) {
print $user->name;
}
</code>
* 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
+++ Datetime functions
+++ Collection functions
* GROUP BY and HAVING clauses can be used for dealing with aggregate functions
* Following aggregate functions are availible on DQL: COUNT, MAX, MIN, AVG, SUM
Selecting alphabetically first user by name.
<code>
SELECT MIN(u.name) FROM User u
</code>
Selecting the sum of all Account amounts.
<code>
SELECT SUM(a.amount) FROM Account a
</code>
* Using an aggregate function in a statement containing no GROUP BY clause, results in grouping on all rows. In the example above we fetch all users and the number of phonenumbers they have.
<code>
SELECT u.*, COUNT(p.id) FROM User u, u.Phonenumber p GROUP BY u.id
</code>
* The HAVING clause can be used for narrowing the results using aggregate values. In the following example we fetch all users which have atleast 2 phonenumbers
<code>
SELECT u.* FROM User u, u.Phonenumber p HAVING COUNT(p.id) >= 2
</code>
<code type="php">
// retrieve all users and the phonenumber count for each user
$users = $conn->query("SELECT u.*, COUNT(p.id) count FROM User u, u.Phonenumber p GROUP BY u.id");
foreach($users as $user) {
print $user->name . ' has ' . $user->Phonenumber[0]->count . ' phonenumbers';
}
</code>
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).
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 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.
<code type="php">
// DO NOT USE THE FOLLOWING CODE
// (using many sql queries for object population):
$users = $conn->getTable('User')->findAll();
foreach($users as $user) {
print $user->name."
";
foreach($user->Phonenumber as $phonenumber) {
print $phonenumber."
";
}
}
// same thing implemented much more efficiently:
// (using only one sql query for object population)
$users = $conn->query("FROM User.Phonenumber");
foreach($users as $user) {
print $user->name."
";
foreach($user->Phonenumber as $phonenumber) {
print $phonenumber."
";
}
}
</code>
<code type="php">
// retrieve the first 20 users and all their associated phonenumbers
$users = $conn->query("SELECT u.*, p.* FROM User u, u.Phonenumber p LIMIT 20");
foreach($users as $user) {
print ' --- '.$user->name.' --- \n';
foreach($user->Phonenumber as $p) {
print $p->phonenumber.'\n';
}
}
</code>
+++ Introduction
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.
+++ 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 LS-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.
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 its smart enough to use different aliases for the tables in the subquery to avoid alias collisions.
DQL QUERY:
<code>
SELECT u.id, u.name, p.* FROM User u LEFT JOIN u.Phonenumber p LIMIT 20
</code>
SQL QUERY:
<code>
SELECT
e.id AS e__id,
e.name AS e__name,
p.id AS p__id,
p.phonenumber AS p__phonenumber,
p.entity_id AS p__entity_id
FROM entity e
LEFT JOIN phonenumber p ON e.id = p.entity_id
WHERE e.id IN (
SELECT DISTINCT e2.id
FROM entity e2
WHERE (e2.type = 0) LIMIT 20) AND (e.type = 0)
</code>
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.
DQL QUERY:
<code>
SELECT u.id, u.name, p.* FROM User u LEFT JOIN u.Phonenumber p LIMIT 20
</code>
SQL QUERY:
<code>
SELECT
e.id AS e__id,
e.name AS e__name,
p.id AS p__id,
p.phonenumber AS p__phonenumber,
p.entity_id AS p__entity_id
FROM entity e
LEFT JOIN phonenumber p ON e.id = p.entity_id
WHERE e.id IN (
SELECT DISTINCT e2.id
FROM entity e2
INNER JOIN phonenumber p2 ON e2.id = p2.entity_id
WHERE (e2.type = 0) LIMIT 20) AND (e.type = 0)
</code>
+++ Introduction
Record collections can be sorted efficiently at the database level using the ORDER BY clause.
Syntax:
<code>
[ORDER BY {ComponentAlias.columnName}
[ASC | DESC], ...]
</code>
Examples:
<code>
FROM User.Phonenumber
ORDER BY User.name, Phonenumber.phonenumber
FROM User u, u.Email e
ORDER BY e.address, u.id
</code>
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.
<code>
FROM User u, u.Email e
ORDER BY e.address DESC, u.id ASC;
</code>
+++ Sorting by an aggregate value
+++ Using random order
{{SELECT}} statement syntax:
<code>
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>}]
</code>
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.
<code>
SELECT a.name, a.amount FROM Account a
</code>
* 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).
<code>
SELECT a.* FROM Account a
</code>
* {{FROM}} clause {{components}} indicates the component or components from which to retrieve records.
<code>
SELECT a.* FROM Account a
SELECT u.*, p.*, g.* FROM User u LEFT JOIN u.Phonenumber p LEFT JOIN u.Group g
</code>
* 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.
<code>
SELECT a.* FROM Account a WHERE a.amount > 2000
</code>
* 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
<code>
SELECT u.* FROM User u LEFT JOIN u.Phonenumber p HAVING COUNT(p.id) > 3
</code>
* The {{ORDER BY}} clause can be used for sorting the results
<code>
SELECT u.* FROM User u ORDER BY u.name
</code>
* The {{LIMIT}} and {{OFFSET}} clauses can be used for efficiently limiting the number of records to a given {{row_count}}
<code>
SELECT u.* FROM User u LIMIT 20
</code>
+++ DISTINCT keyword
+++ Aggregate values
Aggregate value {{SELECT}} syntax:
<code type="php">
// SELECT u.*, COUNT(p.id) num_posts FROM User u, u.Posts p WHERE u.id = 1 GROUP BY u.id
$query = new Doctrine_Query();
$query->select('u.*, COUNT(p.id) num_posts')
->from('User u, u.Posts p')
->where('u.id = ?', 1)
->groupby('u.id');
$users = $query->execute();
echo $users->Posts[0]->num_posts . ' posts found';
</code>
+++ Introduction
+++ Comparisons using subqueries
+++ Conditional expressions
++++ ANY, IN and SOME
++++ ALL
++++ EXISTS and NOT EXISTS
+++ Correlated subqueries
+++ Subqueries in FROM clause
{{UPDATE}} statement syntax:
<code>
UPDATE //component_name//
SET //col_name1//=//expr1// [, //col_name2//=//expr2// ...]
[WHERE //where_condition//]
[ORDER BY ...]
[LIMIT //record_count//]
</code>
* 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.
<code type="php">
$q = 'UPDATE Account SET amount = amount + 200 WHERE id > 200';
$rows = $this->conn->query($q);
// the same query using the query interface
$q = new Doctrine_Query();
$rows = $q->update('Account')
->set('amount', 'amount + 200')
->where('id > 200')
->execute();
print $rows; // the number of affected rows
</code>
Syntax:
<code>
WHERE <where_condition>
</code>
* 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
++ Requirements
++ Installation
++ Compiling
++ Starting new project
++ Working with existing databases
++ Exporting classes
Doctrine is quite big framework and usually dozens of files are being included on each request. This brings a lot of overhead. In fact these file operations are as time consuming as sending multiple queries to database server. The clean separation of class per file works well in developing environment, however when project goes commercial distribution the speed overcomes the clean separation of class per file -convention.
Doctrine offers method called compile() to solve this issue. The compile method makes a single file of most used Doctrine components which can then be included on top of your script. By default the file is created into Doctrine root by the name Doctrine.compiled.php.
Doctrine offers method called {{compile()}} to solve this issue. The compile method makes a single file of most used Doctrine components which can then be included on top of your script. By default the file is created into Doctrine root by the name {{Doctrine.compiled.php}}.
Compiling is a method for making a single file of most used doctrine runtime components including the compiled file instead of multiple files (in worst cases dozens of files) can improve performance by an order of magnitude. In cases where this might fail, a Doctrine_Exception is throw detailing the error.
Compiling is a method for making a single file of most used doctrine runtime components including the compiled file instead of multiple files (in worst cases dozens of files) can improve performance by an order of magnitude. In cases where this might fail, a {{Doctrine_Exception}} is throw detailing the error.
<code type='php'>
Doctrine::compile();
......
The installation of doctrine is very easy. Just get the latest revision of Doctrine from http://doctrine.pengus.net/svn/trunk.
You need a SVN(Subversion) client for downloading Doctrine.
You need a SVN (Subversion) client for downloading Doctrine.
In order to check out Doctrine in the current directory using the **svn** command line tool use the following code:
<code type="bash">
svn co http://doctrine.pengus.net/svn/trunk .
</code>
......
++ Scalar queries
++ Component queries
++ Fetching multiple components
++ Introduction
++ Table and class naming
++ Table options
++ Columns
++ Constraints and validators
++ Record identifiers
++ Indexes
++ Relations
++ Hierarchical data
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.
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.
<code type="php">
class Book extends Doctrine_Record
......
......@@ -53,10 +53,7 @@ The above example will create a character varying field of length 12 characters
++++ Boolean
The boolean data type represents only two values that can be either 1 or 0.
Do not assume that these data types are stored as integers because some DBMS drivers may implement this
type with single character text fields for a matter of efficiency.
Ternary logic is possible by using null as the third possible value that may be assigned to fields of this type.
The 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.
<code type="php">
class Test extends Doctrine_Record {
......@@ -71,8 +68,7 @@ class Test extends Doctrine_Record {
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.
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.
......@@ -177,9 +173,7 @@ class Test extends Doctrine_Record {
++++ 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
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
<code type="php">
class Test extends Doctrine_Record {
......
+++ Introduction
+++ Adjacency list
+++ Nested set
+++ Materialized path
+++ Examples
This is an example to show how you would set up and use the doctrine tree interface with the {{NestedSet}} implementation (currently the most comprehensively supported by Doctrine)
<code type="php">
require_once("path/to/Doctrine.php");
function __autoload($classname) {
return Doctrine::autoload($classname);
}
// define our tree
class Menu extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('menu');
// add this your table definition to set the table as NestedSet tree implementation
$this->actsAsTree('NestedSet');
// you do not need to add any columns specific to the nested set implementation
// these are added for you
$this->hasColumn("name","string",30);
}
// this __toString() function is used to get the name for the path, see node::getPath
public function __toString() {
return $this->get('name');
}
}
// set connections to database
$dsn = 'mysql:dbname=nestedset;host=localhost';
$user = 'user';
$password = 'pass';
try {
$dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
$manager = Doctrine_Manager::getInstance();
$conn = $manager->openConnection($dbh);
// create root
$root = new Menu();
$root->set('name', 'root');
$manager->getTable('Menu')->getTree()->createRoot($root);
// build tree
$two = new Menu();
$two->set('name', '2');
$root->getNode()->addChild($two);
$one = new Menu();
$one->set('name', '1');
$one->getNode()->insertAsPrevSiblingOf($two);
// refresh as node's lft and rgt values have changed
$two->refresh();
$three = new Menu();
$three->set('name', '3');
$three->getNode()->insertAsNextSiblingOf($two);
$two->refresh();
$one_one = new Menu();
$one_one->set('name', '1.1');
$one_one->getNode()->insertAsFirstChildOf($one);
$one->refresh();
$one_two = new Menu();
$one_two->set('name', '1.2');
$one_two->getNode()->insertAsLastChildOf($one);
$one_two->refresh();
$one_two_one = new Menu();
$one_two_one->set('name', '1.2.1');
$one_two->getNode()->addChild($one_two_one);
$root->refresh();
$four = new Menu();
$four->set('name', '4');
$root->getNode()->addChild($four);
$root->refresh();
$five = new Menu();
$five->set('name', '5');
$root->getNode()->addChild($five);
$root->refresh();
$six = new Menu();
$six->set('name', '6');
$root->getNode()->addChild($six);
output_message('initial tree');
output_tree($root);
$one_one->refresh();
$six->set('name', '1.0 (was 6)');
$six->getNode()->moveAsPrevSiblingOf($one_one);
$one_two->refresh();
$five->refresh();
$five->set('name', '1.3 (was 5)');
$five->getNode()->moveAsNextSiblingOf($one_two);
$one_one->refresh();
$four->refresh();
$four->set('name', '1.1.1 (was 4)');
$four->getNode()->moveAsFirstChildOf($one_one);
$root->refresh();
$one_two_one->refresh();
$one_two_one->set('name', 'last (was 1.2.1)');
$one_two_one->getNode()->moveAsLastChildOf($root);
output_message('transformed tree');
output_tree($root);
$one_one->refresh();
$one_one->deleteNode();
output_message('delete 1.1');
output_tree($root);
// now test fetching root
$tree_root = $manager->getTable('Menu')->getTree()->findRoot();
output_message('testing fetch root and outputting tree from the root node');
output_tree($tree_root);
// now test fetching the tree
output_message('testing fetching entire tree using tree::fetchTree()');
$tree = $manager->getTable('Menu')->getTree()->fetchTree();
while($node = $tree->next())
{
output_node($node);
}
// now test fetching the tree
output_message('testing fetching entire tree using tree::fetchTree(), excluding root node');
$tree = $manager->getTable('Menu')->getTree()->fetchTree(array('include_record' => false));
while($node = $tree->next())
{
output_node($node);
}
// now test fetching the branch
output_message('testing fetching branch for 1, using tree::fetchBranch()');
$one->refresh();
$branch = $manager->getTable('Menu')->getTree()->fetchBranch($one->get('id'));
while($node = $branch->next())
{
output_node($node);
}
// now test fetching the tree
output_message('testing fetching branch for 1, using tree::fetchBranch() excluding node 1');
$tree = $manager->getTable('Menu')->getTree()->fetchBranch($one->get('id'), array('include_record' => false));
while($node = $tree->next())
{
output_node($node);
}
// now perform some tests
output_message('descendants for 1');
$descendants = $one->getNode()->getDescendants();
while($descendant = $descendants->next())
{
output_node($descendant);
}
// move one and children under two
$two->refresh();
$one->getNode()->moveAsFirstChildOf($two);
output_message('moved one as first child of 2');
output_tree($root);
output_message('descendants for 2');
$two->refresh();
$descendants = $two->getNode()->getDescendants();
while($descendant = $descendants->next())
{
output_node($descendant);
}
output_message('number descendants for 2');
echo $two->getNode()->getNumberDescendants() .'</br>';
output_message('children for 2 (notice excludes children of children, known as descendants)');
$children = $two->getNode()->getChildren();
while($child = $children->next())
{
output_node($child);
}
output_message('number children for 2');
echo $two->getNode()->getNumberChildren() .'</br>';
output_message('path to 1');
$path = $one->getNode()->getPath(' > ');
echo $path .'
';
output_message('path to 1 (including 1)');
$path = $one->getNode()->getPath(' > ', true);
echo $path .'
';
output_message('1 has parent');
$hasParent = $one->getNode()->hasParent();
$msg = $hasParent ? 'true' : 'false';
echo $msg . '</br/>';
output_message('parent to 1');
$parent = $one->getNode()->getParent();
if($parent->exists())
{
echo $parent->get('name') .'
';
}
output_message('root isRoot?');
$isRoot = $root->getNode()->isRoot();
$msg = $isRoot ? 'true' : 'false';
echo $msg . '</br/>';
output_message('one isRoot?');
$isRoot = $one->getNode()->isRoot();
$msg = $isRoot ? 'true' : 'false';
echo $msg . '</br/>';
output_message('root hasParent');
$hasParent = $root->getNode()->hasParent();
$msg = $hasParent ? 'true' : 'false';
echo $msg . '</br/>';
output_message('root getParent');
$parent = $root->getNode()->getParent();
if($parent->exists())
{
echo $parent->get('name') .'
';
}
output_message('get first child of root');
$record = $root->getNode()->getFirstChild();
if($record->exists())
{
echo $record->get('name') .'
';
}
output_message('get last child of root');
$record = $root->getNode()->getLastChild();
if($record->exists())
{
echo $record->get('name') .'
';
}
$one_two->refresh();
output_message('get prev sibling of 1.2');
$record = $one_two->getNode()->getPrevSibling();
if($record->exists())
{
echo $record->get('name') .'
';
}
output_message('get next sibling of 1.2');
$record = $one_two->getNode()->getNextSibling();
if($record->exists())
{
echo $record->get('name') .'
';
}
output_message('siblings of 1.2');
$siblings = $one_two->getNode()->getSiblings();
foreach($siblings as $sibling)
{
if($sibling->exists())
echo $sibling->get('name') .'
';
}
output_message('siblings of 1.2 (including 1.2)');
$siblings = $one_two->getNode()->getSiblings(true);
foreach($siblings as $sibling)
{
if($sibling->exists())
echo $sibling->get('name') .'
';
}
$new = new Menu();
$new->set('name', 'parent of 1.2');
$new->getNode()->insertAsParentOf($one_two);
output_message('added a parent to 1.2');
output_tree($root);
try {
$dummy = new Menu();
$dummy->set('name', 'dummy');
$dummy->save();
}
catch (Doctrine_Exception $e)
{
output_message('You cannot save a node unless it is in the tree');
}
try {
$fake = new Menu();
$fake->set('name', 'dummy');
$fake->set('lft', 200);
$fake->set('rgt', 1);
$fake->save();
}
catch (Doctrine_Exception $e)
{
output_message('You cannot save a node with bad lft and rgt values');
}
// check last remaining tests
output_message('New parent is descendant of 1');
$one->refresh();
$res = $new->getNode()->isDescendantOf($one);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of 2');
$two->refresh();
$res = $new->getNode()->isDescendantOf($two);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of 1.2');
$one_two->refresh();
$res = $new->getNode()->isDescendantOf($one_two);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of or equal to 1');
$one->refresh();
$res = $new->getNode()->isDescendantOfOrEqualTo($one);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of or equal to 1.2');
$one_two->refresh();
$res = $new->getNode()->isDescendantOfOrEqualTo($one_two);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of or equal to 1.3');
$five->refresh();
$res = $new->getNode()->isDescendantOfOrEqualTo($new);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
function output_tree($root)
{
// display tree
// first we must refresh the node as the tree has been transformed
$root->refresh();
// next we must get the iterator to traverse the tree from the root node
$traverse = $root->getNode()->traverse();
output_node($root);
// now we traverse the tree and output the menu items
while($item = $traverse->next())
{
output_node($item);
}
unset($traverse);
}
function output_node($record)
{
echo str_repeat('-', $record->getNode()->getLevel()) . $record->get('name')
. ' (has children:'.$record->getNode()->hasChildren().') '
. ' (is leaf:'.$record->getNode()->isLeaf().') '.'<br/>';
}
function output_message($msg)
{
echo "
**//$msg//**".'
';
}
</code>
\ No newline at end of file
++++ About
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 [http://en.wikipedia.org/wiki/Tree_data_structure 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 in the following chapters, or see
* [http://www.dbazine.com/oracle/or-articles/tropashko4 http://www.dbazine.com/oracle/or-articles/tropashko4]
* [http://dev.mysql.com/tech-resources/articles/hierarchical-data.html http://dev.mysql.com/tech-resources/articles/hierarchical-data.html]
++++ Setting up
Managing tree structures in doctrine is easy. Doctrine currently fully supports Nested Set, and plans to support the other implementations soon. To set your model to act as a tree, simply add the code below to your models table definition.
Now that Doctrine knows that this model acts as a tree, it will automatically add any required columns for your chosen implementation, so you do not need to set any tree specific columns within your table definition.
Doctrine has standard interface's for managing tree's, that are used by all the implementations. Every record in the table represents a node within the tree (the table), so doctrine provides two interfaces, Tree and Node.
<code type="php">
class Menu extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('menu');
// add this your table definition to set the table as NestedSet tree implementation
// $implName is 'NestedSet' or 'AdjacencyList' or 'MaterializedPath'
// $options is an assoc array of options, see implementation docs for options
$this->option('treeImpl', $implName);
$this->option('treeOptions', $options);
// you do not need to add any columns specific to the nested set implementation,
// these are added for you
$this->hasColumn("name","string",30);
}
// this __toString() function is used to get the name for the path, see node::getPath()
public function __toString() {
return $this->get('name');
}
}
</code>
++++ Node interface
The node interface, for inserting and manipulating nodes within the tree, is accessed on a record level. A full implementation of this interface will be as follows:
<code type="php">
interface Doctrine_Node_Interface {
/**
* insert node into tree
*/
public function insertAsParentOf(Doctrine_Record $dest);
public function insertAsPrevSiblingOf(Doctrine_Record $dest);
public function insertAsNextSiblingOf(Doctrine_Record $dest);
public function insertAsFirstChildOf(Doctrine_Record $dest);
public function insertAsLastChildOf(Doctrine_Record $dest);
public function addChild(Doctrine_Record $record);
/**
* moves node (if has children, moves branch)
*
*/
public function moveAsPrevSiblingOf(Doctrine_Record $dest);
public function moveAsNextSiblingOf(Doctrine_Record $dest);
public function moveAsFirstChildOf(Doctrine_Record $dest);
public function moveAsLastChildOf(Doctrine_Record $dest);
/**
* node information
*/
public function getPrevSibling();
public function getNextSibling();
public function getSiblings($includeNode = false);
public function getFirstChild();
public function getLastChild();
public function getChildren();
public function getDescendants();
public function getParent();
public function getAncestors();
public function getPath($seperator = ' > ', $includeNode = false);
public function getLevel();
public function getNumberChildren();
public function getNumberDescendants();
/**
* node checks
*/
public function hasPrevSibling();
public function hasNextSibling();
public function hasChildren();
public function hasParent();
public function isLeaf();
public function isRoot();
public function isEqualTo(Doctrine_Record $subj);
public function isDescendantOf(Doctrine_Record $subj);
public function isDescendantOfOrEqualTo(Doctrine_Record $subj);
public function isValidNode();
/**
* deletes node and it's descendants
*/
public function delete();
}
// if your model acts as tree you can retrieve the associated node object as follows
$record = $manager->getTable('Model')->find($pk);
$nodeObj = $record->getNode();
</code>
++++ Tree interface
The tree interface, for creating and accessing the tree, is accessed on a table level. A full implementation of this interface would be as follows:
<code type="php">
interface Doctrine_Tree_Interface {
/**
* creates root node from given record or from a new record
*/
public function createRoot(Doctrine_Record $record = null);
/**
* returns root node
*/
public function findRoot($root_id = 1);
/**
* optimised method to returns iterator for traversal of the entire tree
* from root
*/
public function fetchTree($options = array());
/**
* optimised method that returns iterator for traversal of the tree from the
* given record's primary key
*/
public function fetchBranch($pk, $options = array());
}
// if your model acts as tree you can retrieve the associated tree object as follows
$treeObj = $manager->getTable('Model')->getTree();
</code>
++++ Traversing or Walking Trees
You can traverse a Tree in different ways, please see here for more information [http://en.wikipedia.org/wiki/Tree_traversal http://en.wikipedia.org/wiki/Tree_traversal].
The most common way of traversing a tree is Pre Order Traversal as explained in the link above, this is also what is known as walking the tree, this is the default approach when traversing a tree in Doctrine, however Doctrine does plan to provide support for Post and Level Order Traversal (not currently implemented)
<code type="php">
/*
* traverse the entire tree from root
*/
$root = $manager->getTable('Model')->getTree()->fetchRoot();
if($root->exists())
{
$tree = $root->traverse();
while($node = $tree->next())
{
// output your tree here
}
}
// or the optimised approach using tree::fetchTree
$tree = $manager->getTable('Model')->getTree()->fetchTree();
while($node = $tree->next())
{
// output tree here
}
/*
* traverse a branch of the tree
*/
$record = $manager->getTable('Model')->find($pk);
if($record->exists())
{
$branch = $record->traverse();
while($node = $branch->next())
{
// output your tree here
}
}
// or the optimised approach
$branch = $manager->getTable('Model')->getTree()->fetchBranch($pk);
while($node = $branch->traverse())
{
// output your tree here
}
</code>
++++ Read me
If performing batch tree manipulation tasks, then remember to refresh your records (see record::refresh()), as any transformations of the tree are likely to affect all instances of records that you have in your scope.
You can save an already existing node using record::save() without affecting it's position within the tree. Remember to never set the tree specific record attributes manually.
If you are inserting or moving a node within the tree, you must use the appropriate node method. Note: you do not need to save a record once you have inserted it or moved it within the tree, any other changes to your record will also be saved within these operations. You cannot save a new record without inserting it into the tree.
If you wish to delete a record, you MUST delete the node and not the record, using $record->deleteNode() or $record->getNode()->delete(). Deleting a node, will by default delete all its descendants. if you delete a record without using the node::delete() method you tree is likely to become corrupt (and fall down)!
The difference between descendants and children is that descendants include children of children whereas children are direct descendants of their parent (real children not gran children and great gran children etc etc).
++++ Introduction
Basically Nested Set is optimized for traversing trees, as this can be done with minimal queries, however updating the tree can be costly as it will affect all rows within the table.
For more information, read here:
* [http://www.sitepoint.com/article/hierarchical-data-database/2 http://www.sitepoint.com/article/hierarchical-data-database/2]
* [http://dev.mysql.com/tech-resources/articles/hierarchical-data.html http://dev.mysql.com/tech-resources/articles/hierarchical-data.html]
++++ Setting up
To set up your model as Nested Set, you must add the following code to your model's table definition.
<code type="php">
class Menu extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('menu');
// add this your table definition to set the table as NestedSet tree
// implementation
$this->option('treeImpl', 'NestedSet');
$this->option('treeOptions', array());
// you do not need to add any columns specific to the nested set
// implementation, these are added for you
$this->hasColumn("name","string",30);
}
// this __toString() function is used to get the name for the path, see
// node::getPath()
public function __toString() {
return $this->get('name');
}
}
</code>
++++ Tree options
The nested 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 based upon the set up above:
<code type="php">
//use these options in the setTableDefinition
$options = array('hasManyRoots' => true, // enable many roots
'rootColumnName' => 'root_id'); // set root column name, defaults to 'root_id'
// To create new root nodes, if you have manually set the root_id, then it will be used
// otherwise it will automatically use the next available root id
$root = new Menu();
$root->set('name', 'root');
// insert first root, will auto be assigned root_id = 1
$manager->getTable('Menu')->getTree()->createRoot($root);
$another_root = new Menu();
$another_root->set('name', 'another root');
// insert another root, will auto be assigned root_id = 2
$manager->getTable('Menu')->getTree()->createRoot($another_root);
// fetching a specifc root
$root = $manager->getTable('Menu')->getTree()->fetchRoot(1);
$another_root = $manager->getTable('Menu')->getTree()->fetchRoot(2);
// fetching all roots
$roots = $manager->getTable('Menu')->getTree()->fetchRoots();
</code>
++++ Node support
The nested set implementation fully supports the full Doctrine node interface
++++ Tree support
The nested set implementation fully supports the full Doctrine tree interface
++++ Read me
The most effective way to traverse a tree from the root node, is to use the {{tree::fetchTree()}} method. It will by default include the root node in the tree and will return an iterator to traverse the tree.
To traverse a tree from a given node, it will normally cost 3 queries, one to fetch the starting node, one to fetch the branch from this node, and one to determine the level of the start node, the traversal algorithm with then determine the level of each subsequent node for you.
+++ 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 simple calling {{Doctrine_Record::index('indexName', $definition)}} where {{$definition}} is the definition array.
An example of adding a simple index to field called {{name}}:
<code type="php">
class IndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->index('myindex', array('fields' => 'name');
}
}
</code>
An example of adding a multi-column index to field called {{name}}:
<code type="php">
class MultiColumnIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string');
$this->index('myindex', array('fields' => array('name', 'code')));
}
}
</code>
An example of adding a multiple indexes on same table:
<code type="php">
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'));
}
}
</code>
+++ Index options
Doctrine offers many index options, some of them being db-specific. Here is a full list of available options:
<code>
sorting => string('ASC' / 'DESC')
what kind of sorting does the index use (ascending / descending)
length => integer
index length (only some drivers support this)
primary => boolean(true / false)
whether or not the index is primary index
type => string('unique', -- supported by most drivers
'fulltext', -- only availible on Mysql driver
'gist', -- only availible on Pgsql driver
'gin') -- only availible on Pgsql driver
</code>
<code type="php">
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',
));
}
}
</code>
+++ 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'.
<code type="php">
class Article
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('content', 'string');
$this->index('content', array('fields' => 'content',
'type' => 'fulltext'));
}
}
</code>
+++ Introduction
Doctrine supports many kind of identifiers. For most cases it is recommended not to specify any primary keys (Doctrine will then use field name {{id}} as an autoincremented primary key). When using table creation Doctrine is smart enough to emulate the autoincrementation with sequences and triggers on databases that doesn't support it natively.
+++ Autoincremented
Autoincrement primary key is the most basic identifier and its usage is strongly encouraged. Sometimes you may want to use some other name than {{id}} for your autoinc primary key. It can be specified as follows:
<code type="php">
class User extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('uid','integer',20,'primary|autoincrement');
}
}
</code>
+++ Natural
Natural identifier is a property or combination of properties that is unique and non-null. The use of natural identifiers is discouraged. You should consider using autoincremented or sequential primary keys as they make your system more scalable.
<code type="php">
class User extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('name','string',200,'primary');
}
}
</code>
+++ Composite
Composite primary key can be used efficiently in association tables (tables that connect two components together). It is not recommended to use composite primary keys in anywhere else as Doctrine does not support mapping relations on multiple columns.
Due to this fact your doctrine-based system will scale better if it has autoincremented primary key even for association tables.
<code type="php">
class Groupuser extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('user_id', 'integer', 20, 'primary');
$this->hasColumn('group_id', 'integer', 20, 'primary');
}
}
</code>
+++ Sequence
Doctrine supports sequences for generating record identifiers. Sequences are a way of offering unique IDs for data rows. If you do most of your work with e.g. MySQL, think of sequences as another way of doing {{AUTO_INCREMENT}}.
Doctrine knows how to do sequence generation in the background so you don't have to worry about calling database specific queries - Doctrine does it for you, all you need to do is define a column as a sequence column and optionally provide the name of the sequence table and the id column name of the sequence table.
Consider the following record definition:
<code type="php">
class Book extends Doctrine_Record {
public function setTableDefinition()
{
$this->hasColumn('id', 'integer', null, array('primary', 'sequence'));
$this->hasColumn('name', 'string');
}
}
</code>
By default Doctrine uses the following format for sequence tables {{[tablename]_seq}}. If you wish to change this you can use the following piece of code to change the formatting:
<code type="php">
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_SEQNAME_FORMAT, '%s_my_seq');
</code>
Doctrine uses column named id as the sequence generator column of the sequence table. If you wish to change this globally (for all connections and all tables) you can use the following code:
<code type="php">
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_SEQCOL_NAME, 'my_seq_column');
</code>
In the following example we do not wish to change global configuration we just want to make the {{id}} column to use sequence table called {{book_sequence}}. It can be done as follows:
<code type="php">
class Book extends Doctrine_Record {
public function setTableDefinition()
{
$this->hasColumn('id', 'integer', null, array('primary', 'sequence' => 'book_sequence'));
$this->hasColumn('name', 'string');
}
}
</code>
Here we take the preceding example a little further: we want to have a custom sequence column. Here it goes:
<code type="php">
class Book extends Doctrine_Record {
public function setTableDefinition()
{
$this->hasColumn('id', 'integer', null, array('primary', 'sequence' => array('book_sequence', 'sequence')));
$this->hasColumn('name', 'string');
}
}
</code>
+++ Introduction
In Doctrine all record relations are being set with {{hasMany}}, {{hasOne}}, {{ownsMany}} and {{ownsOne}} methods. Doctrine supports almost any kind of database relation from simple one-to-one foreign key relations to join table self-referencing relations.
+++ Relation aliases
Doctrine supports relation aliases through {{as}} keyword.
<code type="php">
class Forum_Board extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('name', 'string', 100);
$this->hasColumn('description', 'string', 5000);
}
public function setUp() {
// notice the 'as' keyword here
$this->ownsMany('Forum_Thread as Threads', 'Forum_Thread.board_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() {
// notice the 'as' keyword here
$this->hasOne('Forum_Board as Board', 'Forum_Thread.board_id');
}
}
$board = new Board();
$board->Threads[0]->updated = time();
</code>
+++ Foreign key associations
+++ Join table associations
+++ Inheritance
+++ Foreign key constraints
++++ One-To-One
Binding One-To-One foreign key associations is done with {{Doctrine_Record::ownsOne()}} and {{Doctrine_Record::hasOne()}} methods. In the following example user owns one email and has one address. So the relationship between user and email is one-to-one composite. The relationship between user and address is one-to-one aggregate.
The {{Email}} component here is mapped to {{User}} component's column {{email_id}} hence their relation is called LOCALKEY relation. On the other hand the {{Address}} component is mapped to {{User}} by it's {{user_id}} column hence the relation between {{User}} and {{Address}} is called FOREIGNKEY relation.
<code type="php">
class User extends Doctrine_Record {
public function setUp() {
$this->hasOne('Address','Address.user_id');
$this->ownsOne('Email','User.email_id');
$this->ownsMany('Phonenumber','Phonenumber.user_id');
}
public function setTableDefinition() {
$this->hasColumn('name','string',50);
$this->hasColumn('loginname','string',20);
$this->hasColumn('password','string',16);
// foreign key column for email ID
$this->hasColumn('email_id','integer');
}
}
class Email extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('address','string',150);
}
}
class Address extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('street','string',50);
$this->hasColumn('user_id','integer');
}
}
</code>
++++ One-to-Many, Many-to-One
<code type="php">
class User extends Doctrine_Record {
public function setUp() {
$this->ownsMany('Phonenumber','Phonenumber.user_id');
}
public function setTableDefinition() {
$this->hasColumn('name','string',50);
$this->hasColumn('loginname','string',20);
$this->hasColumn('password','string',16);
}
}
class Phonenumber extends Doctrine_Record {
public function setTableDefinition() {
$this->hasColumn('phonenumber','string',50);
$this->hasColumn('user_id','integer');
}
}
</code>
++++ Tree structure
<code type="php">
class Task extends Doctrine_Record {
public function setUp() {
$this->hasOne('Task as Parent','Task.parent_id');
$this->hasMany('Task as Subtask','Subtask.parent_id');
}
public function setTableDefinition() {
$this->hasColumn('name','string',100);
$this->hasColumn('parent_id','integer');
}
}
</code>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment