diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad1472a..8bd5f12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Latest * [#331](https://github.com/cleverage/process-bundle/issues/331) `ProcessManager`: the iterable tasks that produced items and the blocking tasks waiting for `proceed()` are tracked by task configuration instead of task code, and forgotten when their process ends. A task of a sub-process (`process_executor`) with the same code as a task of the parent no longer makes the parent lose the output of its blocking task (without error) or skip the flush at the end of its iteration (merged batches), and a failed sub-process no longer leaves its tasks to its next execution. Add tests. * [#333](https://github.com/cleverage/process-bundle/issues/333) `ProcessManager`: restore the previous task configuration (`getTaskConfiguration()`, read by the log processors for `task_code` and `task_service`) when a task, an initialization, a finalization or a sub-process ends. The logs of an iterable task no longer lose their task after its first item, and the logs of a parent process are no longer attributed to a task of its sub-process; the failing task is kept for the logs of the process failure. Add tests. * [#335](https://github.com/cleverage/process-bundle/issues/335) `LineReaderTask` (`line_reader`) and `InputLineReaderTask` (`input_line_reader`): the last line of a file that does not end with a line break is no longer lost. Add tests. +* [#345](https://github.com/cleverage/process-bundle/issues/345) `YamlWriterTask` (`yaml_writer`): a write failure throws a `RuntimeException` instead of a PHP warning (an exception in debug mode only: in production, the path was output as if the file had been written), and the parent directory is created if needed, as for the other writers. Update documentation, add tests. v5.4.1 ----- diff --git a/docs/reference/tasks/yaml_writer_task.md b/docs/reference/tasks/yaml_writer_task.md index 9b0a3c38..c911428c 100644 --- a/docs/reference/tasks/yaml_writer_task.md +++ b/docs/reference/tasks/yaml_writer_task.md @@ -24,7 +24,7 @@ Options | Code | Type | Required | Default | Description | |-------------|----------|:--------:|---------|------------------------------------------------------------------------| -| `file_path` | `string` | **X** | | Path of the file to write to. Its parent directory must exist | +| `file_path` | `string` | **X** | | Path of the file to write to. Its parent directory is created if needed | | `inline` | `int` | | `4` | Level at which the dumper switches to inline YAML (see `Yaml::dump()`) | Examples @@ -44,3 +44,4 @@ Notes * This task is not blocking: when receiving several inputs, each one overwrites the file. Aggregate the data first (e.g. with [AggregateIterableTask](aggregate_iterable_task.md)) if needed. +* When the file cannot be written (e.g. not writable directory), the task fails with a `RuntimeException`. diff --git a/src/Task/File/Yaml/YamlWriterTask.php b/src/Task/File/Yaml/YamlWriterTask.php index a2776224..92d1751d 100644 --- a/src/Task/File/Yaml/YamlWriterTask.php +++ b/src/Task/File/Yaml/YamlWriterTask.php @@ -13,6 +13,7 @@ namespace CleverAge\ProcessBundle\Task\File\Yaml; +use CleverAge\ProcessBundle\Exception\RuntimeException; use CleverAge\ProcessBundle\Model\AbstractConfigurableTask; use CleverAge\ProcessBundle\Model\ProcessState; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -28,8 +29,17 @@ class YamlWriterTask extends AbstractConfigurableTask public function execute(ProcessState $state): void { $options = $this->getOptions($state); - file_put_contents($options['file_path'], Yaml::dump($state->getInput(), $options['inline'])); - $state->setOutput($options['file_path']); + $filePath = $options['file_path']; + // Same as the other writers: the parent directory is created if needed + $dirname = \dirname($filePath); + if (!@mkdir($dirname, 0o755, true) && !is_dir($dirname)) { + throw new RuntimeException(\sprintf('Directory "%s" was not created', $dirname)); + } + // The PHP warning is replaced by the exception below, also outside of the debug mode + if (false === @file_put_contents($filePath, Yaml::dump($state->getInput(), $options['inline']))) { + throw new RuntimeException(\sprintf('Unable to write file "%s": %s', $filePath, error_get_last()['message'] ?? 'unknown error')); + } + $state->setOutput($filePath); } protected function configureOptions(OptionsResolver $resolver): void diff --git a/tests/Task/File/Yaml/YamlWriterTaskTest.php b/tests/Task/File/Yaml/YamlWriterTaskTest.php index 951dc001..c0599882 100644 --- a/tests/Task/File/Yaml/YamlWriterTaskTest.php +++ b/tests/Task/File/Yaml/YamlWriterTaskTest.php @@ -16,6 +16,7 @@ use CleverAge\ProcessBundle\Configuration\ProcessConfiguration; use CleverAge\ProcessBundle\Configuration\TaskConfiguration; use CleverAge\ProcessBundle\Context\ContextualOptionResolver; +use CleverAge\ProcessBundle\Exception\RuntimeException; use CleverAge\ProcessBundle\Model\AbstractConfigurableTask; use CleverAge\ProcessBundle\Model\ProcessHistory; use CleverAge\ProcessBundle\Model\ProcessState; @@ -100,6 +101,23 @@ public function testEachExecutionOverwritesTheFile(): void self::assertSame("second: 2\n", file_get_contents($filePath)); } + public function testTheParentDirectoryIsCreated(): void + { + $filePath = $this->tmpDir.'/sub/dir/file.yaml'; + $this->execute(['file_path' => $filePath], self::DATA); + + self::assertSame(self::DATA, Yaml::parseFile($filePath)); + } + + public function testAWriteFailureThrows(): void + { + // A directory cannot be written as a file + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(\sprintf('Unable to write file "%s": ', $this->tmpDir)); + + $this->execute(['file_path' => $this->tmpDir], self::DATA); + } + public function testScalarInputIsDumped(): void { $filePath = $this->tmpDir.'/file.yaml';