Doctrine 2 & ZF2

Doctrine Project + Zend Framework 2

Note: these slides are combined with a webcast that you can find on this page .

Marco Pivetta Marco Pivetta

Doctrine core team

Zf2 contributor

Modules developer time waster

@Ocramius - Ocramius

Main Libraries

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

Doctrine Project

An incubator for persistence-oriented libraries

asm89 avalanche123 beberlei davidino dbu FabioBatSilva fabpot golovanov guilhermeblanco jmikola jwage kore kriswallsmith lsmith77 naderman nrk Ocramius odino richardfullmer rndstr romanb schmittjoh Seldaek stof

What is Doctrine ORM?

Doctrine ORM is an Object Relational Mapper

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

It is based on a DBAL (DataBase Abstraction Layer)

Allows developers to save and load POPO with SQL

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

Simply put:

Forget the database!

The Modules!

DoctrineModule
basic common functionality

DoctrineORMModule
ORM/SQL Connection

DoctrineMongoODMModule
ODM/MongoDB Connection

Installation!

php composer.phar require doctrine/doctrine-orm-module:0.7.*
Wait a bit
Doctrine ORM Module successfully installed!
php composer.phar require zendframework/zend-developer-tools:dev-master
Wait a bit more
ZendDeveloperTools successfully installed!
cp vendor/zendframework/zend-developer-tools/config/zenddevelopertools.local.php.dist config/autoload/zdt.local.php

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

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',
)))));

Validate mappings

./vendor/bin/doctrine-module orm:validate-schema

Validate schema mappings

Generate the database

Generating databases?! WOAH!

./vendor/bin/doctrine-module orm:schema-tool:create

generating the database

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
}

Examples

Down arrow

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

More tutorials at http://marco-pivetta.com/doctrine2-orm-tutorial

Yo Dawg, I heard you like presentations, so I've put a presentation in your presentation

DoctrineModule goodies

EER UML model

See what your entities look like in a graph:

Example EER UML diagram generated by DoctrineORMModule

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,
    'Application\Entity\City'
);

$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,
    'Application\Entity\City'
);

$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'),
            ),
        ),
    ),
));

More stuff!

Everything works with MongoDB ODM too!

CouchDB ODM/PHPCR ODM/OrientDB ODM

Good Practices

Down arrow

Keep Entities simple

Think of entities as value-objects

Don't add logic to entities (hard to change later!)

Keep entities aware only of themselves + relations

Use doctrine/common API

If you stick with using only doctrine/common API, users of your modules can switch between ORM/ MongoDB ODM/CouchDB ODM/ PHPCR ODM/OrientDB ODM

Use doctrine/common API

Prefer

Doctrine\Common\Persistence\ObjectManager

over

Doctrine\ORM\EntityManager

Use doctrine/common API

Prefer

Doctrine\Common\Persistence\ObjectRepository

over

Doctrine\ORM\EntityRepository

Use collections extensively

Doctrine comes with a powerful collections API

OOP API for 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 example

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);
$recentVisitors = $em
    ->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...

Inject the Object Manager

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

Inject the Object Manager

'factories' => array(
    'my_service' => function ($sl) {
        $objectManager = $sl->get('Doctrine\ORM\EntityManager');
        return new MyService($objectManager);
    }
),
class MyService
{
    public function __construct(ObjectManager $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?

Fork it!

You can find these slides on GitHub at https://github.com/Ocramius/doctrine-orm-zf2-tutorial

Thanks for watching!