Skip to content

Commit e5d98b4

Browse files
committed
chore: cleanup fragile tests, contexts, bundle configs
1 parent 1579541 commit e5d98b4

30 files changed

Lines changed: 2032 additions & 361 deletions

File tree

documentation/components/bridges/symfony-filesystem-bundle.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This bundle integrates Flow PHP's Filesystem library with Symfony applications.
3232
- **Pluggable filesystem factories** — register custom backends with the `#[AsFilesystemFactory]` attribute or a DI tag
3333
- **Built-in factories**`file`, `memory`, `stdout`, `aws_s3`, and `azure_blob` ship out of the box
3434
- **Console commands**`flow:filesystem:*` (alias `flow:fs:*`) for `ls`, `cat`, `cp`, `mv`, `rm`, `stat`, `touch` against any configured filesystem
35+
- **Symfony Cache pools** — register PSR-6 cache pools backed by any mounted filesystem (local disk, S3, Azure Blob …) when [flow-php/symfony-filesystem-cache-bridge](/documentation/components/bridges/symfony-filesystem-cache-bridge.md) is installed
3536
- **Telemetry integration** — wrap every filesystem in `TraceableFilesystem` via OpenTelemetry
3637
- **Multi-fstab support** *(advanced)* — configure several independent `FilesystemTable` services when you really need them
3738

@@ -393,6 +394,119 @@ Remote object stores (S3, Azure Blob, …) do not have a real concept of directo
393394
keyspaces with `/` as a convention. Rather than emulate `mkdir` inconsistently across backends, the bundle
394395
omits the command entirely. Directories appear when files appear inside them.
395396

397+
## Symfony Cache Integration
398+
399+
The bundle integrates with [flow-php/symfony-filesystem-cache-bridge](/documentation/components/bridges/symfony-filesystem-cache-bridge.md) to provide PSR-6 / Symfony Cache pools backed by any filesystem already mounted in a fstab — local disk, S3, Azure Blob, anything the bundle's factories can build. The adapter implements `PruneableInterface`, so `cache:pool:prune` works out of the box.
400+
401+
Each pool resolves its filesystem **through a fstab mount**, not by referencing a service id directly. Filesystems must already be declared under `flow_filesystem.fstabs.<fstab>.filesystems.<protocol>` before a cache pool can target them. This keeps fstab the single place where filesystems live and avoids the cache and the rest of the app drifting into separate filesystem definitions.
402+
403+
### Setup
404+
405+
1. Install the cache bridge:
406+
407+
```bash
408+
composer require flow-php/symfony-filesystem-cache-bridge:~--FLOW_PHP_VERSION--
409+
```
410+
411+
2. Define one or more pools under `flow_filesystem.cache.pools`. Each pool names a fstab mount (the YAML key under `filesystems:`) and a base path inside it:
412+
413+
```yaml
414+
# config/packages/flow_filesystem.yaml
415+
flow_filesystem:
416+
fstabs:
417+
default:
418+
filesystems:
419+
file:
420+
type: file
421+
422+
cache:
423+
pools:
424+
app:
425+
filesystem: file # mount protocol from the default fstab
426+
path: '%kernel.project_dir%/var/cache/flow/app'
427+
default_lifetime: 3600
428+
429+
sessions:
430+
filesystem: file
431+
path: '%kernel.project_dir%/var/cache/flow/sessions'
432+
namespace: 'sess.'
433+
default_lifetime: 86400
434+
```
435+
436+
`fstab` is optional and defaults to the bundle's resolved default fstab — same rule the `flow:filesystem:*` CLI commands follow. Set it explicitly when you want a pool to use a non-default fstab:
437+
438+
```yaml
439+
flow_filesystem:
440+
default_fstab: primary
441+
fstabs:
442+
primary:
443+
filesystems:
444+
file:
445+
type: file
446+
archive:
447+
filesystems:
448+
aws-s3:
449+
type: aws_s3
450+
bucket: '%env(ARCHIVE_BUCKET)%'
451+
452+
cache:
453+
pools:
454+
cold_storage:
455+
fstab: archive
456+
filesystem: aws-s3
457+
path: '/cache/cold'
458+
default_lifetime: 86400
459+
```
460+
461+
Each pool registers as `flow_filesystem.cache.pool.<name>` (public).
462+
463+
3. Wire the pools into Symfony's cache framework via `cache.adapter.psr6`:
464+
465+
```yaml
466+
# config/packages/framework.yaml
467+
framework:
468+
cache:
469+
pools:
470+
cache.app_fs:
471+
adapter: cache.adapter.psr6
472+
provider: flow_filesystem.cache.pool.app
473+
474+
cache.sessions_fs:
475+
adapter: cache.adapter.psr6
476+
provider: flow_filesystem.cache.pool.sessions
477+
```
478+
479+
The `cache.adapter.psr6` wrapper is required because Symfony's `CachePoolPass` overwrites the first constructor argument of any service used directly as `adapter:`, which conflicts with this bridge's strict `Filesystem` typing on argument 0.
480+
481+
### Configuration Options (per pool)
482+
483+
| Option | Default | Description |
484+
|--------------------------|----------------|--------------------------------------------------------------------------------------------------------|
485+
| `fstab` | default fstab | Fstab name. Defaults to the bundle's resolved default fstab when omitted. |
486+
| `filesystem` | *required* | Mount protocol within the chosen fstab (the YAML key under `filesystems:`). |
487+
| `path` | *required* | Base directory inside the chosen filesystem where cache files are stored. |
488+
| `namespace` | `''` | Cache pool namespace; chars in `[-+.A-Za-z0-9]` only. |
489+
| `default_lifetime` | `0` | Default TTL in seconds; `0` means no expiry. |
490+
| `marshaller_service_id` | `null` | Service ID of a custom `MarshallerInterface`. |
491+
492+
Validation runs at container compile time:
493+
494+
- A pool referencing a missing fstab fails with `flow_filesystem.cache.pools.<name>: fstab "<x>" is not declared. Available fstabs: [...]`.
495+
- A pool referencing a mount protocol that is not registered in the chosen fstab fails with `flow_filesystem.cache.pools.<name>: filesystem "<x>" is not mounted in fstab "<y>". Available mounts: [...]`.
496+
- `flow_filesystem.cache.pools` is configured but `flow-php/symfony-filesystem-cache-bridge` is not installed → fails fast with a message pointing at the missing package.
497+
498+
### Pruning
499+
500+
Schedule the standard Symfony command on a cron to remove expired files:
501+
502+
```bash
503+
php bin/console cache:pool:prune
504+
```
505+
506+
Without pruning, expired files accumulate under each pool's directory. They are filtered out on read but only deleted when either the same key is fetched again or `cache:pool:prune` runs.
507+
508+
For full documentation, see the [Symfony Filesystem Cache Bridge](/documentation/components/bridges/symfony-filesystem-cache-bridge.md).
509+
396510
## Multi-Fstab Support
397511

398512
> **Advanced.** Most applications should stick to a single fstab with multiple filesystems mounted under

documentation/components/bridges/symfony-filesystem-cache-bridge.md

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Symfony Filesystem Cache Bridge
22

3-
A Symfony Cache adapter backed by Flow PHP's native filesystem library. Cache items are stored as files using Flow's `Filesystem` abstraction, so the same adapter works with local filesystems, Azure Blob Storage, S3, and any other Flow filesystem implementation.
3+
A Symfony Cache adapter backed by Flow PHP's native `Filesystem` library. Cache items are stored as files on top of any filesystem the library can mount — local disk, in-memory, AWS S3, Azure Blob — without depending on Symfony's `FilesystemAdapter` and the local-only assumptions baked into it.
44

55
- [Back](/documentation/introduction.md)
66
- [Packagist](https://packagist.org/packages/flow-php/symfony-filesystem-cache-bridge)
7+
- [Installation](/documentation/installation/packages/symfony-filesystem-cache-bridge.md)
78
- [GitHub](https://github.com/flow-php/symfony-filesystem-cache-bridge)
89
- [API Reference](/documentation/api/bridge/symfony-filesystem-cache)
910

@@ -15,6 +16,93 @@ A Symfony Cache adapter backed by Flow PHP's native filesystem library. Cache it
1516
composer require flow-php/symfony-filesystem-cache-bridge:~--FLOW_PHP_VERSION--
1617
```
1718

19+
For Symfony framework integration (config-driven pool registration) use this bridge through the [Symfony Filesystem Bundle](/documentation/components/bridges/symfony-filesystem-bundle.md). The page below covers standalone usage.
20+
21+
## How It Works
22+
23+
`FlowFilesystemCacheAdapter` extends Symfony's `AbstractAdapter` and implements `PruneableInterface`. Items are stored one-per-file under a base directory you provide:
24+
25+
```
26+
<base>/
27+
ab/
28+
qZ5K-… ← single cache item
29+
c4/
30+
Mn7p-…
31+
```
32+
33+
- The 2-character shard directory is `substr(str_replace('/', '-', base64_encode(hash('xxh128', $id, true))), 0, 2)` — the same recipe as Symfony's built-in `FilesystemAdapter`, so the on-disk layout is familiar.
34+
- Each file body has three newline-separated parts: a 10-digit zero-padded expiry timestamp (`0000000000` for "no expiry"), the cache id, and the marshalled value.
35+
- **Saves** write to a randomised temp path (`Path::randomize()`), then `mv()` to the final path. If `mv()` returns `false` the temp file is removed and the failure is reported to the configured logger as a `FilesystemCacheException`.
36+
- **Reads** stream the file once, split on `"\n"`, drop the row when the expiry has passed and call `doDelete()` for the expired ids in the same call.
37+
- **`prune()`** walks the base directory recursively, reads only the first line of every file, and deletes any file whose expiry is in the past.
38+
- **Marshalling** uses Symfony's `DefaultMarshaller` by default; pass a custom `MarshallerInterface` to the constructor to override.
39+
40+
## Filesystem Ownership
41+
42+
The adapter does NOT create or own a `Filesystem`. You hand it any `Flow\Filesystem\Filesystem` instance and a `Path` for the base directory:
43+
44+
- **Local disk** — pass `new NativeLocalFilesystem()` and a `Path::from('/var/cache/app')`.
45+
- **Remote object store** — pass an S3 or Azure-backed filesystem; the adapter writes through it transparently.
46+
- **Same instance, multiple pools** — give two adapters the same `Filesystem` but different `Path` directories (or different `namespace` strings) to keep their files isolated.
47+
48+
The `Path` value is the only state the adapter holds about location — every `getItem`/`save`/`clear` call composes the per-item path from it, so changing the directory means re-instantiating the adapter.
49+
1850
## Usage
1951

20-
TODO: Add usage documentation
52+
```php
53+
use Flow\Bridge\Symfony\FilesystemCache\FlowFilesystemCacheAdapter;
54+
use Flow\Filesystem\Local\NativeLocalFilesystem;
55+
use Flow\Filesystem\Path;
56+
57+
$cache = new FlowFilesystemCacheAdapter(
58+
filesystem: new NativeLocalFilesystem(),
59+
directory: Path::from('/var/cache/app'),
60+
namespace: 'app',
61+
defaultLifetime: 3600,
62+
);
63+
64+
$item = $cache->getItem('greeting');
65+
66+
if (!$item->isHit()) {
67+
$item->set('hello world');
68+
$item->expiresAfter(600);
69+
$cache->save($item);
70+
}
71+
72+
echo $cache->getItem('greeting')->get();
73+
```
74+
75+
The same code works against `flow-php/filesystem-async-aws-bridge` or `flow-php/filesystem-azure-bridge` — swap the `Filesystem` argument and the cache lives in S3 or Azure Blob.
76+
77+
## Constructor
78+
79+
```php
80+
new FlowFilesystemCacheAdapter(
81+
Flow\Filesystem\Filesystem $filesystem,
82+
Flow\Filesystem\Path $directory,
83+
string $namespace = '',
84+
int $defaultLifetime = 0,
85+
?Symfony\Component\Cache\Marshaller\MarshallerInterface $marshaller = null,
86+
);
87+
```
88+
89+
`$namespace` is validated against `[-+.A-Za-z0-9]` — same rule Symfony enforces. `$directory` must be a non-pattern path; the adapter creates the shard directories on demand via the underlying filesystem's `writeTo()`.
90+
91+
## File Layout
92+
93+
| Section | Format | Notes |
94+
|---------|--------|-------|
95+
| Line 1 | `printf('%010d', $expiry)` | Zero-padded UNIX timestamp; `0000000000` means "never expires" |
96+
| Line 2 | raw cache id | Used by `clear($prefix)` to match items by namespace prefix |
97+
| Line 3+ | marshaller output | Whatever `MarshallerInterface::marshall()` produced (binary safe) |
98+
99+
The shard prefix keeps any single directory bounded — even with millions of keys, no shard dir holds more than `~items / 256 / 256` entries on average, and the tree never grows wider than 256 sub-directories.
100+
101+
## Pruning
102+
103+
The filesystem has no built-in TTL, so expired files are removed by either of two paths:
104+
105+
- **Lazy** — a read of the same key triggers a delete of that one file.
106+
- **Explicit** — calling `$cache->prune()` (or `bin/console cache:pool:prune` when the adapter is wired into a Symfony app) walks the base directory and deletes everything past its expiry.
107+
108+
Without periodic pruning, dead files accumulate even though they are invisible to readers.

documentation/installation/packages/symfony-filesystem-bundle.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ Mount remote object stores by installing the matching bridge alongside the bundl
3636

3737
## Suggested Dependencies
3838

39+
- [flow-php/symfony-filesystem-cache-bridge](/documentation/installation/packages/symfony-filesystem-cache-bridge.md) — for PSR-6 / Symfony Cache pools backed by any mounted filesystem (local disk, S3, Azure Blob)
3940
- [flow-php/symfony-telemetry-bundle](/documentation/installation/packages/symfony-telemetry-bundle.md) — for telemetry integration (distributed tracing, metrics, logging)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
seo_title: "Installing Symfony Filesystem Cache Bridge"
3+
seo_description: >
4+
How to install flow-php/symfony-filesystem-cache-bridge in your PHP project using Composer.
5+
---
6+
7+
# Symfony Filesystem Cache Bridge
8+
9+
- [Back](/documentation/installation.md)
10+
- [Documentation](/documentation/components/bridges/symfony-filesystem-cache-bridge.md)
11+
- [Packagist](https://packagist.org/packages/flow-php/symfony-filesystem-cache-bridge)
12+
13+
[TOC]
14+
15+
## Composer
16+
17+
```bash
18+
composer require flow-php/symfony-filesystem-cache-bridge:~--FLOW_PHP_VERSION--
19+
```
20+
21+
## Core Dependencies
22+
23+
- [flow-php/filesystem](/documentation/installation/packages/filesystem.md)
24+
- [symfony/cache](https://packagist.org/packages/symfony/cache)
25+
26+
## Recommended Packages
27+
28+
- [flow-php/symfony-filesystem-bundle](/documentation/installation/packages/symfony-filesystem-bundle.md) — registers the cache adapter from `flow_filesystem.cache.pools` config
29+
- [flow-php/filesystem-async-aws-bridge](/documentation/installation/packages/filesystem-async-aws-bridge.md) — to back cache pools with AWS S3
30+
- [flow-php/filesystem-azure-bridge](/documentation/installation/packages/filesystem-azure-bridge.md) — to back cache pools with Azure Blob Storage
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Psr18\Telemetry\Tests\Integration;
6+
7+
/**
8+
* Forks a child process that serves a fixed 200 OK on a free loopback port
9+
* for one test. Avoids depending on public URLs or PHP's built-in web server
10+
* (the latter writes a lock file that's not always writable under sandboxes).
11+
*/
12+
final class LocalHttpServer
13+
{
14+
private int $childPid = 0;
15+
16+
private int $port = 0;
17+
18+
public function port() : int
19+
{
20+
return $this->port;
21+
}
22+
23+
public function start() : void
24+
{
25+
if (!\function_exists('pcntl_fork')) {
26+
throw new \RuntimeException('pcntl_fork() is required to run the local test HTTP server.');
27+
}
28+
29+
$server = \stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
30+
31+
if (!\is_resource($server)) {
32+
throw new \RuntimeException(\sprintf('Failed to bind local test HTTP server: %s (%d)', $errstr, $errno));
33+
}
34+
35+
$name = \stream_socket_get_name($server, false);
36+
37+
if (!\is_string($name)) {
38+
\fclose($server);
39+
40+
throw new \RuntimeException('Failed to read bound socket name.');
41+
}
42+
43+
$colon = \strrpos($name, ':');
44+
45+
if ($colon === false) {
46+
\fclose($server);
47+
48+
throw new \RuntimeException(\sprintf('Unexpected socket name "%s".', $name));
49+
}
50+
51+
$this->port = (int) \substr($name, $colon + 1);
52+
53+
$pid = \pcntl_fork();
54+
55+
if ($pid === -1) {
56+
\fclose($server);
57+
58+
throw new \RuntimeException('pcntl_fork() failed.');
59+
}
60+
61+
if ($pid === 0) {
62+
$this->serveLoop($server);
63+
}
64+
65+
\fclose($server);
66+
$this->childPid = $pid;
67+
}
68+
69+
public function stop() : void
70+
{
71+
if ($this->childPid > 0) {
72+
@\posix_kill($this->childPid, \SIGTERM);
73+
\pcntl_waitpid($this->childPid, $status);
74+
$this->childPid = 0;
75+
}
76+
}
77+
78+
public function url() : string
79+
{
80+
return \sprintf('http://127.0.0.1:%d', $this->port);
81+
}
82+
83+
/**
84+
* @param resource $server
85+
*/
86+
private function serveLoop($server) : never
87+
{
88+
\pcntl_signal(\SIGTERM, static function () : void {
89+
exit(0);
90+
});
91+
92+
while (true) {
93+
\pcntl_signal_dispatch();
94+
$client = @\stream_socket_accept($server, 1.0);
95+
96+
if (!\is_resource($client)) {
97+
continue;
98+
}
99+
100+
\stream_set_timeout($client, 1);
101+
102+
// Read request line + headers; we don't actually need the body.
103+
while (!\feof($client)) {
104+
$line = \fgets($client, 8192);
105+
106+
if ($line === false || $line === "\r\n" || $line === "\n") {
107+
break;
108+
}
109+
}
110+
111+
$body = 'ok';
112+
$response = "HTTP/1.1 200 OK\r\n"
113+
. "Content-Type: text/plain\r\n"
114+
. 'Content-Length: ' . \strlen($body) . "\r\n"
115+
. "Connection: close\r\n"
116+
. "\r\n"
117+
. $body;
118+
119+
\fwrite($client, $response);
120+
\fclose($client);
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)