Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions documentation/components/bridges/symfony-filesystem-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,52 @@ final class ReportBuilder
> Most applications need exactly one fstab with multiple filesystems mounted under it. Multi-fstab support
> exists for advanced cases — see [Multi-Fstab Support](#multi-fstab-support).

### Injecting a Single Filesystem

When a service only needs one mounted filesystem, inject the `Flow\Filesystem\Filesystem` directly instead
of the whole table. Every mount is registered as a private service and exposed two ways.

**Named-argument autowiring** — each mount of the default fstab is aliased as `Filesystem $<protocol>`, and
every mount (of any fstab) as `Filesystem $<fstab><Protocol>`. Protocols containing `-`, `.` or `+` are
camel-cased (`aws-s3` → `awsS3`). This mirrors the `FilesystemTable $<name>Fstab` convention:

```php
use Flow\Filesystem\Filesystem;

final class ReportBuilder
{
public function __construct(
private readonly Filesystem $warehouse, // → default fstab, 'warehouse' mount
private readonly Filesystem $analyticsFile, // → 'analytics' fstab, 'file' mount
) {
}
}
```

**`#[AsFilesystem]` attribute** — for explicit selection or when the argument name should differ from the
protocol. Omitting `fstab` targets the default fstab:

```php
use Flow\Bridge\Symfony\FilesystemBundle\Attribute\AsFilesystem;
use Flow\Filesystem\Filesystem;

final class ReportBuilder
{
public function __construct(
#[AsFilesystem('warehouse')]
private readonly Filesystem $primary,
#[AsFilesystem('cold', fstab: 'archive')]
private readonly Filesystem $coldStorage,
) {
}
}
```

Named-argument aliases work for any autowired service; the attribute requires the consuming service to be
autoconfigured (the default for everything under your `App\` namespace). Both resolve through the fstab's
`FilesystemTable::for()`, so telemetry decoration is preserved. An unknown protocol or fstab fails at
container-compile time with the available options listed.

## Configuration Reference

### Fstabs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\FilesystemBundle\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class AsFilesystem
{
/**
* @param string $mount mount protocol to inject (the YAML key under `filesystems:`)
* @param null|string $fstab fstab name; defaults to the bundle's resolved default fstab when null
*/
public function __construct(
public string $mount,
public ?string $fstab = null,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Flow\Bridge\Symfony\FilesystemBundle\Exception\LogicException;
use Flow\Bridge\Symfony\FilesystemBundle\Filesystem\FstabBuilder;
use Flow\Filesystem\Filesystem;
use Flow\Filesystem\FilesystemTable;
use Flow\Filesystem\Telemetry\FilesystemTelemetryConfig;
use Flow\Filesystem\Telemetry\FilesystemTelemetryOptions;
Expand All @@ -22,6 +23,7 @@
use function lcfirst;
use function sprintf;
use function str_replace;
use function ucfirst;
use function ucwords;

final class BuildFstabsPass implements CompilerPassInterface
Expand All @@ -30,6 +32,8 @@ final class BuildFstabsPass implements CompilerPassInterface

public const string FSTAB_SERVICE_PREFIX = '.flow.filesystem.fstab.';

public const string FS_SERVICE_PREFIX = '.flow.filesystem.fs.';

public const string TELEMETRY_CONFIG_SERVICE_PREFIX = '.flow.filesystem.telemetry_config.';

public function process(ContainerBuilder $container): void
Expand Down Expand Up @@ -111,6 +115,14 @@ public function process(ContainerBuilder $container): void

$aliasId = FilesystemTable::class . ' $' . $this->camelCase($fstabNameStr) . 'Fstab';
$container->setAlias($aliasId, $serviceId)->setPublic(true);

$this->registerMountServices(
$container,
$fstabNameStr,
$serviceId,
array_keys($resolvedFilesystems),
$fstabNameStr === $defaultFstab,
);
}

if ($defaultFstab !== null) {
Expand Down Expand Up @@ -175,7 +187,41 @@ private function buildTelemetryConfigReference(

private function camelCase(string $name): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $name))));
return lcfirst(str_replace(' ', '', ucwords(str_replace(['_', '-', '.', '+'], ' ', $name))));
}

/**
* @param list<string> $protocols mount protocols within the fstab
*/
private function registerMountServices(
ContainerBuilder $container,
string $fstabName,
string $fstabServiceId,
array $protocols,
bool $isDefault,
): void {
foreach ($protocols as $protocol) {
$mountServiceId = self::FS_SERVICE_PREFIX . $fstabName . '.' . $protocol;

$mountDefinition = new Definition(Filesystem::class);
$mountDefinition->setFactory([new Reference($fstabServiceId), 'for']);
$mountDefinition->setArguments([$protocol]);
$mountDefinition->setPublic(false);
$container->setDefinition($mountServiceId, $mountDefinition);

$container
->setAlias(
Filesystem::class . ' $' . $this->camelCase($fstabName) . ucfirst($this->camelCase($protocol)),
$mountServiceId,
)
->setPublic(true);

if ($isDefault) {
$container
->setAlias(Filesystem::class . ' $' . $this->camelCase($protocol), $mountServiceId)
->setPublic(true);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\FilesystemBundle\DependencyInjection\Compiler;

use Flow\Bridge\Symfony\FilesystemBundle\Exception\LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

use function array_key_exists;
use function array_keys;
use function implode;
use function is_array;
use function is_string;
use function sprintf;

final class ResolveFilesystemArgumentsPass implements CompilerPassInterface
{
public const string TAG = 'flow.filesystem.as_filesystem';

public function process(ContainerBuilder $container): void
{
if (!$container->hasParameter(BuildFstabsPass::CONFIG_PARAMETER)) {
return;
}

$config = $container->getParameter(BuildFstabsPass::CONFIG_PARAMETER);

if (!is_array($config)) {
return;
}

$fstabs = is_array($config['fstabs'] ?? null) ? $config['fstabs'] : [];
$defaultFstab = is_string($config['default_fstab'] ?? null) ? $config['default_fstab'] : null;

foreach ($container->findTaggedServiceIds(self::TAG) as $serviceId => $tags) {
$definition = $container->getDefinition($serviceId);

// @mago-expect analysis:mixed-assignment
foreach ($tags as $tag) {
if (!is_array($tag)) {
continue;
}

$argument = is_string($tag['argument'] ?? null) ? $tag['argument'] : '';
$mount = is_string($tag['mount'] ?? null) ? $tag['mount'] : '';
$fstab = is_string($tag['fstab'] ?? null) && $tag['fstab'] !== '' ? $tag['fstab'] : $defaultFstab;

if ($fstab === null) {
throw new LogicException(sprintf(
'Service "%s" uses #[AsFilesystem(\'%s\')] without an fstab, but no default fstab is configured.',
$serviceId,
$mount,
));
}

if (!array_key_exists($fstab, $fstabs) || !is_array($fstabs[$fstab])) {
throw new LogicException(sprintf(
'Service "%s": #[AsFilesystem] references fstab "%s" which is not configured. Available fstabs: [%s].',
$serviceId,
$fstab,
implode(', ', array_keys($fstabs)),
));
}

$mounts = is_array($fstabs[$fstab]['filesystems'] ?? null) ? $fstabs[$fstab]['filesystems'] : [];

if (!array_key_exists($mount, $mounts)) {
throw new LogicException(sprintf(
'Service "%s": #[AsFilesystem] references mount "%s" which is not mounted in fstab "%s". Available mounts: [%s].',
$serviceId,
$mount,
$fstab,
implode(', ', array_keys($mounts)),
));
}

$definition->setArgument(
'$' . $argument,
new Reference(BuildFstabsPass::FS_SERVICE_PREFIX . $fstab . '.' . $mount),
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@

namespace Flow\Bridge\Symfony\FilesystemBundle;

use Flow\Bridge\Symfony\FilesystemBundle\Attribute\AsFilesystem;
use Flow\Bridge\Symfony\FilesystemBundle\Attribute\AsFilesystemFactory;
use Flow\Bridge\Symfony\FilesystemBundle\DependencyInjection\Compiler\BuildFstabsPass;
use Flow\Bridge\Symfony\FilesystemBundle\DependencyInjection\Compiler\RegisterFilesystemFactoriesPass;
use Flow\Bridge\Symfony\FilesystemBundle\DependencyInjection\Compiler\RegisterFstabLocatorPass;
use Flow\Bridge\Symfony\FilesystemBundle\DependencyInjection\Compiler\ResolveFilesystemArgumentsPass;
use Flow\Bridge\Symfony\FilesystemCache\FlowFilesystemCacheAdapter;
use Flow\Filesystem\Filesystem;
use Flow\Filesystem\Path;
use Override;
use ReflectionParameter;
use Reflector;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
Expand Down Expand Up @@ -47,6 +50,7 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new RegisterFilesystemFactoriesPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
$container->addCompilerPass(new BuildFstabsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new RegisterFstabLocatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10);
$container->addCompilerPass(new ResolveFilesystemArgumentsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -20);

$container->registerAttributeForAutoconfiguration(AsFilesystemFactory::class, static function (
ChildDefinition $definition,
Expand All @@ -55,6 +59,18 @@ public function build(ContainerBuilder $container): void
): void {
$definition->addTag(RegisterFilesystemFactoriesPass::TAG, ['type' => $attribute->type]);
});

$container->registerAttributeForAutoconfiguration(AsFilesystem::class, static function (
ChildDefinition $definition,
AsFilesystem $attribute,
ReflectionParameter $parameter,
): void {
$definition->addTag(ResolveFilesystemArgumentsPass::TAG, [
'argument' => $parameter->getName(),
'mount' => $attribute->mount,
'fstab' => $attribute->fstab ?? '',
]);
});
}

#[Override]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\FilesystemBundle\Tests\Context;

use Flow\Bridge\Symfony\FilesystemBundle\DependencyInjection\Compiler\ResolveFilesystemArgumentsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

final class ResolveFilesystemArgumentsPassContext
{
/**
* @param array<string, mixed> $config
*/
public function containerWithConfig(array $config): ContainerBuilder
{
$container = new ContainerBuilder();
$container->setParameter('flow.filesystem.config', $config);

return $container;
}

/**
* @param array{argument: string, mount: string, fstab?: string} $tag
*/
public function registerConsumer(ContainerBuilder $container, string $serviceId, array $tag): Definition
{
$definition = new Definition('Flow\\Consumer');
$definition->addTag(ResolveFilesystemArgumentsPass::TAG, $tag);
$container->setDefinition($serviceId, $definition);

return $definition;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\FilesystemBundle\Tests\Fixtures;

use Flow\Filesystem\Filesystem;

final readonly class AliasFilesystemConsumer
{
public function __construct(
public Filesystem $memory,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\FilesystemBundle\Tests\Fixtures;

use Flow\Bridge\Symfony\FilesystemBundle\Attribute\AsFilesystem;
use Flow\Filesystem\Filesystem;

final readonly class AttributeFilesystemConsumer
{
public function __construct(
#[AsFilesystem('memory')]
public Filesystem $primary,
#[AsFilesystem('file', fstab: 'archive')]
public Filesystem $cold,
) {}
}
Loading
Loading