Skip to content

Commit

Permalink
fix: Allow uuid as path source in materialized path strategy
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
andreakeesys committed Nov 26, 2024
1 parent 090ce9a commit 0182d2e
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Tree/Mapping/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class Validator
'string',
'int',
'float',
'uuid',
];

/**
Expand Down
159 changes: 159 additions & 0 deletions tests/Gedmo/Tree/Fixture/MPCategoryUuid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> 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<int, self>
*
* @ORM\OneToMany(targetEntity="MPCategoryUuid", mappedBy="parent")
*/
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
private Collection $children;

/**
* @var Collection<int, Article>
*
* @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;
}
}
174 changes: 174 additions & 0 deletions tests/Gedmo/Tree/MaterializedPathUuidORMTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> 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 <[email protected]>
* @author Gediminas Morkevicius <[email protected]>
* @author Andrea Bergamasco <[email protected]>
*/
final class MaterializedPathUuidORMTest extends BaseTestCaseORM
{
/**
* @var array<string, mixed>
*/
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<int|string, int|string|UuidV4|null> $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();
}
}

0 comments on commit 0182d2e

Please sign in to comment.