From 914c741578a83dc5a02c2d838846634863d4ec13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 24 Sep 2025 21:48:15 +0200 Subject: [PATCH] Add support for motive descendants in filters and improve Motive entity - Added `getDescendants` method to `Motive` entity to retrieve all descendants recursively. - Updated `TicketACLAwareRepository` to include descendants when filtering by motives. - Updated API specification to clarify that motive descendants are included in query filters. - Added unit tests for `Motive` entity to validate descendant logic. --- .../ChillTicketBundle/chill.api.specs.yaml | 2 +- .../ChillTicketBundle/src/Entity/Motive.php | 21 +++++++ .../Repository/TicketACLAwareRepository.php | 5 +- .../tests/Entity/MotiveTest.php | 63 +++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillTicketBundle/tests/Entity/MotiveTest.php diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index e2eea460b..f0255fb19 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -112,7 +112,7 @@ paths: - no - name: byMotives in: query - description: the motives of the ticket + description: the motives of the ticket. All the descendants of the motive are taken into account. required: false style: form explode: false diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Motive.php b/src/Bundle/ChillTicketBundle/src/Entity/Motive.php index 0ed2f3d5d..08e300d97 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Motive.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Motive.php @@ -195,4 +195,25 @@ class Motive { return $this->parent; } + + /** + * Get the descendants of the current entity. + * + * This method collects all descendant entities recursively, starting from the current entity + * and including all of its children and their descendants. + * + * @return ReadableCollection&Selectable A collection containing the current entity and all its descendants + */ + public function getDescendants(): ReadableCollection&Selectable + { + $collection = new ArrayCollection([$this]); + + foreach ($this->getChildren() as $child) { + foreach ($child->getDescendants() as $descendant) { + $collection->add($descendant); + } + } + + return $collection; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php b/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php index 222199666..ad8fa1387 100644 --- a/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php +++ b/src/Bundle/ChillTicketBundle/src/Repository/TicketACLAwareRepository.php @@ -113,10 +113,11 @@ final readonly class TicketACLAwareRepository implements TicketACLAwareRepositor if (array_key_exists('byMotives', $params)) { $byMotives = $qb->expr()->orX(); foreach ($params['byMotives'] as $motive) { + $motivesWithDescendants = $motive->getDescendants()->toArray(); $byMotives->add( $qb->expr()->exists(sprintf( 'SELECT 1 FROM %s tp_motive_%d WHERE tp_motive_%d.ticket = t - AND tp_motive_%d.motive = :motive_%d AND tp_motive_%d.endDate IS NULL + AND tp_motive_%d.motive IN (:motives_%d) AND tp_motive_%d.endDate IS NULL ', MotiveHistory::class, ++$i, @@ -126,7 +127,7 @@ final readonly class TicketACLAwareRepository implements TicketACLAwareRepositor $i, )) ); - $qb->setParameter(sprintf('motive_%d', $i), $motive); + $qb->setParameter(sprintf('motives_%d', $i), $motivesWithDescendants); } $qb->andWhere($byMotives); } diff --git a/src/Bundle/ChillTicketBundle/tests/Entity/MotiveTest.php b/src/Bundle/ChillTicketBundle/tests/Entity/MotiveTest.php new file mode 100644 index 000000000..c9bba7f6f --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Entity/MotiveTest.php @@ -0,0 +1,63 @@ +setLabel(['fr' => 'Feuille']); + + $collection = $leaf->getDescendants(); + + self::assertCount(1, $collection); + self::assertSame($leaf, $collection->first()); + self::assertContains($leaf, $collection->toArray()); + } + + public function testGetWithDescendantsReturnsSelfAndAllDescendants(): void + { + $parent = new Motive(); + $parent->setLabel(['fr' => 'Parent']); + + $childA = new Motive(); + $childA->setLabel(['fr' => 'Enfant A']); + $childA->setParent($parent); + + $childB = new Motive(); + $childB->setLabel(['fr' => 'Enfant B']); + $childB->setParent($parent); + + $grandChildA1 = new Motive(); + $grandChildA1->setLabel(['fr' => 'Petit-enfant A1']); + $grandChildA1->setParent($childA); + + $descendants = $parent->getDescendants(); + $asArray = $descendants->toArray(); + + // It should contain the parent itself, both children and the grand child + self::assertCount(4, $descendants, 'Expected parent + 2 children + 1 grandchild'); + self::assertContains($parent, $asArray); + self::assertContains($childA, $asArray); + self::assertContains($childB, $asArray); + self::assertContains($grandChildA1, $asArray); + } +}