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 @@ -2,6 +2,7 @@ Latest
------

## Changes
* [#317](https://github.com/cleverage/process-bundle/issues/317) The bundle throws exceptions implementing `ProcessExceptionInterface` instead of raw SPL exceptions (124 `throw`): new `Exception\UnexpectedValueException`, `RuntimeException`, `InvalidArgumentException` and `LogicException`, extending the SPL exceptions thrown before (same messages, the existing `catch` keep working). All the errors of the bundle can be caught with `ProcessExceptionInterface`, except the exceptions of the Symfony components. Update documentation, add tests.
* [#311](https://github.com/cleverage/process-bundle/issues/311) Deprecate the `cleverage_process.event_listener.data_queue` service and `DataQueueEventListener` (it keeps a copy of the `ProcessState` of every `cleverage_process.data_queue` event in memory, without limit): injecting or fetching the service triggers a deprecation, they will be removed in v6.0. Write your own listener of the event dispatched by the `event_dispatcher` task. Update documentation, add tests.
* [#312](https://github.com/cleverage/process-bundle/issues/312) Add read-only interfaces for the process manager and the registries, implemented by the existing classes and autowired by their interface: `ProcessManagerInterface` (`execute()`), `ProcessConfigurationRegistryInterface` (`getProcessConfiguration()`, `getProcessConfigurations()`, `hasProcessConfiguration()`), `TransformerRegistryInterface` (`getTransformer()`, `getTransformers()`, `hasTransformer()`) and `TaskRegistryInterface` (`getTask()`). Type your services on them: the classes will be `final` in v6.0 (the services of the bundle will then be typed on the interfaces). Update documentation, add tests.
* [#310](https://github.com/cleverage/process-bundle/issues/310) Process configuration tree: a task `error_strategy` other than `skip` or `stop` is deprecated (notice when the container is compiled; it was accepted and the process failed with "Unknown error strategy" when the task failed), it will be rejected in v6.0. The process `public` key is a boolean node: a non-boolean value is rejected when the container is compiled, instead of a `TypeError` when the configuration is built. Describe the nodes of the process, task and generic transformer configurations (`config:dump-reference clever_age_process`). Update documentation, add tests.
Expand Down
6 changes: 6 additions & 0 deletions docs/04-advanced_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Note that a task error handled by the `stop` strategy does not surface as the or
throws a `CleverAge\ProcessBundle\Exception\ProcessFailedException` (a `\RuntimeException`), whose message contains
the process code, the task code and the original message. The original exception is available with `getPrevious()`.

The exceptions thrown by the bundle implement `CleverAge\ProcessBundle\Exception\ProcessExceptionInterface` (since 5.4 for
the `UnexpectedValueException`, `RuntimeException`, `InvalidArgumentException` and `LogicException` of the same
namespace, which extend the SPL exceptions thrown before), so that they can all be caught at once. Not concerned: the
exceptions of the Symfony components (e.g. `InvalidOptionsException` for an invalid task option) and the ones of your
own tasks and transformers.

### Executing a process from PHP

The process manager service (`cleverage_process.manager.process`, private) can be autowired by its interface,
Expand Down
7 changes: 4 additions & 3 deletions src/Command/ExecuteProcessCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace CleverAge\ProcessBundle\Command;

use CleverAge\ProcessBundle\Event\ConsoleProcessEvent;
use CleverAge\ProcessBundle\Exception\InvalidArgumentException;
use CleverAge\ProcessBundle\Filesystem\JsonStreamFile;
use CleverAge\ProcessBundle\Manager\ProcessManager;
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
Expand Down Expand Up @@ -125,7 +126,7 @@ protected function parseContextValues(InputInterface $input): array
preg_match($pattern, (string) $contextValue, $parts);
if (3 !== \count($parts)
|| $parts[0] !== $contextValue) {
throw new \InvalidArgumentException(\sprintf('Invalid context %s', $contextValue));
throw new InvalidArgumentException(\sprintf('Invalid context %s', $contextValue));
}
$context[$parts[1]] = $parser->parse($parts[2]);
}
Expand All @@ -150,7 +151,7 @@ protected function handleOutputData(mixed $data, InputInterface $input, OutputIn
} elseif (self::OUTPUT_FORMAT_JSON === $input->getOption('output-format')) {
$output->writeln(json_encode($data, \JSON_THROW_ON_ERROR));
} else {
throw new \InvalidArgumentException(\sprintf("Cannot handle data output with format '%s'", $input->getOption('output-format')));
throw new InvalidArgumentException(\sprintf("Cannot handle data output with format '%s'", $input->getOption('output-format')));
}
}
} elseif (self::OUTPUT_FORMAT_JSON === $input->getOption('output-format')) {
Expand All @@ -165,7 +166,7 @@ protected function handleOutputData(mixed $data, InputInterface $input, OutputIn
$output->writeln(\sprintf("Output stored in '%s'", $input->getOption('output')));
}
} else {
throw new \InvalidArgumentException(\sprintf("Cannot handle data output with format '%s'", $input->getOption('output-format')));
throw new InvalidArgumentException(\sprintf("Cannot handle data output with format '%s'", $input->getOption('output-format')));
}
}
}
10 changes: 6 additions & 4 deletions src/Command/ProcessHelpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Exception\InvalidArgumentException;
use CleverAge\ProcessBundle\Exception\UnexpectedValueException;
use CleverAge\ProcessBundle\Model\BlockingTaskInterface;
use CleverAge\ProcessBundle\Model\FlushableTaskInterface;
use CleverAge\ProcessBundle\Model\IterableTaskInterface;
Expand Down Expand Up @@ -185,7 +187,7 @@ protected function findBestNextTask(
}

if ([] === $taskCandidates) {
throw new \UnexpectedValueException('Cannot find a task to output');
throw new UnexpectedValueException('Cannot find a task to output');
}

// Try to find the task the most on the right
Expand All @@ -198,7 +200,7 @@ protected function findBestNextTask(

// Should never be non-numeric...
if (!is_numeric($key)) {
throw new \UnexpectedValueException('Invalid key type');
throw new UnexpectedValueException('Invalid key type');
}
$weight += $key;
}
Expand Down Expand Up @@ -408,7 +410,7 @@ static function ($taskCode, $i) use ($gapBranches, $origin, $final, $branches):
array_shift($nextTasks);
$origin = array_search($taskCode, $branches, true);
if (false === $origin) {
throw new \UnexpectedValueException("Task '{$taskCode}' is not in any branch");
throw new UnexpectedValueException("Task '{$taskCode}' is not in any branch");
}
$expandBranches = [];
foreach ($nextTasks as $nextTask) {
Expand Down Expand Up @@ -502,7 +504,7 @@ protected function writeBranches(
} elseif (\is_callable($char)) {
$str = $char($branchTask, $i);
} else {
throw new \InvalidArgumentException('Char must be string|callable');
throw new InvalidArgumentException('Char must be string|callable');
}
} elseif (null !== $branchTask) {
$str = self::CHAR_DOWN;
Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/Compiler/RegistryCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace CleverAge\ProcessBundle\DependencyInjection\Compiler;

use CleverAge\ProcessBundle\Exception\LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -39,7 +40,7 @@ public function __construct(
public function process(ContainerBuilder $container): void
{
if (\in_array(null, [$this->registry, $this->tag, $this->method], true)) {
throw new \LogicException('The registry, tag and method of the RegistryCompilerPass must be defined');
throw new LogicException('The registry, tag and method of the RegistryCompilerPass must be defined');
}

if (!$container->has($this->registry)) {
Expand Down
21 changes: 21 additions & 0 deletions src/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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\Exception;

/**
* Thrown by the bundle when an argument or an option does not have the expected value.
*/
class InvalidArgumentException extends \InvalidArgumentException implements ProcessExceptionInterface
{
}
21 changes: 21 additions & 0 deletions src/Exception/LogicException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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\Exception;

/**
* Thrown by the bundle when a wrong usage of the bundle (e.g. an invalid configuration), that should be fixed in the code.
*/
class LogicException extends \LogicException implements ProcessExceptionInterface
{
}
21 changes: 21 additions & 0 deletions src/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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\Exception;

/**
* Thrown by the bundle when an error that can only be found at runtime (e.g. a file that cannot be read).
*/
class RuntimeException extends \RuntimeException implements ProcessExceptionInterface
{
}
21 changes: 21 additions & 0 deletions src/Exception/UnexpectedValueException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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\Exception;

/**
* Thrown by the bundle when a value does not match the expected type or set of values (e.g. an invalid input of a task).
*/
class UnexpectedValueException extends \UnexpectedValueException implements ProcessExceptionInterface
{
}
9 changes: 6 additions & 3 deletions src/Filesystem/CsvFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace CleverAge\ProcessBundle\Filesystem;

use CleverAge\ProcessBundle\Exception\RuntimeException;
use CleverAge\ProcessBundle\Exception\UnexpectedValueException;

/**
* Read and write CSV files through a simple API.
*
Expand All @@ -37,19 +40,19 @@ public function __construct(
if (!\in_array($filePath, ['php://stdin', 'php://stdout', 'php://stderr'], true)) {
$dirname = \dirname($this->filePath);
if (!@mkdir($dirname, 0o755, true) && !is_dir($dirname)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $dirname));
throw new RuntimeException(\sprintf('Directory "%s" was not created', $dirname));
}
}

$resource = fopen($filePath, $mode);
if (false === $resource) {
throw new \UnexpectedValueException("Unable to open file: '{$filePath}' in {$mode} mode");
throw new UnexpectedValueException("Unable to open file: '{$filePath}' in {$mode} mode");
}
// All modes allowing file reading, binary safe modes are handled by stripping out the b during test
$readAllowedModes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
if (null === $headers && !\in_array(str_replace('b', '', $mode), $readAllowedModes, true)) {
// Cannot read headers if the file was just created
throw new \UnexpectedValueException("Invalid headers for {$this->getResourceName()}, you need to pass the headers manually");
throw new UnexpectedValueException("Invalid headers for {$this->getResourceName()}, you need to pass the headers manually");
}

parent::__construct($resource, $delimiter, $enclosure, $escape, $headers);
Expand Down
30 changes: 17 additions & 13 deletions src/Filesystem/CsvResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

namespace CleverAge\ProcessBundle\Filesystem;

use CleverAge\ProcessBundle\Exception\LogicException;
use CleverAge\ProcessBundle\Exception\RuntimeException;
use CleverAge\ProcessBundle\Exception\UnexpectedValueException;

/**
* Read and write CSV resources through a simple API.
*/
Expand Down Expand Up @@ -52,7 +56,7 @@ public function __construct(
) {
if (!\is_resource($resource)) {
$type = \gettype($resource);
throw new \UnexpectedValueException("Resource argument must be a resource, '{$type}' given");
throw new UnexpectedValueException("Resource argument must be a resource, '{$type}' given");
}

$this->handler = $resource;
Expand Down Expand Up @@ -140,7 +144,7 @@ public function writeHeaders(): void
public function getLineNumber(): int
{
if ($this->seekCalled) {
throw new \LogicException('Cannot get current line number after calling "seek": the line number is lost');
throw new LogicException('Cannot get current line number after calling "seek": the line number is lost');
}

return $this->lineNumber;
Expand Down Expand Up @@ -183,20 +187,20 @@ public function readLine(?int $length = null): ?array
return null;
}
$message = "Unable to parse data {$filePosition} for {$this->getResourceName()}";
throw new \UnexpectedValueException($message);
throw new UnexpectedValueException($message);
}

$count = \count($values);
if ($count !== $this->headerCount) {
$message = "Number of columns not matching {$filePosition} for {$this->getResourceName()}: ";
$message .= "{$count} columns for {$this->headerCount} headers";
throw new \UnexpectedValueException($message);
throw new UnexpectedValueException($message);
}

try {
$combined = array_combine($this->headers, $values);
} catch (\ValueError) {
throw new \RuntimeException('Cannot combine headers with values');
throw new RuntimeException('Cannot combine headers with values');
}

return $combined;
Expand Down Expand Up @@ -224,21 +228,21 @@ public function writeLine(array $fields): int
if ($count !== $this->headerCount) {
$message = "Trying to write an invalid number of columns for {$this->getResourceName()}: ";
$message .= "{$count} columns for {$this->headerCount} headers";
throw new \UnexpectedValueException($message);
throw new UnexpectedValueException($message);
}

$parsedFields = [];
foreach ($this->headers as $column) {
if (!\array_key_exists($column, $fields)) {
$message = "Missing column {$column} in given fields for {$this->getResourceName()}";
throw new \UnexpectedValueException($message);
throw new UnexpectedValueException($message);
}
$parsedFields[$column] = $fields[$column];
}

$length = $this->writeRaw($parsedFields);
if (false === $length) {
throw new \RuntimeException("Unable to write data to {$this->getResourceName()}");
throw new RuntimeException("Unable to write data to {$this->getResourceName()}");
}

return $length;
Expand All @@ -251,7 +255,7 @@ public function rewind(): void
{
$this->assertOpened();
if (!rewind($this->handler)) {
throw new \RuntimeException("Unable to rewind '{$this->getResourceName()}'");
throw new RuntimeException("Unable to rewind '{$this->getResourceName()}'");
}
$this->lineNumber = 1;
if (!$this->manualHeaders) {
Expand All @@ -265,7 +269,7 @@ public function tell(): int

$position = ftell($this->handler);
if (false === $position) {
throw new \RuntimeException("Unable to get the position in {$this->getResourceName()}");
throw new RuntimeException("Unable to get the position in {$this->getResourceName()}");
}

return $position;
Expand Down Expand Up @@ -308,7 +312,7 @@ public function getFilePath(): string
protected function assertOpened(): void
{
if ($this->closed) {
throw new \RuntimeException("{$this->getResourceName()} was closed earlier");
throw new RuntimeException("{$this->getResourceName()} was closed earlier");
}
}

Expand All @@ -324,7 +328,7 @@ protected function parseHeaders(?array $headers = null): array
$autoHeaders = $this->readRaw();
// A blank line is read as [null]
if (false === $autoHeaders || [null] === $autoHeaders) {
throw new \UnexpectedValueException("Unable to read headers for {$this->getResourceName()}");
throw new UnexpectedValueException("Unable to read headers for {$this->getResourceName()}");
}
$autoHeaders = array_map(strval(...), $autoHeaders);
// Remove BOM if any
Expand All @@ -337,7 +341,7 @@ protected function parseHeaders(?array $headers = null): array
$this->manualHeaders = true;

if ([] === $headers) {
throw new \UnexpectedValueException("Empty headers for {$this->getResourceName()}, you need to pass the headers manually");
throw new UnexpectedValueException("Empty headers for {$this->getResourceName()}, you need to pass the headers manually");
}

return $headers;
Expand Down
7 changes: 5 additions & 2 deletions src/Filesystem/JsonStreamFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace CleverAge\ProcessBundle\Filesystem;

use CleverAge\ProcessBundle\Exception\RuntimeException;
use CleverAge\ProcessBundle\Exception\UnexpectedValueException;

/**
* Wrapper around JSON files to read them in a stream.
*
Expand Down Expand Up @@ -41,7 +44,7 @@ public function __construct(
if (!\in_array($filename, ['php://stdin', 'php://stdout', 'php://stderr'], true) && !str_starts_with($mode, 'r')) {
$dirname = \dirname($filename);
if (!@mkdir($dirname, 0o755, true) && !is_dir($dirname)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $dirname));
throw new RuntimeException(\sprintf('Directory "%s" was not created', $dirname));
}
}

Expand Down Expand Up @@ -113,7 +116,7 @@ public function readLine(?int $length = null): ?array

$data = json_decode($rawLine, true, 512, $this->jsonFlags);
if (null !== $data && !\is_array($data)) {
throw new \UnexpectedValueException(\sprintf('Line %d of file "%s" must be a JSON object or array, got %s', $currentLineNumber, $this->file->getPathname(), get_debug_type($data)));
throw new UnexpectedValueException(\sprintf('Line %d of file "%s" must be a JSON object or array, got %s', $currentLineNumber, $this->file->getPathname(), get_debug_type($data)));
}

return $data;
Expand Down
Loading
Loading