RSS

Blog

Doctrine ORM behaviors, or how to use traits efficiently

By Konstantin Kudryashov, Florian Klein, Leszek Prabucki
30 March 2012
In the Development category
Tags: behaviors, doctrine, php 5.4, traits

Since php5.4 is here and pretty stable, we decided to experiment on "traits" and their usage in the real world.
Let's see how we can use them with Doctrine2 entities.

Traits

Traits in php are just a bunch of properties and methods you can copy into a class.
All of this is done at the interpreter level and is totally transparent to Doctrine.

They are designed for horizontal reusability, which is a perfect fit for sharing common behaviors on different entities.

Common behaviors

On common request is for automatic timestamping of entites, using created_at and updated_at date properties for example.

This is something that is applicable to any type of entity.

It's typically a good usage of horizontal reusability.
It's also the moment when you're asking yourself: "How can I not repeat myself?"

Introducing Timestampable behavior

Timestampable is a simple trait that you apply to a doctrine entity:

<?php

use Doctrine\ORM\Mapping as ORM;

use Knp\DoctrineBehaviors\ORM as ORMBehaviors;

/**
 * @ORM\Entity
 */
class Category
{
    use ORMBehaviors\Timestampable\Timestampable;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    protected $id;
}

Note the use statement in the class body.

This will append two properties of doctrine type DateTime and the two public methods to the entity, this gets you createdAt and updatedAt values:

<?php

$category = new Category;
$entityManager->persist($category);

$category->getCreatedAt();
$category->getUpdatedAt();

As soon as you update it, getUpdatedAt will return the new date at which the entity was updated.

Installation

Timestampable and other traits are all packaged in the KNP Labs/DoctrineBehaviors github repository.

You can easily install them using composer.
Just put this in the composer.json file at the root directory of your project.

{
    "require": {
        "knplabs/doctrine-behaviors": "dev-master",
    }
}

Then run composer:

curl -s http://getcomposer.org/installer | php
php composer.phar install

Listeners

All of this is possible thanks to Doctrine listeners, that listen for persist or update events on each entity that uses Timestampable.

But, to make this work, you will have to register them.

Using Symfony2, it's really easy! Just import a service definition file:

# app/config/config.yml

imports:
    - { resource: ../../vendor/knplabs/doctrine-behaviors/config/orm-services.yml }

Translatable behavior

A really common requirement is to make entities translatable.
We tried to make it as easy as possible thanks to a simple and logical naming convention.

In order to have a working translatable entity, follow these 2 steps:

− Use the Translatable trait:

<?php

use Doctrine\ORM\Mapping as ORM;

use Knp\DoctrineBehaviors\ORM as ORMBehaviors;

/**
 * @ORM\Entity
 */
class Category
{
    use ORMBehaviors\Translatable\Translatable;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    protected $id;
}

− Define a CategoryTranslation entity with the Translation trait:

<?php

use Doctrine\ORM\Mapping as ORM;

use Knp\DoctrineBehaviors\ORM as ORMBehaviors;

/**
 * @ORM\Entity
 */
class CategoryTranslation
{
    use ORMBehaviors\Translatable\Translation;

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

Here it is!

TranslatableListener will detect the association between these 2 entites without you having to handle it.

All you have to do now is work on them as usual via the OneToMany association, which means you can easily left join your translations for example.

<?php

$category = new Category;
$category->translate('fr')->setName('Chaussures');
$category->translate('en')->setName('Shoes');
$em->persist($category);

$category->translate('en')->getName();

Tree

Tree uses a materialized path implementation to represent trees.

All nodes contain their full path from its root:

 | id  | name       | path       |
 +-----+------------+------------+
 | 1   | fr         | /1         |
 | 2   | villes     | /1/2       |
 | 4   | subNantes  | /1/2/3/4   |
 | 7   | en         | /7         |
 | 8   | villes     | /7/8       |
 | 9   | Nantes     | /7/8/9     |
 | 10  | subNantes  | /7/8/9/10  |
 | 11  | Lorient    | /7/8/11    |
 | 12  | Rouen      | /7/8/12    |
 | 6   | Rouen      | /1/2/6     |
 | 3   | Nantes     | /1/2/3     |
 | 5   | Lorient    | /1/2/5     |

To represent your entities in a tree, all you have to do is use the Tree\Node trait:

<?php

use Doctrine\ORM\Mapping as ORM;

use Knp\DoctrineBehaviors\ORM as ORMBehaviors;

/**
 * @ORM\Entity(repositoryClass="CategoryRepository")
 */
class Category
{
    use ORMBehaviors\Tree\Node;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    protected $id;
}

You also have to use the Tree\Tree trait on the corresponding EntityRepository:

<?php

use Doctrine\ORM\EntityRepository;

use Knp\DoctrineBehaviors\ORM as ORMBehaviors;

class CategoryRepository extends EntityRepository
{
    use ORMBehaviors\Tree\Tree;
}

This entity now has a powerful api to manage its children, parents, move them, traverse, ...

$root = $em->getRepository('Category')->getTree();

$root->getParentNode();
$root->getChildren();
$root[0][1]; // array access of children
$root->isLeafNode();
$root->isRootNode();

For more examples of all the other traits we propose, just check the README.

Next up is controller behaviors: we'll have some really fun things to share with you in the coming weeks!

Cheers! :)

  • 2012-10-02 Jamil123456

     Hie,

    Don't know if it's the right place to post this question, but is there a way to change the generated name in DB createdAt/updatedAt of the Timestampable Behavior (for an underscore name like created_at).

    Thanks in advance.

  • 2012-04-01 Florian Klein

    s/them/ones

  • 2012-04-01 Florian Klein

    Thanks for feedback. Indeed, we maybe badly namespaced these traits. We will rename independant them.

  • 2012-04-01 beberlei

    It looks good.

    Regarding the dependency, its an acceptable dependency as long as the behaviors functionality does not require the ORM to work to be testable.

    The PersistentObject abstract class in Doctrine Common is such an dependency. These traits are not, but i wonder why they are named "ORM" and such, when their usage doesn't really depend on the ORM.

  • 2012-03-31 Florian Klein

    yes, it's true that namespaces could be renamed to reflect better the difference. Thanks for pointing this out :)

  • 2012-03-31 Daniel Holmes

    There might be some better options for the namespaces then to reflect this? DoctrineBehaviours\ORM certainly implies it's tied to doctrine ORM when as you point out, it is really just a helper or tool for modelling. Maybe it's more accurate to have the traits under a Modelling or Lang or Domain namespace or something with the ORM Listeners under the doctrine namespace. e.g.

    Knp\Modelling\Timestampable;
    Knp\DoctrineORMExtensions\TimestampableListener;
    Knp\DoctrineMongoDBExtensions\TimestampableListener;

    etc...

  • 2012-03-30 Florian Klein

    Moreover, you can't know where these traits will be applied (in which entities). 

    Using xml or yaml, you need to define metadata **on entity**, which would force you to know internal metadata of traits. So definitively, annotations are the way to go for "not entity aware metadata".

  • 2012-03-30 Florian Klein

    By the way, we don't mix ORM specific code into entites. Entity trait are nothing more than parts of a usual entity, that you can reuse across different entities. 

    The place where we hide logic (and this is a point) is doctrine event listeners.

  • 2012-03-30 Florian Klein

    Entities using traits are just pure domain objects, containing a few copied methods and properties that manage common logic. 

    The only *optional* dependency is doctrine event listeners, that automatize what you could do  manually without doctrine. So answer is "no, it doesn't add dependency to ORM", Giorgio Sironi  John Kary .Of course, repository traits are doctrine related, that obviously uses doctrine api.

  • 2012-03-30 Florian Klein

    good thing with traits is that it copies logic into a class, it's not hiding logic.

  • 2012-03-30 Florian Klein

    These entity traits are totally independant from doctrine, at least as independant as a classic entity. You can reuse them without doctrine, but you will have to manage lifecycle events by hand. Most of the doctrine specific logic is put in event listeners. Trait methods are just domain logic.

  • 2012-03-30 Florian Klein

    Stof is totally right, it adds no more dependency to doctrine than any mapped property.

  • 2012-03-30 Florian Klein

    Advantage of annotation is that it is self contained with mapped code. With a annotated trait, you package logic AND metadata.

  • 2012-03-30 Giorgio Sironi

    Even with annotations present, the original class still can be parsed and instantiated without Doctrine\ORM, while I think adding the use statement requires Knp\DoctrineBehaviors code to  be present.
    Stof you can test and reuse domain objects with Doctrine annotations without Doctrine coming into play (no EntityManager, no connections, no other code than the domain object's one); a trait is no different from a Active Record superclass to inherit from as it cannot be detached from the class.

  • 2012-03-30 Marco Pivetta

     That doesn't really have to do with mixing ORM in domain objects, but probably more with adding hidden logic to the entities. The traits at least don't make it that painful...

  • 2012-03-30 Evgeniy Guseletov

    chapter about trees seems really good

  • 2012-03-30 Marijn Huizendveld

    Why is it using annotations when there's XML or YAML configuration available? The library looks great though!

  • 2012-03-30 Stof

     John Kary why would it add a dependency to the ORM more than writing the annotated property directly in the class ?

  • 2012-03-30 John Kary

    giorgiosironi brought up a good point on Twitter: doesn't this add your ORM as a dependency for your domain objects? Which is a benefit of the data mapper pattern to begin with?