Skip to content

Commit 4bd02c7

Browse files
committed
Add support to extensions on aliases
1 parent 1cc4020 commit 4bd02c7

File tree

2 files changed

+98
-6
lines changed

2 files changed

+98
-6
lines changed

src/ServiceProviderContainer.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,49 @@ public function get(string $id): mixed
8686
}
8787

8888
try {
89-
$service = $factory[$id]($this->wrapperContainer);
90-
$extensions = $this->serviceProvider->getExtensions();
91-
92-
if (\array_key_exists($id, $extensions) && \is_callable($extensions[$id])) {
93-
$extensions[$id]($this->wrapperContainer, $service);
94-
}
89+
$service = call_user_func($factory[$id], $this->wrapperContainer);
90+
$this->applyServiceExtensions($id, $service);
9591
} catch (ContainerExceptionInterface $containerException) {
9692
throw ContainerException::forInvalidService($id, $containerException);
9793
}
9894

9995
return $this->cache[$id] = $service;
10096
}
97+
98+
/**
99+
* Applies service extensions to the constructed service instance.
100+
*
101+
* This method SHALL inspect the set of extensions returned by the service provider,
102+
* checking both the original service identifier and the concrete class name of the
103+
* service instance. If a corresponding extension is found, it MUST be a callable and
104+
* SHALL be invoked with the container and service instance as arguments.
105+
*
106+
* This mechanism allows for post-construction decoration or augmentation of the
107+
* resolved service. Extensions MAY be used to modify or enhance the behavior or state
108+
* of services after they have been created.
109+
*
110+
* Implementations MUST ensure that only valid callables are executed and SHOULD avoid
111+
* side effects beyond service enhancement. If an extension fails or is not callable,
112+
* the method SHALL silently ignore it unless explicitly configured otherwise.
113+
*
114+
* @param string $id The identifier of the resolved service.
115+
* @param mixed $service The service instance to be extended.
116+
*
117+
* @return void
118+
*
119+
* @throws ContainerException If any extension fails during invocation.
120+
*/
121+
private function applyServiceExtensions(string $id, mixed $service): void
122+
{
123+
$class = get_class($service);
124+
$extensions = $this->serviceProvider->getExtensions();
125+
126+
if (\array_key_exists($id, $extensions) && \is_callable($extensions[$id])) {
127+
$extensions[$id]($this->wrapperContainer, $service);
128+
}
129+
130+
if ($id !== $class && \array_key_exists($class, $extensions) && \is_callable($extensions[$class])) {
131+
$extensions[$class]($this->wrapperContainer, $service);
132+
}
133+
}
101134
}

tests/ServiceProviderContainerTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,63 @@ public function testGetWillReturnFromCacheWhenSericeAlreadyResolved(): void
138138

139139
self::assertSame($service, $this->container->get('foo'));
140140
}
141+
142+
public function testApplyServiceExtensionByClassName(): void
143+
{
144+
$service = new class() {
145+
public bool $extended = false;
146+
};
147+
148+
$extension = static function (ContainerInterface $c, object $service): void {
149+
$service->extended = true;
150+
};
151+
152+
$this->provider->getFactories()->willReturn(['service.id' => static fn () => $service]);
153+
$this->provider->getExtensions()->willReturn([get_class($service) => $extension]);
154+
155+
$resolved = $this->container->get('service.id');
156+
157+
self::assertTrue($resolved->extended);
158+
}
159+
160+
public function testApplyServiceExtensionByIdAndClass(): void
161+
{
162+
$service = new class() {
163+
public array $calls = [];
164+
};
165+
166+
$byIdExtension = static function (ContainerInterface $c, object $service): void {
167+
$service->calls[] = 'id';
168+
};
169+
170+
$byClassExtension = static function (ContainerInterface $c, object $service): void {
171+
$service->calls[] = 'class';
172+
};
173+
174+
$this->provider->getFactories()->willReturn(['dual' => static fn () => $service]);
175+
$this->provider->getExtensions()->willReturn([
176+
'dual' => $byIdExtension,
177+
get_class($service) => $byClassExtension,
178+
]);
179+
180+
$resolved = $this->container->get('dual');
181+
182+
self::assertSame(['id', 'class'], $resolved->calls);
183+
}
184+
185+
public function testApplyServiceIgnoresNonCallableExtensions(): void
186+
{
187+
$service = new \stdClass();
188+
189+
$this->provider->getFactories()->willReturn(['not.callable' => static fn () => $service]);
190+
$this->provider->getExtensions()->willReturn([
191+
'not.callable' => 'not_a_function',
192+
get_class($service) => 123,
193+
]);
194+
195+
$resolved = $this->container->get('not.callable');
196+
197+
self::assertSame($service, $resolved); // Should still return the service
198+
self::assertObjectNotHasProperty('extended', $resolved); // No extension applied
199+
}
141200
}

0 commit comments

Comments
 (0)