Using DRY concept in Symfony2 Entities
This is an effort to make Symfony2 workflow (bit more) DRY where Entities are mapped with Doctrine ORM.
Such mapped entities sometimes share same attributes between them i.e. id
, timestamps
etc. In order to use DRY concept here we can create a base entity and then every new entity can extend it to share those attributes. Let's start by creating two entities with repeated attributes.
An Entity is simply a PHP class.
Here are two Entities User
and Address
, and they are structured as following:
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="app_user")
*/
class User {
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @ORM\Column(name="email", type="string", length=255)
*/
private $email;
/**
* @ORM\Column(name="createdAt", type="datetime")
*/
private $createdAt;
/**
* @ORM\Column(name="updatedAt", type="datetime")
*/
private $updatedAt;
...
// getter setters
}
Here is the Address
entity class.
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="app_address")
*/
class Address {
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="property", type="string", length=255)
*/
private $property;
/**
* @ORM\Column(name="street", type="string", length=255)
*/
private $street;
/**
* @ORM\Column(name="town", type="string", length=255)
*/
private $town;
/**
* @ORM\Column(name="createdAt", type="datetime")
*/
private $createdAt;
/**
* @ORM\Column(name="updatedAt", type="datetime")
*/
private $updatedAt;
...
// getter setters
}
As you can see that we have id
, createdAt
and updatedAt
attributes that are being repeated in both entities. To implement a DRY concept here we are going to create a new entity class that will have the shared/repeated attributes and then can be extended by other entities.
Let's create a BaseEntity
class and move the repeated values to it.
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\MappedSuperclass
*/
class BaseEntity {
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* @ORM\Column(name="updated_at", type="datetime")
*/
private $updatedAt;
...
// getters setters
Note a new annotation @ORM\MappedSuperclass
at top of the BaseEntity
that makes sure that attributes in this entity are properly extended into sub-entities. Now we can remove these attributes from User
and Address
entities and extend our BaseEntity
class into these.
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="app_user")
*/
class User extends BaseEntity {
/**
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @ORM\Column(name="email", type="string", length=255)
*/
private $email;
...
// getter setters
}
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="app_address")
*/
class Address extends BaseEntity {
/**
* @ORM\Column(name="property", type="string", length=255)
*/
private $property;
/**
* @ORM\Column(name="street", type="string", length=255)
*/
private $street;
/**
* @ORM\Column(name="town", type="string", length=255)
*/
private $town;
...
// getter setters
}
Our entities are already started to look cleaner.
Now since we have some shared attributes in one entity, this gives us flexibility to add further enhancements to it as required.
Let's add some automation to make sure that both timestamps createdAt
and updatedAt
are properly set before we persist or update the records to our database. For that we will use @ORM\PrePersist
and @ORM\PreUpdate
annotations in custom methods. We also need an additional annotation @ORM\HasLifecycleCallbacks
at class root level.
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\HasLifecycleCallbacks
* @ORM\MappedSuperclass
*/
class BaseEntity {
...
/**
* @ORM\PrePersist
*/
public function setTimestamps() {
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
/**
* @ORM\PreUpdate
*/
public function setUpdatedTimestamp() {
$this->updatedAt = new \DateTime();
}
...
Now we have two additional custom methods, setTimestamps
and setUpdatedTimestamp
. The names for these methods do not really matter so we can name those as we want. First method sets the values for createdAt
and updatedAt
attributes just before we persist a new record to database. We set the values as DateTime
object and Symfony2/Doctrine will automatically convert those to required format for database entry. Second method only updates the updatedAt
value when a database record is updated.
We can simply extend the
BaseEntity
to any entity and need not worry about generating a primary key attribute (id
) and timestamps now on.
Additional enhancements:
We can have as many as enhancement we like to the BaseEntity
and those will be automatically extended to sub-entities. For example we can have a save
method to persist the record by keeping all logic in BaseEntity
instead of a controller. In current situation if we are creating a user we will use following steps inside a controller:
<?php
...
$user = new User();
$user->setName('Foo');
$user->setEmail('[email protected]');
$em = $this->getDoctrine()->getEntityManager();
$em->persist($user);
$em->flush();
...
By adding following helper method into our BaseEntity
we can shorten it from 3 lines to 1:
<?php namespace Foo\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\HasLifecycleCallbacks
* @ORM\MappedSuperclass
*/
class BaseEntity {
...
/**
* Helper method
*
* @param Doctrine\ORM\EntityManager $em
* @throws \RunTimeException
* @return void
*/
protected function save(EntityManager $em) {
if (!$em instanceof EntityManager) {
throw new \RunTimeException(
sprintf('Expected an instance of Doctrine\ORM\EntityManager but got "%s"', gettype($em)), 400
);
}
$em->persist($this);
$em->flush();
}
...
Now we can simply use the save
method inside our controller:
<?php
...
$user = new User();
$user->setName('Foo');
$user->setEmail('[email protected]');
$user->save();
...
...
$addr = new Address();
$addr->setProperty('123');
$addr->setStreet('Foo Road');
$addr->setTown('Fondon');
$addr->save();
...
I hope that this was useful for anyone refactoring their Symfony2 project. Here is the complete BaseEntity class as a Gist. Feel free to suggest any enhancements.