diff --git a/CHANGELOG.md b/CHANGELOG.md index eb54c41..6d63a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Latest ------ +### Fixes +* [#75](https://github.com/cleverage/flysystem-process-bundle/issues/75) `FileFetchTask` (`flysystem_file_fetch`): an unknown storage fails at initialization with an explicit message (`Unknown storage "…" (option "…")`, with the known storages) instead of `Service "…" not found`, When the task is executed after a failed initialization (process-bundle v5 goes on), it fails with an explicit message instead of `Typed property … must not be accessed before initialization`. Update documentation, add tests. + v3.3 ------ diff --git a/docs/reference/tasks/file_fetch_task.md b/docs/reference/tasks/file_fetch_task.md index e3f2340..6f24c03 100644 --- a/docs/reference/tasks/file_fetch_task.md +++ b/docs/reference/tasks/file_fetch_task.md @@ -98,7 +98,9 @@ Notes ----- * Both storages are resolved when the task is initialized, at the start of the process: an unknown storage name makes - the process fail before any task is executed. + the initialization fail (`Unknown storage "…"`, logged at the `critical` level). With process-bundle v5, the process + goes on and fails when the task is first executed (deprecated, the process will fail before executing any task in + process-bundle v6). * `file_pattern` is only tested on the files located at the root of `source_filesystem` (the listing is not recursive and directories are ignored). The pattern is tested on the path relative to the storage root: to target a sub-directory, configure a dedicated storage whose root is this directory. diff --git a/src/Task/FileFetchTask.php b/src/Task/FileFetchTask.php index 7190e76..a198f47 100644 --- a/src/Task/FileFetchTask.php +++ b/src/Task/FileFetchTask.php @@ -19,6 +19,7 @@ use CleverAge\ProcessBundle\Model\ProcessState; use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemOperator; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -50,6 +51,7 @@ public function __construct(protected readonly ServiceLocator $storages) /** * @throws \InvalidArgumentException + * @throws UnexpectedValueException */ #[\Override] public function initialize(ProcessState $state): void @@ -57,12 +59,8 @@ public function initialize(ProcessState $state): void // Configure options parent::initialize($state); - /** @var string $sourceFilesystemOption */ - $sourceFilesystemOption = $this->getOption($state, 'source_filesystem'); - $this->sourceFS = $this->storages->get($sourceFilesystemOption); - /** @var string $destinationFilesystemOption */ - $destinationFilesystemOption = $this->getOption($state, 'destination_filesystem'); - $this->destinationFS = $this->storages->get($destinationFilesystemOption); + $this->sourceFS = $this->getStorage($state, 'source_filesystem'); + $this->destinationFS = $this->getStorage($state, 'destination_filesystem'); } /** @@ -72,6 +70,11 @@ public function initialize(ProcessState $state): void */ public function execute(ProcessState $state): void { + // With process-bundle v5, a task is executed even if its initialization has failed (the error is logged) + if (!isset($this->sourceFS, $this->destinationFS)) { + throw new UnexpectedValueException('The storages of the task are not available: its initialization has failed, see the previous error'); + } + // The files are listed once per input, when its iteration starts $this->matchingFiles ??= $this->findMatchingFiles($state); @@ -104,6 +107,20 @@ public function next(ProcessState $state): bool return false; } + /** + * @throws UnexpectedValueException + */ + protected function getStorage(ProcessState $state, string $option): FilesystemOperator + { + /** @var string $storage */ + $storage = $this->getOption($state, $option); + try { + return $this->storages->get($storage); + } catch (ServiceNotFoundException $e) { + throw new UnexpectedValueException(\sprintf('Unknown storage "%s" (option "%s"), known storages: "%s"', $storage, $option, implode('", "', array_keys($this->storages->getProvidedServices()))), 0, $e); + } + } + /** * @return list * diff --git a/tests/Task/FileFetchTaskStorageTest.php b/tests/Task/FileFetchTaskStorageTest.php index 316ae59..7349352 100644 --- a/tests/Task/FileFetchTaskStorageTest.php +++ b/tests/Task/FileFetchTaskStorageTest.php @@ -13,6 +13,7 @@ namespace CleverAge\FlysystemProcessBundle\Tests\Task; +use CleverAge\FlysystemProcessBundle\Exception\UnexpectedValueException; use CleverAge\FlysystemProcessBundle\Task\FileFetchTask; use CleverAge\ProcessBundle\Model\ProcessState; use PHPUnit\Framework\Attributes\CoversClass; @@ -143,7 +144,8 @@ public function testNextBeforeExecute(): void public function testUnknownStorage(): void { - $this->expectException(\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException::class); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Unknown storage "unknown" (option "source_filesystem")'); $this->createTask(['source_filesystem' => 'unknown']); } diff --git a/tests/Task/FileFetchTaskTest.php b/tests/Task/FileFetchTaskTest.php index 86bf533..0bd010f 100644 --- a/tests/Task/FileFetchTaskTest.php +++ b/tests/Task/FileFetchTaskTest.php @@ -13,6 +13,7 @@ namespace CleverAge\FlysystemProcessBundle\Tests\Task; +use CleverAge\FlysystemProcessBundle\Exception\UnexpectedValueException; use CleverAge\FlysystemProcessBundle\Task\FileFetchTask; use CleverAge\ProcessBundle\Model\ProcessState; use League\Flysystem\FilesystemOperator; @@ -64,6 +65,31 @@ public function testWriteFailureKeepsSourceAndFails(): void $task->execute($state); } + public function testUnknownStorageFailsAtInitialization(): void + { + $task = $this->createTask(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Unknown storage "unknown" (option "destination_filesystem"), known storages: "source", "destination"'); + $task->initialize($this->createState('unknown')); + } + + public function testTaskFailsExplicitlyWhenExecutedAfterAFailedInitialization(): void + { + $state = $this->createState('unknown'); + $task = $this->createTask(); + try { + $task->initialize($state); + self::fail('The initialization should have failed'); + } catch (UnexpectedValueException) { + } + + $this->destination->expects($this->never())->method('writeStream'); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('The storages of the task are not available: its initialization has failed, see the previous error'); + $task->execute($state); + } + private function createTask(): FileFetchTask { /** @var ServiceLocator $storages */ @@ -75,12 +101,12 @@ private function createTask(): FileFetchTask return new FileFetchTask($storages); } - private function createState(): ProcessState&MockObject + private function createState(string $destination = 'destination'): ProcessState&MockObject { $state = $this->createMock(ProcessState::class); $state->method('getContextualizedOptions')->willReturn([ 'source_filesystem' => 'source', - 'destination_filesystem' => 'destination', + 'destination_filesystem' => $destination, 'remove_source' => true, ]); $state->method('getInput')->willReturn('file.txt');