Skip to content

Commit 643359b

Browse files
committed
Add CallableInvokerPass for managing callable invoker decorators and resolvers
1 parent 8b0b88b commit 643359b

9 files changed

Lines changed: 469 additions & 412 deletions

phpunit.dist.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
<testsuites>
1010
<testsuite name="unit">
1111
<directory>tests/Unit</directory>
12-
<exclude>tests/Unit/DependencyInjection/Compiler/AbstractGroupingPassTest.php</exclude>
1312
</testsuite>
1413
<testsuite name="integration">
1514
<directory>tests/Integration</directory>

src/CallableInvokerBundle.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace OpenSolid\CallableInvoker;
44

55
use OpenSolid\CallableInvoker\Decorator\Attribute\AsCallableDecorator;
6-
use OpenSolid\CallableInvoker\DependecyInjection\Compiler\CallableServiceLocatorPass;
6+
use OpenSolid\CallableInvoker\DependecyInjection\Compiler\CallableInvokerPass;
77
use OpenSolid\CallableInvoker\ValueResolver\Attribute\AsParameterValueResolver;
88
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
99
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -15,15 +15,7 @@ class CallableInvokerBundle extends AbstractBundle
1515
{
1616
public function build(ContainerBuilder $container): void
1717
{
18-
$container->addCompilerPass(new CallableServiceLocatorPass(
19-
serviceId: 'callable_invoker.decorator_groups',
20-
tagName: 'callable_invoker.decorator',
21-
));
22-
23-
$container->addCompilerPass(new CallableServiceLocatorPass(
24-
serviceId: 'callable_invoker.value_resolver_groups',
25-
tagName: 'callable_invoker.value_resolver',
26-
));
18+
$container->addCompilerPass(new CallableInvokerPass());
2719
}
2820

2921
public function configure(DefinitionConfigurator $definition): void
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenSolid\CallableInvoker\DependecyInjection\Compiler;
6+
7+
use OpenSolid\CallableInvoker\CallableInvokerInterface;
8+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
9+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
10+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
11+
use Symfony\Component\DependencyInjection\ContainerBuilder;
12+
use Symfony\Component\DependencyInjection\Reference;
13+
14+
final readonly class CallableInvokerPass implements CompilerPassInterface
15+
{
16+
public function process(ContainerBuilder $container): void
17+
{
18+
[$decoratorGrouped, $decoratorUngrouped] = $this->collectTaggedServices($container, 'callable_invoker.decorator');
19+
[$resolverGrouped, $resolverUngrouped] = $this->collectTaggedServices($container, 'callable_invoker.value_resolver');
20+
21+
// Unified group universe from both tags
22+
$allExplicitGroups = array_keys($decoratorGrouped + $resolverGrouped);
23+
24+
$container->getDefinition('callable_invoker.decorator_groups')
25+
->setArgument(0, new ServiceLocatorArgument($this->buildGroups($decoratorGrouped, $decoratorUngrouped, $allExplicitGroups)));
26+
$container->getDefinition('callable_invoker.value_resolver_groups')
27+
->setArgument(0, new ServiceLocatorArgument($this->buildGroups($resolverGrouped, $resolverUngrouped, $allExplicitGroups)));
28+
}
29+
30+
/**
31+
* @return array{
32+
* array<string, array<string, array{ref: Reference, priority: int}>>,
33+
* array<string, array{ref: Reference, priority: int}>
34+
* }
35+
*/
36+
private function collectTaggedServices(ContainerBuilder $container, string $tagName): array
37+
{
38+
$grouped = [];
39+
$ungrouped = [];
40+
41+
foreach ($container->findTaggedServiceIds($tagName) as $id => $tags) {
42+
$ref = new Reference($id);
43+
$hasExplicitGroup = false;
44+
/** @var int|null $maxPriority */
45+
$maxPriority = null;
46+
47+
/** @var array{priority?: int, groups?: list<string>} $tag */
48+
foreach ($tags as $tag) {
49+
$priority = $tag['priority'] ?? 0;
50+
$maxPriority = null === $maxPriority ? $priority : max($maxPriority, $priority);
51+
foreach ($tag['groups'] ?? [] as $group) {
52+
$grouped[$group][$id] = ['ref' => $ref, 'priority' => $priority];
53+
$hasExplicitGroup = true;
54+
}
55+
}
56+
57+
if (!$hasExplicitGroup) {
58+
$ungrouped[$id] = ['ref' => $ref, 'priority' => $maxPriority ?? 0];
59+
}
60+
}
61+
62+
return [$grouped, $ungrouped];
63+
}
64+
65+
/**
66+
* @param array<string, array<string, array{ref: Reference, priority: int}>> $grouped
67+
* @param array<string, array{ref: Reference, priority: int}> $ungrouped
68+
* @param list<string> $allExplicitGroups
69+
*
70+
* @return array<string, IteratorArgument>
71+
*/
72+
private function buildGroups(array $grouped, array $ungrouped, array $allExplicitGroups): array
73+
{
74+
if ($ungrouped) {
75+
$defaultGroup = CallableInvokerInterface::DEFAULT_GROUP;
76+
$grouped[$defaultGroup] = ($grouped[$defaultGroup] ?? []) + $ungrouped;
77+
foreach ($allExplicitGroups as $group) {
78+
if ($defaultGroup !== $group) {
79+
$grouped[$group] = ($grouped[$group] ?? []) + $ungrouped;
80+
}
81+
}
82+
}
83+
84+
$result = [];
85+
foreach ($grouped as $group => $entries) {
86+
uasort($entries, static fn (array $a, array $b) => $b['priority'] <=> $a['priority']);
87+
$result[$group] = new IteratorArgument(array_column($entries, 'ref'));
88+
}
89+
90+
return $result;
91+
}
92+
}

src/DependecyInjection/Compiler/CallableServiceLocatorPass.php

Lines changed: 0 additions & 77 deletions
This file was deleted.

tests/Integration/CallableInvokerBundleTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,24 @@ public function customValueResolverIsApplied(): void
9797
self::assertSame('Hey!', $result);
9898
}
9999

100+
#[Test]
101+
public function ungroupedResolversWorkWithDecoratorOnlyGroup(): void
102+
{
103+
$invoker = $this->createContainer(static function (ContainerBuilder $container) {
104+
$container->register('test.decorator', ApiGroupDecorator::class)
105+
->addTag('callable_invoker.decorator', ['groups' => ['api']]);
106+
})->get(CallableInvokerInterface::class);
107+
108+
// Built-in ungrouped resolvers must still work even though 'api'
109+
// was never declared in any resolver tag
110+
$result = $invoker->invoke(
111+
static fn (string $name = 'World') => "Hello, $name!",
112+
groups: ['api'],
113+
);
114+
115+
self::assertSame('Hello, World!', $result);
116+
}
117+
100118
/**
101119
* @param \Closure(ContainerBuilder): void|null $configure
102120
*/
@@ -109,6 +127,19 @@ private function createContainer(?\Closure $configure = null): ContainerInterfac
109127
}
110128
}
111129

130+
final class ApiGroupDecorator implements CallableDecoratorInterface
131+
{
132+
public function supports(CallableMetadata $metadata): bool
133+
{
134+
return true;
135+
}
136+
137+
public function decorate(CallableClosure $callable, CallableMetadata $metadata): mixed
138+
{
139+
return $callable->call();
140+
}
141+
}
142+
112143
final class LoggingDecorator implements CallableDecoratorInterface
113144
{
114145
public function supports(CallableMetadata $metadata): bool

0 commit comments

Comments
 (0)