From e23e5790c4acab9ace70566cba5e643e854758ee Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 27 Aug 2026 11:59:50 +0000 Subject: [PATCH 1/6] Add Symfony 8 support Widen Symfony version constraints in composer.json to allow ^8.0 across http-kernel, dependency-injection, expression-language, config, console, process, framework-bundle, finder, yaml and var-exporter. Symfony 8 removes XmlFileLoader support for DI service definitions. Convert all Resources/config/*.xml DI service files (and the test app's services.xml) to PHP ContainerConfigurator format, and switch TaskExtension and TestKernel from XmlFileLoader to PhpFileLoader. Doctrine ORM mapping XML files are untouched, as those use a separate XML driver unaffected by this removal. Bundle::build() gained a void return type in Symfony 8; add it to TaskBundle::build(). Doctrine ORM 3.4+ requires either PHP 8.4 native lazy objects or the legacy VarExporter-based lazy ghost proxies, the latter of which was removed from Symfony 8's ProxyHelper. Enable native lazy objects in the test app's doctrine config when running on PHP 8.4+, since Symfony 8 requires PHP 8.2+ and native lazy objects are the only supported option once VarExporter's generateLazyGhost() is gone on PHP 8.4. Fix a PHP 8.4 deprecation in DebugTasksCommand where round() could receive null when a task execution has no duration yet. --- composer.json | 20 +++--- src/Command/DebugTasksCommand.php | 2 +- src/DependencyInjection/TaskExtension.php | 24 +++---- src/Resources/config/command.php | 67 ++++++++++++++++++++ src/Resources/config/command.xml | 46 -------------- src/Resources/config/executor/inside.php | 21 ++++++ src/Resources/config/executor/inside.xml | 10 --- src/Resources/config/executor/separate.php | 35 ++++++++++ src/Resources/config/executor/separate.xml | 18 ------ src/Resources/config/listener.php | 22 +++++++ src/Resources/config/listener.xml | 12 ---- src/Resources/config/locking/null.php | 20 ++++++ src/Resources/config/locking/null.xml | 8 --- src/Resources/config/locking/services.php | 24 +++++++ src/Resources/config/locking/services.xml | 11 ---- src/Resources/config/locking/storages.php | 22 +++++++ src/Resources/config/locking/storages.xml | 12 ---- src/Resources/config/scheduler.php | 56 ++++++++++++++++ src/Resources/config/scheduler.xml | 33 ---------- src/Resources/config/storage/array.php | 25 ++++++++ src/Resources/config/storage/array.xml | 9 --- src/Resources/config/storage/doctrine.php | 49 ++++++++++++++ src/Resources/config/storage/doctrine.xml | 30 --------- src/Resources/config/task_event_listener.php | 27 ++++++++ src/Resources/config/task_event_listener.xml | 17 ----- src/TaskBundle.php | 2 +- tests/app/TestKernel.php | 10 ++- tests/app/config/config.doctrine.yml | 1 + tests/app/config/services.php | 25 ++++++++ tests/app/config/services.xml | 13 ---- 30 files changed, 425 insertions(+), 246 deletions(-) create mode 100644 src/Resources/config/command.php delete mode 100644 src/Resources/config/command.xml create mode 100644 src/Resources/config/executor/inside.php delete mode 100644 src/Resources/config/executor/inside.xml create mode 100644 src/Resources/config/executor/separate.php delete mode 100644 src/Resources/config/executor/separate.xml create mode 100644 src/Resources/config/listener.php delete mode 100644 src/Resources/config/listener.xml create mode 100644 src/Resources/config/locking/null.php delete mode 100644 src/Resources/config/locking/null.xml create mode 100644 src/Resources/config/locking/services.php delete mode 100644 src/Resources/config/locking/services.xml create mode 100644 src/Resources/config/locking/storages.php delete mode 100644 src/Resources/config/locking/storages.xml create mode 100644 src/Resources/config/scheduler.php delete mode 100644 src/Resources/config/scheduler.xml create mode 100644 src/Resources/config/storage/array.php delete mode 100644 src/Resources/config/storage/array.xml create mode 100644 src/Resources/config/storage/doctrine.php delete mode 100644 src/Resources/config/storage/doctrine.xml create mode 100644 src/Resources/config/task_event_listener.php delete mode 100644 src/Resources/config/task_event_listener.xml create mode 100644 tests/app/config/services.php delete mode 100644 tests/app/config/services.xml diff --git a/composer.json b/composer.json index 5495911..5d4fe85 100644 --- a/composer.json +++ b/composer.json @@ -12,24 +12,24 @@ "require": { "php": "^8.0", "php-task/php-task": "^3.0.1", - "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", - "symfony/config": "^5.4 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0", "doctrine/orm": "^2.5.3 || ^3.0" }, "require-dev": { - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0", "doctrine/doctrine-bundle": "^1.5 || ^2.0", "doctrine/data-fixtures": "^1.3.3", "phpunit/phpunit": "^9.6.5 || ^10", "phpspec/prophecy": "^1.14", "phpspec/prophecy-phpunit": "^2.0", - "symfony/var-exporter": "^5.4 || ^6.4 || ^7.0" + "symfony/var-exporter": "^5.4 || ^6.4 || ^7.0 || ^8.0" }, "autoload": { "psr-4": { diff --git a/src/Command/DebugTasksCommand.php b/src/Command/DebugTasksCommand.php index 3cbd9dc..c15eabb 100644 --- a/src/Command/DebugTasksCommand.php +++ b/src/Command/DebugTasksCommand.php @@ -78,7 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $execution->getHandlerClass(), $execution->getScheduleTime()->format(\DateTimeImmutable::RFC3339), !$execution->getEndTime() ? '' : $execution->getEndTime()->format(\DateTimeImmutable::RFC3339), - (round($execution->getDuration(), 6) * 1000000) . 'ms', + (round($execution->getDuration() ?? 0.0, 6) * 1000000) . 'ms', ] ); } diff --git a/src/DependencyInjection/TaskExtension.php b/src/DependencyInjection/TaskExtension.php index 8f42697..b4e50df 100644 --- a/src/DependencyInjection/TaskExtension.php +++ b/src/DependencyInjection/TaskExtension.php @@ -71,15 +71,15 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('task.lock.storages.' . $key, $config['locking']['storages'][$key]); } - $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load(sprintf('storage/%s.xml', $config['storage'])); - $loader->load('task_event_listener.xml'); - $loader->load('scheduler.xml'); - $loader->load('command.xml'); - $loader->load('locking/services.xml'); + $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load(sprintf('storage/%s.php', $config['storage'])); + $loader->load('task_event_listener.php'); + $loader->load('scheduler.php'); + $loader->load('command.php'); + $loader->load('locking/services.php'); if ($config['run']['mode'] === 'listener') { - $loader->load('listener.xml'); + $loader->load('listener.php'); } $this->loadDoctrineAdapter($config['adapters']['doctrine'], $container); @@ -118,10 +118,10 @@ private function loadDoctrineAdapter(array $config, ContainerBuilder $container) private function loadLockingComponent(array $config, ContainerBuilder $container, LoaderInterface $loader) { if (!$config['enabled'] || 'null' === $config['storage']) { - return $loader->load('locking/null.xml'); + return $loader->load('locking/null.php'); } - $loader->load('locking/services.xml'); + $loader->load('locking/services.php'); $container->setParameter('task.lock.ttl', $config['ttl']); } @@ -134,7 +134,7 @@ private function loadLockingComponent(array $config, ContainerBuilder $container */ private function loadExecutorComponent(array $config, ContainerBuilder $container, LoaderInterface $loader) { - $loader->load('executor/' . $config['type'] . '.xml'); + $loader->load('executor/' . $config['type'] . '.php'); $container->setAlias('task.executor', 'task.executor.' . $config['type']); if (!array_key_exists($config['type'], $config)) { @@ -178,8 +178,8 @@ private function getLockingStorageAliases(ContainerBuilder $container) */ public function getConfiguration(array $config, ContainerBuilder $container) { - $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load('locking/storages.xml'); + $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('locking/storages.php'); return new Configuration($this->getLockingStorageAliases($container)); } diff --git a/src/Resources/config/command.php b/src/Resources/config/command.php new file mode 100644 index 0000000..7745615 --- /dev/null +++ b/src/Resources/config/command.php @@ -0,0 +1,67 @@ +services(); + + $services->set('task.command.run', RunCommand::class) + ->public() + ->args([ + null, + service('task.runner'), + service('task.scheduler'), + // add entity_manager if doctrine storage is enabled + ]) + ->tag('console.command', ['command' => 'task:run']); + + $services->set('task.command.run_handler', RunHandlerCommand::class) + ->public() + ->args([ + null, + service('task.handler.factory'), + ]) + ->tag('console.command', ['command' => 'task:run:handler']); + + $services->set('task.command.executor', ExecuteCommand::class) + ->public() + ->args([ + null, + service('task.handler.factory'), + service('task.storage.task_execution'), + service('event_dispatcher'), + ]) + ->tag('console.command', ['command' => 'task:execute']); + + $services->set('task.command.schedule_task', ScheduleTaskCommand::class) + ->public() + ->args([ + null, + service('task.scheduler'), + // add entity_manager if doctrine storage is enabled + ]) + ->tag('console.command', ['command' => 'task:schedule']); + + $services->set('task.command.debug_tasks', DebugTasksCommand::class) + ->public() + ->args([ + null, + service('task.storage.task_execution'), + ]) + ->tag('console.command', ['command' => 'debug:tasks']); +}; diff --git a/src/Resources/config/command.xml b/src/Resources/config/command.xml deleted file mode 100644 index 935e3c3..0000000 --- a/src/Resources/config/command.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - null - - - - - - - - - null - - - - - - - null - - - - - - - - - null - - - - - - - - null - - - - - - diff --git a/src/Resources/config/executor/inside.php b/src/Resources/config/executor/inside.php new file mode 100644 index 0000000..89c92d6 --- /dev/null +++ b/src/Resources/config/executor/inside.php @@ -0,0 +1,21 @@ +services() + ->set('task.executor.inside', InsideProcessExecutor::class) + ->public() + ->args([service('task.handler.factory')]); +}; diff --git a/src/Resources/config/executor/inside.xml b/src/Resources/config/executor/inside.xml deleted file mode 100644 index 42ab762..0000000 --- a/src/Resources/config/executor/inside.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/src/Resources/config/executor/separate.php b/src/Resources/config/executor/separate.php new file mode 100644 index 0000000..666a30c --- /dev/null +++ b/src/Resources/config/executor/separate.php @@ -0,0 +1,35 @@ +services(); + + $services->set('task.executor.separate', SeparateProcessExecutor::class) + ->public() + ->args([ + service('task.handler.factory'), + service('task.storage.task_execution'), + service('task.executor.separate.process_factory'), + ]); + + $services->set('task.executor.separate.process_factory', ExecutionProcessFactory::class) + ->public() + ->args([ + '%task.executor.console_path%', + '%task.executor.process_timeout%', + '%kernel.environment%', + ]); +}; diff --git a/src/Resources/config/executor/separate.xml b/src/Resources/config/executor/separate.xml deleted file mode 100644 index 7dc25e7..0000000 --- a/src/Resources/config/executor/separate.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - %task.executor.console_path% - %task.executor.process_timeout% - %kernel.environment% - - - diff --git a/src/Resources/config/listener.php b/src/Resources/config/listener.php new file mode 100644 index 0000000..916b4f5 --- /dev/null +++ b/src/Resources/config/listener.php @@ -0,0 +1,22 @@ +services() + ->set('task.event_listener.run', RunListener::class) + ->public() + ->args([service('task.runner')]) + ->tag('kernel.event_listener', ['event' => 'kernel.terminate', 'method' => 'run']); +}; diff --git a/src/Resources/config/listener.xml b/src/Resources/config/listener.xml deleted file mode 100644 index bec8ecc..0000000 --- a/src/Resources/config/listener.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/src/Resources/config/locking/null.php b/src/Resources/config/locking/null.php new file mode 100644 index 0000000..cd1d0e4 --- /dev/null +++ b/src/Resources/config/locking/null.php @@ -0,0 +1,20 @@ +services() + ->set('task.lock', NullLock::class) + ->public(); +}; diff --git a/src/Resources/config/locking/null.xml b/src/Resources/config/locking/null.xml deleted file mode 100644 index aeb6894..0000000 --- a/src/Resources/config/locking/null.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/src/Resources/config/locking/services.php b/src/Resources/config/locking/services.php new file mode 100644 index 0000000..cbf9bba --- /dev/null +++ b/src/Resources/config/locking/services.php @@ -0,0 +1,24 @@ +services() + ->set('task.lock', Lock::class) + ->public() + ->args([ + service('task.lock.storage'), + '%task.lock.ttl%', + ]); +}; diff --git a/src/Resources/config/locking/services.xml b/src/Resources/config/locking/services.xml deleted file mode 100644 index 570317b..0000000 --- a/src/Resources/config/locking/services.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - %task.lock.ttl% - - - diff --git a/src/Resources/config/locking/storages.php b/src/Resources/config/locking/storages.php new file mode 100644 index 0000000..8b89068 --- /dev/null +++ b/src/Resources/config/locking/storages.php @@ -0,0 +1,22 @@ +services() + ->set('task.lock.storage.file', FileLockStorage::class) + ->public() + ->args([expr("parameter('task.lock.storages.file')['directory']")]) + ->tag('task.lock.storage', ['alias' => 'file']); +}; diff --git a/src/Resources/config/locking/storages.xml b/src/Resources/config/locking/storages.xml deleted file mode 100644 index 47d0aac..0000000 --- a/src/Resources/config/locking/storages.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - parameter('task.lock.storages.file')['directory'] - - - - - diff --git a/src/Resources/config/scheduler.php b/src/Resources/config/scheduler.php new file mode 100644 index 0000000..fae556e --- /dev/null +++ b/src/Resources/config/scheduler.php @@ -0,0 +1,56 @@ +services(); + + $services->set('task.builder_factory', TaskBuilderFactory::class) + ->public(); + + $services->set('task.scheduler', TaskScheduler::class) + ->public() + ->args([ + service('task.builder_factory'), + service('task.storage.task'), + service('task.storage.task_execution'), + service('event_dispatcher'), + ]); + + $services->set('task.handler.factory', TaskHandlerFactory::class) + ->public() + ->args([[]]); + + $services->set('task.runner.execution_finder', PendingExecutionFinder::class) + ->public() + ->args([ + service('task.storage.task_execution'), + service('task.handler.factory'), + service('task.lock'), + service('logger')->ignoreOnInvalid(), + ]); + + $services->set('task.runner', TaskRunner::class) + ->public() + ->args([ + service('task.storage.task_execution'), + service('task.runner.execution_finder'), + service('task.executor'), + service('event_dispatcher'), + ]); +}; diff --git a/src/Resources/config/scheduler.xml b/src/Resources/config/scheduler.xml deleted file mode 100644 index 614399c..0000000 --- a/src/Resources/config/scheduler.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/storage/array.php b/src/Resources/config/storage/array.php new file mode 100644 index 0000000..20e3751 --- /dev/null +++ b/src/Resources/config/storage/array.php @@ -0,0 +1,25 @@ +services(); + + $services->set('task.storage.task', ArrayTaskRepository::class) + ->public(); + + $services->set('task.storage.task_execution', ArrayTaskExecutionRepository::class) + ->public(); +}; diff --git a/src/Resources/config/storage/array.xml b/src/Resources/config/storage/array.xml deleted file mode 100644 index 2bd31a7..0000000 --- a/src/Resources/config/storage/array.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/src/Resources/config/storage/doctrine.php b/src/Resources/config/storage/doctrine.php new file mode 100644 index 0000000..c67b34e --- /dev/null +++ b/src/Resources/config/storage/doctrine.php @@ -0,0 +1,49 @@ +services(); + + $services->set('task.repository.task', TaskRepository::class) + ->public() + ->factory([service('doctrine.orm.entity_manager'), 'getRepository']) + ->args([Task::class]); + + $services->alias('task.storage.task', 'task.repository.task') + ->public(); + + $services->set('task.repository.task_execution', TaskExecutionRepository::class) + ->public() + ->factory([service('doctrine.orm.entity_manager'), 'getRepository']) + ->args([TaskExecution::class]); + + $services->alias('task.storage.task_execution', 'task.repository.task_execution') + ->public(); + + $services->set('task.command.schedule_system_tasks', ScheduleSystemTasksCommand::class) + ->public() + ->args([ + null, + '%task.system_tasks%', + service('task.scheduler'), + service('task.repository.task'), + service('task.storage.task_execution'), + ]) + ->tag('console.command', ['command' => 'task:schedule:system-tasks']); +}; diff --git a/src/Resources/config/storage/doctrine.xml b/src/Resources/config/storage/doctrine.xml deleted file mode 100644 index f302fb4..0000000 --- a/src/Resources/config/storage/doctrine.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - Task\TaskBundle\Entity\Task - - - - - - - Task\TaskBundle\Entity\TaskExecution - - - - - null - %task.system_tasks% - - - - - - - - diff --git a/src/Resources/config/task_event_listener.php b/src/Resources/config/task_event_listener.php new file mode 100644 index 0000000..09ad8c1 --- /dev/null +++ b/src/Resources/config/task_event_listener.php @@ -0,0 +1,27 @@ +parameters(); + + $parameters->set('task.events.create', Events::TASK_CREATE); + $parameters->set('task.events.create_execution', Events::TASK_EXECUTION_CREATE); + $parameters->set('task.events.before', Events::TASK_BEFORE); + $parameters->set('task.events.after', Events::TASK_AFTER); + $parameters->set('task.events.finished', Events::TASK_FINISHED); + $parameters->set('task.events.passed', Events::TASK_PASSED); + $parameters->set('task.events.failed', Events::TASK_FAILED); + $parameters->set('task.events.retried', Events::TASK_RETRIED); +}; diff --git a/src/Resources/config/task_event_listener.xml b/src/Resources/config/task_event_listener.xml deleted file mode 100644 index 7dad9b1..0000000 --- a/src/Resources/config/task_event_listener.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Task\Event\Events::TASK_CREATE - Task\Event\Events::TASK_EXECUTION_CREATE - Task\Event\Events::TASK_BEFORE - Task\Event\Events::TASK_AFTER - Task\Event\Events::TASK_FINISHED - Task\Event\Events::TASK_PASSED - Task\Event\Events::TASK_FAILED - Task\Event\Events::TASK_RETRIED - - - diff --git a/src/TaskBundle.php b/src/TaskBundle.php index 36805a5..f89839e 100644 --- a/src/TaskBundle.php +++ b/src/TaskBundle.php @@ -11,7 +11,7 @@ */ class TaskBundle extends Bundle { - public function build(ContainerBuilder $container) + public function build(ContainerBuilder $container): void { parent::build($container); diff --git a/tests/app/TestKernel.php b/tests/app/TestKernel.php index 7295cf6..0e363a3 100644 --- a/tests/app/TestKernel.php +++ b/tests/app/TestKernel.php @@ -14,7 +14,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\HttpKernel\Kernel; use Task\TaskBundle\TaskBundle; @@ -59,11 +59,15 @@ public function registerContainerConfiguration(LoaderInterface $loader): void protected function buildContainer(): ContainerBuilder { $container = parent::buildContainer(); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/config')); - $loader->load('services.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/config')); + $loader->load('services.php'); $container->setParameter('kernel.storage', $this->storage); $container->setParameter('container.build_id', hash('crc32', 'Abc123423456789')); + // Doctrine ORM requires either native lazy objects (PHP 8.4+) or the + // (Symfony < 8) VarExporter-based lazy ghost implementation. Only enable + // native lazy objects when running on a PHP version that supports them. + $container->setParameter('task_test.native_lazy_objects', \PHP_VERSION_ID >= 80400); return $container; } diff --git a/tests/app/config/config.doctrine.yml b/tests/app/config/config.doctrine.yml index 5f286a8..b94d6a3 100644 --- a/tests/app/config/config.doctrine.yml +++ b/tests/app/config/config.doctrine.yml @@ -15,6 +15,7 @@ doctrine: orm: auto_generate_proxy_classes: '%kernel.debug%' auto_mapping: false + enable_native_lazy_objects: '%task_test.native_lazy_objects%' dbal: driver: pdo_sqlite host: localhost diff --git a/tests/app/config/services.php b/tests/app/config/services.php new file mode 100644 index 0000000..f94f64f --- /dev/null +++ b/tests/app/config/services.php @@ -0,0 +1,25 @@ +services(); + + $services->set('test.handler', TestHandler::class) + ->tag('task.handler', ['handler-name' => 'test']); + + $services->set('test.fail_handler', FailTestHandler::class) + ->tag('task.handler', ['handler-name' => 'test']); +}; diff --git a/tests/app/config/services.xml b/tests/app/config/services.xml deleted file mode 100644 index 7af7798..0000000 --- a/tests/app/config/services.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - From 3d34280d139146e8d21dd60d47df03a8dee4df11 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 27 Aug 2026 12:15:40 +0000 Subject: [PATCH 2/6] Guard enable_native_lazy_objects on doctrine-bundle < 2.15 The doctrine.orm.enable_native_lazy_objects config option was only added in doctrine/doctrine-bundle 2.15, which requires PHP >= 8.1. On the PHP 8.0 test job, Composer resolves an older doctrine-bundle that doesn't recognise this key, so the test app's Doctrine schema creation step failed with: Unrecognized option "enable_native_lazy_objects" under "doctrine.orm.entity_managers.default". Move the option into a separate config file that TestKernel only loads when the installed doctrine-bundle version actually supports it, detected via Composer\InstalledVersions. This keeps native lazy objects enabled on PHP 8.4+ with modern doctrine-bundle, without breaking older doctrine-bundle installs resolved on PHP 8.0/8.1. --- tests/app/TestKernel.php | 23 +++++++++++++++++++ tests/app/config/config.doctrine.yml | 1 - .../config.doctrine_native_lazy_objects.yml | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/app/config/config.doctrine_native_lazy_objects.yml diff --git a/tests/app/TestKernel.php b/tests/app/TestKernel.php index 0e363a3..cfd85f1 100644 --- a/tests/app/TestKernel.php +++ b/tests/app/TestKernel.php @@ -51,6 +51,29 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $loader->load(sprintf('%s/config/config.yml', __DIR__)); $loader->load(sprintf('%s/config/config.%s.yml', __DIR__, $this->storage)); + + // The "doctrine.orm.enable_native_lazy_objects" option was only added in + // doctrine/doctrine-bundle 2.15 (which requires PHP >= 8.1). Older + // doctrine-bundle versions, resolved by Composer on PHP 8.0, reject this + // key as unrecognized, so only set it when the installed bundle supports it. + if ('doctrine' === $this->storage && $this->doctrineBundleSupportsNativeLazyObjects()) { + $loader->load(sprintf('%s/config/config.doctrine_native_lazy_objects.yml', __DIR__)); + } + } + + private function doctrineBundleSupportsNativeLazyObjects(): bool + { + if (!class_exists(\Composer\InstalledVersions::class)) { + return false; + } + + if (!\Composer\InstalledVersions::isInstalled('doctrine/doctrine-bundle')) { + return false; + } + + $version = \Composer\InstalledVersions::getVersion('doctrine/doctrine-bundle'); + + return null !== $version && \version_compare($version, '2.15.0', '>='); } /** diff --git a/tests/app/config/config.doctrine.yml b/tests/app/config/config.doctrine.yml index b94d6a3..5f286a8 100644 --- a/tests/app/config/config.doctrine.yml +++ b/tests/app/config/config.doctrine.yml @@ -15,7 +15,6 @@ doctrine: orm: auto_generate_proxy_classes: '%kernel.debug%' auto_mapping: false - enable_native_lazy_objects: '%task_test.native_lazy_objects%' dbal: driver: pdo_sqlite host: localhost diff --git a/tests/app/config/config.doctrine_native_lazy_objects.yml b/tests/app/config/config.doctrine_native_lazy_objects.yml new file mode 100644 index 0000000..a1976d9 --- /dev/null +++ b/tests/app/config/config.doctrine_native_lazy_objects.yml @@ -0,0 +1,3 @@ +doctrine: + orm: + enable_native_lazy_objects: '%task_test.native_lazy_objects%' From ac8f8088b3a70347e198924b2cc5897549070389 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Thu, 27 Aug 2026 15:48:06 +0200 Subject: [PATCH 3/6] Update composer doctrine dependencies --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5d4fe85..8edb283 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,8 @@ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0", "symfony/finder": "^5.4 || ^6.0 || ^7.0 || ^8.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0", - "doctrine/doctrine-bundle": "^1.5 || ^2.0", - "doctrine/data-fixtures": "^1.3.3", + "doctrine/doctrine-bundle": "^1.5 || ^2.0 || ^3.0", + "doctrine/data-fixtures": "^1.3.3 || ^2.0", "phpunit/phpunit": "^9.6.5 || ^10", "phpspec/prophecy": "^1.14", "phpspec/prophecy-phpunit": "^2.0", From c766b6c967f309a4805132ebd056e128b4695309 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Thu, 27 Aug 2026 15:49:55 +0200 Subject: [PATCH 4/6] Update PHP 8.5 --- .github/workflows/test-application.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-application.yaml b/.github/workflows/test-application.yaml index 8c87214..cc71f36 100644 --- a/.github/workflows/test-application.yaml +++ b/.github/workflows/test-application.yaml @@ -48,10 +48,16 @@ jobs: - php-version: '8.4' storage: doctrine - composer-flags: --ignore-platform-reqs - php-version: '8.4' storage: array + + - php-version: '8.5' + storage: doctrine + composer-flags: --ignore-platform-reqs + - php-version: '8.5' + storage: array composer-flags: --ignore-platform-reqs + steps: - name: Checkout project uses: actions/checkout@v2 From 4899908de004f73f08478ae816e555926ec0c6c1 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 27 Aug 2026 14:04:18 +0000 Subject: [PATCH 5/6] Add missing return types for Symfony 8 compatibility Symfony 8 hardens several DependencyInjection interfaces with strict return types. Add matching return types to TaskExtension and HandlerCompilerPass so their declarations stay compatible: - TaskExtension::getConfiguration(): ?ConfigurationInterface - TaskExtension::load(): void - TaskExtension::prepend(): void - HandlerCompilerPass::process(): void Without these, PHP raises a fatal 'Declaration ... must be compatible with ...' error on Symfony 8. --- src/DependencyInjection/HandlerCompilerPass.php | 2 +- src/DependencyInjection/TaskExtension.php | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/DependencyInjection/HandlerCompilerPass.php b/src/DependencyInjection/HandlerCompilerPass.php index f8f9284..baea178 100644 --- a/src/DependencyInjection/HandlerCompilerPass.php +++ b/src/DependencyInjection/HandlerCompilerPass.php @@ -27,7 +27,7 @@ class HandlerCompilerPass implements CompilerPassInterface /** * {@inheritdoc} */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->has(self::REGISTRY_ID)) { return; diff --git a/src/DependencyInjection/TaskExtension.php b/src/DependencyInjection/TaskExtension.php index b4e50df..d5414e4 100644 --- a/src/DependencyInjection/TaskExtension.php +++ b/src/DependencyInjection/TaskExtension.php @@ -11,6 +11,7 @@ namespace Task\TaskBundle\DependencyInjection; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; @@ -29,7 +30,7 @@ */ class TaskExtension extends Extension implements PrependExtensionInterface { - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { $configs = $container->getExtensionConfig($this->getAlias()); $configuration = $this->getConfiguration($configs, $container); @@ -59,7 +60,7 @@ public function prepend(ContainerBuilder $container) /** * {@inheritdoc} */ - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); @@ -176,7 +177,7 @@ private function getLockingStorageAliases(ContainerBuilder $container) /** * {@inheritdoc} */ - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface { $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('locking/storages.php'); From c27213c54d7ab47b3e042fd8be18ea8e48116bc3 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 27 Aug 2026 14:04:24 +0000 Subject: [PATCH 6/6] Fix test app for Symfony 8 and Doctrine Bundle 3 compatibility Symfony\Component\Console\Application::add() was removed in Symfony 8.0 in favour of addCommand(); use addCommand() when available and fall back to add() on older Symfony versions. doctrine/doctrine-bundle 3.0 removed the 'doctrine.orm.auto_generate_proxy_classes' config option, so only load it for older doctrine-bundle versions, the same way enable_native_lazy_objects is already guarded for newer ones. The test app registered DoctrineBundle unconditionally, even for the 'array' storage config which never loads any 'doctrine' extension config. On doctrine-bundle 3.x this made the container compilation fail with 'You have requested a non-existent parameter doctrine.dbal.connection_factory.types', since RegisterDbalTypePass now runs unconditionally for any registered DoctrineBundle. Only register DoctrineBundle when the 'doctrine' storage is selected. --- tests/Functional/BaseCommandTestCase.php | 6 ++- tests/app/TestKernel.php | 44 ++++++++++++++++--- tests/app/config/config.doctrine.yml | 1 - ...g.doctrine_auto_generate_proxy_classes.yml | 3 ++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 tests/app/config/config.doctrine_auto_generate_proxy_classes.yml diff --git a/tests/Functional/BaseCommandTestCase.php b/tests/Functional/BaseCommandTestCase.php index ffc29d7..127cbc6 100644 --- a/tests/Functional/BaseCommandTestCase.php +++ b/tests/Functional/BaseCommandTestCase.php @@ -81,7 +81,11 @@ protected function setUp(): void $command = $this->getCommand(); $this->application = new Application(self::$kernel); - $this->application->add($command); + if (method_exists($this->application, 'addCommand')) { + $this->application->addCommand($command); + } else { + $this->application->add($command); + } $this->command = $this->application->find($command->getName()); $this->commandTester = new CommandTester($this->command); diff --git a/tests/app/TestKernel.php b/tests/app/TestKernel.php index cfd85f1..af03282 100644 --- a/tests/app/TestKernel.php +++ b/tests/app/TestKernel.php @@ -32,11 +32,23 @@ class TestKernel extends Kernel */ public function registerBundles(): array { - return [ + $bundles = [ new FrameworkBundle(), - new DoctrineBundle(), new TaskBundle(), ]; + + if ('doctrine' === $this->getStorage()) { + $bundles[] = new DoctrineBundle(); + } + + return $bundles; + } + + private function getStorage(): string + { + $storage = getenv(self::STORAGE_VAR_NAME); + + return false === $storage ? 'array' : $storage; } /** @@ -44,10 +56,7 @@ public function registerBundles(): array */ public function registerContainerConfiguration(LoaderInterface $loader): void { - $this->storage = getenv(self::STORAGE_VAR_NAME); - if (false === $this->storage) { - $this->storage = 'array'; - } + $this->storage = $this->getStorage(); $loader->load(sprintf('%s/config/config.yml', __DIR__)); $loader->load(sprintf('%s/config/config.%s.yml', __DIR__, $this->storage)); @@ -59,6 +68,14 @@ public function registerContainerConfiguration(LoaderInterface $loader): void if ('doctrine' === $this->storage && $this->doctrineBundleSupportsNativeLazyObjects()) { $loader->load(sprintf('%s/config/config.doctrine_native_lazy_objects.yml', __DIR__)); } + + // The "doctrine.orm.auto_generate_proxy_classes" (and "proxy_dir") options + // were removed in doctrine/doctrine-bundle 3.0, since ORM 3.4+ no longer + // relies on generated proxy classes in the same way. Only set the option + // when the installed bundle still recognises it. + if ('doctrine' === $this->storage && $this->doctrineBundleSupportsAutoGenerateProxyClasses()) { + $loader->load(sprintf('%s/config/config.doctrine_auto_generate_proxy_classes.yml', __DIR__)); + } } private function doctrineBundleSupportsNativeLazyObjects(): bool @@ -76,6 +93,21 @@ private function doctrineBundleSupportsNativeLazyObjects(): bool return null !== $version && \version_compare($version, '2.15.0', '>='); } + private function doctrineBundleSupportsAutoGenerateProxyClasses(): bool + { + if (!class_exists(\Composer\InstalledVersions::class)) { + return true; + } + + if (!\Composer\InstalledVersions::isInstalled('doctrine/doctrine-bundle')) { + return true; + } + + $version = \Composer\InstalledVersions::getVersion('doctrine/doctrine-bundle'); + + return null !== $version && \version_compare($version, '3.0.0', '<'); + } + /** * {@inheritdoc} */ diff --git a/tests/app/config/config.doctrine.yml b/tests/app/config/config.doctrine.yml index 5f286a8..ebd40a5 100644 --- a/tests/app/config/config.doctrine.yml +++ b/tests/app/config/config.doctrine.yml @@ -13,7 +13,6 @@ task: doctrine: orm: - auto_generate_proxy_classes: '%kernel.debug%' auto_mapping: false dbal: driver: pdo_sqlite diff --git a/tests/app/config/config.doctrine_auto_generate_proxy_classes.yml b/tests/app/config/config.doctrine_auto_generate_proxy_classes.yml new file mode 100644 index 0000000..d56bef3 --- /dev/null +++ b/tests/app/config/config.doctrine_auto_generate_proxy_classes.yml @@ -0,0 +1,3 @@ +doctrine: + orm: + auto_generate_proxy_classes: '%kernel.debug%'