From a1334dbe8acc23ade4e04dabc6c9d8dc0d27b0f0 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Thu, 22 Oct 2026 17:30:28 +0200 Subject: [PATCH] #345 - YamlWriterTask: throw a RuntimeException when the file cannot be written instead of outputting its path, and create the parent directory if needed. Update documentation, add tests. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 +++ docs/reference/tasks/yaml_writer_task.md | 3 ++- src/Task/File/Yaml/YamlWriterTask.php | 14 ++++++++++++-- tests/Task/File/Yaml/YamlWriterTaskTest.php | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b33c604..c24e4831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Latest ------ +## Fixes +* [#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';