-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Container.php
363 lines (312 loc) · 11.7 KB
/
Container.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
<?php
/**
* This file is part of the Vection package.
*
* (c) David M. Lung <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Vection\Component\DependencyInjection;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Vection\Component\DependencyInjection\Exception\IllegalConstructorParameterException;
use Vection\Component\DependencyInjection\Exception\InvalidArgumentException;
use Vection\Component\DependencyInjection\Exception\NotFoundException;
use Vection\Component\DependencyInjection\Exception\RuntimeException;
use Vection\Contracts\Cache\CacheAwareInterface;
use Vection\Contracts\Cache\CacheInterface;
use Vection\Contracts\DependencyInjection\InjectorInterface;
use Vection\Contracts\DependencyInjection\InstructionInterface;
use Vection\Contracts\DependencyInjection\ResolverInterface;
/**
* Class Container
*
* This class provides dependency injection by constructor, interface and annotation definition.
* An optional configuration file can be used to register
* and define dependencies.
*
* @package Vection\Component\DependencyInjection
*
* @author David M. Lung <[email protected]>
*/
class Container implements ContainerInterface, LoggerAwareInterface, CacheAwareInterface
{
use LoggerAwareTrait;
protected ResolverInterface $resolver;
protected InjectorInterface $injector;
/** @var string[] */
protected array $allowedNamespacePrefixes = [];
/** @var object[] */
protected array $sharedObjects;
/**
* Container constructor.
*
* @param ResolverInterface|null $resolver
* @param Injector|null $injector
*/
public function __construct(ResolverInterface|null $resolver = null, InjectorInterface|null $injector = null)
{
$this->logger = new NullLogger();
$this->sharedObjects[self::class] = $this;
$this->resolver = $resolver ?: new Resolver();
$this->injector = $injector ?: new Injector($this, $this->resolver);
}
/**
* @inheritDoc
*/
public function setCache(CacheInterface $cache): void
{
$this->resolver->setCache($cache->getPool('DIC'));
}
/**
* All objects of the given namespaces will be auto registered at runtime
* for injection into other objects without the need to define them
* in the config or by set/add methods. Pass ['*'] as wildcard to register all namespaces.
*
* @param string[] $namespacePrefixes
*/
public function setAllowedNamespacePrefixes(array $namespacePrefixes): void
{
$this->allowedNamespacePrefixes = $namespacePrefixes;
}
/**
* Loads an instructions file, which contains the
* instructions for the classes that will be handled by the
* container. This method uses glob to support paths with wildcards
* and dynamic path types that are supported by glob too. This gives the possibility
* to add multiple files.
*
* @param string $path
*/
public function load(string $path): void
{
if ( ($pathArray = glob($path)) === false ) {
throw new InvalidArgumentException("Given path is not valid or doesn't exists.");
}
foreach ( $pathArray as $filePath ) {
// @noinspection PhpIncludeInspection
$instructions = require $filePath;
if ( ! is_array($instructions) ) {
throw new RuntimeException("Cannot load instruction from $filePath.");
}
foreach ( $instructions as $instruction ) {
if ( ! $instruction instanceof InstructionInterface ) {
throw new RuntimeException(
'Invalid configuration file: Each entry must be of type InstructionInterface.'
);
}
$this->resolver->addInstruction($instruction);
}
}
}
/**
* Flush the internal storage for shared objects.
*/
public function flush(): void
{
$this->sharedObjects = [];
}
/**
* @param string $className
* @param array<int, mixed> $constructParams
*
* @return object
*/
private function createObject(string $className, array $constructParams = []): object
{
$factory = $this->resolver->getInstruction($className)?->getFactory();
if ($constructParams && !$factory) {
return new $className(...$constructParams);
}
if ($factory) {
return $factory($this, ...$constructParams);
}
if ($dependencies = $this->resolver->getClassDependencies($className)) {
if ($constructParams = $dependencies['constructor']) {
$paramObjects = [];
$nullableInterfaces = $dependencies['constructor_nullable_interface'] ?? [];
$preventInjectionParams = $dependencies['constructor_prevent_injection'] ?? [];
foreach ( $constructParams as $param ) {
$isNullableInterface = in_array($param, $nullableInterfaces, true);
$isPreventInjectionParam = in_array($param, $preventInjectionParams, true);
if ($isPreventInjectionParam || ($isNullableInterface && !$this->resolver->getInstruction($param))) {
$paramObjects[] = null;
}else{
// @phpstan-ignore-next-line
$paramObjects[] = $this->get($param);
}
}
return new $className(...$paramObjects);
}
if (isset($dependencies['constructor_has_primitives']) && count($constructParams) === 0) {
throw new IllegalConstructorParameterException(
'The use of primitive parameter types at constructor injection can only be used when '.
'creating object with explicit construct parameters e.g. '.
'Container::create(class, [param1, param2,..])), occurred in class '.$className
);
}
}
return new $className();
}
/**
* @param string $id
*
* @return bool
*/
private function evaluate(string $id): bool
{
# Check if there is a defined entry for the given id
if (!$this->has($id)) {
if ($this->allowedNamespacePrefixes) {
# Check if the id is part of a registered namespace scope
foreach ($this->allowedNamespacePrefixes as $namespace) {
if (str_starts_with($id, $namespace)) {
# The id matches a scope, so we allow to register a default instruction for this id
$this->set($id);
return true;
}
}
return false;
}
$this->set($id);
return true;
}
return true;
}
/**
* @param object $object
*/
private function initializeObject(object $object): void
{
if ( method_exists($object, '__init') && is_callable($object->__init()) ) {
$object->__init();
}
}
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
*
* @param string $id Identifier of the entry to look for.
*
* @return bool
*/
public function has(string $id): bool
{
return (bool) $this->resolver->getInstruction(trim($id, "\\"));
}
/**
* Register a new entry by given identifier. The second parameter can be used
* for entry instruction
*
* @param string $className
* @param InstructionInterface|null $instruction
*
* @return Container
*/
public function set(string $className, InstructionInterface|null $instruction = null): Container
{
$className = trim($className, "\\");
$this->resolver->addInstruction($instruction ?: new Instruction($className));
$this->resolver->resolveDependencies($className);
return $this;
}
/**
* Finds an entry of the container by its identifier and returns it.
* If the requested entry has dependencies, these will be injected before
* return this object.
*
* @template T of object
*
* @param class-string<T> $id Identifier of the entry to look for.
*
* @return T
*/
public function get(string $id): object
{
$className = ltrim($id, "\\");
if ( isset($this->sharedObjects[$className]) ) {
return $this->sharedObjects[$className];
}
if ( ! $this->evaluate($className) ) {
throw new NotFoundException('DI Container: Unregistered identifier: '.$className);
}
$this->resolver->resolveDependencies($className);
$object = $this->createObject($className);
$this->injector->injectDependencies($object);
if ( ($instruction = $this->resolver->getInstruction($className)) && $instruction->isShared()) {
$this->sharedObjects[$className] = $object;
}
$this->initializeObject($object);
return $object;
}
/**
* Creates a new object by its identifier. The constructor parameters
* will be resolved and pass by the object container automatically if the
* second parameter is not set, otherwise the new object will be created
* with given parameters.
*
* @template T of object
*
* @param class-string<T> $identifier The identifier of registered entry.
* @param array<int, mixed> $constructParams Parameter that should be passed to constructor.
* @param bool $shared Whether the new object should be shared or not.
*
* @return T The new created object.
*/
public function create(string $identifier, array $constructParams = [], bool $shared = true): object
{
$className = trim($identifier, "\\");
if ( ! $this->evaluate($className) ) {
throw new NotFoundException('DI Container: Unregistered identifier: '.$className);
}
$instruction = $this->resolver->getInstruction($className);
if (!$instruction) {
$this->resolver->addInstruction($instruction = new Instruction($className));
}
$instruction->asShared($shared);
$this->resolver->resolveDependencies($className);
$object = $this->createObject($className, $constructParams);
$this->injector->injectDependencies($object);
if ( $shared ) {
$this->sharedObjects[$className] = $object;
}
$this->initializeObject($object);
return $object;
}
/**
* Adds and register a new shared object to the container. The identifier
* will be the FQCN of the given object.
*
* @template T of object
*
* @param T $object
*
* @return T Returns the given object.
*/
public function add(object $object): object
{
$className = trim(get_class($object), "\\");
$this->set($className);
$this->sharedObjects[$className] = $object;
$this->injector->injectDependencies($object);
return $object;
}
}
/**
* Returns a new instance of InstructionInterface.
*
* @param string $className
*
* @return Instruction
*/
function resolve(string $className): InstructionInterface
{
return new Instruction($className);
}