Skip to content

Commit ab9ddc1

Browse files
authored
Merge pull request #19 from FreeElephants/callable-components
Support callable beans instantiation
2 parents 38a7b4a + e0de8f5 commit ab9ddc1

File tree

10 files changed

+154
-28
lines changed

10 files changed

+154
-28
lines changed

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
66

77
## [Unreleased]
88

9+
## [3.1.0] - 2021-05-17
10+
### Added:
11+
- 'callable' di configuration entries support
12+
913
### Changed
1014
- Register itself as PSR Container
1115

@@ -14,7 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1418
- PHP 8 support
1519

1620
### Changed
17-
- Check that argument type is not builtin, intead deprecated `ReflectionType::getClass` usage
21+
- Check that argument type is not builtin, instead deprecated `ReflectionType::getClass` usage
1822

1923
### Removed
2024
- PHP 7.1 and 7.2 support
@@ -93,7 +97,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
9397
### Added
9498
- All classes.
9599

96-
[Unreleased]: https://github.com/FreeElephants/php-di/compare/3.0.0...HEAD
100+
[Unreleased]: https://github.com/FreeElephants/php-di/compare/3.1.0...HEAD
101+
[3.1.0]: https://github.com/FreeElephants/php-di/compare/3.0.0...3.1.0
97102
[3.0.0]: https://github.com/FreeElephants/php-di/compare/2.1.0...3.0.0
98103
[2.1.0]: https://github.com/FreeElephants/php-di/compare/2.0.2...2.1.0
99104
[2.0.2]: https://github.com/FreeElephants/php-di/compare/2.0.1...2.0.2

README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Framework-agnostic Dependency Injection tool and PSR-11 implementation provider.
1111

1212
## Requirements
1313

14-
PHP >=7.1
14+
PHP >=7.3|8.0
1515

1616
## Installation
1717

@@ -29,7 +29,8 @@ $app = $di->createInstance(\YourApplication::class);
2929
$app->run();
3030
```
3131

32-
Your `components.php` file with dependencies description shoud look like this:
32+
Your `components.php` file with dependencies description should look like this:
33+
3334
```php
3435
<?php
3536

@@ -42,20 +43,37 @@ return [
4243
\ControllerFactory::class,
4344
\SomeService::class,
4445
\AnotherService::class,
45-
\Psr\Log\LoggerInterface::class => \Symfony\Component\Console\Logger\ConsoleLogger::class
46-
// etc
46+
\Psr\Log\LoggerInterface::class => \Symfony\Component\Console\Logger\ConsoleLogger::class,
47+
],
48+
'callable' => [
49+
// if function provided as key value
50+
// first argument passed to callable is psr container
51+
// second is key
52+
Foo::class => function(\Psr\Container\ContainerInterface $container, string $key) {
53+
return (new Foo())->setSomething($container->get('something'));
54+
},
55+
// if array provided as key value
56+
// first argument passed to callable is psr container
57+
// remaining element as ...args tail
58+
Bar::class => [ // array where first element is callable, other is values for last arguments
59+
function(\Psr\Container\ContainerInterface $container, $firstArg, string $secondArg) {
60+
return new Bar($firstArg, $secondArg);
61+
},
62+
100,
63+
500,
64+
],
4765
],
4866
];
4967
```
5068

5169
The main idea: all your components should expect all dependencies as constructor arguments. All other work entrust to Injector.
52-
You do not have to want instantiate any classes directly in your code. Your must inject some factories instead.
70+
You do not have to want to instantiate any classes directly in your code. Your must inject some factories instead.
5371

5472
### Override Components by Environments
5573

5674
```php
5775
<?php
58-
// genenv('ENV') -> 'test'
76+
// getenv('ENV') -> 'test'
5977
$components = (new \FreeElephants\DI\EnvAwareConfigLoader(__DIR__ . '/config', 'ENV'))->readConfig('components');
6078
$di = (new \FreeElephants\DI\InjectorBuilder)->buildFromArray($components);
6179
```

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
}
2020
],
2121
"require": {
22-
"psr/log": "~1.0",
22+
"psr/log": "^1.0",
2323
"psr/container": "^1.0",
24-
"php": "^7.1|^8.0"
24+
"php": "^7.3|^8.0"
2525
},
2626
"provide": {
2727
"psr/container-implementation": "1.0"

src/CallableBeanContainer.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace FreeElephants\DI;
4+
5+
use FreeElephants\DI\Exception\InvalidArgumentException;
6+
use Psr\Container\ContainerInterface;
7+
8+
class CallableBeanContainer
9+
{
10+
/**
11+
* @var callable|mixed|null
12+
*/
13+
private $function;
14+
/**
15+
* @var array
16+
*/
17+
private $args;
18+
19+
public function __construct(string $interface, $callable, ContainerInterface $container)
20+
{
21+
if (is_callable($callable)) {
22+
$function = $callable;
23+
$args = [$container, $interface];
24+
} elseif(is_array($callable)) {
25+
$function = array_shift($callable);
26+
$args = array_merge([$container], $callable);
27+
} else {
28+
throw new InvalidArgumentException();
29+
}
30+
31+
$this->function = $function;
32+
$this->args = $args;
33+
}
34+
35+
/**
36+
* @return mixed
37+
*/
38+
public function __invoke()
39+
{
40+
return call_user_func($this->function, ...$this->args);
41+
}
42+
}

src/Injector.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public function createInstance($class)
7171
$constructorParams[] = $arg->getDefaultValue();
7272
} else {
7373
$extendedMessage = sprintf('%s [Required in %s constructor]', $e->getMessage(), $class);
74-
throw new MissingDependencyException($extendedMessage, null, $e);
74+
throw new MissingDependencyException($extendedMessage, 0, $e);
7575
}
7676
}
7777
} elseif ($arg->isDefaultValueAvailable()) {
@@ -89,7 +89,7 @@ public function createInstance($class)
8989
return $instance;
9090
}
9191

92-
public function registerService(string $implementation, string $interface = null)
92+
public function registerService($implementation, string $interface = null)
9393
{
9494
$interface = $interface ?: $implementation;
9595
if (isset($this->serviceMap[$interface])) {
@@ -116,6 +116,10 @@ public function getService(string $type)
116116
$this->loggerHelper->logLazyLoading($type, $service);
117117
$service = $this->createInstance($service);
118118
$this->setService($type, $service);
119+
} elseif ($service instanceof CallableBeanContainer) {
120+
$this->loggerHelper->logLazyLoading($type, $service);
121+
$service = $service();
122+
$this->setService($type, $service);
119123
}
120124

121125
return $service;
@@ -129,7 +133,8 @@ public function hasImplementation(string $interface): bool
129133
public function merge(
130134
array $components,
131135
string $instancesKey = InjectorBuilder::INSTANCES_KEY,
132-
string $registerKey = InjectorBuilder::REGISTER_KEY
136+
string $registerKey = InjectorBuilder::REGISTER_KEY,
137+
string $callableKey = InjectorBuilder::CALLABLE_KEY
133138
)
134139
{
135140
$beansInstances = $components[$instancesKey] ?? [];
@@ -139,13 +144,19 @@ public function merge(
139144
}
140145
$this->setService($interface, $instance);
141146
}
147+
142148
$registeredBeans = $components[$registerKey] ?? [];
143149
foreach ($registeredBeans as $interface => $implementation) {
144150
if (is_int($interface)) {
145151
$interface = $implementation;
146152
}
147153
$this->registerService($implementation, $interface);
148154
}
155+
156+
$callableBeans = $components[$callableKey] ?? [];
157+
foreach ($callableBeans as $interface => $callable) {
158+
$this->registerService(new CallableBeanContainer($interface, $callable, $this), $interface);
159+
}
149160
}
150161

151162
public function allowNullableConstructorArgs(bool $allow)
@@ -191,6 +202,5 @@ public function useIdAsTypeName(bool $useIdAsTypeName = true)
191202
public function enableLoggerAwareInjection(bool $enable = true)
192203
{
193204
$this->enableLoggerAwareInjection = $enable;
194-
}
195-
}
205+
}}
196206

src/InjectorBuilder.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
class InjectorBuilder
99
{
1010
public const INSTANCES_KEY = 'instances';
11-
public const REGISTER_KEY = 'register';
11+
public const REGISTER_KEY = 'register';
12+
public const CALLABLE_KEY = 'callable';
1213
/**
1314
* @var string
1415
*/
@@ -17,19 +18,28 @@ class InjectorBuilder
1718
* @var string
1819
*/
1920
private $registerKey;
21+
/**
22+
* @var string
23+
*/
24+
private $callableKey;
2025

21-
public function __construct(string $instancesKey = self::INSTANCES_KEY, string $registerKey = self::REGISTER_KEY)
26+
public function __construct(
27+
string $instancesKey = self::INSTANCES_KEY,
28+
string $registerKey = self::REGISTER_KEY,
29+
string $callableKey = self::CALLABLE_KEY
30+
)
2231
{
2332
$this->instancesKey = $instancesKey;
2433
$this->registerKey = $registerKey;
34+
$this->callableKey = $callableKey;
2535
}
2636

2737
public function buildFromArray(array $components): Injector
2838
{
2939
$injector = new Injector();
3040

31-
$injector->merge($components, $this->instancesKey, $this->registerKey);
41+
$injector->merge($components, $this->instancesKey, $this->registerKey, $this->callableKey);
3242

3343
return $injector;
3444
}
35-
}
45+
}

src/LoggerHelper.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ public function logLazyLoading(string $interface, $service)
3838
$this->logger->debug($debugMsg, $context);
3939
}
4040

41-
public function logServiceRegistration(string $implementation, string $interface)
41+
public function logServiceRegistration($implementation, string $interface)
4242
{
43-
$msg = 'Service with type ' . $interface . ' and implementation ' . $implementation . ' register. ';
43+
$msg = 'Service with type ' . $interface . ' and implementation ' . $this->stringifyImplementation($implementation) . ' register. ';
4444
$context = [
4545
'interface' => $interface,
4646
'implementation' => $implementation,
@@ -69,7 +69,7 @@ public function logServiceInstanceReplacing(string $typeName, $service, $previou
6969
return [$debugMsg, $context];
7070
}
7171

72-
public function logRegisterServiceReplacing(string $implementation, string $interface, $oldImplementation)
72+
public function logRegisterServiceReplacing($implementation, string $interface, $oldImplementation)
7373
{
7474
$msg = 'Replace registered service type ' . $interface . ' with another. ';
7575
$context = [
@@ -89,4 +89,14 @@ public function logServiceSetting(string $typeName, $service)
8989
];
9090
$this->logger->debug($debugMsg, $context);
9191
}
92-
}
92+
93+
private function stringifyImplementation($implementation): string
94+
{
95+
// var_export does not handle circular references
96+
// handle this case
97+
if($implementation instanceof CallableBeanContainer) {
98+
$implementation = 'user defined callable';
99+
}
100+
return var_export($implementation, true);
101+
}
102+
}

tests/Fixture/Bar.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
class Bar
99
{
1010

11-
}
11+
}

tests/InjectorBuilderTest.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use Fixture\AnotherService;
66
use Fixture\Bar;
7+
use Fixture\ClassWithDefaultConstructorArgValue;
78
use Fixture\Foo;
9+
use Psr\Container\ContainerInterface;
810

911
/**
1012
* @author samizdam <samizdam@inbox.ru>
@@ -17,17 +19,32 @@ public function testBuildFromArray()
1719
$builder = new InjectorBuilder();
1820

1921
$anotherServiceInstance = new AnotherService();
22+
$bar1 = new Bar();
23+
$bar2 = new Bar();
2024
$injector = $builder->buildFromArray([
2125
'instances' => [
22-
Bar::class => new Bar(),
26+
Bar::class => $bar1,
2327
$anotherServiceInstance,
2428
],
25-
'register' => [
29+
'register' => [
2630
Foo::class
27-
]
31+
],
32+
'callable' => [
33+
ClassWithDefaultConstructorArgValue::class => [
34+
function (ContainerInterface $container, int $value): ClassWithDefaultConstructorArgValue {
35+
return new ClassWithDefaultConstructorArgValue($value);
36+
},
37+
9000,
38+
],
39+
Bar::class => function () use ($bar2) {
40+
return $bar2;
41+
},
42+
],
2843
]);
2944

45+
$this->assertSame($bar2, $injector->getService(Bar::class));
3046
$this->assertInstanceOf(Foo::class, $injector->createInstance(Foo::class));
3147
$this->assertSame($anotherServiceInstance, $injector->getService(AnotherService::class));
48+
$this->assertSame(9000, $injector->get(ClassWithDefaultConstructorArgValue::class)->getValue());
3249
}
33-
}
50+
}

tests/InjectorTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,19 @@ public function testHandleInstantiateInterfaceError()
236236
$injector->get(SomeService::class);
237237
}
238238

239+
public function testGetServiceWithInvalidCallable()
240+
{
241+
$injector = new Injector();
242+
243+
$this->expectException(InvalidArgumentException::class);
244+
245+
$injector->merge([
246+
InjectorBuilder::CALLABLE_KEY => [
247+
'foo' => 'not_callable',
248+
],
249+
]);
250+
}
251+
252+
239253
}
240254

0 commit comments

Comments
 (0)