Skip to content

Commit 5d80d9d

Browse files
andreakeesysphansys
authored andcommitted
fix: Allow uuid as path source in materialized path strategy
- Fixes a path validation error when using an Uuid as primary key in an entity and also as TreePathSource in the MaterializedPath strategy - Added a test to confirm the regression if 'uuid' is removed from the allowed types list in Validator.php
1 parent 7622db1 commit 5d80d9d

File tree

4 files changed

+332
-0
lines changed

4 files changed

+332
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ a release.
2323

2424
### Fixed
2525
- Fix regression with `doctrine/dbal` >= 4.0 that caused MariaDB to improperly attempt LONGTEXT casting in `TranslationWalker` (issue #2887)
26+
- Tree: allow usage of UuidV4 as path source with the materialized path strategy
2627

2728
## [3.17.1] - 2024-10-07
2829
### Fixed

src/Tree/Mapping/Validator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class Validator
6060
'string',
6161
'int',
6262
'float',
63+
'uuid',
6364
];
6465

6566
/**
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Doctrine Behavioral Extensions package.
7+
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Gedmo\Tests\Tree\Fixture;
13+
14+
use Doctrine\Common\Collections\ArrayCollection;
15+
use Doctrine\Common\Collections\Collection;
16+
use Doctrine\DBAL\Types\Types;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Gedmo\Mapping\Annotation as Gedmo;
19+
use Gedmo\Tree\Entity\Repository\MaterializedPathRepository;
20+
use Symfony\Component\Uid\UuidV4;
21+
22+
/**
23+
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository")
24+
*
25+
* @Gedmo\Tree(type="materializedPath")
26+
*/
27+
#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)]
28+
#[Gedmo\Tree(type: 'materializedPath')]
29+
class MPCategoryUuid
30+
{
31+
/**
32+
* @Gedmo\TreePathSource
33+
*
34+
* @ORM\Id
35+
* @ORM\Column(type="uuid")
36+
*/
37+
#[ORM\Id]
38+
#[ORM\Column(type: 'uuid')]
39+
#[Gedmo\TreePathSource]
40+
private UuidV4 $id;
41+
42+
/**
43+
* @Gedmo\TreePath
44+
*
45+
* @ORM\Column(name="path", type="string", length=3000, nullable=true)
46+
*/
47+
#[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)]
48+
#[Gedmo\TreePath]
49+
private ?string $path = null;
50+
51+
/**
52+
* @ORM\Column(name="title", type="string", length=64)
53+
*/
54+
#[ORM\Column(name: 'title', type: Types::STRING, length: 64)]
55+
private ?string $title = null;
56+
57+
/**
58+
* @Gedmo\TreeParent
59+
*
60+
* @ORM\ManyToOne(targetEntity="MPCategoryUuid", inversedBy="children")
61+
* @ORM\JoinColumns({
62+
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
63+
* })
64+
*/
65+
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
66+
#[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
67+
#[Gedmo\TreeParent]
68+
private ?MPCategoryUuid $parentId = null;
69+
70+
/**
71+
* @var int|null
72+
*
73+
* @Gedmo\TreeLevel
74+
*
75+
* @ORM\Column(name="lvl", type="integer", nullable=true)
76+
*/
77+
#[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)]
78+
#[Gedmo\TreeLevel]
79+
private $level;
80+
81+
/**
82+
* @var string|null
83+
*
84+
* @Gedmo\TreeRoot
85+
*
86+
* @ORM\Column(name="tree_root_value", type="string", nullable=true)
87+
*/
88+
#[ORM\Column(name: 'tree_root_value', type: Types::STRING, nullable: true)]
89+
#[Gedmo\TreeRoot]
90+
private $treeRootValue;
91+
92+
/**
93+
* @var Collection<int, self>
94+
*
95+
* @ORM\OneToMany(targetEntity="MPCategoryUuid", mappedBy="parent")
96+
*/
97+
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
98+
private Collection $children;
99+
100+
/**
101+
* @var Collection<int, Article>
102+
*
103+
* @ORM\OneToMany(targetEntity="Article", mappedBy="category")
104+
*/
105+
#[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')]
106+
private Collection $comments;
107+
108+
public function __construct()
109+
{
110+
$this->id = new UuidV4();
111+
$this->children = new ArrayCollection();
112+
$this->comments = new ArrayCollection();
113+
}
114+
115+
public function getId(): ?UuidV4
116+
{
117+
return $this->id;
118+
}
119+
120+
public function getTitle(): ?string
121+
{
122+
return $this->title;
123+
}
124+
125+
public function setTitle(?string $title): void
126+
{
127+
$this->title = $title;
128+
}
129+
130+
public function setParent(?self $parent = null): void
131+
{
132+
$this->parentId = $parent;
133+
}
134+
135+
public function getParent(): ?self
136+
{
137+
return $this->parentId;
138+
}
139+
140+
public function getPath(): ?string
141+
{
142+
return $this->path;
143+
}
144+
145+
public function setPath(?string $path): void
146+
{
147+
$this->path = $path;
148+
}
149+
150+
public function getLevel(): ?int
151+
{
152+
return $this->level;
153+
}
154+
155+
public function getTreeRootValue(): ?string
156+
{
157+
return $this->treeRootValue;
158+
}
159+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Doctrine Behavioral Extensions package.
7+
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Gedmo\Tests\Tree;
13+
14+
use Doctrine\Common\EventManager;
15+
use Gedmo\Tests\Tool\BaseTestCaseORM;
16+
use Gedmo\Tests\Tree\Fixture\MPCategoryUuid;
17+
use Gedmo\Tree\TreeListener;
18+
use Symfony\Component\Uid\UuidV4;
19+
20+
/**
21+
* These are tests for Tree behavior when using Uuid as primary key and TreePathSource.
22+
*
23+
* @author Gustavo Falco <[email protected]>
24+
* @author Gediminas Morkevicius <[email protected]>
25+
* @author Andrea Bergamasco <[email protected]>
26+
*/
27+
final class MaterializedPathUuidORMTest extends BaseTestCaseORM
28+
{
29+
/**
30+
* @var array<string, mixed>
31+
*/
32+
protected array $config;
33+
34+
protected TreeListener $listener;
35+
36+
protected function setUp(): void
37+
{
38+
parent::setUp();
39+
40+
$this->listener = new TreeListener();
41+
42+
$evm = new EventManager();
43+
$evm->addEventSubscriber($this->listener);
44+
45+
$this->getDefaultMockSqliteEntityManager($evm);
46+
47+
$meta = $this->em->getClassMetadata(MPCategoryUuid::class);
48+
$this->config = $this->listener->getConfiguration($this->em, $meta->getName());
49+
}
50+
51+
public function testInsertUpdateAndRemove(): void
52+
{
53+
// Insert
54+
$category = $this->createCategory();
55+
$category->setTitle('1');
56+
static::assertNotNull($category->getId());
57+
$category2 = $this->createCategory();
58+
$category2->setTitle('2');
59+
static::assertNotNull($category2->getId());
60+
$category3 = $this->createCategory();
61+
$category3->setTitle('3');
62+
static::assertNotNull($category3->getId());
63+
$category4 = $this->createCategory();
64+
$category4->setTitle('4');
65+
static::assertNotNull($category4->getId());
66+
67+
$category2->setParent($category);
68+
$category3->setParent($category2);
69+
70+
$this->em->persist($category4);
71+
$this->em->persist($category3);
72+
$this->em->persist($category2);
73+
$this->em->persist($category);
74+
$this->em->flush();
75+
76+
$this->em->refresh($category);
77+
$this->em->refresh($category2);
78+
$this->em->refresh($category3);
79+
$this->em->refresh($category4);
80+
81+
static::assertSame($this->generatePath([$category->getId()]), $category->getPath());
82+
static::assertSame($this->generatePath([$category->getId(), $category2->getId()]), $category2->getPath());
83+
static::assertSame($this->generatePath([$category->getId(), $category2->getId(), $category3->getId()]), $category3->getPath());
84+
static::assertSame($this->generatePath([$category4->getId()]), $category4->getPath());
85+
static::assertSame(1, $category->getLevel());
86+
static::assertSame(2, $category2->getLevel());
87+
static::assertSame(3, $category3->getLevel());
88+
static::assertSame(1, $category4->getLevel());
89+
90+
static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue());
91+
static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue());
92+
static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue());
93+
static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue());
94+
95+
// Update
96+
$category2->setParent(null);
97+
98+
$this->em->persist($category2);
99+
$this->em->flush();
100+
101+
$this->em->refresh($category);
102+
$this->em->refresh($category2);
103+
$this->em->refresh($category3);
104+
105+
static::assertSame($this->generatePath([$category->getId()]), $category->getPath());
106+
static::assertSame($this->generatePath([$category2->getId()]), $category2->getPath());
107+
static::assertSame($this->generatePath([$category2->getId(), $category3->getId()]), $category3->getPath());
108+
static::assertSame(1, $category->getLevel());
109+
static::assertSame(1, $category2->getLevel());
110+
static::assertSame(2, $category3->getLevel());
111+
static::assertSame(1, $category4->getLevel());
112+
113+
static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue());
114+
static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue());
115+
static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue());
116+
static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue());
117+
118+
// Remove
119+
$this->em->remove($category);
120+
$this->em->remove($category2);
121+
$this->em->flush();
122+
123+
$result = $this->em->createQueryBuilder()->select('c')->from(MPCategoryUuid::class, 'c')->getQuery()->getResult();
124+
125+
$firstResult = $result[0];
126+
127+
static::assertCount(1, $result);
128+
static::assertSame('4', $firstResult->getTitle());
129+
static::assertSame(1, $firstResult->getLevel());
130+
static::assertSame($this->getTreeRootValueOfRootNode($firstResult), $firstResult->getTreeRootValue());
131+
}
132+
133+
protected function getUsedEntityFixtures(): array
134+
{
135+
return [
136+
MPCategoryUuid::class,
137+
];
138+
}
139+
140+
private function createCategory(): MPCategoryUuid
141+
{
142+
return new MPCategoryUuid();
143+
}
144+
145+
/**
146+
* @param array<int|string, int|string|UuidV4|null> $sources
147+
*/
148+
private function generatePath(array $sources): string
149+
{
150+
$path = $this->config['path_starts_with_separator']
151+
? $this->config['path_separator']
152+
: '';
153+
154+
$path .= implode($this->config['path_separator'], $sources);
155+
156+
$path .= $this->config['path_ends_with_separator']
157+
? $this->config['path_separator']
158+
: '';
159+
160+
return $path;
161+
}
162+
163+
private function getTreeRootValueOfRootNode(MPCategoryUuid $category): string
164+
{
165+
while (null !== $category->getParent()) {
166+
$category = $category->getParent();
167+
}
168+
169+
return $category->getTreeRootValue();
170+
}
171+
}

0 commit comments

Comments
 (0)