Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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;
}

Comment thread
PhilDaiguille marked this conversation as resolved.
// 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;
Comment thread
PhilDaiguille marked this conversation as resolved.
Outdated

/**
* @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();
}
}