Skip to content

Commit 1cc4020

Browse files
committed
Add CallableFactory
1 parent 84ea16f commit 1cc4020

File tree

6 files changed

+105
-20
lines changed

6 files changed

+105
-20
lines changed

src/Exception/RuntimeException.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,22 @@ public static function forNonPublicMethod(string $class, string $method): self
6565
$method
6666
));
6767
}
68+
69+
/**
70+
* Creates an exception for an invalid parameter type.
71+
*
72+
* This method MUST be used when a parameter expected to represent a class name
73+
* or interface name does not satisfy this constraint.
74+
*
75+
* @param string $parameter the name of the parameter with the invalid type
76+
*
77+
* @return self a RuntimeException instance with a descriptive message
78+
*/
79+
public static function forInvalidParameterType(string $parameter): self
80+
{
81+
return new self(\sprintf(
82+
'Parameter "%s" is not a valid type. It MUST be a class name or an interface name.',
83+
$parameter
84+
));
85+
}
6886
}

src/Factory/CallableFactory.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
namespace FastForward\Container\Factory;
1717

18+
use FastForward\Container\Exception\RuntimeException;
1819
use Psr\Container\ContainerInterface;
1920

2021
/**
@@ -55,6 +56,32 @@ public function __construct(callable $callable)
5556
*/
5657
public function __invoke(ContainerInterface $container): mixed
5758
{
58-
return \call_user_func($this->callable, $container);
59+
$arguments = $this->getArguments($container, new \ReflectionFunction($this->callable));
60+
61+
return $this->callable->call($container, ...$arguments);
62+
}
63+
64+
/**
65+
* Retrieves the arguments for the callable from the container.
66+
*
67+
* @param ContainerInterface $container the PSR-11 container for dependency resolution
68+
* @param \ReflectionFunction $function the reflection function of the callable
69+
*
70+
* @return array the resolved arguments for the callable
71+
*/
72+
private function getArguments(ContainerInterface $container, \ReflectionFunction $function): array
73+
{
74+
$arguments = [];
75+
76+
foreach ($function->getParameters() as $parameter) {
77+
if (!$parameter->getType() || $parameter->getType()->isBuiltin()) {
78+
throw RuntimeException::forInvalidParameterType($parameter->getName());
79+
}
80+
81+
$className = $parameter->getType()->getName();
82+
$arguments[] = $container->get($className);
83+
}
84+
85+
return $arguments;
5986
}
6087
}

src/functions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* composed of all resolved sources.
4040
*
4141
* @param ConfigInterface|PsrContainerInterface|ServiceProviderInterface|string ...$initializers
42-
* A variadic list of container initializers, optionally including config or provider classes.
42+
* A variadic list of container initializers, optionally including config or provider classes.
4343
*
4444
* @return ContainerInterface the composed and autowire-enabled container
4545
*

tests/ContainerFunctionTest.php

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,6 @@ final class ContainerFunctionTest extends TestCase
4545
{
4646
use ProphecyTrait;
4747

48-
private string $configKey;
49-
50-
protected function setUp(): void
51-
{
52-
$this->configKey = \sprintf('%s.%s', ConfigContainer::ALIAS, ContainerInterface::class);
53-
}
54-
5548
public function testReturnsAutowireContainerWrappingAggregate(): void
5649
{
5750
$result = container();
@@ -86,8 +79,8 @@ public function testAcceptsServiceProviderAsInitializer(): void
8679
public function testAcceptsConfigInterfaceAsInitializer(): void
8780
{
8881
$config = $this->prophesize(ConfigInterface::class);
89-
$config->has($this->configKey)->willReturn(true);
90-
$config->get($this->configKey)->willReturn([]);
82+
$config->has(ContainerInterface::class)->willReturn(true);
83+
$config->get(ContainerInterface::class)->willReturn([]);
9184

9285
$container = container($config->reveal());
9386

@@ -115,8 +108,8 @@ public function testConfigContainerWithNestedInitializers(): void
115108
$nested = new DummyContainer();
116109

117110
$config = $this->prophesize(ConfigInterface::class);
118-
$config->has($this->configKey)->willReturn(true);
119-
$config->get($this->configKey)->willReturn([$nested]);
111+
$config->has(ContainerInterface::class)->willReturn(true);
112+
$config->get(ContainerInterface::class)->willReturn([$nested]);
120113

121114
$container = container($config->reveal());
122115

@@ -126,8 +119,8 @@ public function testConfigContainerWithNestedInitializers(): void
126119
public function testContainerSkipsThrowableThrownByConfigContainer(): void
127120
{
128121
$config = $this->prophesize(ConfigInterface::class);
129-
$config->has($this->configKey)->willReturn(true);
130-
$config->get($this->configKey)->willThrow(new \RuntimeException('unexpected'));
122+
$config->has(ContainerInterface::class)->willReturn(true);
123+
$config->get(ContainerInterface::class)->willThrow(new \RuntimeException('unexpected'));
131124

132125
$container = container($config->reveal());
133126

tests/Exception/RuntimeExceptionTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,15 @@ public function testForNonPublicMethodReturnsProperException(): void
4646
$exception->getMessage()
4747
);
4848
}
49+
50+
public function testForInvalidParameterTypeReturnsProperException(): void
51+
{
52+
$exception = RuntimeException::forInvalidParameterType('logger');
53+
54+
self::assertInstanceOf(RuntimeException::class, $exception);
55+
self::assertSame(
56+
'Parameter "logger" is not a valid type. It MUST be a class name or an interface name.',
57+
$exception->getMessage()
58+
);
59+
}
4960
}

tests/Factory/CallableFactoryTest.php

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515

1616
namespace FastForward\Container\Tests\Factory;
1717

18+
use FastForward\Container\Exception\RuntimeException;
1819
use FastForward\Container\Factory\CallableFactory;
20+
use FastForward\Container\Factory\FactoryInterface;
21+
use Interop\Container\ServiceProviderInterface;
1922
use PHPUnit\Framework\Attributes\CoversClass;
23+
use PHPUnit\Framework\Attributes\UsesClass;
2024
use PHPUnit\Framework\TestCase;
2125
use Prophecy\PhpUnit\ProphecyTrait;
2226
use Psr\Container\ContainerInterface;
@@ -25,30 +29,62 @@
2529
* @internal
2630
*/
2731
#[CoversClass(CallableFactory::class)]
32+
#[UsesClass(RuntimeException::class)]
2833
final class CallableFactoryTest extends TestCase
2934
{
3035
use ProphecyTrait;
3136

32-
public function testInvokeExecutesProvidedClosure(): void
37+
public function testInvokeWillReturnProvidedCallableReturns(): void
3338
{
3439
$container = $this->prophesize(ContainerInterface::class)->reveal();
3540

36-
$factory = new CallableFactory(static fn (ContainerInterface $c) => (object) ['resolved' => true]);
41+
$factory = new CallableFactory(fn () => (object) ['resolved' => true]);
3742

3843
$result = $factory($container);
3944

4045
self::assertIsObject($result);
4146
self::assertTrue($result->resolved);
4247
}
4348

44-
public function testClosureReceivesContainerAsArgument(): void
49+
public function testInvokeWillBindToContainer(): void
4550
{
46-
$expected = $this->prophesize(ContainerInterface::class)->reveal();
51+
$container = $this->prophesize(ContainerInterface::class);
52+
$expected = $container->reveal();
4753

48-
$factory = new CallableFactory(static fn (ContainerInterface $container) => $container);
54+
$factory = new CallableFactory(fn () => $this);
4955

5056
$actual = $factory($expected);
5157

5258
self::assertSame($expected, $actual);
5359
}
60+
61+
public function testClosureReceivesContainerDependenciesAsArgument(): void
62+
{
63+
$container = $this->prophesize(ContainerInterface::class);
64+
$factoryInterface = $this->prophesize(FactoryInterface::class)->reveal();
65+
$serviceProvider = $this->prophesize(ServiceProviderInterface::class)->reveal();
66+
67+
$container->get(ServiceProviderInterface::class)->willReturn($serviceProvider);
68+
$container->get(FactoryInterface::class)->willReturn($factoryInterface);
69+
70+
$factory = new CallableFactory(fn (
71+
ServiceProviderInterface $serviceProvider,
72+
FactoryInterface $factoryInterface
73+
) => compact('serviceProvider', 'factoryInterface'));
74+
75+
$actual = $factory($container->reveal());
76+
77+
self::assertSame(compact('serviceProvider', 'factoryInterface'), $actual);
78+
}
79+
80+
public function testInvokeWillThrowRuntimeExceptionIfParameterIsNotAClass(): void
81+
{
82+
$container = $this->prophesize(ContainerInterface::class)->reveal();
83+
84+
$factory = new CallableFactory(fn (string $notAClass) => $notAClass);
85+
86+
$this->expectException(RuntimeException::class);
87+
88+
$factory($container);
89+
}
5490
}

0 commit comments

Comments
 (0)