Doctrine 2 & ZF2

Doctrine Project + Zend Framework 2

Hello!

Marco Pivetta Marco Pivetta

Ocramius on Github Ocramius on Twitter

Ocramius

Roave

Evan Coury (EvanDotPro) Justin Martin (FrozenFire) Ben Scholzen (DASPRiD) Marco Pivetta (Ocramius) Rob Allen (Akrabat), Nineteen Feet) Nigel Lundsten (nlundsten) Aleksey Khudyakov (Xerkus) Gary Hockin (Spabby / GeeH) Priscilla Hubbard Chloe

We're awesome!

(And we love <marquee/>)

Awesome:

Definition of Awesome

Doctrine core team

ZF2 contributor

Modules developer

professional procrastinator

Main Libraries

ProxyManager, BjyAuthorize, AssetManager, ZeffMu, ZfrRest, OcraServiceManager, OcraCachedViewResolver, DoctrineModule, DoctrineORMModule, DoctrineMongoODMModule, VersionEyeModule

Zend Framework 2

Clean MVC(-ish) architecture
Clean dependendency injection
Event driven design and flexibility
Well applied SOC and SRP

Zend\Db

Finally got rid of ActiveRecord
Allows Hydration of POPO
Finally getting away from Fat Models
Introducing the concept of an Entity

Designing with Zend\Db

The Entity

namespace Album\Model;

class Album
{
    protected $id;
    protected $artist;
    protected $title;

    public function exchangeArray($data)
    {
        $this->id     = $data['id'];
        $this->artist = $data['artist'];
        $this->title  = $data['title'];
    }

    // ...
}

The Table

namespace Album\Model;
use Zend\Db\TableGateway\TableGateway;

class AlbumTable {
    public function __construct(TableGateway $gateway) {
        $this->gw = $gateway;
    }

    public function getAlbum($id) {
        return $this->gw->select(array('id' => (int) $id));
    }

    public function saveAlbum(Album $album) {
        $this->gw->insert($album->toArray()); // (or update!)
    }

    public function deleteAlbum($id) {
        $this->gw->delete(array('id' => (int) $id));
    }
}

Wiring it together

public function getServiceConfig() {
    return array(
        'factories' => array(
            'Album\Model\AlbumTable' =>  function($sm) {
                return new AlbumTable($sm->get('AlbumTableGateway'));
            },
            'AlbumTableGateway' => function ($sm) {
                $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                $resultSetPrototype = new ResultSet();
                $resultSetPrototype
                    ->setArrayObjectPrototype(new Album());
                return new TableGateway(
                    'album', $dbAdapter,
                    null, $resultSetPrototype
                );
            },
        ),
    );
}

Using it in a Controller

class AlbumController extends SomeBaseController {
    public function __construct(AlbumTable $table) {
        $this->table = $table;
    }

    public function registerNewAlbum() {
        $data = $this->getValidatedData(); // I'm lazy

        $album = new Album();
        $album->exchangeArray($data);

        $this->table->saveAlbum($album);

        return array('album' => $album);
    }
}

And that's cool...

... but then...

Example (relatively simple) EER Diagram

... business logic happens!

New questions:

Do we need to do it for all tables?

How do we load an album's artist(s)?

Does AlbumTable depend on ArtistTable?

Does the controller depend on AlbumTable and ArtistTable?

How do we avoid useless queries when saving data?

And the list grows!

Time for sharper tools?

I don't know for sure

Decide for yoursef

Introducing
Doctrine Project

An incubator for persistence-oriented libraries

Fabio B. Silva (FabioBatSilva) Marco Pivetta (Ocramius) Jordi Boggiano (Seldaek) Alexander (asm89) Bulat Shakirzyanov (avalanche123) Benjamin Eberlei (beberlei) David Funaro (davidino) David Buchmann (dbu) Fabien Potencier (fabpot) Igor Golovanov (golovanov) Guilherme Blanco (guilhermeblanco) Jeremy Mikola (jmikola) Jonathan H. Wage (jwage) Kore Nordmann (kore) Kris Wallsmith (kriswallsmith) Lukas Kahwe Smith (lsmith77) Nils Adermann (naderman) Daniele Alessandri (nrk) Alessandro Nadalin (odino) Richard Fullmer (richardfullmer) Roland Schilter (rndstr) Roman S. Borschel (romanb) Johannes (schmittjoh) Christophe Coevoet (stof) Michaël Gallego (bakura10) Tim Roediger (superdweebie) Kyle Spraggs (spiffyjr) Steve Müller (deeky666)

What about Doctrine ORM?

It is inspired by Hibernate and the JPA (JSR-317)

It is based on a DBAL (DataBase Abstraction Layer)

Allows saving and loading POPO with an SQL DB

But most importantly...

An
Object
Relational
Mapper

An
Object
Relational
Mapper

An
Object
Relational
Mapper

An
Object
Relational
Mapper

Remember this guy?

Example (relatively simple) EER Diagram

An ORM gives you the impression that you are working with a "virtual" database (graph) composed by objects

Simply put:

Forget the database!

(yes, you still need to know how a RDBMS works!)

The Modules!

DoctrineModule
basic common functionality

DoctrineORMModule
ORM/SQL Connection

DoctrineMongoODMModule
ODM/MongoDB Connection

Installation!

php composer.phar require doctrine/doctrine-orm-module:0.8.*
Wait a bit
php composer.phar require zendframework/zend-developer-tools:0.*

Enabling the modules

config/application.config.php
return array(
    'modules' => array(
        'ZendDeveloperTools',
        'DoctrineModule',
        'DoctrineORMModule',
        'Application',
    ),
    // ...
);

You should see:

Successful toolbar installation

Write your first entity

module/Application/src/Application/Entity/User.php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/** @ORM\Entity */
class User {
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /** @ORM\Column(type="string") */
    protected $fullName;

    // getters/setters
}

Annotations?

Blasphemer!

(stone throwing here)

Annotations are actually OK for non-reusable code

Configure mappings

module/Application/config/module.config.php
return array(
'doctrine' => array(
  'driver' => array(
    'application_entities' => array(
      'class' =>'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
      'cache' => 'array',
      'paths' => array(__DIR__ . '/../src/Application/Entity')
    ),

    'orm_default' => array(
      'drivers' => array(
        'Application\Entity' => 'application_entities'
      )
))), // ...

You should see:

Successful mapping of the user entity

Configure the connection

config/autoload/doctrine.local.php
return array(
  'doctrine' => array(
    'connection' => array(
      'orm_default' => array(
        'driverClass' =>'Doctrine\DBAL\Driver\PDOMySql\Driver',
        'params' => array(
          'host'     => 'localhost',
          'port'     => '3306',
          'user'     => 'username',
          'password' => 'password',
          'dbname'   => 'database',
          'encoding' => 'utf8',
)))));

Validate mappings

php public/index.php orm:validate-schema

Validate schema mappings

Generate the database

php public/index.php orm:schema-tool:create

generating the database

Generating databases?! WOAH!

Test it!

module/Application/src/Application/Controller/IndexController.php
public function indexAction() {
    $objectManager = $this
        ->getServiceLocator()
        ->get('Doctrine\ORM\EntityManager');

    $user = new \Application\Entity\User();
    $user->setFullName('Marco Pivetta');

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

    die(var_dump($user->getId())); // yes, I'm lazy
}

ಠ_ಠ

Some Examples

Not familiar with doctrine?

Persisting an object

$user = new User();
$user->setFullName('Marco Pivetta');

$objectManager->persist($user); // $user is now "managed"
$objectManager->flush(); // commit changes to db

var_dump($user->getId()); // 1

Persisting multiple objects

$user1 = new User();
$user1->setFullName('Marco Pivetta');
$objectManager->persist($user1);

$user2 = new User();
$user2->setFullName('Michaël Gallego');
$objectManager->persist($user2);

$user3 = new User();
$user3->setFullName('Kyle Spraggs');
$objectManager->persist($user3);

$objectManager->flush();

Retrieving an object

$user1 = $objectManager->find('Application\Entity\User', 1);

var_dump($user1->getFullName()); // Marco Pivetta

$user2 = $objectManager
    ->getRepository('Application\Entity\User')
    ->findOneBy(array('fullName' => 'Michaël Gallego'));

var_dump($user2->getFullName()); // Michaël Gallego

Updating an object

$user = $objectManager->find('Application\Entity\User', 1);

$user->setFullName('Guilherme Blanco');

$objectManager->flush();

Deleting an object

$user = $objectManager->find('Application\Entity\User', 1);

$objectManager->remove($user);

$objectManager->flush();

Associations - User

/** @ORM\Entity */
class User {
    // like before

    /** @ORM\ManyToOne(targetEntity="Address") */
    protected $address;

    /** @ORM\ManyToMany(targetEntity="Project") */
    protected $projects;

    public function __construct()
    {
        $this->projects = new ArrayCollection();
    }
    
    // getters/setters
}

Associations - Address

/** @ORM\Entity */
class Address {
    /**
     * @ORM\Id @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /** @ORM\Column(type="string") */
    protected $city;

    /** @ORM\Column(type="string") */
    protected $country;

    // getters/setters etc.
}

Associations - Projects

/** @ORM\Entity */
class Project {
    /**
     * @ORM\Id @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /** @ORM\Column(type="string") */
    protected $name;

    // getters/setters
}

Associations - Persisting associations

$user = new User();
$user->setFullName('Marco Pivetta');
$objectManager->persist($user);

$address = new Address();
$address->setCity('Frankfurt')
$address->setCountry('Germany');
$objectManager->persist($address);

$project = new Project();
$project->setName('Doctrine ORM');
$objectManager->persist($project);

$user->setAddress($address);
$user->getProjects()->add($project);
$objectManager->flush();

Associations - Retrieving associations

$user = $objectManager->find('Application\Entity\User', 1);

var_dump($user->getAddress()->getCity()); // Frankfurt
var_dump($user->getProjects()->first()->getName()) //Doctrine ORM

DoctrineModule goodies

EER UML model

Example EER UML diagram generated by DoctrineORMModule

Generated via ZendDeveloperTools

Paginator Adapter

use Doctrine\Common\Collections\ArrayCollection;
use DoctrineModule\Paginator\Adapter\Collection as Adapter;
use Zend\Paginator\Paginator;

// Create a Doctrine Collection
$collection = new ArrayCollection(range(1, 101));

// Create the paginator itself
$paginator = new Paginator(new Adapter($collection));

$paginator
    ->setCurrentPageNumber(1)
    ->setItemCountPerPage(5);

Paginator Adapter (ORM)

use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator;
use Doctrine\ORM\Tools\Pagination\Paginator as ORMPaginator;
use Zend\Paginator\Paginator;

// Create a Doctrine Collection
$query = $em->createQuery('SELECT f FROM Foo f JOIN f.bar b');

// Create the paginator itself
$paginator = new Paginator(
    new DoctrinePaginator(new ORMPaginator($query))
);

$paginator
    ->setCurrentPageNumber(1)
    ->setItemCountPerPage(5);

Object-Exists Validator

$repository = $objectManager
    ->getRepository('Application\Entity\User');

$validator = new \DoctrineModule\Validator\ObjectExists(array(
    'object_repository' => $repository,
    'fields' => array('email')
));

var_dump($validator->isValid('test@example.com'));
var_dump($validator->isValid(array(
    'email' => 'test@example.com'
)));

Cache adapters

$zendCache = new \Zend\Cache\Storage\Adapter\Memory();

$cache = new \DoctrineModule\Cache\ZendStorageCache($zendCache);
$doctrineCache = new \Doctrine\Common\Cache\ArrayCache();
$options = new \Zend\Cache\Storage\Adapter\AdapterOptions();

$cache = new \DoctrineModule\Cache\DoctrineCacheStorage(
    $options,
    $doctrineCache
);

Hydrator

use DoctrineModule\Stdlib\Hydrator\DoctrineObject;

$hydrator = new DoctrineObject($objectManager);

$city = new City();
$data = array('name' => 'Frankfurt');

$city = $hydrator->hydrate($data, $city);

echo $city->getName(); // prints "Frankfurt"

$dataArray = $hydrator->extract($city);

echo $dataArray['name']; // prints "Frankfurt"

Hydrator (2)

use DoctrineModule\Stdlib\Hydrator\DoctrineObject;

$hydrator = new DoctrineObject($objectManager);

$city = new City();
$data = array('country' => 123);

$city = $hydrator->hydrate($data, $city);

var_dump($city->getCountry());
// prints class Country#1 (1) {
//   protected $name => string(5) "Germany"
// }

Form Element

$form->add(array(
    'type' => 'DoctrineModule\Form\Element\ObjectSelect',
    'name' => 'user',
    'options' => array(
        'object_manager' => $objectManager,
        'target_class'   => 'Module\Entity\User',
        'property'       => 'fullName',
        'is_method'      => true,
        'find_method'    => array(
            'name'   => 'findBy',
            'params' => array(
                'criteria' => array('active' => 1),
                'orderBy'  => array('lastName' => 'ASC'),
            ),
        ),
    ),
));

Everything works with MongoDB ODM too!

CouchDB ODM/PHPCR ODM/OrientDB ODM

Moar stuff coming!

... because the community never sleeps ...

Good Practices

Keep Entities simple

Think of entities as value-objects + ID

Don't add logic to entities (it's data!)

Keep entities aware only of themselves + associations

doctrine/common API

Stick with the doctrine/common interfaces

Unlock ORM/ MongoDB ODM/CouchDB ODM/ PHPCR ODM/OrientDB ODM portability!

doctrine/common API

Instead of hinting
Doctrine\ORM\EntityManager
use
Doctrine\Common\Persistence\ObjectManager

doctrine/common API

Instead of hinting
Doctrine\ORM\EntityRepository
use
Doctrine\Common\Persistence\ObjectRepository

Use Collections everywhere

Doctrine comes with a powerful collections API

OOP API for (PHP-)array-like data structures

Use the Criteria API

Collections provide a Criteria API

Allows you to filter virtually any kind of data structure

Criteria API

use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ArrayCollection;

$collection = new ArrayCollection(array($user1, $user2, $user3));
$criteria   = new Criteria();
$criteria->andWhere(
    $criteria->expr()->gt(
        'lastLogin',
        new \DateTime('-1 day')
    )
);

$recentVisitors = $collection->matching($criteria);

Criteria API

Works also with repositories!

$recentVisitors = $objectManager
    ->getRepository('Application\Entity\Users')
    ->matching($criteria);

Criteria API advantages

Works in ORM Repositories, Collections, etc...

Abstracts the problem of "searching"

Same criteria for different storages (ORM, ODM, Memory, ElasticSearch, cache...)

Allows you to define your own RecentUsersCriteria or InactiveUsersCriteria objects...

Inject the Object Manager

If you fetch the object manager from within your services, replacing it will become very difficult:
Inject it instead!

Inject the Object Manager

class MyService
{
    public function __construct(ObjectManager $objectManager)
    {
        // [...]
    }
}
'factories' => array(
    'my_service' => function ($sl) {
        $objectManager = $sl->get('Doctrine\ORM\EntityManager');

        return new MyService($objectManager);
    }
),

Keep Object Manager out of Controllers

If you inject ObjectManager in your controllers, you're gonna have a bad architecture

Don't use persistence to solve application problems

Filtering data when saved to DB

Validating data when saved to DB

Saving files when records are saved to DB

Using DB-level errors to check input validity

Keep your object graph consistent

An ObjectManager works under the assumption that managed objects are valid!

Assign values to your entities only when data is valid!

Questions?

Send me a tweet @Ocramius!

Fork it!

You can find these slides on GitHub at https://github.com/Ocramius/doctrine-orm-and-zendframework-2

Thanks for listening!

https://joind.in/9275
Joind.in talk page QR code