|
| 1 | +# ltree type usage |
| 2 | + |
| 3 | +## Requirements |
| 4 | + |
| 5 | +The `ltree` data type requires enabling the [`ltree` module](https://www.postgresql.org/docs/current/ltree.html) |
| 6 | +in PostgreSQL. |
| 7 | + |
| 8 | +```sql |
| 9 | +CREATE EXTENSION IF NOT EXISTS ltree; |
| 10 | +``` |
| 11 | + |
| 12 | +For [Symfony](https://symfony.com/), |
| 13 | +customize the migration introducing the `ltree` field by adding this line |
| 14 | +at the beginning of the `up()` method: |
| 15 | + |
| 16 | +```php |
| 17 | +$this->addSql('CREATE EXTENSION IF NOT EXISTS ltree WITH SCHEMA public'); |
| 18 | +``` |
| 19 | + |
| 20 | +## Usage |
| 21 | + |
| 22 | +An example implementation (for a Symfony project) is: |
| 23 | + |
| 24 | +```php |
| 25 | +<?php |
| 26 | + |
| 27 | +declare(strict_types=1); |
| 28 | + |
| 29 | +namespace App\Entity; |
| 30 | + |
| 31 | +use App\EventListener\MyEntityListener; |
| 32 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Ltree; |
| 33 | +use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\LtreeInterface; |
| 34 | +use Doctrine\Common\Collections\ArrayCollection; |
| 35 | +use Doctrine\Common\Collections\Collection; |
| 36 | +use Doctrine\DBAL\Types\Types; |
| 37 | +use Doctrine\ORM\Mapping as ORM; |
| 38 | +use Symfony\Bridge\Doctrine\Types\UuidType; |
| 39 | +use Symfony\Component\Uid\Uuid; |
| 40 | + |
| 41 | +#[ORM\Entity()] |
| 42 | +#[ORM\Index(columns: ['path'])] |
| 43 | +#[ORM\EntityListeners([MyEntityListener::class])] |
| 44 | +class MyEntity implements \Stringable |
| 45 | +{ |
| 46 | + #[ORM\Column(type: UuidType::NAME)] |
| 47 | + #[ORM\GeneratedValue(strategy: 'NONE')] |
| 48 | + #[ORM\Id()] |
| 49 | + private Uuid $id; |
| 50 | + |
| 51 | + #[ORM\Column(type: 'ltree', unique: true)] |
| 52 | + private LtreeInterface $path; |
| 53 | + |
| 54 | + /** |
| 55 | + * @var Collection<array-key,MyEntity> $children |
| 56 | + */ |
| 57 | + #[ORM\OneToMany(targetEntity: MyEntity::class, mappedBy: 'parent')] |
| 58 | + private Collection $children; |
| 59 | + |
| 60 | + /** |
| 61 | + * @SuppressWarnings("PHPMD.StaticAccess") |
| 62 | + */ |
| 63 | + public function __construct( |
| 64 | + #[ORM\Column(unique: true, length: 128)] |
| 65 | + private string $name, |
| 66 | + |
| 67 | + #[ORM\ManyToOne(targetEntity: MyEntity::class, inversedBy: 'children')] |
| 68 | + private ?MyEntity $parent, |
| 69 | + ) { |
| 70 | + $this->id = Uuid::v7(); |
| 71 | + $this->children = new ArrayCollection(); |
| 72 | + |
| 73 | + $this->path = Ltree::fromString($this->id->toBase58()); |
| 74 | + if ($parent instanceof MyEntity) { |
| 75 | + // Initialize the path using the parent. |
| 76 | + $this->setParent($parent); |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + #[\Override] |
| 81 | + public function __toString(): string |
| 82 | + { |
| 83 | + return $this->name; |
| 84 | + } |
| 85 | + |
| 86 | + public function getId(): Uuid |
| 87 | + { |
| 88 | + return $this->id; |
| 89 | + } |
| 90 | + |
| 91 | + public function getParent(): ?MyEntity |
| 92 | + { |
| 93 | + return $this->parent; |
| 94 | + } |
| 95 | + |
| 96 | + public function getName(): string |
| 97 | + { |
| 98 | + return $this->name; |
| 99 | + } |
| 100 | + |
| 101 | + public function getPath(): LtreeInterface |
| 102 | + { |
| 103 | + return $this->path; |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * @return Collection<array-key,MyEntity> |
| 108 | + */ |
| 109 | + public function getChildren(): Collection |
| 110 | + { |
| 111 | + return $this->children; |
| 112 | + } |
| 113 | + |
| 114 | + public function setName(string $name): void |
| 115 | + { |
| 116 | + $this->name = $name; |
| 117 | + } |
| 118 | + |
| 119 | + public function setParent(MyEntity $parent): void |
| 120 | + { |
| 121 | + if ($parent->getId()->equals($this->id)) { |
| 122 | + throw new \InvalidArgumentException("Parent MyEntity can't be self"); |
| 123 | + } |
| 124 | + |
| 125 | + $this->parent = $parent; |
| 126 | + |
| 127 | + // Use createLeaf() to create a new Ltree instance |
| 128 | + // with the parent's path and the current entity's ID. |
| 129 | + $this->path = $parent->getPath()->createLeaf($this->id->toBase58()); |
| 130 | + } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +⚠️ **Important**: Changing an entity's parent requires to cascade the change |
| 135 | +to all its children. |
| 136 | +This is not handled automatically by Doctrine. |
| 137 | +Implement a [preUpdate](https://www.doctrine-project.org/projects/doctrine-orm/en/3.3/reference/events.html#reference-events-pre-update) |
| 138 | +[Doctrine Entity Listener](https://www.doctrine-project.org/projects/doctrine-orm/en/3.3/reference/events.html#entity-listeners-class) |
| 139 | +([Doctrine Entity Listener @ Symfony](https://symfony.com/doc/current/doctrine/events.html#doctrine-entity-listeners)) |
| 140 | +to handle updating the `path` column of the updated entity children |
| 141 | +when the `path` is present in the changed fields: |
| 142 | + |
| 143 | +```php |
| 144 | +<?php |
| 145 | + |
| 146 | +namespace App\EventListener; |
| 147 | + |
| 148 | +use App\Entity\MyEntity; |
| 149 | +use Doctrine\ORM\Event\PreUpdateEventArgs; |
| 150 | +use Doctrine\ORM\Mapping as ORM; |
| 151 | + |
| 152 | +final readonly class MyEntityListener |
| 153 | +{ |
| 154 | + #[ORM\PreUpdate] |
| 155 | + public function preUpdate(MyEntity $entity, PreUpdateEventArgs $eventArgs): void |
| 156 | + { |
| 157 | + if ($eventArgs->hasChangedField('path')) { |
| 158 | + foreach($entity->getChildren() as $child) { |
| 159 | + $child->setParent($myEntity); |
| 160 | + } |
| 161 | + } |
| 162 | + } |
| 163 | +} |
| 164 | +``` |
0 commit comments