Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ a release.
### Changed
- All: Removed the dollar sign from the generated cache ID for extension metadata to ensure only characters mandated by [PSR-6](https://www.php-fig.org/psr/psr-6/#definitions) are used, improving compatibility with caching implementations with strict character requirements (#2978)

### Fixed
- Tree: Fix TreeObjectHydrator compatibility with ORM 3 when parent property is defined before children property in entity

## [3.22.0] - 2025-12-13
### Added
- Support for Symfony 8
Expand Down
13 changes: 13 additions & 0 deletions src/Tree/Hydrator/ORM/TreeObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Gedmo\Exception\InvalidMappingException;
use Gedmo\Tool\ORM\Hydration\EntityManagerRetriever;
Expand Down Expand Up @@ -250,6 +251,18 @@ protected function getChildrenField($entityClass)

$associationMapping = $meta->getAssociationMapping($property->getName());

if ($associationMapping instanceof OneToManyAssociationMapping) {
if ($associationMapping->mappedBy !== $this->parentField) {
continue;
}

return $associationMapping->fieldName;
}

if (!isset($associationMapping['mappedBy'])) {
continue;
}

// Make sure the association is mapped by the parent property
if ($associationMapping['mappedBy'] !== $this->parentField) {
continue;
Expand Down
190 changes: 190 additions & 0 deletions tests/Gedmo/Tree/Fixture/RootCategoryReversed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> 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\NestedTreeRepository;
use Gedmo\Tree\Node;

/**
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*
* @Gedmo\Tree(type="nested")
*/
#[ORM\Entity(repositoryClass: NestedTreeRepository::class)]
#[Gedmo\Tree(type: 'nested')]
class RootCategoryReversed implements Node
{
/**
* @var Collection<int, self>
*
* @ORM\OneToMany(targetEntity="RootCategoryReversed", mappedBy="parent")
*/
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
protected $children;
/**
* @Gedmo\TreeParent
*
* @ORM\ManyToOne(targetEntity="RootCategoryReversed", 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 ?RootCategoryReversed $parent = null;

/**
* @var int|null
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private $id;

/**
* @ORM\Column(name="title", type="string", length=64)
*/
#[ORM\Column(name: 'title', type: Types::STRING, length: 64)]
private ?string $title = null;

/**
* @var int|null
*
* @Gedmo\TreeLeft
*
* @ORM\Column(name="lft", type="integer")
*/
#[ORM\Column(name: 'lft', type: Types::INTEGER)]
#[Gedmo\TreeLeft]
private $lft;

/**
* @var int|null
*
* @Gedmo\TreeRight
*
* @ORM\Column(name="rgt", type="integer")
*/
#[ORM\Column(name: 'rgt', type: Types::INTEGER)]
#[Gedmo\TreeRight]
private $rgt;

/**
* @var int|null
*
* @Gedmo\TreeRoot
*
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: Types::INTEGER)]
#[Gedmo\TreeRoot]
private $root;

/**
* @var int|null
*
* @Gedmo\TreeLevel(base=1)
*
* @ORM\Column(name="lvl", type="integer")
*/
#[ORM\Column(name: 'lvl', type: Types::INTEGER)]
#[Gedmo\TreeLevel(base: 1)]
private $level;

private ?Node $sibling = null;

public function __construct()
{
$this->children = new ArrayCollection();
}

public function getId(): ?int
{
return $this->id;
}

public function setTitle(?string $title): void
{
$this->title = $title;
}

public function getTitle(): ?string
{
return $this->title;
}

public function setParent(?self $parent = null): void
{
$this->parent = $parent;
}

public function getParent(): ?self
{
return $this->parent;
}

public function getRoot(): ?int
{
return $this->root;
}

public function getLeft(): ?int
{
return $this->lft;
}

public function getRight(): ?int
{
return $this->rgt;
}

public function getLevel(): ?int
{
return $this->level;
}

/**
* @return Collection<int, self>
*/
public function getChildren(): Collection
{
return $this->children;
}

/**
* @param Collection<int, self> $children
*/
public function setChildren(Collection $children): void
{
$this->children = $children;
}

public function setSibling(Node $node): void
{
$this->sibling = $node;
}

public function getSibling(): ?Node
{
return $this->sibling;
}
}
71 changes: 71 additions & 0 deletions tests/Gedmo/Tree/TreeObjectHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Gedmo\Tests\Tool\BaseTestCaseORM;
use Gedmo\Tests\Tree\Fixture\Category;
use Gedmo\Tests\Tree\Fixture\RootCategory;
use Gedmo\Tests\Tree\Fixture\RootCategoryReversed;
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
use Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator;
use Gedmo\Tree\TreeListener;
Expand Down Expand Up @@ -166,11 +167,52 @@ public function testMultipleRootNodesTreeHydration(): void
static::assertCount(2, $this->queryLogger->queries);
}

public function testFullTreeHydrationWithReversedFieldOrder(): void
{
$this->populateReversed();
$this->em->clear();

$this->queryLogger->reset();

$repo = $this->em->getRepository(RootCategoryReversed::class);

$result = $repo->createQueryBuilder('node')
->orderBy('node.lft', 'ASC')
->getQuery()
->setHint(Query::HINT_INCLUDE_META_COLUMNS, true)
->getResult('tree');

static::assertCount(1, $result);

$food = $result[0];
static::assertSame('Food', $food->getTitle());
static::assertCount(2, $food->getChildren());

$fruits = $food->getChildren()->get(0);
static::assertSame('Fruits', $fruits->getTitle());
static::assertCount(2, $fruits->getChildren());

$vegetables = $food->getChildren()->get(1);
static::assertSame('Vegetables', $vegetables->getTitle());
static::assertCount(0, $vegetables->getChildren());

$oranges = $fruits->getChildren()->get(0);
static::assertSame('Oranges', $oranges->getTitle());
static::assertCount(0, $oranges->getChildren());

$citrons = $fruits->getChildren()->get(1);
static::assertSame('Citrons', $citrons->getTitle());
static::assertCount(0, $citrons->getChildren());

static::assertCount(1, $this->queryLogger->queries);
}

protected function getUsedEntityFixtures(): array
{
return [
Category::class,
RootCategory::class,
RootCategoryReversed::class,
];
}

Expand Down Expand Up @@ -210,4 +252,33 @@ private function populate(): void

$this->em->flush();
}

private function populateReversed(): void
{
$repo = $this->em->getRepository(RootCategoryReversed::class);

$food = new RootCategoryReversed();
$food->setTitle('Food');

$fruits = new RootCategoryReversed();
$fruits->setTitle('Fruits');

$vegetables = new RootCategoryReversed();
$vegetables->setTitle('Vegetables');

$oranges = new RootCategoryReversed();
$oranges->setTitle('Oranges');

$citrons = new RootCategoryReversed();
$citrons->setTitle('Citrons');

$repo
->persistAsFirstChild($food)
->persistAsLastChildOf($fruits, $food)
->persistAsLastChildOf($vegetables, $food)
->persistAsLastChildOf($oranges, $fruits)
->persistAsLastChildOf($citrons, $fruits);

$this->em->flush();
}
}