From e090e9a5a9a79c53102e26a7874a381e793779bf Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 3 Nov 2025 15:26:22 +0100 Subject: [PATCH 01/37] require php 8.4 --- .github/workflows/ci.yml | 10 ++++------ CHANGELOG.md | 6 ++++++ composer.json | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 189105d..779f162 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,13 +4,11 @@ on: [push, pull_request] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@main - with: - php-version: '8.2' + uses: innmind/github-workflows/.github/workflows/cs.yml@next diff --git a/CHANGELOG.md b/CHANGELOG.md index f538bb7..7d222ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Changed + +- Requires PHP `8.4` + ## 3.5.1 - 2025-08-18 ### Fixed diff --git a/composer.json b/composer.json index bd21b72..c4c665b 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "issues": "http://github.com/innmind/io/issues" }, "require": { - "php": "~8.2", + "php": "~8.4", "innmind/immutable": "~5.13", "innmind/url": "~4.3", "innmind/time-continuum": "^4.0.2", From ccd068d5707b64983d40a2be842c8aac7b96e285 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 3 Nov 2025 15:45:44 +0100 Subject: [PATCH 02/37] update dependencies --- composer.json | 10 +++++----- proofs/frames.php | 2 +- src/Files/Write.php | 2 +- src/Internal/Socket/Server/Unix.php | 2 +- src/Internal/Stream.php | 18 +++++++++--------- src/Streams/Stream/Write.php | 2 +- tests/FunctionalTest.php | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index c4c665b..b47dbb4 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,11 @@ }, "require": { "php": "~8.4", - "innmind/immutable": "~5.13", - "innmind/url": "~4.3", - "innmind/time-continuum": "^4.0.2", - "innmind/ip": "~3.2", - "innmind/validation": "~2.0" + "innmind/immutable": "dev-next", + "innmind/url": "dev-next", + "innmind/time-continuum": "dev-next", + "innmind/ip": "dev-next", + "innmind/validation": "dev-next" }, "autoload": { "psr-4": { diff --git a/proofs/frames.php b/proofs/frames.php index 34af997..365d92b 100644 --- a/proofs/frames.php +++ b/proofs/frames.php @@ -154,7 +154,7 @@ static function($assert, $lines) use ($reader) { ->maybe(static fn($lines, $line) => $line->maybe()->map($lines)), ) ->flatMap(Frame::maybe(...)); - $values = $frame($reader(Str::of($data)))->match( + $values = $frame($reader(Str::of($data, Str\Encoding::ascii)))->match( static fn($values) => $values, static fn() => null, ); diff --git a/src/Files/Write.php b/src/Files/Write.php index fd4f27e..297eb5a 100644 --- a/src/Files/Write.php +++ b/src/Files/Write.php @@ -103,7 +103,7 @@ public function sink(Sequence $chunks): Attempt return $chunks ->map(static fn($chunk) => $chunk->toEncoding(Str\Encoding::ascii)) - ->sink(new SideEffect) + ->sink(SideEffect::identity) ->attempt( static fn($_, $chunk) => $watch() ->map(static fn($ready) => $ready->toWrite()) diff --git a/src/Internal/Socket/Server/Unix.php b/src/Internal/Socket/Server/Unix.php index 8df761c..cb671cf 100644 --- a/src/Internal/Socket/Server/Unix.php +++ b/src/Internal/Socket/Server/Unix.php @@ -73,7 +73,7 @@ public function close(): Attempt }); } - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } /** diff --git a/src/Internal/Stream.php b/src/Internal/Stream.php index d2f4045..f41241c 100644 --- a/src/Internal/Stream.php +++ b/src/Internal/Stream.php @@ -112,7 +112,7 @@ public function nonBlocking(): Maybe $_ = \stream_set_write_buffer($this->resource, 0); $_ = \stream_set_read_buffer($this->resource, 0); - return Maybe::just(new SideEffect); + return Maybe::just(SideEffect::identity); } /** @@ -132,7 +132,7 @@ public function blocking(): Maybe return Maybe::nothing(); } - return Maybe::just(new SideEffect); + return Maybe::just(SideEffect::identity); } /** @@ -157,7 +157,7 @@ public function rewind(): Attempt if ($this->closed()) { /** @var Attempt */ - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } $status = \fseek($this->resource, 0); @@ -165,7 +165,7 @@ public function rewind(): Attempt /** @var Attempt */ return match ($status) { -1 => Attempt::error(new PositionNotSeekable), - default => Attempt::result(new SideEffect), + default => Attempt::result(SideEffect::identity), }; } @@ -225,7 +225,7 @@ public function size(): Maybe public function close(): Attempt { if ($this->closed()) { - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } /** @psalm-suppress InvalidPropertyAssignmentValue */ @@ -237,7 +237,7 @@ public function close(): Attempt $this->closed = true; - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } /** @@ -313,7 +313,7 @@ public function write(Str $data): Attempt } /** @var Attempt */ - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } /** @@ -327,7 +327,7 @@ public function sync(): Attempt } if (!$this->syncable) { - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } $written = @\fsync($this->resource); @@ -338,6 +338,6 @@ public function sync(): Attempt } /** @var Attempt */ - return Attempt::result(new SideEffect); + return Attempt::result(SideEffect::identity); } } diff --git a/src/Streams/Stream/Write.php b/src/Streams/Stream/Write.php index e086f63..6258e15 100644 --- a/src/Streams/Stream/Write.php +++ b/src/Streams/Stream/Write.php @@ -108,7 +108,7 @@ public function sinkAttempts(Sequence $chunks): Attempt ->map(static fn($chunk) => $chunk->map( static fn($chunk) => $chunk->toEncoding(Str\Encoding::ascii), )) - ->sink(new SideEffect) + ->sink(SideEffect::identity) ->attempt( static fn($_, $chunk) => $chunk ->flatMap(static fn($chunk) => match ($abort()) { diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 86131fb..210569e 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -860,7 +860,7 @@ public function testServerPool() ) ->map(static fn($data) => $data->toString()); - $this->assertCount(3, $result); + $this->assertSame(3, $result->size()); $this->assertTrue($result->contains('foo')); $this->assertTrue($result->contains('bar')); $this->assertTrue($result->contains('baz')); From 8753d1baa8c0f3098f71f4e88beb68c03935d0f7 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 11 Dec 2025 13:01:53 +0100 Subject: [PATCH 03/37] bump static analysis --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b47dbb4..7366f1a 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ } }, "require-dev": { - "innmind/static-analysis": "^1.2.1", + "innmind/static-analysis": "dev-next", "innmind/black-box": "~6.1", "innmind/coding-standard": "~2.0" } From dabbd10f3b2598ae3bf3f33d5d8d76cd62306fb1 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 11 Dec 2025 13:16:33 +0100 Subject: [PATCH 04/37] remove warnings --- proofs/files.php | 8 +++---- proofs/sockets.php | 4 ++-- proofs/streams.php | 4 ++-- src/Files/Read.php | 8 +++---- src/Streams/Stream/Read/Frames/Lazy.php | 4 ++-- tests/FunctionalTest.php | 32 ++++++++++++------------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/proofs/files.php b/proofs/files.php index 537bb36..2485e3d 100644 --- a/proofs/files.php +++ b/proofs/files.php @@ -55,7 +55,7 @@ static function($assert, $chunks, $size) { ->number($loaded->size()) ->int() ->greaterThan(0); - $loaded + $_ = $loaded ->dropEnd(1) ->foreach(static fn($chunk) => $assert->same( $size, @@ -90,7 +90,7 @@ static function($assert, $chunks, $size, $encoding) { $data = \implode('', $chunks); \file_put_contents($tmp, $data); - IO::fromAmbientAuthority() + $_ = IO::fromAmbientAuthority() ->files() ->read(Path::of($tmp)) ->toEncoding($encoding) @@ -128,7 +128,7 @@ static function($assert, $lines) { ->number($loaded->size()) ->int() ->greaterThan(0); - $loaded + $_ = $loaded ->dropEnd(1) ->foreach(static fn($line) => $assert->true( $line->endsWith("\n"), @@ -175,7 +175,7 @@ static function($assert, $lines, $encoding) { $data = \implode("\n", $lines); \file_put_contents($tmp, $data); - IO::fromAmbientAuthority() + $_ = IO::fromAmbientAuthority() ->files() ->read(Path::of($tmp)) ->toEncoding($encoding) diff --git a/proofs/sockets.php b/proofs/sockets.php index 8d9d834..bf6165e 100644 --- a/proofs/sockets.php +++ b/proofs/sockets.php @@ -99,8 +99,8 @@ static function($assert) { ); $assert->same('foo', $result); - $client->close()->memoize(); - $server->close()->memoize(); + $_ = $client->close()->memoize(); + $_ = $server->close()->memoize(); }, ); }; diff --git a/proofs/streams.php b/proofs/streams.php index 0482c27..64f1d7f 100644 --- a/proofs/streams.php +++ b/proofs/streams.php @@ -285,7 +285,7 @@ static function($assert, $a, $b, $encoding) { ->chunks(); $assert->same(2, $chunks->size()); - $chunks->foreach(static fn($chunk) => $assert->same( + $_ = $chunks->foreach(static fn($chunk) => $assert->same( $encoding, $chunk->value()->encoding(), )); @@ -338,7 +338,7 @@ static function($assert, $a, $b, $encoding) { ->chunks(); $assert->same(2, $chunks->size()); - $chunks->foreach(static fn($chunk) => $assert->same( + $_ = $chunks->foreach(static fn($chunk) => $assert->same( $encoding, $chunk->value()->encoding(), )); diff --git a/src/Files/Read.php b/src/Files/Read.php index 1813413..1b2550c 100644 --- a/src/Files/Read.php +++ b/src/Files/Read.php @@ -135,7 +135,7 @@ public function chunks(int $size): Sequence $rewind(); if ($autoClose) { - $stream->close()->match( + $_ = $stream->close()->match( static fn() => null, static fn() => throw new FailedToLoadStream, ); @@ -161,7 +161,7 @@ public function chunks(int $size): Sequence $rewind(); if ($autoClose) { - $stream->close()->match( + $_ = $stream->close()->match( static fn() => null, static fn() => throw new FailedToLoadStream, ); @@ -197,7 +197,7 @@ public function lines(): Sequence $rewind(); if ($autoClose) { - $stream->close()->match( + $_ = $stream->close()->match( static fn() => null, static fn() => throw new FailedToLoadStream, ); @@ -223,7 +223,7 @@ public function lines(): Sequence $rewind(); if ($autoClose) { - $stream->close()->match( + $_ = $stream->close()->match( static fn() => null, static fn() => throw new FailedToLoadStream, ); diff --git a/src/Streams/Stream/Read/Frames/Lazy.php b/src/Streams/Stream/Read/Frames/Lazy.php index 7bc6cd6..dd79f75 100644 --- a/src/Streams/Stream/Read/Frames/Lazy.php +++ b/src/Streams/Stream/Read/Frames/Lazy.php @@ -140,13 +140,13 @@ public function sequence(): Sequence true => $stream->blocking(), false => $stream->nonBlocking(), }; - $result->match( + $_ = $result->match( static fn() => null, static fn() => throw new \RuntimeException('Failed to set blocking mode'), ); if ($rewindable) { - $stream->rewind()->match( + $_ = $stream->rewind()->match( static fn() => null, static fn() => throw new FailedToLoadStream, ); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 210569e..d637f67 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -486,8 +486,8 @@ public function testSocketClientSend() ), ); - $client->close()->memoize(); - $server->close()->memoize(); + $_ = $client->close()->memoize(); + $_ = $server->close()->memoize(); } public function testSocketClientHeartbeatWithSocketClosing() @@ -543,7 +543,7 @@ public function testSocketClientHeartbeatWithSocketClosing() static fn() => null, ), ); - $client->close()->memoize(); + $_ = $client->close()->memoize(); } return Sequence::of(); @@ -557,7 +557,7 @@ public function testSocketClientHeartbeatWithSocketClosing() $this->assertSame(2, $heartbeats); $this->assertFalse($result, 'It should fail due to the closing of the socket'); - $server->close(); + $_ = $server->close(); } public function testSocketClientHeartbeat() @@ -631,8 +631,8 @@ public function testSocketClientHeartbeat() $this->assertSame(2, $heartbeats); $this->assertSame('bar', $result); - $client->close()->memoize(); - $server->close()->memoize(); + $_ = $client->close()->memoize(); + $_ = $server->close()->memoize(); } public function testSocketAbort() @@ -703,8 +703,8 @@ public function testSocketAbort() $this->assertSame(3, $heartbeats); $this->assertNull($result); - $client->close()->memoize(); - $server->close()->memoize(); + $_ = $client->close()->memoize(); + $_ = $server->close()->memoize(); } public function testServerAcceptConnection() @@ -754,8 +754,8 @@ public function testServerAcceptConnection() ); $this->assertSame('foo', $result); - $client->close()->memoize(); - $server->close()->memoize(); + $_ = $client->close()->memoize(); + $_ = $server->close()->memoize(); } public function testServerPool() @@ -864,11 +864,11 @@ public function testServerPool() $this->assertTrue($result->contains('foo')); $this->assertTrue($result->contains('bar')); $this->assertTrue($result->contains('baz')); - $clientFoo->close()->memoize(); - $clientBar->close()->memoize(); - $clientBaz->close()->memoize(); - $serverFoo->close()->memoize(); - $serverBar->close()->memoize(); - $serverBaz->close()->memoize(); + $_ = $clientFoo->close()->memoize(); + $_ = $clientBar->close()->memoize(); + $_ = $clientBaz->close()->memoize(); + $_ = $serverFoo->close()->memoize(); + $_ = $serverBar->close()->memoize(); + $_ = $serverBaz->close()->memoize(); } } From 913b193e2cdddf67a2eb5ec4eb8442952c4ca775 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 11 Dec 2025 13:20:19 +0100 Subject: [PATCH 05/37] add Files::require() --- .gitattributes | 1 + CHANGELOG.md | 4 ++++ fixtures/to-load.php | 3 +++ proofs/files.php | 25 +++++++++++++++++++++++++ src/Files.php | 9 +++++++++ src/Internal/Capabilities/Files.php | 25 ++++++++++++++++++++++++- 6 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 fixtures/to-load.php diff --git a/.gitattributes b/.gitattributes index 18e14aa..c25dee4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ /tests export-ignore +/fixtures export-ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d222ce..d3bc86b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `Innmind\IO\Files::require()` + ### Changed - Requires PHP `8.4` diff --git a/fixtures/to-load.php b/fixtures/to-load.php new file mode 100644 index 0000000..188daf9 --- /dev/null +++ b/fixtures/to-load.php @@ -0,0 +1,3 @@ +require()', + static function($assert) { + $assert->same( + 42, + IO::fromAmbientAuthority() + ->files() + ->require(Path::of('fixtures/to-load.php')) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + $assert->false( + IO::fromAmbientAuthority() + ->files() + ->require(Path::of('fixtures/unknown.php')) + ->match( + static fn() => true, + static fn() => false, + ), + ); + }, + ); }; diff --git a/src/Files.php b/src/Files.php index c19bcb6..b631860 100644 --- a/src/Files.php +++ b/src/Files.php @@ -13,6 +13,7 @@ use Innmind\Immutable\{ Str, Attempt, + Maybe, Sequence, }; @@ -41,6 +42,14 @@ public function write(Path $path): Write return Write::of($this->capabilities, $path); } + /** + * @return Maybe + */ + public function require(Path $path): Maybe + { + return $this->capabilities->files()->require($path); + } + /** * @param Sequence $chunks * diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 42398aa..d3676eb 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -8,7 +8,10 @@ Exception\RuntimeException, }; use Innmind\Url\Path; -use Innmind\Immutable\Attempt; +use Innmind\Immutable\{ + Attempt, + Maybe, +}; /** * @internal @@ -59,6 +62,26 @@ public function acquire($resource): Stream return Stream::of($resource); } + /** + * @return Maybe + */ + public function require(Path $path): Maybe + { + $path = $path->toString(); + + if (!\file_exists($path) || \is_dir($path)) { + /** @var Maybe */ + return Maybe::nothing(); + } + + /** + * @psalm-suppress UnresolvableInclude + * @psalm-suppress MixedArgument + * @var Maybe + */ + return Maybe::just(require $path); + } + /** * @return Attempt */ From eaff2ff20f5147ae51c0eb3aeccd6f009c5db575 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 11 Dec 2025 13:28:08 +0100 Subject: [PATCH 06/37] let Attempts throw exceptions --- src/Files/Read.php | 37 ++++++------------------- src/Files/Write.php | 5 +--- src/Streams/Stream/Read/Frames/Lazy.php | 18 ++++-------- 3 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/Files/Read.php b/src/Files/Read.php index 1b2550c..9182811 100644 --- a/src/Files/Read.php +++ b/src/Files/Read.php @@ -6,7 +6,6 @@ use Innmind\IO\{ Stream\Size, Internal, - Exception\FailedToLoadStream, Internal\Capabilities, Internal\Watch, }; @@ -15,6 +14,7 @@ Str, Maybe, Sequence, + SideEffect, }; final class Read @@ -45,10 +45,7 @@ public static function of( static fn() => $capabilities ->files() ->read($path) - ->match( - static fn($stream) => $stream, - static fn() => throw new \RuntimeException('Failed to read file'), - ), + ->unwrap(), $capabilities->watch(), $encoding, true, @@ -126,19 +123,13 @@ public function chunks(int $size): Sequence $chunks = Sequence::lazy(static function($register) use ($size, $load, $watch, $autoClose) { $stream = $load(); $wait = Internal\Stream\Wait::of($watch, $stream); - $rewind = static fn(): null => $stream->rewind()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $rewind = static fn(): SideEffect => $stream->rewind()->unwrap(); $register(static function() use ($rewind, $stream, $autoClose) { $rewind(); if ($autoClose) { - $_ = $stream->close()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $_ = $stream->close()->unwrap(); } }); $rewind(); @@ -161,10 +152,7 @@ public function chunks(int $size): Sequence $rewind(); if ($autoClose) { - $_ = $stream->close()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $_ = $stream->close()->unwrap(); } }); @@ -188,19 +176,13 @@ public function lines(): Sequence $chunks = Sequence::lazy(static function($register) use ($load, $watch, $autoClose) { $stream = $load(); $wait = Internal\Stream\Wait::of($watch, $stream); - $rewind = static fn(): null => $stream->rewind()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $rewind = static fn(): SideEffect => $stream->rewind()->unwrap(); $register(static function() use ($rewind, $stream, $autoClose) { $rewind(); if ($autoClose) { - $_ = $stream->close()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $_ = $stream->close()->unwrap(); } }); $rewind(); @@ -223,10 +205,7 @@ public function lines(): Sequence $rewind(); if ($autoClose) { - $_ = $stream->close()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $_ = $stream->close()->unwrap(); } }); diff --git a/src/Files/Write.php b/src/Files/Write.php index 297eb5a..6a0e4e3 100644 --- a/src/Files/Write.php +++ b/src/Files/Write.php @@ -42,10 +42,7 @@ public static function of(Capabilities $capabilities, Path $path): self static fn() => $capabilities ->files() ->write($path) - ->match( - static fn($stream) => $stream, - static fn() => throw new \RuntimeException('Failed to open file'), - ), + ->unwrap(), true, false, ); diff --git a/src/Streams/Stream/Read/Frames/Lazy.php b/src/Streams/Stream/Read/Frames/Lazy.php index dd79f75..24d75b8 100644 --- a/src/Streams/Stream/Read/Frames/Lazy.php +++ b/src/Streams/Stream/Read/Frames/Lazy.php @@ -6,7 +6,6 @@ use Innmind\IO\{ Streams\Stream\Write, Frame, - Exception\FailedToLoadStream, Internal\Stream, Internal\Watch, Internal\Reader, @@ -140,23 +139,16 @@ public function sequence(): Sequence true => $stream->blocking(), false => $stream->nonBlocking(), }; - $_ = $result->match( - static fn() => null, - static fn() => throw new \RuntimeException('Failed to set blocking mode'), - ); + $_ = $result + ->attempt(static fn() => new \RuntimeException('Failed to set blocking mode')) + ->unwrap(); if ($rewindable) { - $_ = $stream->rewind()->match( - static fn() => null, - static fn() => throw new FailedToLoadStream, - ); + $_ = $stream->rewind()->unwrap(); } while (!$stream->end()) { - yield $frame($reader)->match( - static fn($frame): mixed => $frame, - static fn($e) => throw $e, - ); + yield $frame($reader)->unwrap(); } }); } From 591deed88de02a1cd2182f2356373c5386bc4c06 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 11 Dec 2025 13:28:45 +0100 Subject: [PATCH 07/37] remove unused exception --- src/Exception/FailedToLoadStream.php | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/Exception/FailedToLoadStream.php diff --git a/src/Exception/FailedToLoadStream.php b/src/Exception/FailedToLoadStream.php deleted file mode 100644 index 25ddbac..0000000 --- a/src/Exception/FailedToLoadStream.php +++ /dev/null @@ -1,8 +0,0 @@ - Date: Thu, 11 Dec 2025 13:45:53 +0100 Subject: [PATCH 08/37] allow io capabilities composition --- src/IO.php | 7 +++++-- src/Internal/Capabilities.php | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/IO.php b/src/IO.php index 1d2a94c..fedea54 100644 --- a/src/IO.php +++ b/src/IO.php @@ -23,9 +23,12 @@ public static function fromAmbientAuthority(): self * * @internal */ - public static function async(Clock $clock): self + public static function async(self $io, Clock $clock): self { - return new self(Capabilities::async($clock)); + return new self(Capabilities::async( + $io->capabilities, + $clock, + )); } public function files(): Files diff --git a/src/Internal/Capabilities.php b/src/Internal/Capabilities.php index 24b72bd..ef36557 100644 --- a/src/Internal/Capabilities.php +++ b/src/Internal/Capabilities.php @@ -3,6 +3,11 @@ namespace Innmind\IO\Internal; +use Innmind\IO\Internal\Capabilities\{ + Implementation, + AmbientAuthority, + Async, +}; use Innmind\TimeContinuum\Clock; /** @@ -10,13 +15,8 @@ */ final class Capabilities { - /** - * The async nature is determined by the presence of the clock only as the - * sync implementation don't need one. And having a separate bool flag would - * not be understood by Psalm. - */ private function __construct( - private ?Clock $clock, + private Implementation $implementation, ) { } @@ -25,37 +25,37 @@ private function __construct( */ public static function fromAmbientAuthority(): self { - return new self(null); + return new self(AmbientAuthority::of()); } /** * @internal */ - public static function async(Clock $clock): self + public static function async(self $capabilities, Clock $clock): self { - return new self($clock); + return new self(Async::of( + $capabilities->implementation, + $clock, + )); } public function files(): Capabilities\Files { - return Capabilities\Files::of(); + return $this->implementation->files(); } public function streams(): Capabilities\Streams { - return Capabilities\Streams::of(); + return $this->implementation->streams(); } public function sockets(): Capabilities\Sockets { - return Capabilities\Sockets::of(); + return $this->implementation->sockets(); } public function watch(): Watch { - return match ($this->clock) { - null => Watch::sync(), - default => Watch::async($this->clock), - }; + return $this->implementation->watch(); } } From 69dcc98286db5cd04211c3f08c8f9aa2aa21b735 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 11 Dec 2025 13:55:13 +0100 Subject: [PATCH 09/37] add missing classes --- .../Capabilities/AmbientAuthority.php | 48 +++++++++++++++++ src/Internal/Capabilities/Async.php | 51 +++++++++++++++++++ src/Internal/Capabilities/Implementation.php | 17 +++++++ 3 files changed, 116 insertions(+) create mode 100644 src/Internal/Capabilities/AmbientAuthority.php create mode 100644 src/Internal/Capabilities/Async.php create mode 100644 src/Internal/Capabilities/Implementation.php diff --git a/src/Internal/Capabilities/AmbientAuthority.php b/src/Internal/Capabilities/AmbientAuthority.php new file mode 100644 index 0000000..4ad6376 --- /dev/null +++ b/src/Internal/Capabilities/AmbientAuthority.php @@ -0,0 +1,48 @@ +capabilities->files(); + } + + #[\Override] + public function streams(): Streams + { + return $this->capabilities->streams(); + } + + #[\Override] + public function sockets(): Sockets + { + return $this->capabilities->sockets(); + } + + #[\Override] + public function watch(): Watch + { + return Watch::async($this->clock); + } +} diff --git a/src/Internal/Capabilities/Implementation.php b/src/Internal/Capabilities/Implementation.php new file mode 100644 index 0000000..ca8706b --- /dev/null +++ b/src/Internal/Capabilities/Implementation.php @@ -0,0 +1,17 @@ + Date: Thu, 11 Dec 2025 14:31:56 +0100 Subject: [PATCH 10/37] remove unused method --- src/Internal/Capabilities/Files.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index d3676eb..82fe859 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -54,14 +54,6 @@ public function temporary(): Attempt return $this->open('php://temp', 'r+'); } - /** - * @param resource $resource - */ - public function acquire($resource): Stream - { - return Stream::of($resource); - } - /** * @return Maybe */ From df25b75f02dcc39196ccc2dd15378557efea9f2c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Dec 2025 13:51:19 +0100 Subject: [PATCH 11/37] simplify type juggling --- src/Files/Temporary/Push.php | 5 +---- src/Files/Write.php | 5 +---- src/Frame/Maybe.php | 5 ++--- src/Internal/Stream/Wait.php | 5 +---- src/Streams/Stream/Write.php | 5 +---- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/Files/Temporary/Push.php b/src/Files/Temporary/Push.php index 3068d05..16235b2 100644 --- a/src/Files/Temporary/Push.php +++ b/src/Files/Temporary/Push.php @@ -64,10 +64,7 @@ public function chunk(Str $chunk): Attempt static fn($ready) => $ready ->toWrite() ->find(static fn($ready) => $ready === $stream) - ->match( - Attempt::result(...), - static fn() => Attempt::error(new \RuntimeException('Stream not ready')), - ), + ->attempt(static fn() => new \RuntimeException('Stream not ready')), ) ->flatMap(static fn($stream) => $stream->write($chunk->toEncoding(Str\Encoding::ascii))); } diff --git a/src/Files/Write.php b/src/Files/Write.php index 6a0e4e3..d4cda91 100644 --- a/src/Files/Write.php +++ b/src/Files/Write.php @@ -107,10 +107,7 @@ public function sink(Sequence $chunks): Attempt ->flatMap( static fn($toWrite) => $toWrite ->find(static fn($ready) => $ready === $stream) - ->match( - static fn($stream) => Attempt::result($stream), - static fn() => Attempt::error(new RuntimeException('Stream not ready to write to')), - ), + ->attempt(static fn() => new RuntimeException('Stream not ready to write to')), ) ->flatMap(static fn($stream) => $stream->write($chunk)), ) diff --git a/src/Frame/Maybe.php b/src/Frame/Maybe.php index 9b5e957..4c7aa7b 100644 --- a/src/Frame/Maybe.php +++ b/src/Frame/Maybe.php @@ -35,9 +35,8 @@ private function __construct( #[\Override] public function __invoke(Reader|Reader\Buffer $reader): Attempt { - return $this->value->match( - static fn($value) => Attempt::result($value), - static fn() => Attempt::error(new RuntimeException('No value provided')), + return $this->value->attempt( + static fn() => new RuntimeException('No value provided'), ); } diff --git a/src/Internal/Stream/Wait.php b/src/Internal/Stream/Wait.php index b13e7a7..f5bacfc 100644 --- a/src/Internal/Stream/Wait.php +++ b/src/Internal/Stream/Wait.php @@ -41,10 +41,7 @@ public function __invoke(): Attempt fn($toRead) => $toRead ->find(fn($ready) => $ready === $this->stream) ->keep(Instance::of(Stream::class)) - ->match( - static fn($stream) => Attempt::result($stream), - static fn() => Attempt::error(new RuntimeException('Stream not ready')), - ), + ->attempt(static fn() => new RuntimeException('Stream not ready')), ); } diff --git a/src/Streams/Stream/Write.php b/src/Streams/Stream/Write.php index 6258e15..9a60839 100644 --- a/src/Streams/Stream/Write.php +++ b/src/Streams/Stream/Write.php @@ -121,10 +121,7 @@ public function sinkAttempts(Sequence $chunks): Attempt ->flatMap( static fn($toWrite) => $toWrite ->find(static fn($ready) => $ready === $stream) - ->match( - static fn($stream) => Attempt::result($stream), - static fn() => Attempt::error(new RuntimeException('Stream not ready to write to')), - ), + ->attempt(static fn() => new RuntimeException('Stream not ready to write to')), ) ->flatMap(static fn($stream) => $stream->write($chunk)), ), From 777af53e53182dc42acf995928056cf6577b7154 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Dec 2025 16:18:50 +0100 Subject: [PATCH 12/37] add Files::mediaType() and Files::list() --- src/Files.php | 21 ++++++++++++++ src/Internal/Capabilities/Files.php | 43 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/Files.php b/src/Files.php index b631860..c4e7176 100644 --- a/src/Files.php +++ b/src/Files.php @@ -7,6 +7,7 @@ Files\Read, Files\Temporary, Files\Write, + Files\Name, Internal\Capabilities, }; use Innmind\Url\Path; @@ -69,4 +70,24 @@ public function temporary(Sequence $chunks): Attempt ->map(static fn() => Temporary::of($capabilities, $tmp)), ); } + + /** + * @experimental + * + * @return Sequence + */ + public function list(Path $path): Sequence + { + return $this->capabilities->files()->list($path); + } + + /** + * @experimental + * + * @return Attempt + */ + public function mediaType(Path $path): Attempt + { + return $this->capabilities->files()->mediaType($path); + } } diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 82fe859..50a2cac 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -5,12 +5,14 @@ use Innmind\IO\{ Internal\Stream, + Files\Name, Exception\RuntimeException, }; use Innmind\Url\Path; use Innmind\Immutable\{ Attempt, Maybe, + Sequence, }; /** @@ -74,6 +76,47 @@ public function require(Path $path): Maybe return Maybe::just(require $path); } + /** + * @return Sequence + */ + public function list(Path $path): Sequence + { + return Sequence::lazy(static function() use ($path): \Generator { + $files = new \FilesystemIterator($path->toString()); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + if ($file->isLink()) { + continue; + } + + $name = $file->getBasename(); + + if ($name === '') { + continue; + } + + yield Name::of( + $name, + $file->isDir(), + ); + } + }); + } + + /** + * @return Attempt + */ + public function mediaType(Path $path): Attempt + { + $mediaType = @\mime_content_type($path->toString()); + + return match ($mediaType) { + false => Attempt::error(new \RuntimeException('Failed to access media type')), + default => Attempt::result($mediaType), + }; + } + /** * @return Attempt */ From cde3a70451d03ba17400a4c75bb50201cfac9a40 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Dec 2025 16:38:50 +0100 Subject: [PATCH 13/37] add missing class --- src/Files/Name.php | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Files/Name.php diff --git a/src/Files/Name.php b/src/Files/Name.php new file mode 100644 index 0000000..5d7156b --- /dev/null +++ b/src/Files/Name.php @@ -0,0 +1,42 @@ +directory; + } + + /** + * @return non-empty-string + */ + public function toString(): string + { + return $this->name; + } +} From 858aa8927675ab8147fe55125832d1243554dad2 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 13 Dec 2025 16:52:39 +0100 Subject: [PATCH 14/37] add Files::exists() --- src/Files.php | 8 ++++++++ src/Internal/Capabilities/Files.php | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Files.php b/src/Files.php index c4e7176..7cfacaf 100644 --- a/src/Files.php +++ b/src/Files.php @@ -90,4 +90,12 @@ public function mediaType(Path $path): Attempt { return $this->capabilities->files()->mediaType($path); } + + /** + * @experimental + */ + public function exists(Path $path): bool + { + return $this->capabilities->files()->exists($path); + } } diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 50a2cac..1108279 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -117,6 +117,22 @@ public function mediaType(Path $path): Attempt }; } + public function exists(Path $path): bool + { + if (!\file_exists($path->toString())) { + return false; + } + + if (\is_link($path->toString())) { + return false; + } + + return match ($path->directory()) { + false => true, + true => \is_dir($path->toString()), + }; + } + /** * @return Attempt */ From 0c796a30941e502d85cd1c41ce5863841bba769e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 12:54:41 +0100 Subject: [PATCH 15/37] add Files::create() --- src/Files.php | 11 ++++++ src/Internal/Capabilities/Files.php | 60 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/Files.php b/src/Files.php index 7cfacaf..fb7da75 100644 --- a/src/Files.php +++ b/src/Files.php @@ -16,6 +16,7 @@ Attempt, Maybe, Sequence, + SideEffect, }; final class Files @@ -98,4 +99,14 @@ public function exists(Path $path): bool { return $this->capabilities->files()->exists($path); } + + /** + * @experimental + * + * @return Attempt + */ + public function create(Path $path): Attempt + { + return $this->capabilities->files()->create($path); + } } diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 1108279..9470d9b 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -13,6 +13,7 @@ Attempt, Maybe, Sequence, + SideEffect, }; /** @@ -133,6 +134,17 @@ public function exists(Path $path): bool }; } + /** + * @return Attempt + */ + public function create(Path $path): Attempt + { + return match ($path->directory()) { + true => $this->createDirectory($path), + false => $this->touch($path), + }; + } + /** * @return Attempt */ @@ -147,4 +159,52 @@ private function open(string $path, string $mode): Attempt return Attempt::result(Stream::file($stream)); } + + /** + * @return Attempt + */ + private function createDirectory(Path $path): Attempt + { + $path = $path->toString(); + + // We do not check the result of this function as it will return false + // if the path already exist. This can lead to race conditions where + // another process created the directory between the condition that + // checked if it existed and the call to this method. The only important + // part is to check wether the directory exists or not afterward. + @\mkdir($path, recursive: true); + + if (!\is_dir($path)) { + return Attempt::error(new \RuntimeException(\sprintf( + "Failed to create directory '%s'", + $path, + ))); + } + + return Attempt::result(SideEffect::identity); + } + + /** + * @return Attempt + */ + private function touch(Path $path): Attempt + { + $path = $path->toString(); + + if (!@\touch($path)) { + return Attempt::error(new \RuntimeException(\sprintf( + "Failed to create file '%s'", + $path, + ))); + } + + if (!\file_exists($path)) { + return Attempt::error(new \RuntimeException(\sprintf( + "Failed to create file '%s'", + $path, + ))); + } + + return Attempt::result(SideEffect::identity); + } } From 5b609bd4ba629a3ab876b21f57b737877840f899 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 13:32:55 +0100 Subject: [PATCH 16/37] add Files::remove() --- src/Files.php | 10 +++++ src/Internal/Capabilities/Files.php | 58 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/Files.php b/src/Files.php index fb7da75..574dad9 100644 --- a/src/Files.php +++ b/src/Files.php @@ -109,4 +109,14 @@ public function create(Path $path): Attempt { return $this->capabilities->files()->create($path); } + + /** + * @experimental + * + * @return Attempt + */ + public function remove(Path $path): Attempt + { + return $this->capabilities->files()->remove($path); + } } diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 9470d9b..3fd9a78 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -145,6 +145,21 @@ public function create(Path $path): Attempt }; } + /** + * @return Attempt + */ + public function remove(Path $path): Attempt + { + if (!\file_exists($path->toString())) { + return Attempt::result(SideEffect::identity); + } + + return match ($path->directory() && \is_dir($path->toString())) { + true => $this->rmdir($path), + false => $this->unlink($path->toString()), + }; + } + /** * @return Attempt */ @@ -207,4 +222,47 @@ private function touch(Path $path): Attempt return Attempt::result(SideEffect::identity); } + + /** + * @return Attempt + */ + private function rmdir(Path $path): Attempt + { + return $this + ->list($path) + ->map(static fn($name) => \sprintf( + '%s%s%s', + $path->toString(), + $name->toString(), + match ($name->directory()) { + true => '/', + false => '', + }, + )) + ->map(Path::of(...)) + ->sink(SideEffect::identity) + ->attempt(fn($_, $file) => $this->remove($file)) + ->map(static fn() => @\rmdir($path->toString())) + ->flatMap(static fn($removed) => match ($removed) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException(\sprintf( + "Failed to remove directory '%s'", + $path->toString(), + ))), + }); + } + + /** + * @return Attempt + */ + private function unlink(string $path): Attempt + { + return match (@\unlink($path)) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException(\sprintf( + "Failed to remove file '%s'", + $path, + ))), + }; + } } From 2cfed6d6375ad381ca49077d1ad74091a8f0078a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 13:47:32 +0100 Subject: [PATCH 17/37] fix logic to remove directories when a / is missing --- src/Internal/Capabilities/Files.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 3fd9a78..5c37dff 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -154,10 +154,17 @@ public function remove(Path $path): Attempt return Attempt::result(SideEffect::identity); } - return match ($path->directory() && \is_dir($path->toString())) { - true => $this->rmdir($path), - false => $this->unlink($path->toString()), - }; + if ($path->directory() && \is_dir($path->toString())) { + return $this->rmdir($path->toString()); + } + + $path = $path->toString(); + + if (\is_dir($path)) { + return $this->rmdir($path.'/'); + } + + return $this->unlink($path); } /** @@ -226,13 +233,13 @@ private function touch(Path $path): Attempt /** * @return Attempt */ - private function rmdir(Path $path): Attempt + private function rmdir(string $path): Attempt { return $this - ->list($path) + ->list(Path::of($path)) ->map(static fn($name) => \sprintf( '%s%s%s', - $path->toString(), + $path, $name->toString(), match ($name->directory()) { true => '/', @@ -242,12 +249,12 @@ private function rmdir(Path $path): Attempt ->map(Path::of(...)) ->sink(SideEffect::identity) ->attempt(fn($_, $file) => $this->remove($file)) - ->map(static fn() => @\rmdir($path->toString())) + ->map(static fn() => @\rmdir($path)) ->flatMap(static fn($removed) => match ($removed) { true => Attempt::result(SideEffect::identity), false => Attempt::error(new \RuntimeException(\sprintf( "Failed to remove directory '%s'", - $path->toString(), + $path, ))), }); } From 1eb10736c75388beb214e857bd60a9292cb56a78 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 14:42:10 +0100 Subject: [PATCH 18/37] better split the responsibilities on what can be done to which kind of file --- src/Files.php | 31 +++++++++---------- src/Files/Directory.php | 38 +++++++++++++++++++++++ src/Files/File.php | 48 +++++++++++++++++++++++++++++ src/Files/Kind.php | 14 +++++++++ src/Files/Link.php | 19 ++++++++++++ src/Internal/Capabilities/Files.php | 19 ++++++++++++ 6 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 src/Files/Directory.php create mode 100644 src/Files/File.php create mode 100644 src/Files/Kind.php create mode 100644 src/Files/Link.php diff --git a/src/Files.php b/src/Files.php index 574dad9..82e5331 100644 --- a/src/Files.php +++ b/src/Files.php @@ -7,7 +7,10 @@ Files\Read, Files\Temporary, Files\Write, - Files\Name, + Files\File, + Files\Directory, + Files\Link, + Files\Kind, Internal\Capabilities, }; use Innmind\Url\Path; @@ -73,23 +76,19 @@ public function temporary(Sequence $chunks): Attempt } /** - * @experimental - * - * @return Sequence - */ - public function list(Path $path): Sequence - { - return $this->capabilities->files()->list($path); - } - - /** - * @experimental - * - * @return Attempt + * @return Attempt */ - public function mediaType(Path $path): Attempt + public function access(Path $path): Attempt { - return $this->capabilities->files()->mediaType($path); + return $this + ->capabilities + ->files() + ->kind($path) + ->map(fn($kind) => match ($kind) { + Kind::directory => Directory::of($this->capabilities, $path), + Kind::file => File::of($this->capabilities, $path), + Kind::link => Link::of(), + }); } /** diff --git a/src/Files/Directory.php b/src/Files/Directory.php new file mode 100644 index 0000000..fde1b23 --- /dev/null +++ b/src/Files/Directory.php @@ -0,0 +1,38 @@ + + */ + public function list(): Sequence + { + return $this + ->capabilities + ->files() + ->list($this->path); + } +} diff --git a/src/Files/File.php b/src/Files/File.php new file mode 100644 index 0000000..0cdb1f1 --- /dev/null +++ b/src/Files/File.php @@ -0,0 +1,48 @@ +capabilities, $this->path); + } + + public function write(): Write + { + return Write::of($this->capabilities, $this->path); + } + + /** + * @return Attempt + */ + public function mediaType(): Attempt + { + return $this + ->capabilities + ->files() + ->mediaType($this->path); + } +} diff --git a/src/Files/Kind.php b/src/Files/Kind.php new file mode 100644 index 0000000..c896c3b --- /dev/null +++ b/src/Files/Kind.php @@ -0,0 +1,14 @@ + + */ + public function kind(Path $path): Attempt + { + $path = $path->toString(); + + if (!\file_exists($path)) { + return Attempt::error(new \RuntimeException('File not found')); + } + + return Attempt::result(match (true) { + \is_dir($path) => Kind::directory, + \is_link($path) => Kind::link, + default => Kind::file, + }); + } + public function exists(Path $path): bool { if (!\file_exists($path->toString())) { From 1b404fc15af566bea696956d33c77617523ee864 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 15:07:50 +0100 Subject: [PATCH 19/37] give access to the kind of created file --- src/Files.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Files.php b/src/Files.php index 82e5331..168260c 100644 --- a/src/Files.php +++ b/src/Files.php @@ -92,21 +92,26 @@ public function access(Path $path): Attempt } /** - * @experimental + * @return Attempt */ - public function exists(Path $path): bool + public function create(Path $path): Attempt { - return $this->capabilities->files()->exists($path); + return $this + ->capabilities + ->files() + ->create($path) + ->map(fn() => match ($path->directory()) { + true => Directory::of($this->capabilities, $path), + false => File::of($this->capabilities, $path), + }); } /** * @experimental - * - * @return Attempt */ - public function create(Path $path): Attempt + public function exists(Path $path): bool { - return $this->capabilities->files()->create($path); + return $this->capabilities->files()->exists($path); } /** From 3fed078b0c933920eb59a0d59eeb78e909ed8efa Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 15:28:38 +0100 Subject: [PATCH 20/37] remove assumptions that links are not usable --- src/Files/Name.php | 10 +++++----- src/Internal/Capabilities/Files.php | 20 ++++++++------------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Files/Name.php b/src/Files/Name.php index 5d7156b..3188840 100644 --- a/src/Files/Name.php +++ b/src/Files/Name.php @@ -13,7 +13,7 @@ final class Name */ private function __construct( private string $name, - private bool $directory, + private Kind $kind, ) { } @@ -22,14 +22,14 @@ private function __construct( * * @param non-empty-string $name */ - public static function of(string $name, bool $directory): self + public static function of(string $name, Kind $kind): self { - return new self($name, $directory); + return new self($name, $kind); } - public function directory(): bool + public function kind(): Kind { - return $this->directory; + return $this->kind; } /** diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 172071c..1fd3e6c 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -88,10 +88,6 @@ public function list(Path $path): Sequence /** @var \SplFileInfo $file */ foreach ($files as $file) { - if ($file->isLink()) { - continue; - } - $name = $file->getBasename(); if ($name === '') { @@ -100,7 +96,11 @@ public function list(Path $path): Sequence yield Name::of( $name, - $file->isDir(), + match (true) { + $file->isDir() => Kind::directory, + $file->isLink() => Kind::link, + default => Kind::file, + }, ); } }); @@ -143,10 +143,6 @@ public function exists(Path $path): bool return false; } - if (\is_link($path->toString())) { - return false; - } - return match ($path->directory()) { false => true, true => \is_dir($path->toString()), @@ -260,9 +256,9 @@ private function rmdir(string $path): Attempt '%s%s%s', $path, $name->toString(), - match ($name->directory()) { - true => '/', - false => '', + match ($name->kind()) { + Kind::directory => '/', + default => '', }, )) ->map(Path::of(...)) From 270f3c95dfa003b0296aff378c11cf5a54aafe36 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 15:34:27 +0100 Subject: [PATCH 21/37] only allow to remove files and directories --- src/Files.php | 11 ----------- src/Files/Directory.php | 17 ++++++++++++++++- src/Files/File.php | 16 +++++++++++++++- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Files.php b/src/Files.php index 168260c..805a50b 100644 --- a/src/Files.php +++ b/src/Files.php @@ -19,7 +19,6 @@ Attempt, Maybe, Sequence, - SideEffect, }; final class Files @@ -113,14 +112,4 @@ public function exists(Path $path): bool { return $this->capabilities->files()->exists($path); } - - /** - * @experimental - * - * @return Attempt - */ - public function remove(Path $path): Attempt - { - return $this->capabilities->files()->remove($path); - } } diff --git a/src/Files/Directory.php b/src/Files/Directory.php index fde1b23..18f8f9d 100644 --- a/src/Files/Directory.php +++ b/src/Files/Directory.php @@ -5,7 +5,11 @@ use Innmind\IO\Internal\Capabilities; use Innmind\Url\Path; -use Innmind\Immutable\Sequence; +use Innmind\Immutable\{ + Sequence, + Attempt, + SideEffect, +}; final class Directory { @@ -35,4 +39,15 @@ public function list(): Sequence ->files() ->list($this->path); } + + /** + * @return Attempt + */ + public function remove(): Attempt + { + return $this + ->capabilities + ->files() + ->remove($this->path); + } } diff --git a/src/Files/File.php b/src/Files/File.php index 0cdb1f1..686bcfb 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -5,7 +5,10 @@ use Innmind\IO\Internal\Capabilities; use Innmind\Url\Path; -use Innmind\Immutable\Attempt; +use Innmind\Immutable\{ + Attempt, + SideEffect, +}; final class File { @@ -35,6 +38,17 @@ public function write(): Write return Write::of($this->capabilities, $this->path); } + /** + * @return Attempt + */ + public function remove(): Attempt + { + return $this + ->capabilities + ->files() + ->remove($this->path); + } + /** * @return Attempt */ From 9aa756bfecceea7314b5ccbbb1b9cca6616cfecd Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 15:40:49 +0100 Subject: [PATCH 22/37] remove experimental flag --- src/Files.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Files.php b/src/Files.php index 805a50b..9e6dbf6 100644 --- a/src/Files.php +++ b/src/Files.php @@ -105,9 +105,6 @@ public function create(Path $path): Attempt }); } - /** - * @experimental - */ public function exists(Path $path): bool { return $this->capabilities->files()->exists($path); From 45005a5f2114148a3805b1629895d453d8be23fe Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 15:41:46 +0100 Subject: [PATCH 23/37] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3bc86b..a56ccef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Added - `Innmind\IO\Files::require()` +- `Innmind\IO\Files::access()` +- `Innmind\IO\Files::create()` +- `Innmind\IO\Files::exists()` ### Changed From b863639a3f8c11cd518371ec9131ffb2a7fe2be8 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 16:05:26 +0100 Subject: [PATCH 24/37] lay the structure for simulated files --- src/Internal/Capabilities.php | 9 + .../Capabilities/AmbientAuthority.php | 2 +- src/Internal/Capabilities/Files.php | 216 ++------------ .../Capabilities/Files/AmbientAuthority.php | 273 ++++++++++++++++++ .../Capabilities/Files/Implementation.php | 70 +++++ .../Capabilities/Files/Simulation.php | 90 ++++++ src/Internal/Capabilities/Simulation.php | 49 ++++ 7 files changed, 517 insertions(+), 192 deletions(-) create mode 100644 src/Internal/Capabilities/Files/AmbientAuthority.php create mode 100644 src/Internal/Capabilities/Files/Implementation.php create mode 100644 src/Internal/Capabilities/Files/Simulation.php create mode 100644 src/Internal/Capabilities/Simulation.php diff --git a/src/Internal/Capabilities.php b/src/Internal/Capabilities.php index ef36557..7c03817 100644 --- a/src/Internal/Capabilities.php +++ b/src/Internal/Capabilities.php @@ -7,6 +7,7 @@ Implementation, AmbientAuthority, Async, + Simulation, }; use Innmind\TimeContinuum\Clock; @@ -39,6 +40,14 @@ public static function async(self $capabilities, Clock $clock): self )); } + /** + * @internal + */ + public static function simulation(self $capabilities): self + { + return new self(Simulation::of($capabilities->implementation)); + } + public function files(): Capabilities\Files { return $this->implementation->files(); diff --git a/src/Internal/Capabilities/AmbientAuthority.php b/src/Internal/Capabilities/AmbientAuthority.php index 4ad6376..3a3035a 100644 --- a/src/Internal/Capabilities/AmbientAuthority.php +++ b/src/Internal/Capabilities/AmbientAuthority.php @@ -25,7 +25,7 @@ public static function of(): self #[\Override] public function files(): Files { - return Files::of(); + return Files::fromAmbientAuthority(); } #[\Override] diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 1fd3e6c..029a42c 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -5,9 +5,11 @@ use Innmind\IO\{ Internal\Stream, + Internal\Capabilities\Files\Implementation, + Internal\Capabilities\Files\AmbientAuthority, + Internal\Capabilities\Files\Simulation, Files\Name, Files\Kind, - Exception\RuntimeException, }; use Innmind\Url\Path; use Innmind\Immutable\{ @@ -22,16 +24,25 @@ */ final class Files { - private function __construct() + private function __construct( + private Implementation $implementation, + ) { + } + + /** + * @internal + */ + public static function fromAmbientAuthority(): self { + return new self(AmbientAuthority::of()); } /** * @internal */ - public static function of(): self + public static function simulation(self $files): self { - return new self; + return new self(Simulation::of($files->implementation)); } /** @@ -39,7 +50,7 @@ public static function of(): self */ public function read(Path $path): Attempt { - return $this->open($path->toString(), 'r'); + return $this->implementation->read($path); } /** @@ -47,7 +58,7 @@ public function read(Path $path): Attempt */ public function write(Path $path): Attempt { - return $this->open($path->toString(), 'w'); + return $this->implementation->write($path); } /** @@ -55,7 +66,7 @@ public function write(Path $path): Attempt */ public function temporary(): Attempt { - return $this->open('php://temp', 'r+'); + return $this->implementation->temporary(); } /** @@ -63,19 +74,7 @@ public function temporary(): Attempt */ public function require(Path $path): Maybe { - $path = $path->toString(); - - if (!\file_exists($path) || \is_dir($path)) { - /** @var Maybe */ - return Maybe::nothing(); - } - - /** - * @psalm-suppress UnresolvableInclude - * @psalm-suppress MixedArgument - * @var Maybe - */ - return Maybe::just(require $path); + return $this->implementation->require($path); } /** @@ -83,27 +82,7 @@ public function require(Path $path): Maybe */ public function list(Path $path): Sequence { - return Sequence::lazy(static function() use ($path): \Generator { - $files = new \FilesystemIterator($path->toString()); - - /** @var \SplFileInfo $file */ - foreach ($files as $file) { - $name = $file->getBasename(); - - if ($name === '') { - continue; - } - - yield Name::of( - $name, - match (true) { - $file->isDir() => Kind::directory, - $file->isLink() => Kind::link, - default => Kind::file, - }, - ); - } - }); + return $this->implementation->list($path); } /** @@ -111,12 +90,7 @@ public function list(Path $path): Sequence */ public function mediaType(Path $path): Attempt { - $mediaType = @\mime_content_type($path->toString()); - - return match ($mediaType) { - false => Attempt::error(new \RuntimeException('Failed to access media type')), - default => Attempt::result($mediaType), - }; + return $this->implementation->mediaType($path); } /** @@ -124,29 +98,12 @@ public function mediaType(Path $path): Attempt */ public function kind(Path $path): Attempt { - $path = $path->toString(); - - if (!\file_exists($path)) { - return Attempt::error(new \RuntimeException('File not found')); - } - - return Attempt::result(match (true) { - \is_dir($path) => Kind::directory, - \is_link($path) => Kind::link, - default => Kind::file, - }); + return $this->implementation->kind($path); } public function exists(Path $path): bool { - if (!\file_exists($path->toString())) { - return false; - } - - return match ($path->directory()) { - false => true, - true => \is_dir($path->toString()), - }; + return $this->implementation->exists($path); } /** @@ -154,10 +111,7 @@ public function exists(Path $path): bool */ public function create(Path $path): Attempt { - return match ($path->directory()) { - true => $this->createDirectory($path), - false => $this->touch($path), - }; + return $this->implementation->create($path); } /** @@ -165,126 +119,6 @@ public function create(Path $path): Attempt */ public function remove(Path $path): Attempt { - if (!\file_exists($path->toString())) { - return Attempt::result(SideEffect::identity); - } - - if ($path->directory() && \is_dir($path->toString())) { - return $this->rmdir($path->toString()); - } - - $path = $path->toString(); - - if (\is_dir($path)) { - return $this->rmdir($path.'/'); - } - - return $this->unlink($path); - } - - /** - * @return Attempt - */ - private function open(string $path, string $mode): Attempt - { - $stream = \fopen($path, $mode); - - if ($stream === false) { - /** @var Attempt */ - return Attempt::error(new RuntimeException("Failed to open file '$path'")); - } - - return Attempt::result(Stream::file($stream)); - } - - /** - * @return Attempt - */ - private function createDirectory(Path $path): Attempt - { - $path = $path->toString(); - - // We do not check the result of this function as it will return false - // if the path already exist. This can lead to race conditions where - // another process created the directory between the condition that - // checked if it existed and the call to this method. The only important - // part is to check wether the directory exists or not afterward. - @\mkdir($path, recursive: true); - - if (!\is_dir($path)) { - return Attempt::error(new \RuntimeException(\sprintf( - "Failed to create directory '%s'", - $path, - ))); - } - - return Attempt::result(SideEffect::identity); - } - - /** - * @return Attempt - */ - private function touch(Path $path): Attempt - { - $path = $path->toString(); - - if (!@\touch($path)) { - return Attempt::error(new \RuntimeException(\sprintf( - "Failed to create file '%s'", - $path, - ))); - } - - if (!\file_exists($path)) { - return Attempt::error(new \RuntimeException(\sprintf( - "Failed to create file '%s'", - $path, - ))); - } - - return Attempt::result(SideEffect::identity); - } - - /** - * @return Attempt - */ - private function rmdir(string $path): Attempt - { - return $this - ->list(Path::of($path)) - ->map(static fn($name) => \sprintf( - '%s%s%s', - $path, - $name->toString(), - match ($name->kind()) { - Kind::directory => '/', - default => '', - }, - )) - ->map(Path::of(...)) - ->sink(SideEffect::identity) - ->attempt(fn($_, $file) => $this->remove($file)) - ->map(static fn() => @\rmdir($path)) - ->flatMap(static fn($removed) => match ($removed) { - true => Attempt::result(SideEffect::identity), - false => Attempt::error(new \RuntimeException(\sprintf( - "Failed to remove directory '%s'", - $path, - ))), - }); - } - - /** - * @return Attempt - */ - private function unlink(string $path): Attempt - { - return match (@\unlink($path)) { - true => Attempt::result(SideEffect::identity), - false => Attempt::error(new \RuntimeException(\sprintf( - "Failed to remove file '%s'", - $path, - ))), - }; + return $this->implementation->remove($path); } } diff --git a/src/Internal/Capabilities/Files/AmbientAuthority.php b/src/Internal/Capabilities/Files/AmbientAuthority.php new file mode 100644 index 0000000..172bb8a --- /dev/null +++ b/src/Internal/Capabilities/Files/AmbientAuthority.php @@ -0,0 +1,273 @@ +open($path->toString(), 'r'); + } + + #[\Override] + public function write(Path $path): Attempt + { + return $this->open($path->toString(), 'w'); + } + + #[\Override] + public function temporary(): Attempt + { + return $this->open('php://temp', 'r+'); + } + + #[\Override] + public function require(Path $path): Maybe + { + $path = $path->toString(); + + if (!\file_exists($path) || \is_dir($path)) { + /** @var Maybe */ + return Maybe::nothing(); + } + + /** + * @psalm-suppress UnresolvableInclude + * @psalm-suppress MixedArgument + * @var Maybe + */ + return Maybe::just(require $path); + } + + #[\Override] + public function list(Path $path): Sequence + { + return Sequence::lazy(static function() use ($path): \Generator { + $files = new \FilesystemIterator($path->toString()); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $name = $file->getBasename(); + + if ($name === '') { + continue; + } + + yield Name::of( + $name, + match (true) { + $file->isDir() => Kind::directory, + $file->isLink() => Kind::link, + default => Kind::file, + }, + ); + } + }); + } + + #[\Override] + public function mediaType(Path $path): Attempt + { + $mediaType = @\mime_content_type($path->toString()); + + return match ($mediaType) { + false => Attempt::error(new \RuntimeException('Failed to access media type')), + default => Attempt::result($mediaType), + }; + } + + #[\Override] + public function kind(Path $path): Attempt + { + $path = $path->toString(); + + if (!\file_exists($path)) { + return Attempt::error(new \RuntimeException('File not found')); + } + + return Attempt::result(match (true) { + \is_dir($path) => Kind::directory, + \is_link($path) => Kind::link, + default => Kind::file, + }); + } + + #[\Override] + public function exists(Path $path): bool + { + if (!\file_exists($path->toString())) { + return false; + } + + return match ($path->directory()) { + false => true, + true => \is_dir($path->toString()), + }; + } + + #[\Override] + public function create(Path $path): Attempt + { + return match ($path->directory()) { + true => $this->createDirectory($path), + false => $this->touch($path), + }; + } + + #[\Override] + public function remove(Path $path): Attempt + { + if (!\file_exists($path->toString())) { + return Attempt::result(SideEffect::identity); + } + + if ($path->directory() && \is_dir($path->toString())) { + return $this->rmdir($path->toString()); + } + + $path = $path->toString(); + + if (\is_dir($path)) { + return $this->rmdir($path.'/'); + } + + return $this->unlink($path); + } + + /** + * @return Attempt + */ + private function open(string $path, string $mode): Attempt + { + $stream = \fopen($path, $mode); + + if ($stream === false) { + /** @var Attempt */ + return Attempt::error(new RuntimeException("Failed to open file '$path'")); + } + + return Attempt::result(Stream::file($stream)); + } + + /** + * @return Attempt + */ + private function createDirectory(Path $path): Attempt + { + $path = $path->toString(); + + // We do not check the result of this function as it will return false + // if the path already exist. This can lead to race conditions where + // another process created the directory between the condition that + // checked if it existed and the call to this method. The only important + // part is to check wether the directory exists or not afterward. + @\mkdir($path, recursive: true); + + if (!\is_dir($path)) { + return Attempt::error(new \RuntimeException(\sprintf( + "Failed to create directory '%s'", + $path, + ))); + } + + return Attempt::result(SideEffect::identity); + } + + /** + * @return Attempt + */ + private function touch(Path $path): Attempt + { + $path = $path->toString(); + + if (!@\touch($path)) { + return Attempt::error(new \RuntimeException(\sprintf( + "Failed to create file '%s'", + $path, + ))); + } + + if (!\file_exists($path)) { + return Attempt::error(new \RuntimeException(\sprintf( + "Failed to create file '%s'", + $path, + ))); + } + + return Attempt::result(SideEffect::identity); + } + + /** + * @return Attempt + */ + private function rmdir(string $path): Attempt + { + return $this + ->list(Path::of($path)) + ->map(static fn($name) => \sprintf( + '%s%s%s', + $path, + $name->toString(), + match ($name->kind()) { + Kind::directory => '/', + default => '', + }, + )) + ->map(Path::of(...)) + ->sink(SideEffect::identity) + ->attempt(fn($_, $file) => $this->remove($file)) + ->map(static fn() => @\rmdir($path)) + ->flatMap(static fn($removed) => match ($removed) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException(\sprintf( + "Failed to remove directory '%s'", + $path, + ))), + }); + } + + /** + * @return Attempt + */ + private function unlink(string $path): Attempt + { + return match (@\unlink($path)) { + true => Attempt::result(SideEffect::identity), + false => Attempt::error(new \RuntimeException(\sprintf( + "Failed to remove file '%s'", + $path, + ))), + }; + } +} diff --git a/src/Internal/Capabilities/Files/Implementation.php b/src/Internal/Capabilities/Files/Implementation.php new file mode 100644 index 0000000..7040580 --- /dev/null +++ b/src/Internal/Capabilities/Files/Implementation.php @@ -0,0 +1,70 @@ + + */ + public function read(Path $path): Attempt; + + /** + * @return Attempt + */ + public function write(Path $path): Attempt; + + /** + * @return Attempt + */ + public function temporary(): Attempt; + + /** + * @return Maybe + */ + public function require(Path $path): Maybe; + + /** + * @return Sequence + */ + public function list(Path $path): Sequence; + + /** + * @return Attempt + */ + public function mediaType(Path $path): Attempt; + + /** + * @return Attempt + */ + public function kind(Path $path): Attempt; + + public function exists(Path $path): bool; + + /** + * @return Attempt + */ + public function create(Path $path): Attempt; + + /** + * @return Attempt + */ + public function remove(Path $path): Attempt; +} diff --git a/src/Internal/Capabilities/Files/Simulation.php b/src/Internal/Capabilities/Files/Simulation.php new file mode 100644 index 0000000..107c835 --- /dev/null +++ b/src/Internal/Capabilities/Files/Simulation.php @@ -0,0 +1,90 @@ +files->read($path); + } + + #[\Override] + public function write(Path $path): Attempt + { + return $this->files->write($path); + } + + #[\Override] + public function temporary(): Attempt + { + return $this->files->temporary(); + } + + #[\Override] + public function require(Path $path): Maybe + { + return $this->files->require($path); + } + + #[\Override] + public function list(Path $path): Sequence + { + return $this->files->list($path); + } + + #[\Override] + public function mediaType(Path $path): Attempt + { + return Attempt::error(new \LogicException('Media types not supported in simulated environment')); + } + + #[\Override] + public function kind(Path $path): Attempt + { + return $this->files->kind($path); + } + + #[\Override] + public function exists(Path $path): bool + { + return $this->files->exists($path); + } + + #[\Override] + public function create(Path $path): Attempt + { + return $this->files->create($path); + } + + #[\Override] + public function remove(Path $path): Attempt + { + return $this->files->remove($path); + } +} diff --git a/src/Internal/Capabilities/Simulation.php b/src/Internal/Capabilities/Simulation.php new file mode 100644 index 0000000..e439990 --- /dev/null +++ b/src/Internal/Capabilities/Simulation.php @@ -0,0 +1,49 @@ +capabilities->files()); + } + + #[\Override] + public function streams(): Streams + { + return $this->capabilities->streams(); + } + + #[\Override] + public function sockets(): Sockets + { + return $this->capabilities->sockets(); + } + + #[\Override] + public function watch(): Watch + { + return $this->capabilities->watch(); + } +} From 3e5a06b997d09ea8f68a29a0ef10faae7af32876 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 16:07:05 +0100 Subject: [PATCH 25/37] make sure the directory path always end with a / --- src/Files/Directory.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Files/Directory.php b/src/Files/Directory.php index 18f8f9d..2dc8d80 100644 --- a/src/Files/Directory.php +++ b/src/Files/Directory.php @@ -26,6 +26,10 @@ public static function of( Capabilities $capabilities, Path $path, ): self { + if (!$path->directory()) { + $path = Path::of($path->toString().'/'); + } + return new self($capabilities, $path); } From e86707321bde7c466095aee9d09c9dac8e020aaa Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 16:39:16 +0100 Subject: [PATCH 26/37] forward simulated files capabilities to a simulated disk --- composer.json | 1 + src/Internal/Capabilities.php | 18 ++-- src/Internal/Capabilities/Files.php | 8 +- .../Capabilities/Files/Simulation.php | 54 +++++++++-- src/Internal/Capabilities/Simulation.php | 15 ++- src/Simulation/Disk.php | 92 +++++++++++++++++++ src/Simulation/Disk/Directory.php | 37 ++++++++ src/Simulation/Disk/File.php | 22 +++++ src/Simulation/Disk/File/Content.php | 40 ++++++++ 9 files changed, 264 insertions(+), 23 deletions(-) create mode 100644 src/Simulation/Disk.php create mode 100644 src/Simulation/Disk/Directory.php create mode 100644 src/Simulation/Disk/File.php create mode 100644 src/Simulation/Disk/File/Content.php diff --git a/composer.json b/composer.json index 7366f1a..fef6e3c 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "require": { "php": "~8.4", "innmind/immutable": "dev-next", + "innmind/mutable": "dev-next", "innmind/url": "dev-next", "innmind/time-continuum": "dev-next", "innmind/ip": "dev-next", diff --git a/src/Internal/Capabilities.php b/src/Internal/Capabilities.php index 7c03817..c10195b 100644 --- a/src/Internal/Capabilities.php +++ b/src/Internal/Capabilities.php @@ -3,11 +3,12 @@ namespace Innmind\IO\Internal; -use Innmind\IO\Internal\Capabilities\{ - Implementation, - AmbientAuthority, - Async, - Simulation, +use Innmind\IO\{ + Internal\Capabilities\Implementation, + Internal\Capabilities\AmbientAuthority, + Internal\Capabilities\Async, + Internal\Capabilities\Simulation, + Simulation\Disk, }; use Innmind\TimeContinuum\Clock; @@ -43,9 +44,12 @@ public static function async(self $capabilities, Clock $clock): self /** * @internal */ - public static function simulation(self $capabilities): self + public static function simulation(self $capabilities, Disk $disk): self { - return new self(Simulation::of($capabilities->implementation)); + return new self(Simulation::of( + $capabilities->implementation, + $disk, + )); } public function files(): Capabilities\Files diff --git a/src/Internal/Capabilities/Files.php b/src/Internal/Capabilities/Files.php index 029a42c..ce28ee4 100644 --- a/src/Internal/Capabilities/Files.php +++ b/src/Internal/Capabilities/Files.php @@ -10,6 +10,7 @@ Internal\Capabilities\Files\Simulation, Files\Name, Files\Kind, + Simulation\Disk, }; use Innmind\Url\Path; use Innmind\Immutable\{ @@ -40,9 +41,12 @@ public static function fromAmbientAuthority(): self /** * @internal */ - public static function simulation(self $files): self + public static function simulation(self $files, Disk $disk): self { - return new self(Simulation::of($files->implementation)); + return new self(Simulation::of( + $files->implementation, + $disk, + )); } /** diff --git a/src/Internal/Capabilities/Files/Simulation.php b/src/Internal/Capabilities/Files/Simulation.php index 107c835..46b9638 100644 --- a/src/Internal/Capabilities/Files/Simulation.php +++ b/src/Internal/Capabilities/Files/Simulation.php @@ -3,11 +3,17 @@ namespace Innmind\IO\Internal\Capabilities\Files; +use Innmind\IO\{ + Simulation\Disk, + Files\Kind, + Files\Name, +}; use Innmind\Url\Path; use Innmind\Immutable\{ Attempt, Maybe, Sequence, + Predicate\Instance, }; /** @@ -17,27 +23,35 @@ final class Simulation implements Implementation { private function __construct( private Implementation $files, + private Disk $disk, ) { } /** * @internal */ - public static function of(Implementation $files): self + public static function of(Implementation $files, Disk $disk): self { - return new self($files); + return new self($files, $disk); } #[\Override] public function read(Path $path): Attempt { - return $this->files->read($path); + return $this + ->disk + ->access($path) + ->flatMap(static fn($file) => match (true) { + $file instanceof Disk\File => Attempt::result($file->content()->stream()), + default => Attempt::error(new \RuntimeException('No such file')), + }); } #[\Override] public function write(Path $path): Attempt { - return $this->files->write($path); + // simulated files streams must be readable and writable at the same time + return $this->read($path); } #[\Override] @@ -49,13 +63,26 @@ public function temporary(): Attempt #[\Override] public function require(Path $path): Maybe { - return $this->files->require($path); + return $this + ->disk + ->access($path) + ->maybe() + ->keep(Instance::of(Disk\File::class)) + ->map(static fn($file) => $file->content()->read()) + ->map(static fn($file): mixed => eval($file)); } #[\Override] public function list(Path $path): Sequence { - return $this->files->list($path); + return $this + ->disk + ->list($path) + ->map(static fn($name, $file) => match (true) { + $file instanceof Disk\Directory => Name::of($name, Kind::directory), + $file instanceof Disk\File => Name::of($name, Kind::file), + }) + ->values(); } #[\Override] @@ -67,24 +94,31 @@ public function mediaType(Path $path): Attempt #[\Override] public function kind(Path $path): Attempt { - return $this->files->kind($path); + return $this + ->disk + ->access($path) + ->map(static fn($file) => match (true) { + $file instanceof Disk\File => Kind::file, + $file instanceof Disk\Directory => Kind::directory, + }); } #[\Override] public function exists(Path $path): bool { - return $this->files->exists($path); + return $this->disk->exists($path); } #[\Override] public function create(Path $path): Attempt { - return $this->files->create($path); + // todo should the Io be injected at the creation of the disk ? + return $this->disk->create($this->files, $path); } #[\Override] public function remove(Path $path): Attempt { - return $this->files->remove($path); + return $this->disk->remove($path); } } diff --git a/src/Internal/Capabilities/Simulation.php b/src/Internal/Capabilities/Simulation.php index e439990..f461156 100644 --- a/src/Internal/Capabilities/Simulation.php +++ b/src/Internal/Capabilities/Simulation.php @@ -3,7 +3,10 @@ namespace Innmind\IO\Internal\Capabilities; -use Innmind\IO\Internal\Watch; +use Innmind\IO\{ + Internal\Watch, + Simulation\Disk, +}; /** * @internal @@ -12,21 +15,25 @@ final class Simulation implements Implementation { private function __construct( private Implementation $capabilities, + private Disk $disk, ) { } /** * @internal */ - public static function of(Implementation $capabilities): self + public static function of(Implementation $capabilities, Disk $disk): self { - return new self($capabilities); + return new self($capabilities, $disk); } #[\Override] public function files(): Files { - return Files::simulation($this->capabilities->files()); + return Files::simulation( + $this->capabilities->files(), + $this->disk, + ); } #[\Override] diff --git a/src/Simulation/Disk.php b/src/Simulation/Disk.php new file mode 100644 index 0000000..ad6cf74 --- /dev/null +++ b/src/Simulation/Disk.php @@ -0,0 +1,92 @@ + + */ + public function access(Path $path): Attempt + { + if ($path instanceof RelativePath) { + return Attempt::error(new \LogicException(\sprintf( + 'Path "%s" must absolute', + $path->toString(), + ))); + } + + $parts = \explode('/', $path->toString()); + /** @var Directory|File */ + $parent = $this->root; + + return Sequence::of(...$parts) + ->exclude(static fn($part) => $part === '') + ->sink($parent) + ->attempt(static fn($parent, $part) => match (true) { + $parent instanceof File => Attempt::error(new \RuntimeException('No such file or directory')), + $parent instanceof Directory => $parent->get($part), + }); + } + + /** + * @internal + * + * @return Attempt + */ + public function create(Files $files, Path $path): Attempt + { + return Attempt::result(SideEffect::identity); + } + + /** + * @internal + * + * @return Attempt + */ + public function remove(Path $path): Attempt + { + return Attempt::result(SideEffect::identity); + } + + /** + * @internal + */ + public function exists(Path $path): bool + { + return false; + } + + /** + * @return Map + */ + public function list(Path $path): Map + { + return Map::of(); + } +} diff --git a/src/Simulation/Disk/Directory.php b/src/Simulation/Disk/Directory.php new file mode 100644 index 0000000..a74c245 --- /dev/null +++ b/src/Simulation/Disk/Directory.php @@ -0,0 +1,37 @@ + $files + */ + private function __construct( + private Map $files, + ) { + } + + public static function new(): self + { + return new self(Map::of()); + } + + /** + * @return Attempt + */ + public function get(string $name): Attempt + { + return $this + ->files + ->get($name) + ->attempt(static fn() => new \RuntimeException('No such file or directory')); + } +} diff --git a/src/Simulation/Disk/File.php b/src/Simulation/Disk/File.php new file mode 100644 index 0000000..454eefa --- /dev/null +++ b/src/Simulation/Disk/File.php @@ -0,0 +1,22 @@ +content; + } +} diff --git a/src/Simulation/Disk/File/Content.php b/src/Simulation/Disk/File/Content.php new file mode 100644 index 0000000..9cc36f2 --- /dev/null +++ b/src/Simulation/Disk/File/Content.php @@ -0,0 +1,40 @@ +stream; + } + + public function read(): string + { + return $this + ->stream + ->rewind() + ->flatMap(fn() => $this->stream->read()) + ->unwrap() + ->toString(); + } +} From 38f9c2d3f5f614d6f5e2b7f7df922c92eb48f245 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 17:07:47 +0100 Subject: [PATCH 27/37] add IO::simulation() --- src/IO.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/IO.php b/src/IO.php index fedea54..3de3811 100644 --- a/src/IO.php +++ b/src/IO.php @@ -31,6 +31,21 @@ public static function async(self $io, Clock $clock): self )); } + /** + * This is an internal feature for the innmind/testing package. + * + * @internal + */ + public static function simulation( + self $io, + Simulation\Disk $disk, + ): self { + return new self(Capabilities::simulation( + $io->capabilities, + $disk, + )); + } + public function files(): Files { return Files::of($this->capabilities); From 5989eb24bb9f2757565977f66eb5a4ca6a0607fc Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 17:08:38 +0100 Subject: [PATCH 28/37] typo --- src/Internal/Capabilities/Files/Simulation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/Capabilities/Files/Simulation.php b/src/Internal/Capabilities/Files/Simulation.php index 46b9638..0ed7e74 100644 --- a/src/Internal/Capabilities/Files/Simulation.php +++ b/src/Internal/Capabilities/Files/Simulation.php @@ -112,7 +112,7 @@ public function exists(Path $path): bool #[\Override] public function create(Path $path): Attempt { - // todo should the Io be injected at the creation of the disk ? + // todo should the IO be injected at the creation of the disk ? return $this->disk->create($this->files, $path); } From aeb9c75f7971752a29490642cc68e53209614291 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 14 Dec 2025 17:28:25 +0100 Subject: [PATCH 29/37] implement simulation disk --- .../Capabilities/Files/Simulation.php | 20 ++-- src/Simulation/Disk.php | 97 ++++++++++++++++--- src/Simulation/Disk/Directory.php | 44 ++++++++- src/Simulation/Disk/File.php | 19 +++- src/Simulation/Disk/File/Content.php | 2 +- 5 files changed, 159 insertions(+), 23 deletions(-) diff --git a/src/Internal/Capabilities/Files/Simulation.php b/src/Internal/Capabilities/Files/Simulation.php index 0ed7e74..9e9ed83 100644 --- a/src/Internal/Capabilities/Files/Simulation.php +++ b/src/Internal/Capabilities/Files/Simulation.php @@ -75,14 +75,18 @@ public function require(Path $path): Maybe #[\Override] public function list(Path $path): Sequence { - return $this - ->disk - ->list($path) - ->map(static fn($name, $file) => match (true) { - $file instanceof Disk\Directory => Name::of($name, Kind::directory), - $file instanceof Disk\File => Name::of($name, Kind::file), - }) - ->values(); + return Sequence::lazy(function() use ($path) { + // to make sure to have the current state of the filesystem + yield $this + ->disk + ->list($path) + ->snapshot() + ->map(static fn($name, $file) => match (true) { + $file instanceof Disk\Directory => Name::of($name, Kind::directory), + $file instanceof Disk\File => Name::of($name, Kind::file), + }) + ->values(); + })->flatMap(static fn($files) => $files); } #[\Override] diff --git a/src/Simulation/Disk.php b/src/Simulation/Disk.php index ad6cf74..2e0f6e7 100644 --- a/src/Simulation/Disk.php +++ b/src/Simulation/Disk.php @@ -12,16 +12,14 @@ Path, RelativePath, }; +use Innmind\Validation\Is; +use Innmind\Mutable\Map; use Innmind\Immutable\{ Attempt, Sequence, - Map, SideEffect, }; -/** - * @internal - */ final class Disk { private function __construct( @@ -29,7 +27,14 @@ private function __construct( ) { } + public static function new(): self + { + return new self(Directory::new()); + } + /** + * @internal + * * @return Attempt */ public function access(Path $path): Attempt @@ -41,12 +46,10 @@ public function access(Path $path): Attempt ))); } - $parts = \explode('/', $path->toString()); /** @var Directory|File */ $parent = $this->root; - return Sequence::of(...$parts) - ->exclude(static fn($part) => $part === '') + return self::parts($path) ->sink($parent) ->attempt(static fn($parent, $part) => match (true) { $parent instanceof File => Attempt::error(new \RuntimeException('No such file or directory')), @@ -61,7 +64,19 @@ public function access(Path $path): Attempt */ public function create(Files $files, Path $path): Attempt { - return Attempt::result(SideEffect::identity); + return $this + ->parent($path) + ->flatMap( + static fn($parent) => self::parts($path) + ->last() + ->attempt(static fn() => new \LogicException('Empty path')) + ->flatMap(static fn($name) => match ($path->directory()) { + true => $parent->add($name, Directory::new()), + false => File::new($files)->flatMap( + static fn($file) => $parent->add($name, $file), + ), + }), + ); } /** @@ -71,7 +86,18 @@ public function create(Files $files, Path $path): Attempt */ public function remove(Path $path): Attempt { - return Attempt::result(SideEffect::identity); + if ($path->equals(Path::of('/'))) { + return Attempt::error(new \RuntimeException('Root directory cannot be removed')); + } + + return $this + ->parent($path) + ->flatMap( + static fn($parent) => self::parts($path) + ->last() + ->attempt(static fn() => new \LogicException('Empty path')) + ->flatMap($parent->remove(...)), + ); } /** @@ -79,7 +105,10 @@ public function remove(Path $path): Attempt */ public function exists(Path $path): bool { - return false; + return $this->access($path)->match( + static fn() => true, + static fn() => false, + ); } /** @@ -87,6 +116,52 @@ public function exists(Path $path): bool */ public function list(Path $path): Map { - return Map::of(); + return $this + ->access($path) + ->flatMap(static fn($file) => match (true) { + $file instanceof Directory => Attempt::result($file), + default => Attempt::error(new \Exception), + }) + ->match( + static fn($directory) => $directory->list(), + static fn() => Map::of(), + ); + } + + /** + * @return Attempt + */ + private function parent(Path $path): Attempt + { + if ($path instanceof RelativePath) { + return Attempt::error(new \LogicException(\sprintf( + 'Path "%s" must absolute', + $path->toString(), + ))); + } + + /** @var Directory|File */ + $parent = $this->root; + + return self::parts($path) + ->dropEnd(1) + ->sink($parent) + ->attempt(static fn($parent, $part) => match (true) { + $parent instanceof File => Attempt::error(new \RuntimeException('No such file or directory')), + $parent instanceof Directory => $parent->get($part), + }) + ->flatMap(static fn($file) => match (true) { + $file instanceof File => Attempt::error(new \RuntimeException('No such file or directory')), + $file instanceof Directory => Attempt::result($file), + }); + } + + /** + * @return Sequence + */ + private static function parts(Path $path): Sequence + { + return Sequence::of(...\explode('/', $path->toString())) + ->keep(Is::string()->nonEmpty()->asPredicate()); } } diff --git a/src/Simulation/Disk/Directory.php b/src/Simulation/Disk/Directory.php index a74c245..6d3efac 100644 --- a/src/Simulation/Disk/Directory.php +++ b/src/Simulation/Disk/Directory.php @@ -4,7 +4,10 @@ namespace Innmind\IO\Simulation\Disk; use Innmind\Mutable\Map; -use Innmind\Immutable\Attempt; +use Innmind\Immutable\{ + Attempt, + SideEffect, +}; /** * @internal @@ -12,19 +15,24 @@ final class Directory { /** - * @param Map $files + * @param Map $files */ private function __construct( private Map $files, ) { } + /** + * @internal + */ public static function new(): self { return new self(Map::of()); } /** + * @param non-empty-string $name + * * @return Attempt */ public function get(string $name): Attempt @@ -34,4 +42,36 @@ public function get(string $name): Attempt ->get($name) ->attempt(static fn() => new \RuntimeException('No such file or directory')); } + + /** + * @param non-empty-string $name + * + * @return Attempt + */ + public function add(string $name, self|File $child): Attempt + { + $this->files->put($name, $child); + + return Attempt::result(SideEffect::identity); + } + + /** + * @param non-empty-string $name + * + * @return Attempt + */ + public function remove(string $name): Attempt + { + $this->files->remove($name); + + return Attempt::result(SideEffect::identity); + } + + /** + * @return Map + */ + public function list(): Map + { + return $this->files; + } } diff --git a/src/Simulation/Disk/File.php b/src/Simulation/Disk/File.php index 454eefa..4a05a67 100644 --- a/src/Simulation/Disk/File.php +++ b/src/Simulation/Disk/File.php @@ -3,7 +3,11 @@ namespace Innmind\IO\Simulation\Disk; -use Innmind\IO\Simulation\Disk\File\Content; +use Innmind\IO\{ + Simulation\Disk\File\Content, + Internal\Capabilities\Files\Implementation as Files, +}; +use Innmind\Immutable\Attempt; /** * @internal @@ -15,6 +19,19 @@ private function __construct( ) { } + /** + * @internal + * + * @return Attempt + */ + public static function new(Files $files): Attempt + { + return $files + ->temporary() + ->map(Content::of(...)) + ->map(static fn($content) => new self($content)); + } + public function content(): Content { return $this->content; diff --git a/src/Simulation/Disk/File/Content.php b/src/Simulation/Disk/File/Content.php index 9cc36f2..774ea69 100644 --- a/src/Simulation/Disk/File/Content.php +++ b/src/Simulation/Disk/File/Content.php @@ -18,7 +18,7 @@ private function __construct( /** * @internal */ - public static function new(Stream $stream): self + public static function of(Stream $stream): self { return new self($stream); } From 5e4b69f950437b1811a558b84790f3a401871005 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 15 Dec 2025 17:38:40 +0100 Subject: [PATCH 30/37] extract stream implementation into a sub class --- src/Internal/Stream.php | 230 ++---------------- src/Internal/Stream/Implementation.php | 90 +++++++ src/Internal/Stream/Native.php | 323 +++++++++++++++++++++++++ 3 files changed, 434 insertions(+), 209 deletions(-) create mode 100644 src/Internal/Stream/Implementation.php create mode 100644 src/Internal/Stream/Native.php diff --git a/src/Internal/Stream.php b/src/Internal/Stream.php index f41241c..f8da74d 100644 --- a/src/Internal/Stream.php +++ b/src/Internal/Stream.php @@ -4,27 +4,16 @@ namespace Innmind\IO\Internal; use Innmind\IO\{ + Internal\Stream\Implementation, + Internal\Stream\Native, Stream\Size, - Exception\InvalidArgumentException, - Exception\DataPartiallyWritten, - Exception\FailedToCloseStream, - Exception\FailedToWriteToStream, - Exception\PositionNotSeekable, - Exception\RuntimeException, -}; -use Innmind\Validation\{ - Is, - Of, - Constraint, - Failure, }; +use Innmind\Validation\Of; use Innmind\Immutable\{ Str, Maybe, Attempt, SideEffect, - Validation, - Predicate\Instance, }; /** @@ -32,39 +21,9 @@ */ final class Stream { - /** @var resource */ - private $resource; - private bool $file; - private bool $closed = false; - private bool $seekable = false; - private bool $syncable = false; - - /** - * @param resource $resource - */ - private function __construct($resource, bool $file) - { - /** - * @psalm-suppress DocblockTypeContradiction - * @psalm-suppress RedundantConditionGivenDocblockType - */ - if (!\is_resource($resource) || \get_resource_type($resource) !== 'stream') { - throw new InvalidArgumentException; - } - - $this->resource = $resource; - $this->file = $file; - $meta = \stream_get_meta_data($resource); - - if ($meta['seekable'] && \substr($meta['uri'], 0, 9) !== 'php://std') { - //stdin, stdout and stderr are not seekable - $this->seekable = true; - $this->rewind(); - } - - if ($this->seekable && \substr($meta['uri'] ?? '', 0, 10) !== 'php://temp') { - $this->syncable = true; - } + private function __construct( + private Implementation $implementation, + ) { } /** @@ -74,7 +33,7 @@ private function __construct($resource, bool $file) */ public static function of($resource): self { - return new self($resource, false); + return new self(Native::of($resource)); } /** @@ -84,12 +43,12 @@ public static function of($resource): self */ public static function file($resource): self { - return new self($resource, true); + return new self(Native::file($resource)); } public function isFile(): bool { - return $this->file; + return $this->implementation->isFile(); } /** @@ -97,22 +56,7 @@ public function isFile(): bool */ public function nonBlocking(): Maybe { - if ($this->closed()) { - /** @var Maybe */ - return Maybe::nothing(); - } - - $return = \stream_set_blocking($this->resource, false); - - if ($return === false) { - /** @var Maybe */ - return Maybe::nothing(); - } - - $_ = \stream_set_write_buffer($this->resource, 0); - $_ = \stream_set_read_buffer($this->resource, 0); - - return Maybe::just(SideEffect::identity); + return $this->implementation->nonBlocking(); } /** @@ -120,19 +64,7 @@ public function nonBlocking(): Maybe */ public function blocking(): Maybe { - if ($this->closed()) { - /** @var Maybe */ - return Maybe::nothing(); - } - - $return = \stream_set_blocking($this->resource, false); - - if ($return === false) { - /** @var Maybe */ - return Maybe::nothing(); - } - - return Maybe::just(SideEffect::identity); + return $this->implementation->blocking(); } /** @@ -142,7 +74,7 @@ public function blocking(): Maybe */ public function resource() { - return $this->resource; + return $this->implementation->resource(); } /** @@ -150,23 +82,7 @@ public function resource() */ public function rewind(): Attempt { - if (!$this->seekable) { - /** @var Attempt */ - return Attempt::error(new PositionNotSeekable); - } - - if ($this->closed()) { - /** @var Attempt */ - return Attempt::result(SideEffect::identity); - } - - $status = \fseek($this->resource, 0); - - /** @var Attempt */ - return match ($status) { - -1 => Attempt::error(new PositionNotSeekable), - default => Attempt::result(SideEffect::identity), - }; + return $this->implementation->rewind(); } /** @@ -174,11 +90,7 @@ public function rewind(): Attempt */ public function end(): bool { - if ($this->closed()) { - return true; - } - - return \feof($this->resource); + return $this->implementation->end(); } /** @@ -188,35 +100,7 @@ public function end(): bool */ public function size(): Maybe { - if ($this->closed()) { - /** @var Maybe */ - return Maybe::nothing(); - } - - /** @var Constraint> */ - $positive = Is::value(0)->or( - Is::int()->positive(), - ); - /** @var Constraint */ - $resource = Of::callable(static fn(mixed $resource) => match (\is_resource($resource)) { - true => Validation::success($resource), - false => Validation::fail(Failure::of('not a resource')), - }); - $validate = $resource - ->map(\fstat(...)) - ->and(Is::shape( - 'size', - Is::string() - ->or(Is::int()) - ->map(static fn($size) => (int) $size) - ->and($positive) - ->map(Size::of(...)), - )) - ->map(static fn(array $stat): mixed => $stat['size']); - - return $validate($this->resource) - ->maybe() - ->keep(Instance::of(Size::class)); + return $this->implementation->size(); } /** @@ -224,20 +108,7 @@ public function size(): Maybe */ public function close(): Attempt { - if ($this->closed()) { - return Attempt::result(SideEffect::identity); - } - - /** @psalm-suppress InvalidPropertyAssignmentValue */ - $return = \fclose($this->resource); - - if ($return === false) { - return Attempt::error(new FailedToCloseStream); - } - - $this->closed = true; - - return Attempt::result(SideEffect::identity); + return $this->implementation->close(); } /** @@ -245,8 +116,7 @@ public function close(): Attempt */ public function closed(): bool { - /** @psalm-suppress DocblockTypeContradiction */ - return $this->closed || !\is_resource($this->resource); + return $this->implementation->closed(); } /** @@ -256,20 +126,7 @@ public function closed(): bool */ public function read(?int $length = null): Attempt { - if ($this->closed()) { - /** @var Attempt */ - return Attempt::error(new RuntimeException('Stream closed')); - } - - $data = \stream_get_contents( - $this->resource, - $length ?? -1, - ); - - return match ($data) { - false => Attempt::error(new RuntimeException('Failed to read the stream')), - default => Attempt::result(Str::of($data)), - }; + return $this->implementation->read($length); } /** @@ -277,17 +134,7 @@ public function read(?int $length = null): Attempt */ public function readLine(): Attempt { - if ($this->closed()) { - /** @var Attempt */ - return Attempt::error(new RuntimeException('Stream closed')); - } - - $line = \fgets($this->resource); - - return match ($line) { - false => Attempt::error(new RuntimeException('Failed to read the stream')), - default => Attempt::result(Str::of($line)), - }; + return $this->implementation->readLine(); } /** @@ -295,25 +142,7 @@ public function readLine(): Attempt */ public function write(Str $data): Attempt { - if ($this->closed()) { - /** @var Attempt */ - return Attempt::error(new FailedToWriteToStream); - } - - $written = @\fwrite($this->resource, $data->toString()); - - if ($written === false) { - /** @var Attempt */ - return Attempt::error(new FailedToWriteToStream); - } - - if ($written !== $data->length()) { - /** @var Attempt */ - return Attempt::error(DataPartiallyWritten::of($data, $written)); - } - - /** @var Attempt */ - return Attempt::result(SideEffect::identity); + return $this->implementation->write($data); } /** @@ -321,23 +150,6 @@ public function write(Str $data): Attempt */ public function sync(): Attempt { - if ($this->closed()) { - /** @var Attempt */ - return Attempt::error(new FailedToWriteToStream); - } - - if (!$this->syncable) { - return Attempt::result(SideEffect::identity); - } - - $written = @\fsync($this->resource); - - if ($written === false) { - /** @var Attempt */ - return Attempt::error(new FailedToWriteToStream); - } - - /** @var Attempt */ - return Attempt::result(SideEffect::identity); + return $this->implementation->sync(); } } diff --git a/src/Internal/Stream/Implementation.php b/src/Internal/Stream/Implementation.php new file mode 100644 index 0000000..528e6c0 --- /dev/null +++ b/src/Internal/Stream/Implementation.php @@ -0,0 +1,90 @@ + + */ + public function nonBlocking(): Maybe; + + /** + * @return Maybe + */ + public function blocking(): Maybe; + + /** + * @psalm-mutation-free + * + * @return resource stream + */ + public function resource(); + + /** + * @return Attempt + */ + public function rewind(): Attempt; + + /** + * @psalm-mutation-free + */ + public function end(): bool; + + /** + * @psalm-mutation-free + * + * @return Maybe + */ + public function size(): Maybe; + + /** + * @return Attempt + */ + public function close(): Attempt; + + /** + * @psalm-mutation-free + */ + public function closed(): bool; + + /** + * @param int<1, max>|null $length When omitted will read the remaining of the stream + * + * @return Attempt + */ + public function read(?int $length = null): Attempt; + + /** + * @return Attempt + */ + public function readLine(): Attempt; + + /** + * @return Attempt + */ + public function write(Str $data): Attempt; + + /** + * @return Attempt + */ + public function sync(): Attempt; +} diff --git a/src/Internal/Stream/Native.php b/src/Internal/Stream/Native.php new file mode 100644 index 0000000..c3ba5a1 --- /dev/null +++ b/src/Internal/Stream/Native.php @@ -0,0 +1,323 @@ +resource = $resource; + $this->file = $file; + $meta = \stream_get_meta_data($resource); + + if ($meta['seekable'] && \substr($meta['uri'], 0, 9) !== 'php://std') { + //stdin, stdout and stderr are not seekable + $this->seekable = true; + $this->rewind(); + } + + if ($this->seekable && \substr($meta['uri'] ?? '', 0, 10) !== 'php://temp') { + $this->syncable = true; + } + } + + /** + * @internal + * + * @param resource $resource + */ + public static function of($resource): self + { + return new self($resource, false); + } + + /** + * @internal + * + * @param resource $resource + */ + public static function file($resource): self + { + return new self($resource, true); + } + + #[\Override] + public function isFile(): bool + { + return $this->file; + } + + #[\Override] + public function nonBlocking(): Maybe + { + if ($this->closed()) { + /** @var Maybe */ + return Maybe::nothing(); + } + + $return = \stream_set_blocking($this->resource, false); + + if ($return === false) { + /** @var Maybe */ + return Maybe::nothing(); + } + + $_ = \stream_set_write_buffer($this->resource, 0); + $_ = \stream_set_read_buffer($this->resource, 0); + + return Maybe::just(SideEffect::identity); + } + + #[\Override] + public function blocking(): Maybe + { + if ($this->closed()) { + /** @var Maybe */ + return Maybe::nothing(); + } + + $return = \stream_set_blocking($this->resource, false); + + if ($return === false) { + /** @var Maybe */ + return Maybe::nothing(); + } + + return Maybe::just(SideEffect::identity); + } + + #[\Override] + public function resource() + { + return $this->resource; + } + + #[\Override] + public function rewind(): Attempt + { + if (!$this->seekable) { + /** @var Attempt */ + return Attempt::error(new PositionNotSeekable); + } + + if ($this->closed()) { + /** @var Attempt */ + return Attempt::result(SideEffect::identity); + } + + $status = \fseek($this->resource, 0); + + /** @var Attempt */ + return match ($status) { + -1 => Attempt::error(new PositionNotSeekable), + default => Attempt::result(SideEffect::identity), + }; + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function end(): bool + { + if ($this->closed()) { + return true; + } + + return \feof($this->resource); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function size(): Maybe + { + if ($this->closed()) { + /** @var Maybe */ + return Maybe::nothing(); + } + + /** @var Constraint> */ + $positive = Is::value(0)->or( + Is::int()->positive(), + ); + /** @var Constraint */ + $resource = Of::callable(static fn(mixed $resource) => match (\is_resource($resource)) { + true => Validation::success($resource), + false => Validation::fail(Failure::of('not a resource')), + }); + $validate = $resource + ->map(\fstat(...)) + ->and(Is::shape( + 'size', + Is::string() + ->or(Is::int()) + ->map(static fn($size) => (int) $size) + ->and($positive) + ->map(Size::of(...)), + )) + ->map(static fn(array $stat): mixed => $stat['size']); + + return $validate($this->resource) + ->maybe() + ->keep(Instance::of(Size::class)); + } + + #[\Override] + public function close(): Attempt + { + if ($this->closed()) { + return Attempt::result(SideEffect::identity); + } + + /** @psalm-suppress InvalidPropertyAssignmentValue */ + $return = \fclose($this->resource); + + if ($return === false) { + return Attempt::error(new FailedToCloseStream); + } + + $this->closed = true; + + return Attempt::result(SideEffect::identity); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function closed(): bool + { + /** @psalm-suppress DocblockTypeContradiction */ + return $this->closed || !\is_resource($this->resource); + } + + #[\Override] + public function read(?int $length = null): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new RuntimeException('Stream closed')); + } + + $data = \stream_get_contents( + $this->resource, + $length ?? -1, + ); + + return match ($data) { + false => Attempt::error(new RuntimeException('Failed to read the stream')), + default => Attempt::result(Str::of($data)), + }; + } + + #[\Override] + public function readLine(): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new RuntimeException('Stream closed')); + } + + $line = \fgets($this->resource); + + return match ($line) { + false => Attempt::error(new RuntimeException('Failed to read the stream')), + default => Attempt::result(Str::of($line)), + }; + } + + #[\Override] + public function write(Str $data): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new FailedToWriteToStream); + } + + $written = @\fwrite($this->resource, $data->toString()); + + if ($written === false) { + /** @var Attempt */ + return Attempt::error(new FailedToWriteToStream); + } + + if ($written !== $data->length()) { + /** @var Attempt */ + return Attempt::error(DataPartiallyWritten::of($data, $written)); + } + + /** @var Attempt */ + return Attempt::result(SideEffect::identity); + } + + #[\Override] + public function sync(): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new FailedToWriteToStream); + } + + if (!$this->syncable) { + return Attempt::result(SideEffect::identity); + } + + $written = @\fsync($this->resource); + + if ($written === false) { + /** @var Attempt */ + return Attempt::error(new FailedToWriteToStream); + } + + /** @var Attempt */ + return Attempt::result(SideEffect::identity); + } +} From a56e888280cc6255585b143901eec66957c72f52 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 15 Dec 2025 17:39:47 +0100 Subject: [PATCH 31/37] rename Native to AmbientAuthority for consistency --- src/Internal/Stream.php | 6 +++--- src/Internal/Stream/{Native.php => AmbientAuthority.php} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/Internal/Stream/{Native.php => AmbientAuthority.php} (99%) diff --git a/src/Internal/Stream.php b/src/Internal/Stream.php index f8da74d..5644566 100644 --- a/src/Internal/Stream.php +++ b/src/Internal/Stream.php @@ -5,7 +5,7 @@ use Innmind\IO\{ Internal\Stream\Implementation, - Internal\Stream\Native, + Internal\Stream\AmbientAuthority, Stream\Size, }; use Innmind\Validation\Of; @@ -33,7 +33,7 @@ private function __construct( */ public static function of($resource): self { - return new self(Native::of($resource)); + return new self(AmbientAuthority::of($resource)); } /** @@ -43,7 +43,7 @@ public static function of($resource): self */ public static function file($resource): self { - return new self(Native::file($resource)); + return new self(AmbientAuthority::file($resource)); } public function isFile(): bool diff --git a/src/Internal/Stream/Native.php b/src/Internal/Stream/AmbientAuthority.php similarity index 99% rename from src/Internal/Stream/Native.php rename to src/Internal/Stream/AmbientAuthority.php index c3ba5a1..7d08128 100644 --- a/src/Internal/Stream/Native.php +++ b/src/Internal/Stream/AmbientAuthority.php @@ -30,7 +30,7 @@ /** * @internal */ -final class Native implements Implementation +final class AmbientAuthority implements Implementation { /** @var resource */ private $resource; From dbaa20902761659ad979feb1fe8838764d3f497d Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 15 Dec 2025 18:06:09 +0100 Subject: [PATCH 32/37] prevent the simulated stream from being really closed --- .../Capabilities/Files/Simulation.php | 2 +- src/Internal/Stream.php | 9 + src/Internal/Stream/Simulated.php | 183 ++++++++++++++++++ src/Simulation/Disk/File/Content.php | 15 +- 4 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 src/Internal/Stream/Simulated.php diff --git a/src/Internal/Capabilities/Files/Simulation.php b/src/Internal/Capabilities/Files/Simulation.php index 9e9ed83..4551d10 100644 --- a/src/Internal/Capabilities/Files/Simulation.php +++ b/src/Internal/Capabilities/Files/Simulation.php @@ -42,7 +42,7 @@ public function read(Path $path): Attempt ->disk ->access($path) ->flatMap(static fn($file) => match (true) { - $file instanceof Disk\File => Attempt::result($file->content()->stream()), + $file instanceof Disk\File => $file->content()->stream(), default => Attempt::error(new \RuntimeException('No such file')), }); } diff --git a/src/Internal/Stream.php b/src/Internal/Stream.php index 5644566..d492e01 100644 --- a/src/Internal/Stream.php +++ b/src/Internal/Stream.php @@ -6,6 +6,7 @@ use Innmind\IO\{ Internal\Stream\Implementation, Internal\Stream\AmbientAuthority, + Internal\Stream\Simulated, Stream\Size, }; use Innmind\Validation\Of; @@ -46,6 +47,14 @@ public static function file($resource): self return new self(AmbientAuthority::file($resource)); } + /** + * @internal + */ + public static function simulated(self $stream): self + { + return new self(Simulated::of($stream->implementation)); + } + public function isFile(): bool { return $this->implementation->isFile(); diff --git a/src/Internal/Stream/Simulated.php b/src/Internal/Stream/Simulated.php new file mode 100644 index 0000000..1316884 --- /dev/null +++ b/src/Internal/Stream/Simulated.php @@ -0,0 +1,183 @@ +closed()) { + /** @var Maybe */ + return Maybe::nothing(); + } + + return $this->implementation->nonBlocking(); + } + + #[\Override] + public function blocking(): Maybe + { + if ($this->closed()) { + /** @var Maybe */ + return Maybe::nothing(); + } + + return $this->implementation->blocking(); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function resource() + { + return $this->implementation->resource(); + } + + #[\Override] + public function rewind(): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::result(SideEffect::identity); + } + + return $this->implementation->rewind(); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function end(): bool + { + if ($this->closed()) { + return true; + } + + return $this->implementation->end(); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function size(): Maybe + { + if ($this->closed()) { + /** @var Maybe */ + return Maybe::nothing(); + } + + return $this->implementation->size(); + } + + #[\Override] + public function close(): Attempt + { + if ($this->closed()) { + return Attempt::result(SideEffect::identity); + } + + $this->closed = true; + + return Attempt::result(SideEffect::identity); + } + + /** + * @psalm-mutation-free + */ + #[\Override] + public function closed(): bool + { + return $this->closed || $this->implementation->closed(); + } + + #[\Override] + public function read(?int $length = null): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new RuntimeException('Stream closed')); + } + + return $this->implementation->read($length); + } + + #[\Override] + public function readLine(): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new RuntimeException('Stream closed')); + } + + return $this->implementation->readLine(); + } + + #[\Override] + public function write(Str $data): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new FailedToWriteToStream); + } + + return $this->implementation->write($data); + } + + #[\Override] + public function sync(): Attempt + { + if ($this->closed()) { + /** @var Attempt */ + return Attempt::error(new FailedToWriteToStream); + } + + return $this->implementation->sync(); + } +} diff --git a/src/Simulation/Disk/File/Content.php b/src/Simulation/Disk/File/Content.php index 774ea69..64797fe 100644 --- a/src/Simulation/Disk/File/Content.php +++ b/src/Simulation/Disk/File/Content.php @@ -4,6 +4,7 @@ namespace Innmind\IO\Simulation\Disk\File; use Innmind\IO\Internal\Stream; +use Innmind\Immutable\Attempt; /** * @internal @@ -23,9 +24,19 @@ public static function of(Stream $stream): self return new self($stream); } - public function stream(): Stream + /** + * @return Attempt + */ + public function stream(): Attempt { - return $this->stream; + // By wrapping the stream each time it's accessed we allow to reset the + // closed flag. This way it simulates a real file by "reopening it" each + // time it's accessed. + $stream = Stream::simulated($this->stream); + + return $stream + ->rewind() + ->map(static fn() => $stream); } public function read(): string From de8f953dc1dbe8e1194b2403a35f1bc305230c9f Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 25 Jan 2026 16:23:10 +0100 Subject: [PATCH 33/37] tag dependencies --- .github/workflows/ci.yml | 8 ++++---- CHANGELOG.md | 1 + composer.json | 14 +++++++------- proofs/files.php | 10 +++++----- proofs/streams.php | 6 +++--- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 779f162..2f3eecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,11 @@ on: [push, pull_request] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@next + uses: innmind/github-workflows/.github/workflows/cs.yml@main diff --git a/CHANGELOG.md b/CHANGELOG.md index a56ccef..a57d4c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Changed - Requires PHP `8.4` +- Requires `innmind/immutable:~6.0` ## 3.5.1 - 2025-08-18 diff --git a/composer.json b/composer.json index fef6e3c..fbc92d5 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,12 @@ }, "require": { "php": "~8.4", - "innmind/immutable": "dev-next", - "innmind/mutable": "dev-next", - "innmind/url": "dev-next", - "innmind/time-continuum": "dev-next", - "innmind/ip": "dev-next", - "innmind/validation": "dev-next" + "innmind/immutable": "~6.0", + "innmind/mutable": "~1.0", + "innmind/url": "~5.0", + "innmind/time-continuum": "~5.1", + "innmind/ip": "~4.0", + "innmind/validation": "~3.0" }, "autoload": { "psr-4": { @@ -34,7 +34,7 @@ } }, "require-dev": { - "innmind/static-analysis": "dev-next", + "innmind/static-analysis": "~1.3", "innmind/black-box": "~6.1", "innmind/coding-standard": "~2.0" } diff --git a/proofs/files.php b/proofs/files.php index b4a6651..aab248e 100644 --- a/proofs/files.php +++ b/proofs/files.php @@ -72,7 +72,7 @@ static function($assert, $chunks, $size) { $assert->same( $data, $loaded - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); }, @@ -143,7 +143,7 @@ static function($assert, $lines) { $assert->same( $data, $loaded - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); @@ -298,14 +298,14 @@ static function($assert, $chunks) { $expected, $read ->lines() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); $assert->same( $expected, $read ->lines() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), 'Temporary file should be accessible multiple times', ); @@ -386,7 +386,7 @@ static function($assert, $chunks, $encoding) { $tmp ->read() ->chunks(8192) - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); }, diff --git a/proofs/streams.php b/proofs/streams.php index 64f1d7f..59c5f10 100644 --- a/proofs/streams.php +++ b/proofs/streams.php @@ -159,7 +159,7 @@ static function($assert, $lines) { $assert->same( \implode("\n", $lines), $load() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); }, @@ -207,7 +207,7 @@ static function($assert, $lines) { $assert->same( \implode("\n", $lines), $load() - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); }, @@ -248,7 +248,7 @@ static function($assert, $lines) { $assert->same( \implode("\n", $lines), $sequence - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); }, From f2acbddb0fa1d4957479b5c5ffe0d1bb33074e72 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 25 Jan 2026 16:30:20 +0100 Subject: [PATCH 34/37] replace time-continuum by time --- CHANGELOG.md | 1 + composer.json | 2 +- proofs/sockets.php | 2 +- src/IO.php | 2 +- src/Internal/Async/Suspended.php | 8 ++++---- src/Internal/Capabilities.php | 2 +- src/Internal/Capabilities/Async.php | 2 +- src/Internal/Watch.php | 2 +- src/Internal/Watch/Async.php | 2 +- src/Internal/Watch/Sync.php | 2 +- src/Sockets/Clients/Client.php | 2 +- src/Sockets/Servers/Server.php | 2 +- src/Streams/Stream/Read.php | 2 +- src/Streams/Stream/Read/Pool.php | 2 +- tests/FunctionalTest.php | 2 +- 15 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a57d4c1..c460478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Requires PHP `8.4` - Requires `innmind/immutable:~6.0` +- Requires `innmind/time:~1.0` ## 3.5.1 - 2025-08-18 diff --git a/composer.json b/composer.json index fbc92d5..72978a7 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "innmind/immutable": "~6.0", "innmind/mutable": "~1.0", "innmind/url": "~5.0", - "innmind/time-continuum": "~5.1", + "innmind/time": "~1.0", "innmind/ip": "~4.0", "innmind/validation": "~3.0" }, diff --git a/proofs/sockets.php b/proofs/sockets.php index bf6165e..1b7fa0d 100644 --- a/proofs/sockets.php +++ b/proofs/sockets.php @@ -6,7 +6,7 @@ Frame, Sockets\Unix\Address, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, diff --git a/src/IO.php b/src/IO.php index 3de3811..84b6271 100644 --- a/src/IO.php +++ b/src/IO.php @@ -4,7 +4,7 @@ namespace Innmind\IO; use Innmind\IO\Internal\Capabilities; -use Innmind\TimeContinuum\Clock; +use Innmind\Time\Clock; final class IO { diff --git a/src/Internal/Async/Suspended.php b/src/Internal/Async/Suspended.php index 0c1cdd5..0841182 100644 --- a/src/Internal/Async/Suspended.php +++ b/src/Internal/Async/Suspended.php @@ -9,9 +9,9 @@ Stream, Socket\Server, }; -use Innmind\TimeContinuum\{ +use Innmind\Time\{ Clock, - PointInTime, + Point, Period, }; use Innmind\Immutable\{ @@ -33,7 +33,7 @@ final class Suspended * @param Sequence $write */ private function __construct( - private PointInTime $at, + private Point $at, private Maybe $timeout, private Sequence $read, private Sequence $write, @@ -49,7 +49,7 @@ private function __construct( * @param Sequence $write */ public static function of( - PointInTime $at, + Point $at, Maybe $timeout, Sequence $read, Sequence $write, diff --git a/src/Internal/Capabilities.php b/src/Internal/Capabilities.php index c10195b..3a66a73 100644 --- a/src/Internal/Capabilities.php +++ b/src/Internal/Capabilities.php @@ -10,7 +10,7 @@ Internal\Capabilities\Simulation, Simulation\Disk, }; -use Innmind\TimeContinuum\Clock; +use Innmind\Time\Clock; /** * @internal diff --git a/src/Internal/Capabilities/Async.php b/src/Internal/Capabilities/Async.php index 8c15f3b..780a22f 100644 --- a/src/Internal/Capabilities/Async.php +++ b/src/Internal/Capabilities/Async.php @@ -4,7 +4,7 @@ namespace Innmind\IO\Internal\Capabilities; use Innmind\IO\Internal\Watch; -use Innmind\TimeContinuum\Clock; +use Innmind\Time\Clock; /** * @internal diff --git a/src/Internal/Watch.php b/src/Internal/Watch.php index 07bdf4b..68d2515 100644 --- a/src/Internal/Watch.php +++ b/src/Internal/Watch.php @@ -9,7 +9,7 @@ Internal\Watch\Ready, Internal\Socket\Server, }; -use Innmind\TimeContinuum\{ +use Innmind\Time\{ Clock, Period, }; diff --git a/src/Internal/Watch/Async.php b/src/Internal/Watch/Async.php index 36f6efa..eb037e4 100644 --- a/src/Internal/Watch/Async.php +++ b/src/Internal/Watch/Async.php @@ -9,7 +9,7 @@ Internal\Async\Suspended, Internal\Async\Resumable, }; -use Innmind\TimeContinuum\{ +use Innmind\Time\{ Clock, Period, }; diff --git a/src/Internal/Watch/Sync.php b/src/Internal/Watch/Sync.php index dd1ba35..75831e7 100644 --- a/src/Internal/Watch/Sync.php +++ b/src/Internal/Watch/Sync.php @@ -8,7 +8,7 @@ Internal\Socket\Server, Exception\RuntimeException, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Immutable\{ Map, Maybe, diff --git a/src/Sockets/Clients/Client.php b/src/Sockets/Clients/Client.php index 07e93b9..9c893d7 100644 --- a/src/Sockets/Clients/Client.php +++ b/src/Sockets/Clients/Client.php @@ -8,7 +8,7 @@ Streams\Stream, Frame, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Immutable\{ Str, Attempt, diff --git a/src/Sockets/Servers/Server.php b/src/Sockets/Servers/Server.php index 4ae1847..b7a2e1d 100644 --- a/src/Sockets/Servers/Server.php +++ b/src/Sockets/Servers/Server.php @@ -12,7 +12,7 @@ Internal\Watch, Exception\RuntimeException, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Immutable\{ Attempt, SideEffect, diff --git a/src/Streams/Stream/Read.php b/src/Streams/Stream/Read.php index c14f085..00b2c01 100644 --- a/src/Streams/Stream/Read.php +++ b/src/Streams/Stream/Read.php @@ -11,7 +11,7 @@ Internal\Stream, Internal\Watch, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Immutable\{ Str, Maybe, diff --git a/src/Streams/Stream/Read/Pool.php b/src/Streams/Stream/Read/Pool.php index a177a6c..baddd0c 100644 --- a/src/Streams/Stream/Read/Pool.php +++ b/src/Streams/Stream/Read/Pool.php @@ -8,7 +8,7 @@ Internal, Internal\Capabilities, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Immutable\{ Map, Sequence, diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index d637f67..ab1e427 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -8,7 +8,7 @@ Frame, Sockets\Unix\Address, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, From 5f5f7817c3cd14e372ca060cde740425e67c8f33 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 25 Jan 2026 16:45:34 +0100 Subject: [PATCH 35/37] flag simulation disk as internal --- src/Simulation/Disk.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Simulation/Disk.php b/src/Simulation/Disk.php index 2e0f6e7..124a16e 100644 --- a/src/Simulation/Disk.php +++ b/src/Simulation/Disk.php @@ -20,6 +20,9 @@ SideEffect, }; +/** + * @internal + */ final class Disk { private function __construct( @@ -27,14 +30,15 @@ private function __construct( ) { } + /** + * @internal + */ public static function new(): self { return new self(Directory::new()); } /** - * @internal - * * @return Attempt */ public function access(Path $path): Attempt @@ -58,8 +62,6 @@ public function access(Path $path): Attempt } /** - * @internal - * * @return Attempt */ public function create(Files $files, Path $path): Attempt @@ -80,8 +82,6 @@ public function create(Files $files, Path $path): Attempt } /** - * @internal - * * @return Attempt */ public function remove(Path $path): Attempt @@ -100,9 +100,6 @@ public function remove(Path $path): Attempt ); } - /** - * @internal - */ public function exists(Path $path): bool { return $this->access($path)->match( From 3707f2fa19337b625d5fe6191cd0c2625525782f Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 25 Jan 2026 16:47:07 +0100 Subject: [PATCH 36/37] fix documentation --- documentation/sockets.md | 4 ++-- documentation/streams.md | 2 +- documentation/use-cases/socket/heartbeat.md | 2 +- documentation/use-cases/socket/read.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/documentation/sockets.md b/documentation/sockets.md index 9675364..a6a243a 100644 --- a/documentation/sockets.md +++ b/documentation/sockets.md @@ -81,7 +81,7 @@ This is the default behaviour. ### `->timeoutAfter()` ```php -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; $server = $server->timeoutAfter(Period::second(1)); ``` @@ -172,7 +172,7 @@ This is default behaviour. ### `->timeoutAfter()` ```php -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; $client = $client->timeoutAfter(Period::second(1)); ``` diff --git a/documentation/streams.md b/documentation/streams.md index b7ccc11..b6b05a7 100644 --- a/documentation/streams.md +++ b/documentation/streams.md @@ -52,7 +52,7 @@ This is default behaviour. ### `->timeoutAfter()` ```php -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; $read = $read->timeoutAfter(Period::second(1)); ``` diff --git a/documentation/use-cases/socket/heartbeat.md b/documentation/use-cases/socket/heartbeat.md index ce6acb1..9a26117 100644 --- a/documentation/use-cases/socket/heartbeat.md +++ b/documentation/use-cases/socket/heartbeat.md @@ -11,7 +11,7 @@ use Innmind\IO\{ Frame, Sockets\Internet\Transport, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Url\Url; use Innmind\Immutable\{ Str, diff --git a/documentation/use-cases/socket/read.md b/documentation/use-cases/socket/read.md index 65d8f8b..5abddb0 100644 --- a/documentation/use-cases/socket/read.md +++ b/documentation/use-cases/socket/read.md @@ -11,7 +11,7 @@ use Innmind\IO\{ Frame, Sockets\Internet\Transport, }; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\Url\Url; $status = IO::fromAmbienAuthority() From abd238b366038544803e25483da21e90476435a3 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 25 Jan 2026 16:51:09 +0100 Subject: [PATCH 37/37] add extensive CI --- .github/workflows/extensive.yml | 12 ++++++++++++ blackbox.php | 4 ++++ 2 files changed, 16 insertions(+) create mode 100644 .github/workflows/extensive.yml diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 0000000..257f139 --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main diff --git a/blackbox.php b/blackbox.php index c402132..c109cd0 100644 --- a/blackbox.php +++ b/blackbox.php @@ -11,6 +11,10 @@ }; Application::new($argv) + ->when( + \getenv('BLACKBOX_SET_SIZE') !== false, + static fn(Application $app) => $app->scenariiPerProof((int) \getenv('BLACKBOX_SET_SIZE')), + ) ->when( \getenv('ENABLE_COVERAGE') !== false, static fn(Application $app) => $app