From 0182d2e858bcb0e0ffa59b6e32c8bbbe78f4019b Mon Sep 17 00:00:00 2001 From: Andrea Bergamasco Date: Tue, 26 Nov 2024 09:08:56 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + src/Tree/Mapping/Validator.php | 1 + tests/Gedmo/Tree/Fixture/MPCategoryUuid.php | 159 ++++++++++++++++ .../Tree/MaterializedPathUuidORMTest.php | 174 ++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 tests/Gedmo/Tree/Fixture/MPCategoryUuid.php create mode 100644 tests/Gedmo/Tree/MaterializedPathUuidORMTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b829d7119..f0668f8c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ a release. ### Fixed - Fix regression with `doctrine/dbal` >= 4.0 that caused MariaDB to improperly attempt LONGTEXT casting in `TranslationWalker` (issue #2887) +- Tree: allow usage of UuidV4 as path source with the materialized path strategy ## [3.17.1] - 2024-10-07 ### Fixed diff --git a/src/Tree/Mapping/Validator.php b/src/Tree/Mapping/Validator.php index f2d0fcd9fa..20619b082d 100644 --- a/src/Tree/Mapping/Validator.php +++ b/src/Tree/Mapping/Validator.php @@ -60,6 +60,7 @@ class Validator 'string', 'int', 'float', + 'uuid', ]; /** diff --git a/tests/Gedmo/Tree/Fixture/MPCategoryUuid.php b/tests/Gedmo/Tree/Fixture/MPCategoryUuid.php new file mode 100644 index 0000000000..b0b517da78 --- /dev/null +++ b/tests/Gedmo/Tree/Fixture/MPCategoryUuid.php @@ -0,0 +1,159 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree\Fixture; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; +use Symfony\Component\Uid\UuidV4; + +/** + * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository") + * + * @Gedmo\Tree(type="materializedPath") + */ +#[ORM\Entity(repositoryClass: MaterializedPathRepository::class)] +#[Gedmo\Tree(type: 'materializedPath')] +class MPCategoryUuid +{ + /** + * @Gedmo\TreePathSource + * + * @ORM\Id + * @ORM\Column(type="uuid") + */ + #[ORM\Id] + #[ORM\Column(type: 'uuid')] + #[Gedmo\TreePathSource] + private UuidV4 $id; + + /** + * @Gedmo\TreePath + * + * @ORM\Column(name="path", type="string", length=3000, nullable=true) + */ + #[ORM\Column(name: 'path', type: Types::STRING, length: 3000, nullable: true)] + #[Gedmo\TreePath] + private ?string $path = null; + + /** + * @ORM\Column(name="title", type="string", length=64) + */ + #[ORM\Column(name: 'title', type: Types::STRING, length: 64)] + private ?string $title = null; + + /** + * @Gedmo\TreeParent + * + * @ORM\ManyToOne(targetEntity="MPCategoryUuid", inversedBy="children") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * }) + */ + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[Gedmo\TreeParent] + private ?MPCategoryUuid $parentId = null; + + /** + * @var int|null + * + * @Gedmo\TreeLevel + * + * @ORM\Column(name="lvl", type="integer", nullable=true) + */ + #[ORM\Column(name: 'lvl', type: Types::INTEGER, nullable: true)] + #[Gedmo\TreeLevel] + private $level; + + /** + * @var string|null + * + * @Gedmo\TreeRoot + * + * @ORM\Column(name="tree_root_value", type="string", nullable=true) + */ + #[ORM\Column(name: 'tree_root_value', type: Types::STRING, nullable: true)] + #[Gedmo\TreeRoot] + private $treeRootValue; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="MPCategoryUuid", mappedBy="parent") + */ + #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Article", mappedBy="category") + */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')] + private Collection $comments; + + public function __construct() + { + $this->id = new UuidV4(); + $this->children = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } + + public function getId(): ?UuidV4 + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function setParent(?self $parent = null): void + { + $this->parentId = $parent; + } + + public function getParent(): ?self + { + return $this->parentId; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): void + { + $this->path = $path; + } + + public function getLevel(): ?int + { + return $this->level; + } + + public function getTreeRootValue(): ?string + { + return $this->treeRootValue; + } +} diff --git a/tests/Gedmo/Tree/MaterializedPathUuidORMTest.php b/tests/Gedmo/Tree/MaterializedPathUuidORMTest.php new file mode 100644 index 0000000000..b197f935e9 --- /dev/null +++ b/tests/Gedmo/Tree/MaterializedPathUuidORMTest.php @@ -0,0 +1,174 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\Tree; + +use Doctrine\Common\EventManager; +use Gedmo\Tests\Tool\BaseTestCaseORM; +use Gedmo\Tests\Tree\Fixture\MPCategoryUuid; +use Gedmo\Tree\TreeListener; +use Symfony\Component\Uid\UuidV4; + +/** + * These are tests for Tree behavior when using Uuid as primary key and TreePathSource. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius + * @author Andrea Bergamasco + */ +final class MaterializedPathUuidORMTest extends BaseTestCaseORM +{ + /** + * @var array + */ + protected $config; + + /** + * @var TreeListener + */ + protected $listener; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = new TreeListener(); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->listener); + + $this->getDefaultMockSqliteEntityManager($evm); + + $meta = $this->em->getClassMetadata(MPCategoryUuid::class); + $this->config = $this->listener->getConfiguration($this->em, $meta->getName()); + } + + public function testInsertUpdateAndRemove(): void + { + // Insert + $category = $this->createCategory(); + $category->setTitle('1'); + static::assertNotNull($category->getId()); + $category2 = $this->createCategory(); + $category2->setTitle('2'); + static::assertNotNull($category2->getId()); + $category3 = $this->createCategory(); + $category3->setTitle('3'); + static::assertNotNull($category3->getId()); + $category4 = $this->createCategory(); + $category4->setTitle('4'); + static::assertNotNull($category4->getId()); + + $category2->setParent($category); + $category3->setParent($category2); + + $this->em->persist($category4); + $this->em->persist($category3); + $this->em->persist($category2); + $this->em->persist($category); + $this->em->flush(); + + $this->em->refresh($category); + $this->em->refresh($category2); + $this->em->refresh($category3); + $this->em->refresh($category4); + + static::assertSame($this->generatePath([$category->getId()]), $category->getPath()); + static::assertSame($this->generatePath([$category->getId(), $category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath([$category->getId(), $category2->getId(), $category3->getId()]), $category3->getPath()); + static::assertSame($this->generatePath([$category4->getId()]), $category4->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(2, $category2->getLevel()); + static::assertSame(3, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue()); + + // Update + $category2->setParent(null); + + $this->em->persist($category2); + $this->em->flush(); + + $this->em->refresh($category); + $this->em->refresh($category2); + $this->em->refresh($category3); + + static::assertSame($this->generatePath([$category->getId()]), $category->getPath()); + static::assertSame($this->generatePath([$category2->getId()]), $category2->getPath()); + static::assertSame($this->generatePath([$category2->getId(), $category3->getId()]), $category3->getPath()); + static::assertSame(1, $category->getLevel()); + static::assertSame(1, $category2->getLevel()); + static::assertSame(2, $category3->getLevel()); + static::assertSame(1, $category4->getLevel()); + + static::assertSame($this->getTreeRootValueOfRootNode($category), $category->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category2), $category2->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category3), $category3->getTreeRootValue()); + static::assertSame($this->getTreeRootValueOfRootNode($category4), $category4->getTreeRootValue()); + + // Remove + $this->em->remove($category); + $this->em->remove($category2); + $this->em->flush(); + + $result = $this->em->createQueryBuilder()->select('c')->from(MPCategoryUuid::class, 'c')->getQuery()->getResult(); + + $firstResult = $result[0]; + + static::assertCount(1, $result); + static::assertSame('4', $firstResult->getTitle()); + static::assertSame(1, $firstResult->getLevel()); + static::assertSame($this->getTreeRootValueOfRootNode($firstResult), $firstResult->getTreeRootValue()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + MPCategoryUuid::class, + ]; + } + + private function createCategory(): MPCategoryUuid + { + return new MPCategoryUuid(); + } + + /** + * @param array $sources + */ + private function generatePath(array $sources): string + { + $path = $this->config['path_starts_with_separator'] + ? $this->config['path_separator'] + : ''; + + $path .= implode($this->config['path_separator'], $sources); + + $path .= $this->config['path_ends_with_separator'] + ? $this->config['path_separator'] + : ''; + + return $path; + } + + private function getTreeRootValueOfRootNode(MPCategoryUuid $category): string + { + while (null !== $category->getParent()) { + $category = $category->getParent(); + } + + return $category->getTreeRootValue(); + } +}