Skip to content

Commit 85195de

Browse files
committed
docs: add an implementation example for the ltree type
1 parent 5d1a8d5 commit 85195de

File tree

2 files changed

+168
-1
lines changed

2 files changed

+168
-1
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ This package provides comprehensive Doctrine support for PostgreSQL features:
5858
- **Range Types**
5959
- Date and time ranges (`daterange`, `tsrange`, `tstzrange`)
6060
- Numeric ranges (`numrange`, `int4range`, `int8range`)
61-
- [ltree](https://www.postgresql.org/docs/current/ltree.html) data type (`ltree`)
61+
- **Hierarchical Types**
62+
- [ltree](https://www.postgresql.org/docs/current/ltree.html) (`ltree`)
6263

6364
### PostgreSQL Operators
6465
- **Array Operations**
@@ -115,6 +116,8 @@ composer require martin-georgiev/postgresql-for-doctrine
115116
## 💡 Usage Examples
116117
See our [Common Use Cases and Examples](docs/USE-CASES-AND-EXAMPLES.md) for detailed code samples.
117118

119+
See our [ltree type usage guide](docs/LTREE-TYPE.md) for an example of how to use the `ltree` type.
120+
118121
## 🧪 Testing
119122

120123
### Unit Tests

docs/LTREE-TYPE.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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

Comments
 (0)