Using Traits in Doctrine Entities
Since PHP 5.4.0, PHP supports a pretty way to reuse code called “Traits” – a set of methods that you can include within another class in order not to repeat yourself. You can read more about traits in previously published SitePoint posts: here, here and here.
Today, I am going to show you how they can be used with Doctrine ORM in a Symfony Environment.
Trait Basics
<?php
trait ExampleTrait {
public function sayHello() {
echo "Hello";
}
}
class A {
use ExampleTrait;
}
class B {
use ExampleTrait;
}
$one = new A();
$one->sayHello(); /* return `Hello` */
$two = new B();
$two->sayHello(); /* return `Hello`, too */
As we can see, a basic method sayHello()
is declared inside a Trait that is implemented by both A
and B
classes with a use
statement. Easy, right? This example is really short but it should give you the basic knowledge to work with Traits.
If you are interested in Traits, I recommend you read the official documentation and previously published SitePoint posts here, here and here, to fully grasp the concept.
Allow me to warn you about the fact that many people tend not to see a difference between Traits and Interfaces. Here is a pragmatic explanation:
An interface is a contract that says “this object is able to do this thing”, whereas a Trait is giving the object the ability to do the thing.
For a more in-depth explanation, feel free to take a look at this insightful post by Philip Brown, which the previous quote comes from.
When it comes to organizing one’s database architecture, it is not uncommon to face code duplication. As an example, suppose we have to develop the usual blog application. At some point, it is likely that we are going to create a basic Article
entity and probably a Comment
entity as well.
Both entities would benefit from having created_at
and updated_at
fields (so we can sort results on those columns later). But before digging into traits, let’s see how we could build those entities in Doctrine without them.
Step 1: create the entities
Article entity
<?php
namespace Blog\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="article")
* @ORM\Entity(repositoryClass="Blog\AppBundle\Entity\ArticleRepository")
*/
class Article
{
/**
* @ORM\Column(name="idArticle" type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/* Other properties you need in your entity: $title, $content, $author... */
/** @ORM\Column(name="created_at" type="datetime") */
private $createdAt;
/** @ORM\Column(name="updated_at" type="datetime") */
private $updatedAt;
/* Getters & Setters */
}
Comment entity
<?php
namespace Blog\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="comment")
* @ORM\Entity(repositoryClass="Blog\AppBundle\Entity\CommentRepository")
*/
class Comment
{
/**
* @ORM\Column(name="idComment" type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/* Other properties you need in your entity */
/** @ORM\Column(name="created_at" type="datetime") */
private $createdAt;
/** @ORM\Column(name="updated_at" type="datetime") */
private $updatedAt;
/* Getters & Setters */
}
The same properties $createdAt
and $updatedAt
are included in both classes. This is far from DRY. Would Traits be able to help us clean this code up?
Step 2: create the Trait
<?php
// src/Blog/AppBundle/Entity/Traits/TimestampableTrait.php
namespace Blog\AppBundle\Entity\Traits;
use Doctrine\ORM\Mapping as ORM;
trait TimestampableTrait
{
/**
* @var datetime $createdAt
*
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* @var datetime $updatedAt
*
* @ORM\Column(name="updated_at", type="datetime")
*/
private $updatedAt;
/**
* Get createdAt
*
* @return datetime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set createdAt
*
* @param datetime $createdAt
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get updatedAt
*
* @return datetime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Set updatedAt
*
* @param datetime $updatedAt
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
}
Here is a pretty trait file into which we have moved the initial duplicated code. Both $createdAt
and $updatedAt
as well as all the associated methods are now separated from the entities. As a result, it will be much easier to use them somewhere else. Remember the introduction section with the keyword use
.
Step 3: refactor the entities
Article entity
<?php
// src/Blog/AppBundle/Entity/Article.php
namespace Blog\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Blog\AppBundle\Entity\Traits\TimestampableTrait;
class Article
{
use TimestampableTrait;
/**
* @ORM\Column(name="idArticle" type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/* Other properties you need in your entity */
/* Getters & Setters */
}
Comment entity
<?php
// src/Blog/AppBundle/Entity/Comment.php
namespace Blog\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Blog\AppBundle\Entity\Traits\TimestampableTrait;
/**
* @ORM\Table(name="comment")
* @ORM\Entity(repositoryClass="Blog\AppBundle\Entity\CommentRepository")
*/
class Comment
{
use TimestampableTrait;
/**
* @ORM\Column(name="idComment" type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/* Other properties you need in your entity */
/* Getters & Setters */
}
Done! Let’s play with the command line. First, let’s create the entities in our database:
php app/console doctrine:schema:create
This command would yield:
`Article Entity`
| idArticle | *All our other fields...* | created_at | updated_at |
|-----------|---------------------------|------------|------------|
`Comment Entity`
| idComment | *All our other fields...* | created_at | updated_at |
|-----------|---------------------------|------------|------------|
Now, if you want to create new objects from these classes, you would find that they both have the common methods available:
<?php
$article = new Article();
$article->setUpdatedAt(new \Datetime('now'));
$comment = new Comment();
$comment->setUpdatedAt(new \Datetime('now'));
?>
Obviously, we are now ready to persist the data.
Going further
Currently, in the Symfony sphere, many bundles and extensions tend to stick to this way of doing things. The DoctrineBehaviors library from KNPLabs provides a great collection of Traits for entities and repositories. In the same state of mind, I recommend you have an in-depth look at the well known DoctrineExtensions bundle and especially everything about the Timestampable behavior extension.
Final Thoughts
Traits are not difficult to absorb. They are an excellent way to produce lighter and more flexible code. Be careful not to abuse them: sometimes, it is better to construct a unique class implementation. I can’t stress enough how crucial it is to take enough time in order to properly design your app. Give them a go if you think they could help you. Create yours, test them and tell us how you used them!