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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Latest
------

### Changes
* [#53](https://github.com/cleverage/cache-process-bundle/issues/53) AdapterRegistry: the adapters are loaded lazily from a service locator indexed by code: an adapter is only instantiated when a task uses it (an error in an adapter that a process does not use no longer makes it fail). The code is given by the new `code` attribute of the `cleverage.cache.adapter` tag, or by the new `#[AsCacheAdapter(code: '...')]` attribute (autoconfiguration); it must be the one returned by `getCode()`. Duplicate or empty codes fail the container compilation. An adapter tagged without `code` is still registered eagerly, with `AdapterRegistry::addAdapter()`, which is deprecated (the `code` will be required in 3.0). New optional `AdapterRegistry` constructor argument (the locator), new `hasAdapter()` and `getAdapters()` methods. Require `symfony/service-contracts`. Update documentation, add tests.
* [#51](https://github.com/cleverage/cache-process-bundle/issues/51) Use the PHP 8.3 to 8.5 functions through the Symfony polyfills ([cleverage/process-bundle#299](https://github.com/cleverage/process-bundle/issues/299)): require `symfony/polyfill-php83`, `symfony/polyfill-php84` and `symfony/polyfill-php85`. Rector is configured with the minimum supported PHP version (`withPhpVersion(PhpVersion::PHP_82)`, it used PHP 8.5 features) and `withPhpSets()` without explicit version, which applies the rules using a newer function when its polyfill is required (no change on the current code). Document it in `CONTRIBUTING.md`.
* [#49](https://github.com/cleverage/cache-process-bundle/issues/49) Add a `CleverAge\CacheProcessBundle\Registry\AdapterRegistry` alias of the `cleverage_cache_process.registry.adapter` service: the registry can be autowired, e.g. in a custom cache task extending `AbstractCacheTask`. Update documentation, add tests.
* [#43](https://github.com/cleverage/cache-process-bundle/issues/43) Give a code to the tasks ([cleverage/process-bundle#91](https://github.com/cleverage/process-bundle/issues/91)): `cache_get` and `cache_set` (`task: cache_get`, with `cleverage/process-bundle` 5.4 or later, referencing them by `service` being deprecated there). The tasks have their class name as service id; fetching them directly from the service container is deprecated (`container.private`), they will be private in 3.0. The former ids `cleverage_cache_process.task.get` / `cleverage_cache_process.task.set` are deprecated aliases, removed in 3.0. Update documentation, add tests.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"symfony/options-resolver": "^6.4 || ^7.4 || ^8",
"symfony/polyfill-php83": "^1.33",
"symfony/polyfill-php84": "^1.33",
"symfony/polyfill-php85": "^1.33"
"symfony/polyfill-php85": "^1.33",
"symfony/service-contracts": "^2.5 || ^3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
Expand Down
2 changes: 2 additions & 0 deletions config/services/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ services:
cleverage_cache_process.registry.adapter:
class: CleverAge\CacheProcessBundle\Registry\AdapterRegistry
public: false
arguments:
- ~ # Service locator of the adapters by code, see RegisterAdaptersPass

CleverAge\CacheProcessBundle\Registry\AdapterRegistry:
alias: cleverage_cache_process.registry.adapter
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbooks/cache_warmup.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ services:
class: CleverAge\CacheProcessBundle\Adapter\Adapter
arguments: ['@app.cache.catalog', 'catalog']
tags:
- { name: cleverage.cache.adapter }
- { name: cleverage.cache.adapter, code: catalog }
```

Then define a process storing each line of the file, and another one reading a line by key:
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbooks/share_data_between_branches.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ services:
app.cleverage_cache_process.adapter.memory:
class: App\Adapter\MemoryAdapter
tags:
- { name: cleverage.cache.adapter }
- { name: cleverage.cache.adapter, code: memory }
```

Then define the process:
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ CleverAge\CacheProcessBundle\CleverAgeCacheProcessBundle::class => ['all' => tru
The bundle has no semantic configuration. To use the cache tasks, declare at least one cache
[adapter](reference/adapter.md): a service implementing `CleverAge\CacheProcessBundle\Adapter\AdapterInterface`
(usually `CleverAge\CacheProcessBundle\Adapter\Adapter`, wrapping any Symfony cache pool), tagged
`cleverage.cache.adapter`. Its code is then used in the `adapter` option of the tasks.
`cleverage.cache.adapter` with its code (or with the `#[AsCacheAdapter]` attribute). This code is then used in the
`adapter` option of the tasks.

The tasks are registered as services (non-shared) and are used in your processes by their code, `cache_get` and
`cache_set` (e.g. `task: cache_get`, requires `cleverage/process-bundle` 5.4 or later, see its
Expand All @@ -43,7 +44,7 @@ services:
class: CleverAge\CacheProcessBundle\Adapter\Adapter
arguments: ['@cache.app', 'app']
tags:
- { name: cleverage.cache.adapter }
- { name: cleverage.cache.adapter, code: app }
```

## Custom cache tasks
Expand Down
25 changes: 18 additions & 7 deletions docs/reference/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ Adapter reference
option of the tasks)
* **Base class**: `CleverAge\CacheProcessBundle\Adapter\Adapter`, decorates any
`Symfony\Component\Cache\Adapter\AdapterInterface` (every PSR-6 method is forwarded to it)
* **Service tag**: `cleverage.cache.adapter`, every tagged service is registered in the
`cleverage_cache_process.registry.adapter` registry (`CleverAge\CacheProcessBundle\Registry\AdapterRegistry`)
* **Service tag**: `cleverage.cache.adapter`, with a `code` attribute (`- { name: cleverage.cache.adapter, code: memory }`):
every tagged service is registered in the `cleverage_cache_process.registry.adapter` registry
(`CleverAge\CacheProcessBundle\Registry\AdapterRegistry`, autowirable by its class name)
* **Attribute**: `CleverAge\CacheProcessBundle\Attribute\AsCacheAdapter` (`#[AsCacheAdapter(code: 'memory')]`, with
autoconfiguration), equivalent to the tag

Constructor arguments
---------------------
Expand Down Expand Up @@ -56,7 +59,7 @@ services:
app.cleverage_cache_process.adapter.memory:
class: App\Adapter\MemoryAdapter
tags:
- { name: cleverage.cache.adapter }
- { name: cleverage.cache.adapter, code: memory }
```

* Persistent adapter, wrapping a cache pool of the FrameworkBundle, without any PHP class
Expand All @@ -80,15 +83,23 @@ services:
- '@app.cache.catalog'
- 'catalog'
tags:
- { name: cleverage.cache.adapter }
- { name: cleverage.cache.adapter, code: catalog }
```

Notes
-----

* Codes must be unique: registering two adapters with the same code throws an `\UnexpectedValueException` giving the
ids of both services (`Adapter <code> is already defined by service "<id>", cannot register service "<id>"`) when
the registry is instantiated, i.e. the first time a cache task is used.
* The adapters are registered by the `code` attribute of their tag, in a service locator built when the container is
compiled: an adapter is only instantiated when a task uses it (an error in an adapter that a process does not use
does not make it fail). The `code` must be the one returned by `getCode()` (an `\UnexpectedValueException` is thrown
otherwise when it is fetched). Two adapters with the same code, or an empty code, fail the container compilation.
`AdapterRegistry::hasAdapter()` and `getAdapters()` (which instantiates every adapter) are available, e.g. in a
custom cache task.
* **Deprecated**: a service tagged without `code` is still registered, with `AdapterRegistry::addAdapter()`,
instantiated with the registry (deprecated since 2.3, the `code` will be required in 3.0). Two adapters with the same
code then throw an `\UnexpectedValueException` giving the ids of both services
(`Adapter <code> is already defined by service "<id>", cannot register service "<id>"`) when the registry is
instantiated, i.e. the first time a cache task is used.
* Using a code that is not registered throws a `CleverAge\CacheProcessBundle\Exception\MissingAdapterException`
(`Adapter <code> is missing`) when the task is executed.
* The lifetime of the items is the default lifetime of the decorated pool (`default_lifetime` of a FrameworkBundle
Expand Down
29 changes: 29 additions & 0 deletions src/Attribute/AsCacheAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the CleverAge/CacheProcessBundle package.
*
* Copyright (c) Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\CacheProcessBundle\Attribute;

/**
* Registers a cache adapter service under its code, so that it is only instantiated when a process uses it.
*
* Requires autoconfiguration; equivalent to a `cleverage.cache.adapter` tag with a `code` attribute. The code must be
* the one returned by `AdapterInterface::getCode()`.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final readonly class AsCacheAdapter
{
public function __construct(
public string $code,
) {
}
}
9 changes: 9 additions & 0 deletions src/DependencyInjection/CleverAgeCacheProcessExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

namespace CleverAge\CacheProcessBundle\DependencyInjection;

use CleverAge\CacheProcessBundle\Attribute\AsCacheAdapter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
Expand All @@ -29,6 +31,13 @@ class CleverAgeCacheProcessExtension extends Extension
public function load(array $configs, ContainerBuilder $container): void
{
$this->findServices($container, __DIR__.'/../../config/services');

$container->registerAttributeForAutoconfiguration(
AsCacheAdapter::class,
static function (ChildDefinition $definition, AsCacheAdapter $attribute): void {
$definition->addTag('cleverage.cache.adapter', ['code' => $attribute->code]);
}
);
}

/**
Expand Down
36 changes: 32 additions & 4 deletions src/DependencyInjection/Compiler/RegisterAdaptersPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
namespace CleverAge\CacheProcessBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
* Adds the tagged cache adapters to the adapter registry, with their service id.
* Registers the `cleverage.cache.adapter` tagged services in the adapter registry: in a service locator indexed by the
* `code` attribute of the tag (lazy), or, without `code` (deprecated), with an addAdapter() call (eager).
*/
class RegisterAdaptersPass implements CompilerPassInterface
{
Expand All @@ -27,10 +30,35 @@ public function process(ContainerBuilder $container): void
if (!$container->has('cleverage_cache_process.registry.adapter')) {
return;
}

$definition = $container->findDefinition('cleverage_cache_process.registry.adapter');
foreach (array_keys($container->findTaggedServiceIds('cleverage.cache.adapter')) as $id) {
$definition->addMethodCall('addAdapter', [new Reference($id), $id]);

$services = [];
$ids = [];
foreach ($container->findTaggedServiceIds('cleverage.cache.adapter') as $id => $tags) {
$codes = [];
foreach ($tags as $attributes) {
if (!\is_array($attributes) || !isset($attributes['code'])) {
continue;
}
if (!\is_string($attributes['code']) || '' === $attributes['code']) {
throw new InvalidArgumentException(\sprintf('The "code" attribute of the "cleverage.cache.adapter" tag of the "%s" service must be a non-empty string.', $id));
}
$codes[] = $attributes['code'];
}
if ([] === $codes) {
// Deprecated: instantiated with the registry, addAdapter() triggers the deprecation
$definition->addMethodCall('addAdapter', [new Reference($id), $id]);
continue;
}
foreach (array_unique($codes) as $code) {
if (isset($ids[$code])) {
throw new InvalidArgumentException(\sprintf('The cache adapter code "%s" is used by the "%s" and "%s" services.', $code, $ids[$code], $id));
}
$ids[$code] = $id;
$services[$code] = new Reference($id);
}
}

$definition->setArgument(0, ServiceLocatorTagPass::register($container, $services));
}
}
75 changes: 67 additions & 8 deletions src/Registry/AdapterRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,57 @@

use CleverAge\CacheProcessBundle\Adapter\AdapterInterface;
use CleverAge\CacheProcessBundle\Exception\MissingAdapterException;
use Symfony\Contracts\Service\ServiceProviderInterface;

/**
* Holds all tagged cache adapters services.
* Holds the cache adapters, indexed by code.
*
* The adapter services are fetched lazily from a service locator indexed by code (`code` attribute of the
* `cleverage.cache.adapter` tag, or `#[AsCacheAdapter]`, see RegisterAdaptersPass): an adapter is only instantiated
* when it is used.
*/
class AdapterRegistry
{
/** @var AdapterInterface[] */
/**
* Adapters already fetched from the locator, and the ones registered with addAdapter() (deprecated).
*
* @var array<string, AdapterInterface>
*/
private array $adapters = [];

/** @var array<string, string|null> Service ids of the adapters, indexed by code */
/** @var array<string, string|null> Service ids of the adapters registered with addAdapter(), indexed by code */
private array $serviceIds = [];

/**
* @param ServiceProviderInterface<mixed>|null $locator Adapter services, checked when fetched
*/
public function __construct(
private readonly ?ServiceProviderInterface $locator = null,
) {
}

/**
* @param string|null $serviceId Id of the adapter service, used to identify the adapters with the same code
*
* @deprecated since 2.3, tag the adapter service with a "code" attribute or use #[AsCacheAdapter] instead
*/
public function addAdapter(AdapterInterface $adapter, ?string $serviceId = null): void
{
$code = $adapter->getCode();
if (\array_key_exists($code, $this->adapters)) {
trigger_deprecation(
'cleverage/cache-process-bundle',
'2.3',
'Registering the cache adapter "%s" (%s) with "%s()" is deprecated, tag its service with a "code" attribute ({ name: cleverage.cache.adapter, code: %s }) or use the #[AsCacheAdapter] attribute instead, so that it is only instantiated when used. It will be required in 3.0.',
$code,
$adapter::class,
__METHOD__,
$code,
);
if ($this->hasAdapter($code)) {
$message = "Adapter {$code} is already defined";
if (null !== $this->serviceIds[$code] && null !== $serviceId) {
$message .= " by service \"{$this->serviceIds[$code]}\", cannot register service \"{$serviceId}\"";
$definedBy = $this->serviceIds[$code] ?? null;
if (null !== $definedBy && null !== $serviceId) {
$message .= " by service \"{$definedBy}\", cannot register service \"{$serviceId}\"";
}

throw new \UnexpectedValueException($message);
Expand All @@ -45,15 +74,45 @@ public function addAdapter(AdapterInterface $adapter, ?string $serviceId = null)
$this->serviceIds[$code] = $serviceId;
}

/**
* Instantiates every adapter of the locator.
*
* @return array<string, AdapterInterface>
*/
public function getAdapters(): array
{
foreach (array_keys($this->locator?->getProvidedServices() ?? []) as $code) {
$this->getAdapter((string) $code);
}

return $this->adapters;
}

/**
* @throws MissingAdapterException
*/
public function getAdapter(string $code): AdapterInterface
{
if (!\array_key_exists($code, $this->adapters)) {
if (isset($this->adapters[$code])) {
return $this->adapters[$code];
}
if (!$this->locator?->has($code)) {
throw new MissingAdapterException("Adapter {$code} is missing");
}

return $this->adapters[$code];
$adapter = $this->locator->get($code);
if (!$adapter instanceof AdapterInterface) {
throw new \UnexpectedValueException(\sprintf('The service of the cache adapter "%s" must implement "%s".', $code, AdapterInterface::class));
}
if ($code !== $adapter->getCode()) {
throw new \UnexpectedValueException(\sprintf('The cache adapter "%s" is registered with the code "%s" but its getCode() method returns "%s": both must be the same.', $adapter::class, $code, $adapter->getCode()));
}

return $this->adapters[$code] = $adapter;
}

public function hasAdapter(string $code): bool
{
return isset($this->adapters[$code]) || ($this->locator?->has($code) ?? false);
}
}
20 changes: 20 additions & 0 deletions tests/DependencyInjection/CleverAgeCacheProcessExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,26 @@

namespace CleverAge\CacheProcessBundle\Tests\DependencyInjection;

use CleverAge\CacheProcessBundle\Attribute\AsCacheAdapter;
use CleverAge\CacheProcessBundle\DependencyInjection\CleverAgeCacheProcessExtension;
use CleverAge\CacheProcessBundle\Registry\AdapterRegistry;
use CleverAge\CacheProcessBundle\Task\GetTask;
use CleverAge\CacheProcessBundle\Task\SetTask;
use CleverAge\CacheProcessBundle\Tests\Fixtures\AttributedAdapter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

#[CoversClass(CleverAgeCacheProcessExtension::class)]
#[UsesClass(AdapterRegistry::class)]
#[CoversClass(AsCacheAdapter::class)]
class CleverAgeCacheProcessExtensionTest extends TestCase
{
public function testRegistryIsRegistered(): void
Expand All @@ -38,6 +45,19 @@ public function testRegistryIsRegistered(): void
self::assertTrue($definition->isShared());
}

public function testAsCacheAdapterAttributeRegistersTheCode(): void
{
$container = new ContainerBuilder();
(new CleverAgeCacheProcessExtension())->load([], $container);
$container->setDefinition('app.attributed', (new Definition(AttributedAdapter::class))->setAutoconfigured(true));

(new AttributeAutoconfigurationPass())->process($container);
(new ResolveInstanceofConditionalsPass())->process($container);
(new ResolveChildDefinitionsPass())->process($container);

self::assertSame([['code' => 'app_attributed']], $container->getDefinition('app.attributed')->getTag('cleverage.cache.adapter'));
}

public function testRegistryCanBeAutowired(): void
{
$container = new ContainerBuilder();
Expand Down
Loading
Loading