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
7 changes: 7 additions & 0 deletions src/Tree/Hydrator/ORM/TreeObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ protected function getChildrenField($entityClass)

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

// ORM 3 mapping objects implement ArrayAccess, so both ORM 2 arrays
// and ORM 3 objects go through the same path. Owning-side mappings (the parent
// ManyToOne) have no `mappedBy`, hence the isset() guard.
if (!isset($associationMapping['mappedBy'])) {
continue;
}

// Make sure the association is mapped by the parent property
if ($associationMapping['mappedBy'] !== $this->parentField) {
continue;
Expand Down
191 changes: 191 additions & 0 deletions tests/Gedmo/Tree/Fixture/RootCategoryReversed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?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
{
/**
* @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 Collection<int, self>
*
* @ORM\OneToMany(targetEntity="RootCategoryReversed", mappedBy="parent")
*/
#[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
private $children;

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