Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
3 changes: 3 additions & 0 deletions docs/02-task_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/03-custom_tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion docs/04-advanced_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions src/Manager/ProcessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
206 changes: 206 additions & 0 deletions tests/Manager/ProcessManagerInitializationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

declare(strict_types=1);

/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (c) Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Tests\Manager;

use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
use CleverAge\ProcessBundle\Event\ProcessEvent;
use CleverAge\ProcessBundle\Logger\AbstractLogger;
use CleverAge\ProcessBundle\Logger\ProcessLogger;
use CleverAge\ProcessBundle\Logger\TaskLogger;
use CleverAge\ProcessBundle\Manager\ProcessManager;
use CleverAge\ProcessBundle\Model\InitializableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessHistory;
use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Model\TaskInterface;
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
* An exception thrown by initialize() does not abort the process (deprecated since v5, the process will fail before
* executing any task in v6.0).
*/
#[\PHPUnit\Framework\Attributes\CoversClass(ProcessManager::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfiguration::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(TaskConfiguration::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ContextualOptionResolver::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessEvent::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(AbstractLogger::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessHistory::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessState::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfigurationRegistry::class)]
class ProcessManagerInitializationTest extends TestCase
{
/** @var list<string> */
private array $executedTasks = [];

/** @var list<string> */
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<string> $outputs
*
* @return array<string, mixed>
*/
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,
];
}
}
Loading