Skip to content

[Tree] Multiple event listeners initialization problem #2936

Open
@secit-pl

Description

@secit-pl

Environment

Package

show

name : gedmo/doctrine-extensions descrip. : Doctrine behavioral extensions keywords : Blameable, behaviors, doctrine, extensions, gedmo, loggable, nestedset, odm, orm, sluggable, sortable, timestampable, translatable, tree, uploadable versions : * v3.19.0 latest : v3.19.0 type : library license : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText homepage : http://gediminasm.org/ source : [git] https://github.com/doctrine-extensions/DoctrineExtensions.git 5b0b8a4 dist : [zip] https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/5b0b8a442d19e6701ae64535dc08f7944e2895d2 5b0b8a4 path : /Users/suriv/Sites/localhost/speed-champions-2025/vendor/gedmo/doctrine-extensions names : gedmo/doctrine-extensions

support
email : [email protected]
issues : https://github.com/doctrine-extensions/DoctrineExtensions/issues
source : https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.19.0
wiki : https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc

autoload
psr-4
Gedmo\ => src/

requires
behat/transliterator ^1.2
doctrine/collections ^1.2 || ^2.0
doctrine/deprecations ^1.0
doctrine/event-manager ^1.2 || ^2.0
doctrine/persistence ^2.2 || ^3.0 || ^4.0
php ^7.4 || ^8.0
psr/cache ^1 || ^2 || ^3
psr/clock ^1
symfony/cache ^5.4 || ^6.0 || ^7.0

requires (dev)
doctrine/annotations ^1.13 || ^2.0
doctrine/cache ^1.11 || ^2.0
doctrine/common ^2.13 || ^3.0
doctrine/dbal ^3.7 || ^4.0
doctrine/doctrine-bundle ^2.3
doctrine/mongodb-odm ^2.3
doctrine/orm ^2.20 || ^3.3
friendsofphp/php-cs-fixer ^3.70
nesbot/carbon ^2.71 || ^3.0
phpstan/phpstan ^2.1.1
phpstan/phpstan-doctrine ^2.0.1
phpstan/phpstan-phpunit ^2.0.3
phpunit/phpunit ^9.6
rector/rector ^2.0.6
symfony/console ^5.4 || ^6.0 || ^7.0
symfony/doctrine-bridge ^5.4 || ^6.0 || ^7.0
symfony/phpunit-bridge ^6.0 || ^7.0
symfony/uid ^5.4 || ^6.0 || ^7.0
symfony/yaml ^5.4 || ^6.0 || ^7.0

suggests
doctrine/mongodb-odm to use the extensions with the MongoDB ODM
doctrine/orm to use the extensions with the ORM

conflicts
doctrine/annotations <1.13 || >=3.0
doctrine/common <2.13 || >=4.0
doctrine/dbal <3.7 || >=5.0
doctrine/mongodb-odm <2.3 || >=3.0
doctrine/orm <2.20 || >=3.0 <3.3 || >=4.0

Doctrine packages

Not related to Doctrine

Symfony packages

Probably all Symfony 7.x versions (tested on 7.0.0, 7.0.2, 7.0.8, 7.1.11, 7.2.4)

show

Direct dependencies required in composer.json: symfony/asset v7.0.8 v7.2.0 Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files symfony/browser-kit v7.0.8 v7.2.4 Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically symfony/console v7.0.10 v7.2.1 Eases the creation of beautiful and testable command line interfaces symfony/css-selector v7.0.8 v7.2.0 Converts CSS selectors to XPath expressions symfony/debug-bundle v7.0.8 v7.2.0 Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework symfony/doctrine-messenger v7.0.9 v7.2.3 Symfony Doctrine Messenger Bridge symfony/dotenv v7.0.10 v7.2.0 Registers environment variables from a .env file symfony/expression-language v7.0.8 v7.2.0 Provides an engine that can compile and evaluate expressions symfony/flex v2.5.0 v2.5.0 Composer plugin for Symfony symfony/form v7.0.10 v7.2.4 Allows to easily create, process and reuse HTML forms symfony/framework-bundle v7.0.10 v7.2.4 Provides a tight integration between Symfony components and the Symfony full-stack framework symfony/html-sanitizer v7.0.8 v7.2.3 Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM. symfony/http-client v7.0.10 v7.2.4 Provides powerful methods to fetch HTTP resources synchronously or asynchronously symfony/intl v7.0.8 v7.2.0 Provides access to the localization data of the ICU library symfony/lock v7.0.8 v7.2.4 Creates and manages locks, a mechanism to provide exclusive access to a shared resource symfony/mailer v7.0.9 v7.2.3 Helps sending emails symfony/maker-bundle v1.62.1 v1.62.1 Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code. symfony/mime v7.0.9 v7.2.4 Allows manipulating MIME messages symfony/monolog-bundle v3.10.0 v3.10.0 Symfony MonologBundle symfony/notifier v7.0.9 v7.2.0 Sends notifications via one or more channels (email, SMS, ...) symfony/one-signal-notifier v7.0.8 v7.2.0 Symfony OneSignal Notifier Bridge symfony/phpunit-bridge v7.2.0 v7.2.0 Provides utilities for PHPUnit, especially user deprecation notices management symfony/process v7.0.8 v7.2.4 Executes commands in sub-processes symfony/property-access v7.0.8 v7.2.3 Provides functions to read and write from/to an object or array using a simple string notation symfony/property-info v7.0.10 v7.2.3 Extracts information about PHP class' properties using metadata of popular sources symfony/rate-limiter v7.0.8 v7.2.0 Provides a Token Bucket implementation to rate limit input and output in your application symfony/runtime v7.0.8 v7.2.3 Enables decoupling PHP applications from global state symfony/security-bundle v7.0.10 v7.2.3 Provides a tight integration of the Security component into the Symfony full-stack framework symfony/serializer v7.0.10 v7.2.4 Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON. symfony/smsapi-notifier v7.0.8 v7.2.0 Symfony Smsapi Notifier Bridge symfony/stopwatch v7.0.8 v7.2.4 Provides a way to profile code symfony/string v7.0.10 v7.2.0 Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way symfony/translation v7.0.10 v7.2.4 Provides tools to internationalize your application symfony/twig-bundle v7.0.8 v7.2.0 Provides a tight integration of Twig into the Symfony full-stack framework symfony/uid v7.0.8 v7.2.0 Provides an object-oriented API to generate and represent UIDs symfony/validator v7.0.10 v7.2.4 Provides tools to validate values symfony/var-exporter v7.0.9 v7.2.4 Allows exporting any serializable PHP data structure to plain PHP code symfony/web-link v7.0.8 v7.2.0 Manages links between resources symfony/web-profiler-bundle v7.0.10 v7.2.4 Provides a development tool that gives detailed information about the execution of any request symfony/webpack-encore-bundle v2.2.0 v2.2.0 Integration of your Symfony app with Webpack Encore symfony/workflow v7.0.8 v7.2.0 Provides tools for managing a workflow or finite state machine symfony/yaml v7.0.8 v7.2.3 Loads and dumps YAML files

Transitive dependencies not required in composer.json:
symfony/cache v7.0.10 v7.2.4 Provides extended PSR-6, PSR-16 (and tags) implementations
symfony/cache-contracts v3.5.1 v3.5.1 Generic abstractions related to caching
symfony/clock v7.0.8 v7.2.0 Decouples applications from the system clock
symfony/config v7.0.8 v7.2.3 Helps you find, load, combine, autofill and validate configuration values of any kind
symfony/dependency-injection v7.0.10 v7.2.4 Allows you to standardize and centralize the way objects are constructed in your application
symfony/deprecation-contracts v3.5.1 v3.5.1 A generic function and convention to trigger deprecation notices
symfony/doctrine-bridge v7.0.10 v7.2.4 Provides integration for Doctrine with various Symfony components
symfony/dom-crawler v7.0.8 v7.2.4 Eases DOM navigation for HTML and XML documents
symfony/error-handler v7.0.10 v7.2.4 Provides tools to manage errors and ease debugging PHP code
symfony/event-dispatcher v7.0.8 v7.2.0 Provides tools that allow your application components to communicate with each other by dispatching events and listening to them
symfony/event-dispatcher-contracts v3.5.1 v3.5.1 Generic abstractions related to dispatching event
symfony/filesystem v7.0.9 v7.2.0 Provides basic utilities for the filesystem
symfony/finder v7.0.10 v7.2.2 Finds files and directories via an intuitive fluent interface
symfony/http-client-contracts v3.5.2 v3.5.2 Generic abstractions related to HTTP clients
symfony/http-foundation v7.0.10 v7.2.3 Defines an object-oriented layer for the HTTP specification
symfony/http-kernel v7.0.10 v7.2.4 Provides a structured process for converting a Request into a Response
symfony/messenger v7.0.10 v7.2.4 Helps applications send and receive messages to/from other applications or via message queues
symfony/monolog-bridge v7.0.8 v7.2.0 Provides integration for Monolog with various Symfony components
symfony/options-resolver v7.0.8 v7.2.0 Provides an improved replacement for the array_replace PHP function
symfony/password-hasher v7.0.8 v7.2.0 Provides password hashing utilities
symfony/polyfill-intl-grapheme v1.31.0 v1.31.0 Symfony polyfill for intl's grapheme_* functions
symfony/polyfill-intl-icu v1.31.0 v1.31.0 Symfony polyfill for intl's ICU-related data and classes
symfony/polyfill-intl-idn v1.31.0 v1.31.0 Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions
symfony/polyfill-intl-normalizer v1.31.0 v1.31.0 Symfony polyfill for intl's Normalizer class and related functions
symfony/polyfill-mbstring v1.31.0 v1.31.0 Symfony polyfill for the Mbstring extension
symfony/polyfill-php83 v1.31.0 v1.31.0 Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions
symfony/polyfill-php84 v1.31.0 v1.31.0 Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions
symfony/polyfill-uuid v1.31.0 v1.31.0 Symfony polyfill for uuid functions
symfony/routing v7.0.10 v7.2.3 Maps an HTTP request to a set of configuration variables
symfony/security-core v7.0.10 v7.2.3 Symfony Security Component - Core Library
symfony/security-csrf v7.0.8 v7.2.3 Symfony Security Component - CSRF Library
symfony/security-http v7.0.9 v7.2.4 Symfony Security Component - HTTP Integration
symfony/service-contracts v3.5.1 v3.5.1 Generic abstractions related to writing services
symfony/translation-contracts v3.5.1 v3.5.1 Generic abstractions related to translation
symfony/twig-bridge v7.0.8 v7.2.4 Provides integration for Twig with various Symfony components
symfony/var-dumper v7.0.10 v7.2.3 Provides mechanisms for walking through any arbitrary PHP variable

PHP version

PHP 8.3.2 (cli) (built: Jan 18 2024 18:40:26) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.2, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.2, Copyright (c), by Zend Technologies

Confirmed also on other 8.3.x

Subject

If there are several EventListeners implementing a given event (e.g. onFlush) and the first one (in alphabetical order) will in the constructor require a
repository extending the Gedmo\Tree\Entity\Repository\NestedTreeRepository then the first listener will be initialised (its constructor will be called)
as many times as the number of events for which it is attached (#[AsDoctrineListener(...)]). This additionally causes the given listener to lose the ability to pass values between, for example, postPersist and
postFlush via an internal variable, as the postPersist event will be called for the first listener instance and postFlush for the last.

I hope I have described it in an understandable way. The problem is quite intricate and looks like a kind of race condition/use before initialisation.

The problem causes most likely getting the list of listeners in the class Gedmo\Tree\Entity\Repository\AbstractTreeRepository line 53
https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/src/Tree/Entity/Repository/AbstractTreeRepository.php#L53

Please see "Steps to reproduce" for more details.

I don't know if this is the right place where I should report this problem as it lies somewhere between this bundle and Symfony.

Minimal repository with the bug

Tested on v3.19.0.

Steps to reproduce

1. Create repository which implements Gedmo\Tree\Entity\Repository\NestedTreeRepository

<?php

namespace App\Repository;

use App\Entity\Stage;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
use Doctrine\Persistence\ManagerRegistry;
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;

/**
 * @method Stage|null find($id, $lockMode = null, $lockVersion = null)
 * @method Stage|null findOneBy(array $criteria, array $orderBy = null)
 * @method Stage[]    findAll()
 * @method Stage[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class StageRepository extends NestedTreeRepository implements ServiceEntityRepositoryInterface
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry->getManager(), $registry->getManager()->getClassMetadata(Stage::class));
    }
}

2. Create 3 listeners

<?php

namespace App\EventListener;

use App\Repository\StageRepository;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Events;

#[AsDoctrineListener(event: Events::prePersist)]
#[AsDoctrineListener(event: Events::preUpdate)]
#[AsDoctrineListener(event: Events::postFlush)]
class A1Listener
{
    public function __construct(
        private readonly StageRepository $stageRepository,
    ) {
        dump('================= A1 =================');
    }

    public function postFlush(): void
    {
    }
}
<?php

namespace App\EventListener;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Events;

#[AsDoctrineListener(event: Events::postFlush)]
class A2Listener
{
    public function __construct()
    {
        dump('================= A2 =================');
    }

    public function postFlush(): void
    {
    }
}
<?php

namespace App\EventListener;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Events;

#[AsDoctrineListener(event: Events::postFlush)]
class A3Listener
{
    public function __construct()
    {
        dump('================= A3 =================');
    }

    public function postFlush(): void
    {
    }
}

3. Create simple test command

<?php

namespace App\Command;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
    name: 'app:bug',
)]
class BugCommand extends Command
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->entityManager->flush();

        return Command::SUCCESS;
    }
}

Testing

Run command in console

php bin/console app:bug

Expected results

A1Listener.php on line 18:
"================= A1 ================="
A2Listener.php on line 30:
"================= A2 ================="
A3Listener.php on line 30:
"================= A3 ================="

Actual results

[FAIL] Base test code

A1Listener.php on line 18:
"================= A1 ================="
A2Listener.php on line 30:
"================= A2 ================="
A3Listener.php on line 30:
"================= A3 ================="
A1Listener.php on line 18:
"================= A1 ================="
A1Listener.php on line 18:
"================= A1 ================="

[OK] After removing StageRepository from A1Listener

A1Listener.php on line 18:
"================= A1 ================="
A2Listener.php on line 30:
"================= A2 ================="
A3Listener.php on line 30:
"================= A3 ================="

[OK] After moving StageRepository from A1Listener to A2Listener or A3Listener

A1Listener.php on line 18:
"================= A1 ================="
A2Listener.php on line 30:
"================= A2 ================="
A3Listener.php on line 30:
"================= A3 ================="

[FAIL] After removing the "#[AsDoctrineListener(event: Events::prePersist)]" in A1Listener (there are now two A1 constructor calls)

A1Listener.php on line 18:
"================= A1 ================="
A2Listener.php on line 32:
"================= A2 ================="
A3Listener.php on line 30:
"================= A3 ================="
A1Listener.php on line 18:
"================= A1 ================="

[OK] after commenting out highlighted lines https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/src/Tree/Entity/Repository/AbstractTreeRepository.php#L52-L72

"================= A1 ================="
"================= A2 ================="
"================= A3 ================="

[FAIL] After replacing highlighted lines https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/src/Tree/Entity/Repository/AbstractTreeRepository.php#L52-L72 with only $em->getEventManager()->getAllListeners();

A1Listener.php on line 18:
"================= A1 ================="
A2Listener.php on line 32:
"================= A2 ================="
A3Listener.php on line 30:
"================= A3 ================="
A1Listener.php on line 18:
"================= A1 ================="
A1Listener.php on line 18:
"================= A1 ================="

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions