Doctrine ORM behaviors, or how to use traits efficiently

Published on

Mar 29, 2012

technical

Mar 30, 2012 − 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.

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 DoctrineORMMapping as ORM;

use KnpDoctrineBehaviorsORM as ORMBehaviors;

/**
 * @ORMEntity
 */
class Category
{
    use ORMBehaviorsTimestampableTimestampable;

    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(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 DoctrineORMMapping as ORM;

use KnpDoctrineBehaviorsORM as ORMBehaviors;

/**
 * @ORMEntity
 */
class Category
{
    use ORMBehaviorsTranslatableTranslatable;

    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="NONE")
     */
    protected $id;
}

− Define a CategoryTranslation entity with the Translation trait:

<?php

use DoctrineORMMapping as ORM;

use KnpDoctrineBehaviorsORM as ORMBehaviors;

/**
 * @ORMEntity
 */
class CategoryTranslation
{
    use ORMBehaviorsTranslatableTranslation;

    /**
     * @ORMColumn(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 TreeNode trait:

<?php

use DoctrineORMMapping as ORM;

use KnpDoctrineBehaviorsORM as ORMBehaviors;

/**
 * @ORMEntity(repositoryClass="CategoryRepository")
 */
class Category
{
    use ORMBehaviorsTreeNode;

    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="NONE")
     */
    protected $id;
}

You also have to use the TreeTree trait on the corresponding EntityRepository:

<?php

use DoctrineORMEntityRepository;

use KnpDoctrineBehaviorsORM as ORMBehaviors;

class CategoryRepository extends EntityRepository
{
    use ORMBehaviorsTreeTree;
}

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! :)

Written by

KNP Labs
KNP Labs

Comments