1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | /* |
||||
6 | * This file is part of DivineNii opensource projects. |
||||
7 | * |
||||
8 | * PHP version 7.4 and above required |
||||
9 | * |
||||
10 | * @author Divine Niiquaye Ibok <[email protected]> |
||||
11 | * @copyright 2021 DivineNii (https://divinenii.com/) |
||||
12 | * @license https://opensource.org/licenses/BSD-3-Clause License |
||||
13 | * |
||||
14 | * For the full copyright and license information, please view the LICENSE |
||||
15 | * file that was distributed with this source code. |
||||
16 | */ |
||||
17 | |||||
18 | namespace Rade\DI; |
||||
19 | |||||
20 | use PhpParser\Node\{ |
||||
21 | Expr\ArrayDimFetch, |
||||
22 | Expr\Assign, |
||||
23 | Expr\BinaryOp, |
||||
24 | Expr\StaticPropertyFetch, |
||||
25 | Name, |
||||
26 | Scalar\String_, |
||||
27 | Stmt\Return_, |
||||
28 | UnionType |
||||
29 | }; |
||||
30 | use Rade\DI\{Builder\Statement, Exceptions\ServiceCreationException}; |
||||
31 | use Rade\DI\Resolvers\Resolver; |
||||
32 | |||||
33 | /** |
||||
34 | * Represents definition of standard service. |
||||
35 | * |
||||
36 | * @method string getId() Get the definition's id. |
||||
37 | * @method mixed getEntity() Get the definition's entity. |
||||
38 | * @method array<string,mixed> getParameters() Get the definition's parameters. |
||||
39 | * @method string|string[] getType() Get the return types for definition. |
||||
40 | * @method array<string,mixed> getCalls() Get the bind calls to definition. |
||||
41 | * @method array<int,mixed> getExtras() Get the list of extras binds. |
||||
42 | * @method string[] getDeprecation() Return a non-empty array if definition is deprecated. |
||||
43 | * @method bool isDeprecated() Whether this definition is deprecated, that means it should not be used anymore. |
||||
44 | * @method bool isLazy() Whether this service is lazy. |
||||
45 | * @method bool isFactory() Whether this service is not a shared service. |
||||
46 | * @method bool isPublic() Whether this service is a public type. |
||||
47 | * @method bool isAutowired() Whether this service is autowired. |
||||
48 | * |
||||
49 | * @author Divine Niiquaye Ibok <[email protected]> |
||||
50 | */ |
||||
51 | class Definition |
||||
52 | { |
||||
53 | use Traits\ResolveTrait; |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
54 | |||||
55 | /** Marks a definition as being a factory service. */ |
||||
56 | public const FACTORY = 1; |
||||
57 | |||||
58 | /** This is useful when you want to autowire a callable or class string lazily. */ |
||||
59 | public const LAZY = 2; |
||||
60 | |||||
61 | /** Marks a definition as a private service. */ |
||||
62 | public const PRIVATE = 4; |
||||
63 | |||||
64 | /** Use in second parameter of bind method. */ |
||||
65 | public const EXTRA_BIND = '@code@'; |
||||
66 | |||||
67 | /** supported call in get() method. */ |
||||
68 | private const SUPPORTED_GET = [ |
||||
69 | 'id' => 'id', |
||||
70 | 'entity' => 'entity', |
||||
71 | 'parameters' => 'parameters', |
||||
72 | 'type' => 'type', |
||||
73 | 'calls' => 'calls', |
||||
74 | 'extras' => 'extras', |
||||
75 | ]; |
||||
76 | |||||
77 | private const IS_TYPE_OF = [ |
||||
78 | 'isLazy' => 'lazy', |
||||
79 | 'isFactory' => 'factory', |
||||
80 | 'isPublic' => 'public', |
||||
81 | 'isAutowired' => 'autowired', |
||||
82 | 'isDeprecated' => 'deprecated', |
||||
83 | ]; |
||||
84 | |||||
85 | private string $id; |
||||
86 | |||||
87 | private bool $factory = false; |
||||
88 | |||||
89 | private bool $lazy = false; |
||||
90 | |||||
91 | private bool $public = true; |
||||
92 | |||||
93 | /** @var array<string,string> */ |
||||
94 | private array $deprecated = []; |
||||
95 | |||||
96 | /** |
||||
97 | * Definition constructor. |
||||
98 | * |
||||
99 | * @param mixed $entity |
||||
100 | * @param array<int|string,mixed> $arguments |
||||
101 | */ |
||||
102 | 117 | public function __construct($entity, array $arguments = []) |
|||
103 | { |
||||
104 | 117 | $this->replace($entity, true); |
|||
105 | 117 | $this->parameters = $arguments; |
|||
106 | 117 | } |
|||
107 | |||||
108 | /** |
||||
109 | * @param string $method |
||||
110 | * @param mixed[] $arguments |
||||
111 | * |
||||
112 | * @throws \BadMethodCallException |
||||
113 | * |
||||
114 | * @return mixed |
||||
115 | */ |
||||
116 | 49 | public function __call($method, $arguments) |
|||
117 | { |
||||
118 | 49 | if (isset(self::IS_TYPE_OF[$method])) { |
|||
119 | 38 | return (bool) $this->{self::IS_TYPE_OF[$method]}; |
|||
120 | } |
||||
121 | |||||
122 | 22 | return $this->get(\strtolower((string) \preg_replace('/^get([A-Z]{1}[a-z]+)$/', '\1', $method, 1))); |
|||
123 | } |
||||
124 | |||||
125 | /** |
||||
126 | * The method name generated for a service definition. |
||||
127 | */ |
||||
128 | 21 | final public static function createMethod(string $id): string |
|||
129 | { |
||||
130 | 21 | return 'get' . \str_replace(['.', '_', '\\'], '', \ucwords($id, '._')); |
|||
131 | } |
||||
132 | |||||
133 | /** |
||||
134 | * Attach the missing id and resolver to this definition. |
||||
135 | * NB: This method is used internally and should not be used directly. |
||||
136 | * |
||||
137 | * @internal |
||||
138 | */ |
||||
139 | 91 | final public function withContainer(string $id, AbstractContainer $container): void |
|||
140 | { |
||||
141 | 91 | $this->id = $id; |
|||
142 | 91 | $this->container = $container; |
|||
143 | |||||
144 | 91 | if ($container instanceof ContainerBuilder) { |
|||
145 | 49 | $this->builder = $container->getBuilder(); |
|||
146 | } |
||||
147 | 91 | } |
|||
148 | |||||
149 | /** |
||||
150 | * Get any of (id, entity, parameters, type, calls, extras, deprecation). |
||||
151 | * |
||||
152 | * @throws \BadMethodCallException if $name does not exist as property |
||||
153 | * |
||||
154 | * @return mixed |
||||
155 | */ |
||||
156 | 57 | final public function get(string $name) |
|||
157 | { |
||||
158 | 57 | if ('deprecation' === $name) { |
|||
159 | 5 | $deprecation = $this->deprecated; |
|||
160 | |||||
161 | 5 | if (isset($deprecation['message'])) { |
|||
162 | 5 | $deprecation['message'] = \sprintf($deprecation['message'], $this->id); |
|||
163 | } |
||||
164 | |||||
165 | 5 | return $deprecation; |
|||
166 | } |
||||
167 | |||||
168 | 54 | if (!isset(self::SUPPORTED_GET[$name])) { |
|||
169 | 1 | throw new \BadMethodCallException(\sprintf('Property call for %s invalid, %s::get(\'%1$s\') not supported.', $name, __CLASS__)); |
|||
170 | } |
||||
171 | |||||
172 | 54 | return $this->{$name}; |
|||
173 | } |
||||
174 | |||||
175 | /** |
||||
176 | * Replace existing entity to a new entity. |
||||
177 | * |
||||
178 | * NB: Using this method must be done before autowiring |
||||
179 | * else autowire manually. |
||||
180 | * |
||||
181 | * @param mixed $entity |
||||
182 | * @param bool $if rule matched |
||||
183 | * |
||||
184 | * @return $this |
||||
185 | */ |
||||
186 | 117 | final public function replace($entity, bool $if): self |
|||
187 | { |
||||
188 | 117 | if ($entity instanceof RawDefinition) { |
|||
189 | 1 | throw new ServiceCreationException(\sprintf('An instance of %s is not a valid definition entity.', RawDefinition::class)); |
|||
190 | } |
||||
191 | |||||
192 | 117 | if ($if /* Replace if matches a rule */) { |
|||
193 | 117 | $this->entity = $entity; |
|||
194 | } |
||||
195 | |||||
196 | 117 | return $this; |
|||
197 | } |
||||
198 | |||||
199 | /** |
||||
200 | * Sets the arguments to pass to the service constructor/factory method. |
||||
201 | * |
||||
202 | * @param array<int|string,mixed> $arguments |
||||
203 | * |
||||
204 | * @return $this |
||||
205 | */ |
||||
206 | 8 | final public function args(array $arguments): self |
|||
207 | { |
||||
208 | 8 | $this->parameters = $arguments; |
|||
209 | |||||
210 | 8 | return $this; |
|||
211 | } |
||||
212 | |||||
213 | /** |
||||
214 | * Sets/Replace one argument to pass to the service constructor/factory method. |
||||
215 | * |
||||
216 | * @param int|string $key |
||||
217 | * @param mixed $value |
||||
218 | * |
||||
219 | * @return $this |
||||
220 | */ |
||||
221 | 2 | final public function arg($key, $value): self |
|||
222 | { |
||||
223 | 2 | $this->parameters[$key] = $value; |
|||
224 | |||||
225 | 2 | return $this; |
|||
226 | } |
||||
227 | |||||
228 | /** |
||||
229 | * Sets method, property, Class|@Ref::Method or php code bindings. |
||||
230 | * |
||||
231 | * Binding map method name, property name, mixed type or php code that should be |
||||
232 | * injected in the definition's entity as assigned property, method or extra code added in running that entity. |
||||
233 | * |
||||
234 | * @param string $nameOrMethod A parameter name, a method name, or self::EXTRA_BIND |
||||
235 | * @param mixed $valueOrRef The value, reference or statement to bind |
||||
236 | * |
||||
237 | * @return $this |
||||
238 | */ |
||||
239 | 19 | final public function bind(string $nameOrMethod, $valueOrRef): self |
|||
240 | { |
||||
241 | 19 | if (self::EXTRA_BIND === $nameOrMethod) { |
|||
242 | 3 | $this->extras[] = $valueOrRef; |
|||
243 | |||||
244 | 3 | return $this; |
|||
245 | } |
||||
246 | |||||
247 | 19 | $this->calls[$nameOrMethod] = $valueOrRef; |
|||
248 | |||||
249 | 19 | return $this; |
|||
250 | } |
||||
251 | |||||
252 | /** |
||||
253 | * Enables autowiring. |
||||
254 | * |
||||
255 | * @param array<int,string> $types |
||||
256 | * |
||||
257 | * @return $this |
||||
258 | */ |
||||
259 | 27 | final public function autowire(array $types = []): self |
|||
260 | { |
||||
261 | 27 | $this->autowired = true; |
|||
262 | 27 | $service = $this->entity; |
|||
263 | |||||
264 | 27 | if ($service instanceof Statement) { |
|||
265 | 1 | $service = $service->value; |
|||
266 | } |
||||
267 | |||||
268 | 27 | if ([] === $types && null !== $service) { |
|||
269 | 25 | $types = Resolver::autowireService($service); |
|||
270 | } |
||||
271 | |||||
272 | 27 | $this->container->type($this->id, $types); |
|||
273 | |||||
274 | 27 | return $this->typeOf($types); |
|||
275 | } |
||||
276 | |||||
277 | /** |
||||
278 | * Represents a PHP type-hinted for this definition. |
||||
279 | * |
||||
280 | * @param string[]|string $types |
||||
281 | * |
||||
282 | * @return $this |
||||
283 | */ |
||||
284 | 39 | final public function typeOf($types): self |
|||
285 | { |
||||
286 | 39 | if (\is_array($types) && (1 === \count($types) || \PHP_VERSION_ID < 80000)) { |
|||
287 | 18 | foreach ($types as $type) { |
|||
288 | 18 | if (\class_exists($type)) { |
|||
289 | 18 | $types = $type; |
|||
290 | |||||
291 | 18 | break; |
|||
292 | } |
||||
293 | } |
||||
294 | } |
||||
295 | |||||
296 | 39 | $this->type = $types; |
|||
297 | |||||
298 | 39 | return $this; |
|||
299 | } |
||||
300 | |||||
301 | /** |
||||
302 | * Whether this definition is deprecated, that means it should not be used anymore. |
||||
303 | * |
||||
304 | * @param string $package The name of the composer package that is triggering the deprecation |
||||
305 | * @param float|null $version The version of the package that introduced the deprecation |
||||
306 | * @param string|null $message The deprecation message to use |
||||
307 | * |
||||
308 | * @return $this |
||||
309 | */ |
||||
310 | 6 | final public function deprecate(string $package = '', float $version = null, string $message = null): self |
|||
311 | { |
||||
312 | 6 | $this->deprecated['package'] = $package; |
|||
313 | 6 | $this->deprecated['version'] = $version ?? ''; |
|||
314 | 6 | $this->deprecated['message'] = $message ?? 'The "%s" service is deprecated. You should stop using it, as it will be removed in the future.'; |
|||
315 | |||||
316 | 6 | return $this; |
|||
317 | } |
||||
318 | |||||
319 | /** |
||||
320 | * Assign a set of tags to the definition. |
||||
321 | * |
||||
322 | * @param array<int|string,mixed> $tags |
||||
323 | */ |
||||
324 | public function tag(array $tags): self |
||||
325 | { |
||||
326 | $this->container->tag($this->id, $tags); |
||||
327 | |||||
328 | return $this; |
||||
329 | } |
||||
330 | |||||
331 | /** |
||||
332 | * Should the this definition be a type of |
||||
333 | * self::FACTORY|self::PRIVATE|self::LAZY, then set enabled or not. |
||||
334 | * |
||||
335 | * @return $this |
||||
336 | */ |
||||
337 | 72 | public function should(int $be = self::FACTORY, bool $enabled = true): self |
|||
338 | { |
||||
339 | switch ($be) { |
||||
340 | 72 | case self::FACTORY: |
|||
341 | 65 | $this->factory = $enabled; |
|||
342 | |||||
343 | 65 | break; |
|||
344 | |||||
345 | 64 | case self::LAZY: |
|||
346 | 57 | $this->lazy = $enabled; |
|||
347 | |||||
348 | 57 | break; |
|||
349 | |||||
350 | 36 | case self::PRIVATE: |
|||
351 | 34 | $this->public = !$enabled; |
|||
352 | |||||
353 | 34 | break; |
|||
354 | |||||
355 | 4 | case self::PRIVATE | self::FACTORY: |
|||
356 | 1 | $this->public = !$enabled; |
|||
357 | 1 | $this->factory = $enabled; |
|||
358 | |||||
359 | 1 | break; |
|||
360 | |||||
361 | 4 | case self::PRIVATE | self::LAZY: |
|||
362 | 3 | $this->public = !$enabled; |
|||
363 | 3 | $this->lazy = $enabled; |
|||
364 | |||||
365 | 3 | break; |
|||
366 | |||||
367 | 2 | case self::FACTORY | self::LAZY: |
|||
368 | 2 | $this->factory = $enabled; |
|||
369 | 2 | $this->lazy = $enabled; |
|||
370 | |||||
371 | 2 | break; |
|||
372 | |||||
373 | 1 | case self::FACTORY | self::LAZY | self::PRIVATE: |
|||
374 | 1 | $this->public = !$enabled; |
|||
375 | 1 | $this->factory = $enabled; |
|||
376 | 1 | $this->lazy = $enabled; |
|||
377 | |||||
378 | 1 | break; |
|||
379 | } |
||||
380 | |||||
381 | 72 | return $this; |
|||
382 | } |
||||
383 | |||||
384 | /** |
||||
385 | * Resolves the Definition when in use in ContainerBuilder. |
||||
386 | */ |
||||
387 | 6 | public function resolve(): \PhpParser\Node\Expr |
|||
388 | { |
||||
389 | 6 | $resolved = $this->builder->methodCall($this->builder->var('this'), self::createMethod($this->id)); |
|||
390 | |||||
391 | 6 | if ($this->factory) { |
|||
392 | 2 | return $resolved; |
|||
393 | } |
||||
394 | |||||
395 | 6 | return new BinaryOp\Coalesce( |
|||
396 | 6 | new ArrayDimFetch( |
|||
397 | 6 | new StaticPropertyFetch(new Name('self'), $this->public ? 'services' : 'privates'), |
|||
398 | 6 | new String_($this->id) |
|||
399 | ), |
||||
400 | $resolved |
||||
401 | ); |
||||
402 | } |
||||
403 | |||||
404 | /** |
||||
405 | * Build the definition service. |
||||
406 | * |
||||
407 | * @throws \ReflectionException |
||||
408 | */ |
||||
409 | 21 | public function build(): \PhpParser\Builder\Method |
|||
410 | { |
||||
411 | 21 | $node = $this->builder->method(self::createMethod($this->id))->makeProtected(); |
|||
412 | 21 | $factory = $this->resolveEntity($this->entity, $this->parameters); |
|||
413 | |||||
414 | 14 | if ([] !== $deprecation = $this->deprecated) { |
|||
415 | 1 | $deprecation[] = $this->id; |
|||
416 | 1 | $node->addStmt($this->builder->funcCall('\trigger_deprecation', \array_values($deprecation))); |
|||
417 | } |
||||
418 | |||||
419 | 14 | if (!empty($this->calls + $this->extras)) { |
|||
420 | 6 | $node->addStmt(new Assign($resolved = $this->builder->var($this->public ? 'service' : 'private'), $factory)); |
|||
421 | 6 | $node = $this->resolveCalls($resolved, $factory, $node); |
|||
422 | } |
||||
423 | |||||
424 | 14 | if (!empty($types = $this->type)) { |
|||
425 | 14 | $node->setReturnType(\is_array($types) ? new UnionType(\array_map(fn ($type) => new Name($type), $types)) : $types); |
|||
0 ignored issues
–
show
It seems like
is_array($types) ? new P...*/ }, $types)) : $types can also be of type PhpParser\Node\UnionType ; however, parameter $type of PhpParser\Builder\FunctionLike::setReturnType() does only seem to accept PhpParser\Node\Name|PhpP...ode\NullableType|string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
426 | } |
||||
427 | |||||
428 | 14 | if (!$this->factory) { |
|||
429 | 13 | $cached = new StaticPropertyFetch(new Name('self'), $this->public ? 'services' : 'privates'); |
|||
430 | 13 | $resolved = new Assign(new ArrayDimFetch($cached, new String_($this->id)), $resolved ?? $factory); |
|||
431 | } |
||||
432 | |||||
433 | 14 | return $node->addStmt(new Return_($resolved ?? $factory)); |
|||
434 | } |
||||
435 | } |
||||
436 |