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
* [#67](https://github.com/cleverage/cache-process-bundle/issues/67) A Symfony cache pool (a service implementing `Symfony\Component\Cache\Adapter\AdapterInterface`, e.g. `parent: cache.adapter.filesystem` or `class: ArrayAdapter`) can be tagged directly `cleverage.cache.adapter` with a `code`, without any adapter class: `RegisterAdaptersPass` wraps it in an `Adapter` with this code, loaded lazily. A tagged service implementing neither this interface nor the `AdapterInterface` of the bundle fails the container compilation. Update documentation, add tests.
* [#65](https://github.com/cleverage/cache-process-bundle/issues/65) Configuring a key under `clever_age_cache_process` is deprecated: the bundle has no configuration and the keys were ignored without notice (new internal `Configuration`, empty; notice when the container is compiled). They will be rejected in 3.0. Require `symfony/deprecation-contracts`, used directly. Update documentation, add tests.
* [#63](https://github.com/cleverage/cache-process-bundle/issues/63) The bundle throws exceptions implementing `ProcessExceptionInterface` of the process bundle instead of raw SPL exceptions (as [cleverage/process-bundle#317](https://github.com/cleverage/process-bundle/issues/317)): new `Exception\UnexpectedValueException`, extending the SPL exceptions thrown before (same messages, the existing `catch` keep working). `MissingAdapterException` implements `ProcessExceptionInterface`. Add tests.
* [#61](https://github.com/cleverage/cache-process-bundle/issues/61) Composer: require PHP `^8.2` instead of `>=8.2` (PHP 9 is no longer accepted before being tested), remove the authorization of the `symfony/flex` plugin, which is not required (the CI installs it globally), as [cleverage/process-bundle#319](https://github.com/cleverage/process-bundle/issues/319).
Expand Down
27 changes: 4 additions & 23 deletions docs/cookbooks/share_data_between_branches.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,14 @@ Share data between process branches
This recipe uses an in-memory cache to store data in a first branch of a process, then read it in another branch of
the same execution. Nothing is persisted: the values only live during the current PHP process.

First, declare an in-memory cache [adapter](../reference/adapter.md) with the `memory` code:

```php
<?php
// src/Adapter/MemoryAdapter.php

declare(strict_types=1);

namespace App\Adapter;

use CleverAge\CacheProcessBundle\Adapter\Adapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

class MemoryAdapter extends Adapter
{
public function __construct()
{
parent::__construct(new ArrayAdapter(), 'memory');
}
}
```
First, declare an in-memory cache [adapter](../reference/adapter.md) with the `memory` code: a Symfony `ArrayAdapter`
tagged directly, no PHP class is needed:

```yaml
# config/services.yaml
services:
app.cleverage_cache_process.adapter.memory:
class: App\Adapter\MemoryAdapter
app.cache.memory:
class: Symfony\Component\Cache\Adapter\ArrayAdapter
tags:
- { name: cleverage.cache.adapter, code: memory }
```
Expand Down
35 changes: 34 additions & 1 deletion docs/reference/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Adapter reference
(`CleverAge\CacheProcessBundle\Registry\AdapterRegistry`, autowirable by its class name)
* **Attribute**: `CleverAge\CacheProcessBundle\Attribute\AsCacheAdapter` (`#[AsCacheAdapter(code: 'memory')]`, with
autoconfiguration), equivalent to the tag
* **Symfony cache pool**: a service implementing `Symfony\Component\Cache\Adapter\AdapterInterface` (every Symfony
adapter and pool) can be tagged directly, without any adapter class: it is wrapped in an `Adapter` with the `code` of
the tag

Constructor arguments
---------------------
Expand Down Expand Up @@ -62,7 +65,32 @@ services:
- { name: cleverage.cache.adapter, code: memory }
```

* Persistent adapter, wrapping a cache pool of the FrameworkBundle, without any PHP class
* In-memory adapter, without any PHP class: a Symfony adapter tagged directly

```yaml
# config/services.yaml
services:
app.cache.memory:
class: Symfony\Component\Cache\Adapter\ArrayAdapter
tags:
- { name: cleverage.cache.adapter, code: memory }
```

* Persistent adapter, a cache pool defined as a service (as the ones of `framework.cache.pools`: namespace, lifetime
and clearer handled by the FrameworkBundle) and tagged directly

```yaml
# config/services.yaml
services:
app.cache.catalog:
parent: cache.adapter.filesystem
tags:
- { name: cache.pool, default_lifetime: 86400 }
- { name: cleverage.cache.adapter, code: catalog }
```

* Persistent adapter, wrapping a pool of `framework.cache.pools` (its service is created by the FrameworkBundle, it
cannot be tagged by the application)

```yaml
# config/packages/cache.yaml
Expand Down Expand Up @@ -95,6 +123,11 @@ Notes
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.
* A tagged Symfony cache pool (a service implementing `Symfony\Component\Cache\Adapter\AdapterInterface`, but not the
`AdapterInterface` of the bundle) is wrapped in an `Adapter` (`.cleverage_cache_process.adapter.<code>` service),
instantiated with the pool when a task uses it; several codes on one pool share its items. The interface is checked
from the class of the service (or of its `parent`): a tagged service implementing neither interface fails the container
compilation. A pool must be tagged with a `code`.
* **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
Expand Down
43 changes: 42 additions & 1 deletion src/DependencyInjection/Compiler/RegisterAdaptersPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@

namespace CleverAge\CacheProcessBundle\DependencyInjection\Compiler;

use CleverAge\CacheProcessBundle\Adapter\Adapter;
use CleverAge\CacheProcessBundle\Adapter\AdapterInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
* 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).
*
* A Symfony cache pool tagged with a `code` (a service implementing the AdapterInterface of the Cache component, but not
* the one of the bundle) is wrapped in an Adapter with this code (`.cleverage_cache_process.adapter.<code>` service).
*
* @internal
*/
class RegisterAdaptersPass implements CompilerPassInterface
Expand Down Expand Up @@ -52,15 +59,49 @@ public function process(ContainerBuilder $container): void
$definition->addMethodCall('addAdapter', [new Reference($id), $id]);
continue;
}
$isPool = $this->isPool($container, $id);
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);
if ($isPool) {
$container->setDefinition($adapterId = '.cleverage_cache_process.adapter.'.$code, new Definition(Adapter::class, [new Reference($id), $code]));
$services[$code] = new Reference($adapterId);
} else {
$services[$code] = new Reference($id);
}
}
}

$definition->setArgument(0, ServiceLocatorTagPass::register($container, $services));
}

/**
* Whether the service is a Symfony cache pool to wrap in an Adapter, from its class (the one of its parents for a
* pool defined with "parent", as the FrameworkBundle pools). A service whose class cannot be loaded is registered as
* is, and checked when it is fetched.
*/
private function isPool(ContainerBuilder $container, string $id): bool
{
$definition = $container->getDefinition($id);
$class = $definition->getClass();
while (null === $class && $definition instanceof ChildDefinition) {
$definition = $container->findDefinition($definition->getParent());
$class = $definition->getClass();
}
$class = $container->getParameterBag()->resolveValue($class);
if (!\is_string($class)) {
return false;
}
$reflectionClass = $container->getReflectionClass($class, false);
if (!$reflectionClass instanceof \ReflectionClass || $reflectionClass->implementsInterface(AdapterInterface::class)) {
return false;
}
if ($reflectionClass->implementsInterface(\Symfony\Component\Cache\Adapter\AdapterInterface::class)) {
return true;
}

throw new InvalidArgumentException(\sprintf('The "%s" service, tagged "cleverage.cache.adapter", must implement "%s" or "%s" (a Symfony cache pool), "%s" given.', $id, AdapterInterface::class, \Symfony\Component\Cache\Adapter\AdapterInterface::class, $class));
}
}
11 changes: 9 additions & 2 deletions tests/App/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
use function Symfony\Component\DependencyInjection\Loader\Configurator\inline_service;

/**
* Application used by the integration tests: process-bundle and this bundle, with an adapter declared by tag and one
* by attribute, in a temporary directory of its own (per PHP process).
* Application used by the integration tests: process-bundle and this bundle, with an adapter declared by tag, one by
* attribute and two Symfony cache pools tagged directly, in a temporary directory of its own (per PHP process).
*/
final class TestKernel extends Kernel
{
Expand Down Expand Up @@ -80,6 +80,13 @@ protected function configureContainer(ContainerConfigurator $container): void
$services->set(AttributedAdapter::class)
->autoconfigure()
->args([inline_service(ArrayAdapter::class), 'app_attributed']);
// Symfony cache pools tagged directly: wrapped in an Adapter
$services->set('app.cache_pool', ArrayAdapter::class)
->tag('cleverage.cache.adapter', ['code' => 'app_pool']);
$services->set('app.cache_framework_pool')
->parent('cache.adapter.array')
->tag('cache.pool')
->tag('cleverage.cache.adapter', ['code' => 'app_framework_pool']);
// Exposed to the tests
$services->alias('test.adapter_registry', AdapterRegistry::class)->public();

Expand Down
85 changes: 85 additions & 0 deletions tests/DependencyInjection/Compiler/RegisterAdaptersPassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
Expand Down Expand Up @@ -87,6 +88,90 @@ public function testEmptyCodeFailsTheCompilation(): void
$container->compile(true);
}

public function testPoolsAreWrappedInAnAdapter(): void
{
$container = $this->createContainer([]);
$container->setParameter('app.pool_class', ArrayAdapter::class);
$container->setDefinition('app.pool', new Definition(ArrayAdapter::class))
->setPublic(true)
->addTag('cleverage.cache.adapter', ['code' => 'pool']);
// Pool defined with a parent, as the FrameworkBundle pools
$container->setDefinition('app.abstract_pool', new Definition('%app.pool_class%'))
->setAbstract(true);
$container->setDefinition('app.child_pool', new ChildDefinition('app.abstract_pool'))
->addTag('cleverage.cache.adapter', ['code' => 'child_pool'])
->addTag('cleverage.cache.adapter', ['code' => 'child_pool_other']);
$container->compile(true);

/** @var AdapterRegistry $registry */
$registry = $container->get('cleverage_cache_process.registry.adapter');
// Not instantiated with the registry
self::assertFalse($container->initialized('app.pool'));
$adapter = $registry->getAdapter('pool');
self::assertInstanceOf(Adapter::class, $adapter);
self::assertSame('pool', $adapter->getCode());
self::assertTrue($container->initialized('app.pool'));

// The items are stored in the pool
$item = $adapter->getItem('key');
$item->set('value');
$adapter->save($item);
/** @var ArrayAdapter $pool */
$pool = $container->get('app.pool');
self::assertSame('value', $pool->getItem('key')->get());

$childAdapter = $registry->getAdapter('child_pool');
self::assertInstanceOf(Adapter::class, $childAdapter);
self::assertSame('child_pool', $childAdapter->getCode());
// One pool shared by both codes
$item = $childAdapter->getItem('key');
$item->set('child');
$childAdapter->save($item);
self::assertSame('child', $registry->getAdapter('child_pool_other')->getItem('key')->get());
}

public function testServiceWithAnUnknownClassIsRegisteredAsIs(): void
{
$container = $this->createContainer([]);
// Checked when fetched
$container->setDefinition('app.adapter', (new Definition('App\\UnknownAdapter'))->setFactory([self::class, 'createAdapter']))
->setPublic(true)
->addTag('cleverage.cache.adapter', ['code' => 'factory']);
$container->compile(true);

/** @var AdapterRegistry $registry */
$registry = $container->get('cleverage_cache_process.registry.adapter');
self::assertSame($container->get('app.adapter'), $registry->getAdapter('factory'));
}

public function testServiceWithoutClassIsRegisteredAsIs(): void
{
$container = new ContainerBuilder();
$container->setDefinition('cleverage_cache_process.registry.adapter', new Definition(AdapterRegistry::class, [null]));
// Rejected later by the container compilation
$container->setDefinition('app.adapter', new Definition())
->addTag('cleverage.cache.adapter', ['code' => 'adapter']);
(new RegisterAdaptersPass())->process($container);

self::assertFalse($container->hasDefinition('.cleverage_cache_process.adapter.adapter'));
}

public function testServiceThatIsNeitherAnAdapterNorAPoolFailsTheCompilation(): void
{
$container = $this->createContainer([]);
$container->setDefinition('app.adapter', new Definition(\stdClass::class))
->addTag('cleverage.cache.adapter', ['code' => 'adapter']);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The "app.adapter" service, tagged "cleverage.cache.adapter", must implement "CleverAge\CacheProcessBundle\Adapter\AdapterInterface" or "Symfony\Component\Cache\Adapter\AdapterInterface" (a Symfony cache pool), "stdClass" given.');
$container->compile(true);
}

public static function createAdapter(): Adapter
{
return new Adapter(new ArrayAdapter(), 'factory');
}

public function testWithoutRegistry(): void
{
$container = new ContainerBuilder();
Expand Down
17 changes: 17 additions & 0 deletions tests/Functional/KernelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ public function testAdaptersAreResolvedByCode(): void
self::assertInstanceOf(AttributedAdapter::class, $registry->getAdapter('app_attributed'));
}

public function testPoolsAreWrappedInAnAdapter(): void
{
$registry = self::getContainer()->get('test.adapter_registry');
self::assertInstanceOf(AdapterRegistry::class, $registry);

foreach (['app_pool', 'app_framework_pool'] as $code) {
$adapter = $registry->getAdapter($code);
self::assertInstanceOf(Adapter::class, $adapter);
self::assertSame($code, $adapter->getCode());

$item = $adapter->getItem('key');
$item->set($code);
$adapter->save($item);
self::assertSame($code, $adapter->getItem('key')->get());
}
}

public function testTasksAreResolvedByCode(): void
{
if (!class_exists(TaskRegistry::class)) {
Expand Down
Loading