Inspiration

In text below, you may find some ideas on how to use runopencode/metadata library in your projects.

Implementing timestampable behavior using runopencode/metadata

Assume that you want to implement, in example, timestampable behavior for your Doctrine entities and you do not want to use 3rd party library (such as gedmo/doctrine-extensions, https://github.com/doctrine-extensions/DoctrineExtensions) for that, this libray makes that a trivial task.

First thing which you would need is an attribute so you can mark properties which should be automatically updated with current timestamp on entity creation and update:

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace App\Doctrine\Attribute;
 6
 7#[\Attribute(\Attribute::TARGET_PROPERTY)]
 8final readonly class Timestampable
 9{
10     public const string CREATE = 'create';
11     public const string UPDATE = 'update';
12
13     public function __construct(public string $on)
14     {
15         // noop.
16     }
17}

Then, you may add these attributes to your entity properties which you would like to update automatically when entity is created or updated:

 1<?php
 2
 3namespace App\Entity;
 4
 5use App\Doctrine\Attribute\Timestampable;
 6
 7class MyEntity
 8{
 9      #[Timestampable(Timestampable::CREATE)]
10      private \DateTimeImmutable $createdAt;
11
12      #[Timestampable(Timestampable::UPDATE)]
13      private \DateTimeImmutable $updatedAt;
14
15      // ...
16}

The last piece of the puzzle is to implement the logic which will read these attributes and update the properties accordingly. This can be done using RunOpenCode\Metadata\MetadataReader class (that is, RunOpenCode\Component\Metadata\Contract\ReaderInterface, assuming you are following dependency inversion principle):

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace App\Doctrine\Listener;
 6
 7use App\Doctrine\Attribute\Timestampable;
 8use Doctrine\ORM\Event\LifecycleEventArgs;
 9use RunOpenCode\Component\Metadata\Contract\ReaderInterface;
10
11final readonly class TimestampableListener
12{
13     public function __construct(private ReaderInterface $reader)
14     {
15         // noop.
16     }
17
18     public function prePersist(LifecycleEventArgs $args): void
19     {
20         $entity = $args->getObject();
21         $when   = new \DateTimeImmutable('now');
22
23         $this->touch($entity, $when, Timestampable::CREATE, Timestampable::UPDATE);
24     }
25
26     public function preUpdate(LifecycleEventArgs $args): void
27     {
28         $entity = $args->getObject();
29         $when   = new \DateTimeImmutable('now');
30
31         $this->touch($entity, $when, Timestampable::UPDATE);
32     }
33
34     private function touch(object $entity, \DateTimeImmutable $when, string ...$on): void
35     {
36         $properties = $this->reader->properties($entity, Timestampable::class);
37
38         if (0 === \count($properties)) {
39             return;
40         }
41
42         foreach ($on as $condition) {
43             foreach ($properties as $property) {
44                 $attribute = $property->get(Timestampable::class);
45
46                 if ($condition !== $attribute->on) {
47                     continue;
48                 }
49
50                 $property->write($entity, $when);
51             }
52         }
53     }
54}