When it comes to adding entity metadata timestamps like createdAt and updatedAt, there are many ways to implement it.
For this example, I will use Symfony7 and the DoctrineBundle 2.11.3.
One could simply use the Timestampable from DoctrineExtensions and easily add the createdAt and updatedAt fields to the entities.
But using libraries for such simple tasks might not always be the best solution, especially when you need control over things.
In this article, we will see how to add createdAt and updatedAt fields to the entities using Symfony Clock component
and also the same approach could be used for other similar behaviors.
I will start by creating a simple PHP trait that will be used to add the createdAt and updatedAt fields and getters/setters to the entities.
Traits are not always the best solution, but in this case, it will be a good fit. Since it will give us a simple way to add the fields to the entities and also keep the code DRY.
<?php
namespace App\Util;
use Doctrine\ORM\Mapping as ORM;
trait TimestampableEntityTrait
{
#[ORM\Column]
private \DateTimeImmutable $createdAt;
#[ORM\Column]
private \DateTimeImmutable $updatedAt;
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
}
Now, let's create a simple entity Post and use the TimestampableEntityTrait to add the createdAt and updatedAt fields.
<?php
namespace App\Entity;
use App\Repository\BlogPostRepository;
use App\Util\TimestampableEntityTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BlogPostRepository::class)]
class Post
{
use TimestampableEntityTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
#[ORM\Column(length: 255)]
private string $title;
// getters and setters...
}
Since we are adding new fields, migrations will be required to update the database schema.
For the last part, we will use the Symfony Clock component to set the createdAt and updatedAt fields when the entity is created and updated.
This can be easily done by hooking into the Doctrine lifecycle events, using a Doctrine Lifecycle Listener.
We will need an event listener and the AsDoctrineListener attribute from the doctrine bundle to register the listener.
<?php
namespace App\EventListener;
use App\Util\TimestampableEntityTrait;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Clock\ClockInterface;
#[AsDoctrineListener(event: Events::prePersist)]
#[AsDoctrineListener(event: Events::preUpdate)]
class TimestampableEntityListener
{
public function __construct(
private ClockInterface $clock
) {
}
public function prePersist(PrePersistEventArgs $args): void
{
if (in_array(TimestampableEntityTrait::class, class_uses($entity = $args->getObject()))) {
$entity->setCreatedAt($now = $this->clock->now());
$entity->setUpdatedAt($now);
}
}
public function preUpdate(PreUpdateEventArgs $args): void
{
if (in_array(TimestampableEntityTrait::class, class_uses($entity = $args->getObject()))) {
$entity->setUpdatedAt($this->clock->now());
}
}
}
By checking if the entity uses the TimestampableEntityTrait, we can set the createdAt and updatedAt fields in the appropriate lifecycle events,
using the ClockInterface to get the current time.
in_array(TimestampableEntityTrait::class, class_uses($entity = $args->getObject()
Checkin if the entity uses the TimestampableEntityTrait in both methods is duplicated code.
We can create a new private method to handle this check and call it in both methods:
private function supportsTrait(object $entity): bool
{
return in_array(TimestampableEntityTrait::class, class_uses($entity));
}
Which makes our final trait code look like:
<?php
namespace App\EventListener;
use App\Util\TimestampableEntityTrait;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Clock\ClockInterface;
#[AsDoctrineListener(event: Events::prePersist)]
#[AsDoctrineListener(event: Events::preUpdate)]
class TimestampableEntityListener
{
public function __construct(
private ClockInterface $clock
) {
}
public function prePersist(PrePersistEventArgs $args): void
{
if ($this->supportsTrait($entity = $args->getObject())) {
$entity->setCreatedAt($now = $this->clock->now());
$entity->setUpdatedAt($now);
}
}
public function preUpdate(PreUpdateEventArgs $args): void
{
if ($this->supportsTrait($entity = $args->getObject())) {
$entity->setUpdatedAt($this->clock->now());
}
}
private function supportsTrait(object $entity): bool
{
return in_array(TimestampableEntityTrait::class, class_uses($entity));
}
}
This approach allows you to easily access the createdAt and updatedAt fields and also gives you control over the behavior.
Plus, using the Symfony Clock component, you can easily test the behavior of the entities by mocking the ClockInterface.