88
Jonathan H. Wage | OpenSky The Doctrine Project

ZendCon2010 The Doctrine Project

Embed Size (px)

DESCRIPTION

 

Citation preview

Page 1: ZendCon2010 The Doctrine Project

Jonathan H. Wage | OpenSky

The Doctrine Project

Page 2: ZendCon2010 The Doctrine Project

Who am I?Jonathan H. Wage

PHP Developer for over 10 yearsSymfony ContributorDoctrine ContributorPublished AuthorBusiness OwnerNashville, TN Resident

http://www.twitter.com/jwagehttp://www.facebook.com/jwage

Page 3: ZendCon2010 The Doctrine Project

I work at

What is OpenSky?“a social commerce platform”

Based in New York and is a major opensource software advocate

http://www.shopopensky.com

Page 4: ZendCon2010 The Doctrine Project

OpenSky TechnologiesPHP 5.3.2Apache2Symfony2Doctrine2jQuerymule, stomp, hornetqMongoDBnginxvarnish

Page 5: ZendCon2010 The Doctrine Project

What is Doctrine?- Open Source PHP Project started in

2006- Specializes in database functionality

- Database Abstraction Layer (DBAL)- Database Migrations- Object Relational Mapper (DBAL)- MongoDB Object Document Manager (ODM)- CouchDB Object Document Manager (ODM)

Page 6: ZendCon2010 The Doctrine Project

Who is on the team?

• Roman S. Borschel

• Guilherme Blanco

• Benjamin Eberlei

• Bulat Shakirzyanov

• Jonathan H. Wage

Page 7: ZendCon2010 The Doctrine Project

Project History- First commit April 13th 2006

- First stable version finished and Released September 1st 2008

- One of the first ORM implementations for PHP

- 1.0 is First LTS(long term support) release. Maintained until March 1st 2010

- Integrated with many popular frameworks: Symfony, Zend Framework, Code Igniter

Page 8: ZendCon2010 The Doctrine Project

Doctrine Libraries- Database Abstraction Layer

- Database Migrations

- Object Relational Mapper

- MongoDB Object Document Manager

- CouchDB Object Document Manager

Page 9: ZendCon2010 The Doctrine Project

DBALDatabase Abstraction Layer

Page 10: ZendCon2010 The Doctrine Project

Database Abstraction LayerThe Doctrine Database AbstractionLayer (DBAL) is a thin layer on top ofPDO, it offers:

- select, update, delete, transactions

- database schema introspection

- schema management

Page 11: ZendCon2010 The Doctrine Project

Can be used standalone

Page 12: ZendCon2010 The Doctrine Project

Evolved fork of PEAR MDB, MDB2, Zend_Db, etc.

Page 13: ZendCon2010 The Doctrine Project

DownloadYou can download a standalonepackage to get started using the DBAL:

http://www.doctrine-project.org/projects/dbal/download

Page 14: ZendCon2010 The Doctrine Project

AutoloaderTo use any Doctrine library you mustregister an autoloader:

use Doctrine\Common\ClassLoader;

require '/path/to/doctrine-common/lib/Doctrine/Common/ClassLoader.php';

$classLoader = new ClassLoader('Doctrine\DBAL', '/path/to/doctrine-dbal/lib');$classLoader->register();

Page 15: ZendCon2010 The Doctrine Project

Create a Connection

$config = new \Doctrine\DBAL\Configuration();//..$connectionParams = array( 'dbname' => 'mydb', 'user' => 'user', 'password' => 'secret', 'host' => 'localhost', 'driver' => 'pdo_mysql',);$conn = DriverManager::getConnection($connectionParams);

Page 16: ZendCon2010 The Doctrine Project

Data APIprepare($sql) - Prepare a given sql statement and return the \Doctrine\DBAL\Driver

\Statement instance.executeUpdate($sql, array $params) - Executes a prepared statement with the given sql

and parameters and returns the affected rows count.execute($sql, array $params) - Creates a prepared statement for the given sql and passes

the parameters to the execute method, then returning the statement.fetchAll($sql, array $params) - Execute the query and fetch all results into an array.fetchArray($sql, array $params) - Numeric index retrieval of first result row of the given

query.fetchBoth($sql, array $params) - Both numeric and assoc column name retrieval of the first

result row.fetchColumn($sql, array $params, $colnum) - Retrieve only the given column of the first

result row.fetchRow($sql, array $params) - Retrieve assoc row of the first result row.select($sql, $limit, $offset) - Modify the given query with a limit clause.delete($tableName, array $identifier) - Delete all rows of a table matching the given

identifier, where keys are column names.insert($tableName, array $data) - Insert a row into the given table name using the key

value pairs of data.

Page 17: ZendCon2010 The Doctrine Project

Very Similar to PDO

$users = $conn->fetchAll('SELECT * FROM users');

Page 18: ZendCon2010 The Doctrine Project

Schema ManagerLearn about and modify your databasethrough the SchemaManager:

$sm = $conn->getSchemaManager();

Page 19: ZendCon2010 The Doctrine Project

Introspection APIlistDatabases()listFunctions()listSequences()listTableColumns($tableName)listTableConstraints($tableName)listTableDetails($tableName)listTableForeignKeys($tableName)listTableIndexes($tableName)listTables()

Page 20: ZendCon2010 The Doctrine Project

Introspection API

$tables = $sm->listTables();foreach ($tables as $table) { $columns = $sm->listTableColumns($table); // ...}

Page 21: ZendCon2010 The Doctrine Project

DDL StatementsProgromatically issue DDL statements:

$columns = array( 'id' => array( 'type' => \Doctrine\DBAL\Type::getType('integer'), 'autoincrement' => true, 'primary' => true, 'notnull' => true ), 'test' => array( 'type' => \Doctrine\DBAL\Type::getType('string'), 'length' => 255 ));

$options = array();

$sm->createTable('new_table', $columns, $options);

Page 22: ZendCon2010 The Doctrine Project

DDL StatementsProgromatically issue DDL statements:

$definition = array( 'name' => 'user_id_fk', 'local' => 'user_id', 'foreign' => 'id', 'foreignTable' => 'user');$sm->createForeignKey('profile', $definition);

Page 23: ZendCon2010 The Doctrine Project

Try a MethodYou can try a method and return true ifthe operation was successful:

if ($sm->tryMethod('createTable', 'new_table', $columns, $options)) { // do something}

Page 24: ZendCon2010 The Doctrine Project

Drop and Create Database

try { $sm->dropDatabase('test_db');} catch (Exception $e) {}

$sm->createDatabase('test_db');

Page 25: ZendCon2010 The Doctrine Project

Drop and Create DatabaseA little better! Every drop and createfunctionality in the API has a methodthat follows the dropAndCreate pattern:

$sm->dropAndCreateDatabase('test_db');

Page 26: ZendCon2010 The Doctrine Project

Schema Representation

$platform = $em->getConnection()->getDatabasePlatform();

$schema = new \Doctrine\DBAL\Schema\Schema();$myTable = $schema->createTable("my_table");$myTable->addColumn("id", "integer", array("unsigned" => true));$myTable->addColumn("username", "string", array("length" => 32));$myTable->setPrimaryKey(array("id"));

// get queries to create this schema.$queries = $schema->toSql($platform);

Array( [0] => CREATE TABLE my_table (id INTEGER NOT NULL, username VARCHAR(32) NOT NULL, PRIMARY KEY("id")))

Page 27: ZendCon2010 The Doctrine Project

Schema Representation

Array( [0] => DROP TABLE my_table)

Returns the reverse SQL of what toSql() returns

// ......

// get queries to safely delete this schema.$dropSchema = $schema->toDropSql($platform);

Array( [0] => DROP TABLE my_table)

Page 28: ZendCon2010 The Doctrine Project

Comparing Schemas$fromSchema = new \Doctrine\DBAL\Schema\Schema();$myTable = $fromSchema->createTable("my_table");$myTable->addColumn("id", "integer", array("unsigned" => true));$myTable->addColumn("username", "string", array("length" => 32));$myTable->setPrimaryKey(array("id"));

$toSchema = new \Doctrine\DBAL\Schema\Schema();$myTable = $toSchema->createTable("my_table");$myTable->addColumn("id", "integer", array("unsigned" => true));$myTable->addColumn("username", "string", array("length" => 32));$myTable->addColumn("email", "string", array("length" => 255));$myTable->setPrimaryKey(array("id"));

$comparator = new \Doctrine\DBAL\Schema\Comparator();$schemaDiff = $comparator->compare($fromSchema, $toSchema);

// queries to get from one to another schema.$queries = $schemaDiff->toSql($platform);

print_r($queries);

ALTER TABLE my_table ADD email VARCHAR(255) NOT NULL

Page 29: ZendCon2010 The Doctrine Project

ORMObject Relational Mapper

Page 30: ZendCon2010 The Doctrine Project

What is ORM?“Technique for converting data between incompatible type systems in object-oriented programming languages.”

http://en.wikipedia.org/wiki/Object-relational_mapping

Page 31: ZendCon2010 The Doctrine Project

The ORM is built on top of Common and DBAL

Page 32: ZendCon2010 The Doctrine Project

ORM Goals- Maintain transparency

- Keep domain and persistence layer separated

- Performance

- Consistent and decoupled API

- Well defined semantics

Page 33: ZendCon2010 The Doctrine Project

http://www.doctrine-project.org/projects/orm/download

Download

Page 34: ZendCon2010 The Doctrine Project

ArchitectureEntities - Lightweight persistent domain object - Regular PHP class - Does not extend any base Doctrine class - Cannot be final or contain final methods - Any two entities in a hierarchy of classes must not have

a mapped property with the same name - Supports inheritance, polymorphic associations and

polymorphic queries. - Both abstract and concrete classes can be entities - Entities may extend non-entity classes as well as entity

classes, and non-entity classes may extend entity classes

Page 35: ZendCon2010 The Doctrine Project

Architecture- No more base class required

- Values stored in object properties

- Persistence is done transparentlynamespace Entities;

class User{ private $id; private $name;}

Page 36: ZendCon2010 The Doctrine Project

ArchitectureThe EntityManager - Central access point to the ORM functionality provided by

Doctrine 2. API is used to manage the persistence of your objects and to query for persistent objects.

- Employes transactional write behind strategy that delays the execution of SQL statements in order to execute them in the most efficient way

- Execute at end of transaction so that all write locks are quickly releases

- Internally an EntityManager uses a UnitOfWork to keep track of your objects

Page 37: ZendCon2010 The Doctrine Project

Create EntityManagerCreate a new EntityManager instance:

$config = new \Doctrine\ORM\Configuration();$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);$driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Entities"));$config->setMetadataDriverImpl($driverImpl);

$config->setProxyDir(__DIR__ . '/Proxies');$config->setProxyNamespace('Proxies');

$em = \Doctrine\ORM\EntityManager::create($conn, $config);

Page 38: ZendCon2010 The Doctrine Project

Map entities to RDBMS tablesEntities are just regular PHP objects

namespace Entities;

class User{ private $id; private $name;}

Page 39: ZendCon2010 The Doctrine Project

Map entities to RDBMS tablesEntities are just regular PHP objects

Mapped By:- Annotations

namespace Entities;

/** * @Entity @Table(name="users") */class User{ /** @Id @Column(type="integer") @GeneratedValue */ private $id;

/** @Column(length=50) */ private $name;}

Page 40: ZendCon2010 The Doctrine Project

Map entities to RDBMS tablesEntities are just regular PHP objects:

Mapped By:- Annotations- YAML

Entities\User: type: entity table: users id: id: type: integer generator: strategy: AUTO fields: name: type: string length: 255

Page 41: ZendCon2010 The Doctrine Project

Map entities to RDBMS tablesEntities are just regular PHP objects:

Mapped By:- Annotations- YAML- XML

<?xml version="1.0" encoding="UTF-8"?><doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

<entity name="Entities\User" table="users"> <id name="id" type="integer"> <generator strategy="AUTO"/> </id> <field name="name" type="string" length="50"/> </entity>

</doctrine-mapping>

Page 42: ZendCon2010 The Doctrine Project

Mapping Performance- Only parsed once

- Cached using configured cache driver

- Subsequent requests pull mapping information from configured cache driver

Page 43: ZendCon2010 The Doctrine Project

Working with ObjectsUse the $em to manage the persistenceof your entities:

$user = new User;$user->setName('Jonathan H. Wage');

$em->persist($user);$em->flush();

Page 44: ZendCon2010 The Doctrine Project

Working with ObjectsUpdating an object:

$user = $em->getRepository('User') ->find(array('name' => 'jwage'));

// modify the already managed object$user->setPassword('changed');$em->flush(); // issues update

Page 45: ZendCon2010 The Doctrine Project

Working with ObjectsRemoving an object:

$user = $em->getRepository('User') ->find(array('name' => 'jwage'));

// schedule for deletion$em->remove($user);$em->flush(); // issues delete

Page 46: ZendCon2010 The Doctrine Project

TransactionsImplicit:

EntityManager#flush() will begin and commit/rollback a transaction

$user = new User;$user->setName('George');$em->persist($user);$em->flush();

Page 47: ZendCon2010 The Doctrine Project

TransactionsExplicit:

// $em instanceof EntityManager$em->getConnection()->beginTransaction(); // suspend auto-committry { //... do some work $user = new User; $user->setName('George'); $em->persist($user); $em->flush(); $em->getConnection()->commit();} catch (Exception $e) { $em->getConnection()->rollback(); $em->close(); throw $e;}

Page 48: ZendCon2010 The Doctrine Project

TransactionsA more convenient explicit transaction:

// $em instanceof EntityManager$em->transactional(function($em) { //... do some work $user = new User; $user->setName('George'); $em->persist($user);});

Page 49: ZendCon2010 The Doctrine Project

Transactions and Performance

for ($i = 0; $i < 20; ++$i) { $user = new User; $user->name = 'Jonathan H. Wage'; $em->persist($user);}

$s = microtime(true);$em->flush();$e = microtime(true);echo $e - $s;

Page 50: ZendCon2010 The Doctrine Project

Transactions and PerformanceHow you use transactions can greatlyaffect performance. Here is the samething using raw PHP code:

$s = microtime(true);for ($i = 0; $i < 20; ++$i) { mysql_query("INSERT INTO users (name) VALUES ('Jonathan H. Wage')", $link);}$e = microtime(true);echo $e - $s;

Page 51: ZendCon2010 The Doctrine Project

Which is faster?- The one using no ORM, and no

abstraction at all?

- Or the one using the Doctrine ORM?

Page 52: ZendCon2010 The Doctrine Project

Which is faster?- The one using no ORM, and no

abstraction at all?

- Or the one using the Doctrine ORM?

- Doctrine2 wins! How?

Doctrine2 0.0094 seconds

mysql_query 0.0165 seconds

Page 53: ZendCon2010 The Doctrine Project

Not FasterDoctrine just automatically performed the inserts inside one transaction. Here is the code updated to use transactions:

$s = microtime(true);mysql_query('START TRANSACTION', $link);for ($i = 0; $i < 20; ++$i) { mysql_query("INSERT INTO users (name) VALUES ('Jonathan H. Wage')", $link);}mysql_query('COMMIT', $link);$e = microtime(true);echo $e - $s;

Page 54: ZendCon2010 The Doctrine Project

Much FasterTransactions matter and can affectperformance greater than any codeoptimization!

Doctrine2 0.0094 seconds

mysql_query 0.0165 seconds0.0028

Page 55: ZendCon2010 The Doctrine Project

Locking SupportOptimistic locking with integer:

class User{ // ... /** @Version @Column(type="integer") */ private $version; // ...}

Page 56: ZendCon2010 The Doctrine Project

Locking SupportOptimistic locking with timestamp:

class User{ // ... /** @Version @Column(type="datetime") */ private $version; // ...}

Page 57: ZendCon2010 The Doctrine Project

Locking SupportVerify version when finding:

use Doctrine\DBAL\LockMode;use Doctrine\ORM\OptimisticLockException;

$theEntityId = 1;$expectedVersion = 184;

try { $entity = $em->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);

// do the work

$em->flush();} catch(OptimisticLockException $e) { echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";}

Page 58: ZendCon2010 The Doctrine Project

Locking SupportExample implementation:

$post = $em->find('BlogPost', 123456);

echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';

$postId = (int) $_GET['id'];$postVersion = (int) $_GET['version'];

$post = $em->find('BlogPost', $postId, \Doctrine\DBAL\LockMode::OPTIMISTIC, $postVersion);

Page 59: ZendCon2010 The Doctrine Project

DQLDoctrine Query Language

Page 60: ZendCon2010 The Doctrine Project

DQL - DQL stands for Doctrine Query Language and is an

Object Query Language derivate that is very similar to the Hibernate Query Language (HQL) or the Java Persistence Query Language (JPQL).

- DQL provides powerful querying capabilities over your object model. Imagine all your objects lying around in some storage (like an object database). When writing DQL queries, think about querying that storage to find a certain subset of your objects.

Page 61: ZendCon2010 The Doctrine Project

DQL Parser- Parser completely re-written from

scratch

- Parsed by top down recursive descent lexer parser that constructs an AST(Abstract Syntax Tree)

- Platform specific SQL is generated from AST

Page 62: ZendCon2010 The Doctrine Project

Doctrine Query Language

$q = $em->createQuery('SELECT u FROM User u');$users = $q->execute();

Page 63: ZendCon2010 The Doctrine Project

Query BuilderSame query built using the QueryBuilder

$qb = $em->createQueryBuilder() ->select('u') ->from('User', 'u');

$q = $qb->getQuery();$users = $q->execute();

Page 64: ZendCon2010 The Doctrine Project

More Examples$query = $em->createQuery( 'SELECT u, g, FROM User u ' . 'LEFT JOIN u.Groups g ' . 'ORDER BY u.name ASC, g.name ASC');$users = $query->execute();

$qb = $em->createQueryBuilder() ->select('u, g') ->from('User', 'u') ->leftJoin('u.Groups', 'g') ->orderBy('u.name', 'ASC') ->addOrderBy('g.name', 'ASC');

$query = $qb->getQuery();

Page 65: ZendCon2010 The Doctrine Project

Executing QueriesExecuting and getting results

$users = $query->execute();

foreach ($users as $user) { // ... foreach ($user->getGroups() as $group) { // ... }}

Page 66: ZendCon2010 The Doctrine Project

Executing QueriesExecute query and iterate over resultskeeping memory usage low:

foreach ($query->iterate() as $user) { // ... foreach ($user->getGroups() as $group) { // ... }}

Page 67: ZendCon2010 The Doctrine Project

Result CacheOptionally cache the results of your queries in your driver of choice:

$cacheDriver = new \Doctrine\Common\Cache\ApcCache();$config->setResultCacheImpl($cacheDriver);

$query = $em->createQuery('select u from Entities\User u');$query->useResultCache(true, 3600, 'my_query_name');

$users = $query->execute();

$users = $query->execute(); // 2nd time pulls from cache

Page 68: ZendCon2010 The Doctrine Project

InheritanceDoctrine supports mapping entities thatuse inheritance with the followingstrategies:

- Mapped Superclass- Single Table Inheritance- Class Table Inheritance

Page 69: ZendCon2010 The Doctrine Project

Mapped Superclasses/** @MappedSuperclass */abstract class MappedSuperclassBase{ /** @Column(type="integer") */ private $mapped1; /** @Column(type="string") */ private $mapped2; /** * @OneToOne(targetEntity="MappedSuperclassRelated1") * @JoinColumn(name="related1_id", referencedColumnName="id") */ private $mappedRelated1;

// ... more fields and methods}

/** @Entity */class EntitySubClass extends MappedSuperclassBase{ /** @Id @Column(type="integer") */ private $id; /** @Column(type="string") */ private $name;

// ... more fields and methods}

Page 70: ZendCon2010 The Doctrine Project

Single Table Inheritance

/** * @Entity * @InheritanceType("SINGLE_TABLE") * @DiscriminatorColumn(name="discr", type="string") * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"}) */class Person{ // ...}

/** * @Entity */class Employee extends Person{ // ...}

Page 71: ZendCon2010 The Doctrine Project

Single Table Inheritance- All entities share one table.

- To distinguish which row represents which type in the hierarchy a so-called discriminator column is used.

Page 72: ZendCon2010 The Doctrine Project

Class Table Inheritance

/** * @Entity * @InheritanceType("JOINED") * @DiscriminatorColumn(name="discr", type="string") * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"}) */class Person{ // ...}

/** @Entity */class Employee extends Person{ // ...}

Page 73: ZendCon2010 The Doctrine Project

Class Table Inheritance- Each class in a hierarchy is mapped to several

tables: its own table and the tables of all parent classes.

- The table of a child class is linked to the table of a parent class through a foreign key constraint.

- A discriminator column is used in the topmost table of the hierarchy because this is the easiest way to achieve polymorphic queries.

Page 74: ZendCon2010 The Doctrine Project

Bulk Inserts with DomainInsert 10000 objects batches of 20:

$batchSize = 20;for ($i = 1; $i <= 10000; ++$i) { $user = new User; $user->setStatus('user'); $user->setUsername('user' . $i); $user->setName('Mr.Smith-' . $i); $em->persist($user); if ($i % $batchSize == 0) { $em->flush(); $em->clear(); // Detaches all objects from Doctrine! }}

Page 75: ZendCon2010 The Doctrine Project

Bulk Update with DQL

$q = $em->createQuery('update Manager m set m.salary = m.salary * 0.9');$numUpdated = $q->execute();

Page 76: ZendCon2010 The Doctrine Project

Bulk Update with DomainUpdate objects in batches of 20:

$batchSize = 20;$i = 0;$q = $em->createQuery('select u from User u');$iterableResult = $q->iterate();foreach($iterableResult AS $row) { $user = $row[0]; $user->increaseCredit(); $user->calculateNewBonuses(); if (($i % $batchSize) == 0) { $em->flush(); // Executes all updates. $em->clear(); // Detaches all objects from Doctrine! } ++$i;}

Page 77: ZendCon2010 The Doctrine Project

Bulk Delete with DQL

$q = $em->createQuery('delete from Manager m where m.salary > 100000');$numDeleted = $q->execute();

Page 78: ZendCon2010 The Doctrine Project

Bulk Delete with Domain

$batchSize = 20;$i = 0;$q = $em->createQuery('select u from User u');$iterableResult = $q->iterate();while (($row = $iterableResult->next()) !== false) { $em->remove($row[0]); if (($i % $batchSize) == 0) { $em->flush(); // Executes all deletions. $em->clear(); // Detaches all objects from Doctrine! } ++$i;}

Page 79: ZendCon2010 The Doctrine Project

EventsDoctrine triggers events throughout thelifecycle of objects it manages:

- preRemove- postRemove- prePersist- postPersist- preUpdate- postUpdate- preLoad- postLoad

Page 80: ZendCon2010 The Doctrine Project

Example

/** * @Entity * @HasLifecycleCallbacks */class BlogPost{ // ...

/** @PreUpdate */ public function prePersist() { $this->createdAt = new DateTime(); }

/** @PreUpdate */ public function preUpdate() { $this->updatedAt = new DateTime(); }}

Page 81: ZendCon2010 The Doctrine Project

Using Raw SQL- Write a raw SQL string

- Map the result set of the SQL query using a ResultSetMapping instance

Page 82: ZendCon2010 The Doctrine Project

Using Raw SQL

$sql = 'SELECT id, name FROM users WHERE username = ?';

$rsm = new ResultSetMapping;$rsm->addEntityResult('User', 'u');$rsm->addFieldResult('u', 'id', 'id');$rsm->addFieldResult('u', 'name', 'name');

$query = $this->_em->createNativeQuery($sql, $rsm);$query->setParameter(1, 'jwage');

$users = $query->getResult();

Page 83: ZendCon2010 The Doctrine Project

Why use an object mapper?

Page 84: ZendCon2010 The Doctrine Project

Encapsulate your domain in an object oriented interface

Encapsulation

Page 85: ZendCon2010 The Doctrine Project

The organization of your domain logic in an OO way improved maintainability

Maintainability

Page 86: ZendCon2010 The Doctrine Project

Keeping a clean OO domain model makes your business logic easily testable for improved stability

Testability

Page 87: ZendCon2010 The Doctrine Project

Write portable and thin application controller code and fat models.

Portability

Page 88: ZendCon2010 The Doctrine Project

Questions?

- http://www.twitter.com/jwage- http://www.facebook.com/jwage- http://www.jwage.com

OpenSky is hiring! Inquire via e-mail at [email protected] or in person after this presentation!