From 3391e08225a337b5696b9b7a67e431b3b8b62cbf Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Fri, 23 Oct 2026 16:15:24 +0200 Subject: [PATCH] #368 - YamlWriterTask: create the parent directory for a local path only Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 ++ docs/reference/tasks/yaml_writer_task.md | 4 ++- src/Task/File/Yaml/YamlWriterTask.php | 11 +++--- tests/Task/File/Yaml/YamlWriterTaskTest.php | 40 +++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e55a44b..8a1c2f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Latest ------ +## Fixes +* [#368](https://github.com/cleverage/process-bundle/issues/368) `YamlWriterTask` (`yaml_writer`): the parent directory is created for a local path only (no scheme, or `file://`), not for a stream wrapper (`compress.zlib://`, `s3://`...), which failed, or `php://stdout`, which created a `php:` directory. Update documentation, add tests. + v5.5 ----- diff --git a/docs/reference/tasks/yaml_writer_task.md b/docs/reference/tasks/yaml_writer_task.md index c911428c..7a604a98 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 is created if needed | +| `file_path` | `string` | **X** | | Path of the file to write to. Its parent directory is created if needed (local path only) | | `inline` | `int` | | `4` | Level at which the dumper switches to inline YAML (see `Yaml::dump()`) | Examples @@ -44,4 +44,6 @@ 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. +* The parent directory is created only for a local path (no scheme, or `file://`): a stream wrapper path + (`compress.zlib://`, `s3://`, `php://stdout`...) is written as is, its directory is never created. * 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 92d1751d..ba87cdb4 100644 --- a/src/Task/File/Yaml/YamlWriterTask.php +++ b/src/Task/File/Yaml/YamlWriterTask.php @@ -30,10 +30,13 @@ public function execute(ProcessState $state): void { $options = $this->getOptions($state); $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)); + // Same as the other writers: the parent directory is created if needed, for a local path only (not for a stream + // wrapper such as compress.zlib://, s3:// or php://stdout) + if (!preg_match('#^[a-z][a-z0-9+.-]*://#i', $filePath) || str_starts_with(strtolower($filePath), 'file://')) { + $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']))) { diff --git a/tests/Task/File/Yaml/YamlWriterTaskTest.php b/tests/Task/File/Yaml/YamlWriterTaskTest.php index c0599882..5d46d433 100644 --- a/tests/Task/File/Yaml/YamlWriterTaskTest.php +++ b/tests/Task/File/Yaml/YamlWriterTaskTest.php @@ -109,6 +109,46 @@ public function testTheParentDirectoryIsCreated(): void self::assertSame(self::DATA, Yaml::parseFile($filePath)); } + public function testTheParentDirectoryOfAFileUrlIsCreated(): void + { + $filePath = $this->tmpDir.'/url/file.yaml'; + $this->execute(['file_path' => 'file://'.$filePath], self::DATA); + + self::assertSame(self::DATA, Yaml::parseFile($filePath)); + } + + public function testAStreamWrapperPathIsWritten(): void + { + $filePath = 'compress.zlib://'.$this->tmpDir.'/file.yaml.gz'; + $state = $this->execute(['file_path' => $filePath], self::DATA); + + self::assertSame($filePath, $state->getOutput()); + self::assertSame(self::DATA, Yaml::parse((string) file_get_contents($filePath))); + self::assertSame("\x1f\x8b", substr((string) file_get_contents($this->tmpDir.'/file.yaml.gz'), 0, 2)); + } + + public function testAStreamWrapperPathDoesNotCreateADirectory(): void + { + $cwd = getcwd(); + self::assertNotFalse($cwd); + chdir($this->tmpDir); + try { + $this->execute(['file_path' => 'php://memory'], self::DATA); + ob_start(); + try { + $this->execute(['file_path' => 'php://output'], self::DATA); + } finally { + $output = ob_get_clean(); + } + } finally { + chdir($cwd); + } + + self::assertSame(self::DATA, Yaml::parse((string) $output)); + + self::assertSame([], glob($this->tmpDir.'/*')); + } + public function testAWriteFailureThrows(): void { // A directory cannot be written as a file