From 34ac1d92945f209afdd5a8d1fd6a7b0971ddf8ea Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Mon, 28 Sep 2026 16:47:10 +0200 Subject: [PATCH] feat(manager) #222 ProcessManager: going on with the process after an exception thrown by a task `initialize()` is deprecated (an `E_USER_DEPRECATED` error is triggered). In v6.0, the process will fail before executing any task. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + docs/02-task_types.md | 3 + docs/03-custom_tasks.md | 4 +- docs/04-advanced_workflow.md | 3 +- src/Manager/ProcessManager.php | 10 + .../ProcessManagerInitializationTest.php | 206 ++++++++++++++++++ 6 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 tests/Manager/ProcessManagerInitializationTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 237cb95b..7d1a097e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Latest ## Deprecated * [#189](https://github.com/cleverage/process-bundle/issues/189) EventDispatcherTask: when `event_name` is set, listening to `CleverAge\ProcessBundle\Event\EventDispatcherTaskEvent` is deprecated (the event is still dispatched under its class name, with an `E_USER_DEPRECATED` error, if it has listeners). Listen to the configured `event_name` instead: the BC layer will be removed in v6.0. +* [#222](https://github.com/cleverage/process-bundle/issues/222) ProcessManager: going on with the process after an exception thrown by a task `initialize()` is deprecated (an `E_USER_DEPRECATED` error is triggered). In v6.0, the process will fail before executing any task. v5.0 ----- diff --git a/docs/02-task_types.md b/docs/02-task_types.md index 53400464..9701f680 100644 --- a/docs/02-task_types.md +++ b/docs/02-task_types.md @@ -121,6 +121,9 @@ goes on. The failure only surfaces when the task is first reached: for [configur the options are resolved again and the process fails at that point (upstream tasks may already have run); for other tasks, the branch stops after this first execution. If the task is never reached, the process ends normally. +This behaviour is deprecated since v5 (an `E_USER_DEPRECATED` error is triggered for each initialization failure): in +v6.0, an initialization failure will make the process fail before any task is executed. + ## Configurable tasks and options Most tasks aim to have a generic behavior. This provides reusability, but each usage needs a slightly different diff --git a/docs/03-custom_tasks.md b/docs/03-custom_tasks.md index cefbcda4..1020c0cd 100644 --- a/docs/03-custom_tasks.md +++ b/docs/03-custom_tasks.md @@ -94,7 +94,9 @@ Based on [Symfony's OptionsResolver Component](https://symfony.com/doc/current/c abstract class allows you to implement its `configureOptions` method to add your requirements, default values and normalizers. Options are resolved (once) during the task initialization, and can be read with `getOptions($state)` or `getOption($state, $code)`. If the resolution fails during initialization, the error is logged and the options are -resolved again (failing the process) when the task is first executed. +resolved again (failing the process) when the task is first executed. Going on with the process after an +initialization failure is deprecated since v5: in v6.0, the process will fail before any task is executed (see +[initializable tasks](02-task_types.md#initializable-tasks)). ```php namespace App\Task; diff --git a/docs/04-advanced_workflow.md b/docs/04-advanced_workflow.md index 77354d8c..3948e0d5 100644 --- a/docs/04-advanced_workflow.md +++ b/docs/04-advanced_workflow.md @@ -12,7 +12,8 @@ When a process is executed (with the `cleverage:process:execute` command or with 3. **initializes** every task, in the order they are configured: the service is fetched from the container and `initialize` is called on [initializable tasks](02-task_types.md#initializable-tasks) (for configurable tasks, this is where options are validated). An exception thrown by `initialize` is logged as critical and flags the task as - stopped, but does not abort the process: it only fails when this task is first executed + stopped, but does not abort the process: it only fails when this task is first executed. This is deprecated since v5 + (an `E_USER_DEPRECATED` error is triggered): in v6.0, the process will fail at this step, before any task is executed 4. gives the process input to the `entry_point` task, if one is defined (otherwise the input is ignored and a warning is logged) 5. **resolves** the tasks of the main branch (see below) diff --git a/src/Manager/ProcessManager.php b/src/Manager/ProcessManager.php index e561366f..73ca9381 100644 --- a/src/Manager/ProcessManager.php +++ b/src/Manager/ProcessManager.php @@ -255,6 +255,16 @@ protected function initialize(TaskConfiguration $taskConfiguration): void ]; $this->taskLogger->critical($e->getMessage(), $logContext); $state->stop($e); + // @deprecated since v5, in v6.0 the process will fail right away, before any task is executed + @trigger_error( + \sprintf( + 'The initialization of the task "%s" of the process "%s" has failed with message "%s". Going on with the process after an initialization failure is deprecated since v5: in v6.0, the process will fail before executing any task.', + $taskConfiguration->getCode(), + $state->getProcessConfiguration()->getCode(), + $e->getMessage(), + ), + \E_USER_DEPRECATED + ); } } $this->handleState($taskConfiguration->getState()); diff --git a/tests/Manager/ProcessManagerInitializationTest.php b/tests/Manager/ProcessManagerInitializationTest.php new file mode 100644 index 00000000..6f2646a1 --- /dev/null +++ b/tests/Manager/ProcessManagerInitializationTest.php @@ -0,0 +1,206 @@ + */ + private array $executedTasks = []; + + /** @var list */ + private array $deprecations = []; + + public function testInitializationFailureIsDeprecatedAndUpstreamTasksStillRun(): void + { + $processManager = $this->createProcessManager(skipBeforeBadTask: false); + + $exception = null; + try { + $this->execute($processManager); + } catch (\Throwable $exception) { + } + + self::assertNotNull($exception, 'The process should fail when reaching the badly initialized task'); + self::assertStringContainsString('Invalid configuration', $exception->getMessage()); + self::assertSame(['entry', 'bad'], $this->executedTasks); + self::assertSame([$this->getExpectedDeprecation()], $this->deprecations); + } + + public function testInitializationFailureOfAnUnreachedTaskIsDeprecated(): void + { + $processManager = $this->createProcessManager(skipBeforeBadTask: true); + + $this->execute($processManager); + + self::assertSame(['entry'], $this->executedTasks); + self::assertSame([$this->getExpectedDeprecation()], $this->deprecations); + } + + public function testNoDeprecationWithoutInitializationFailure(): void + { + $processManager = $this->createProcessManager(skipBeforeBadTask: false, failInitialization: false); + + $this->execute($processManager); + + self::assertSame(['entry', 'bad'], $this->executedTasks); + self::assertSame([], $this->deprecations); + } + + private function getExpectedDeprecation(): string + { + return 'The initialization of the task "bad" of the process "test.process" has failed with message "Invalid configuration". Going on with the process after an initialization failure is deprecated since v5: in v6.0, the process will fail before executing any task.'; + } + + private function execute(ProcessManager $processManager): void + { + set_error_handler(function (int $errno, string $errstr): bool { + $this->deprecations[] = $errstr; + + return true; + }, \E_USER_DEPRECATED); + try { + $processManager->execute('test.process'); + } finally { + restore_error_handler(); + } + } + + private function createProcessManager(bool $skipBeforeBadTask, bool $failInitialization = true): ProcessManager + { + $recorder = function (string $taskCode): void { + $this->executedTasks[] = $taskCode; + }; + + $entryTask = new class($recorder, $skipBeforeBadTask) implements TaskInterface { + public function __construct( + private readonly \Closure $recorder, + private readonly bool $skip, + ) { + } + + public function execute(ProcessState $state): void + { + ($this->recorder)('entry'); + $state->setOutput('value'); + $state->setSkipped($this->skip); + } + }; + + $badTask = new class($recorder, $failInitialization) implements InitializableTaskInterface { + public function __construct( + private readonly \Closure $recorder, + private readonly bool $fail, + ) { + } + + public function initialize(ProcessState $state): void + { + if ($this->fail) { + throw new \InvalidArgumentException('Invalid configuration'); + } + } + + /** + * Like configurable tasks (options are resolved again), fail again when executed. + */ + public function execute(ProcessState $state): void + { + ($this->recorder)('bad'); + $this->initialize($state); + } + }; + + $container = new Container(); + $container->set('test.entry', $entryTask); + $container->set('test.bad', $badTask); + + $registry = new ProcessConfigurationRegistry( + [ + 'test.process' => [ + 'options' => [], + 'entry_point' => null, + 'end_point' => null, + 'description' => '', + 'help' => '', + 'public' => true, + 'tasks' => [ + 'entry' => $this->createTaskConfiguration('@test.entry', ['bad']), + 'bad' => $this->createTaskConfiguration('@test.bad'), + ], + ], + ], + 'stop' + ); + + return new ProcessManager( + $container, + new ProcessLogger(new NullLogger()), + new TaskLogger(new NullLogger()), + $registry, + new ContextualOptionResolver(), + new EventDispatcher(), + ); + } + + /** + * @param list $outputs + * + * @return array + */ + private function createTaskConfiguration(string $service, array $outputs = []): array + { + return [ + 'service' => $service, + 'options' => [], + 'description' => '', + 'help' => '', + 'outputs' => $outputs, + 'errors' => [], + 'error_outputs' => [], + 'error_strategy' => null, + 'log_level' => null, + ]; + } +}