Passed
Push — master ( 6a389c...42f43f )
by Divine Niiquaye
01:03 queued 12s
created

Definition::withContainer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
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
The trait Rade\DI\Traits\ResolveTrait requires some properties which are not provided by Rade\DI\Definition: $left, $args, $dim, $value
Loading history...
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
    private array $deprecated = [];
94
95
    /**
96
     * Definition constructor.
97
     *
98
     * @param mixed                   $entity
99
     * @param array<int|string,mixed> $arguments
100
     */
101 116
    public function __construct($entity, array $arguments = [])
102
    {
103 116
        $this->replace($entity, true);
104 116
        $this->parameters = $arguments;
105 116
    }
106
107
    /**
108
     * @param string  $method
109
     * @param mixed[] $arguments
110
     *
111
     * @throws \BadMethodCallException
112
     *
113
     * @return mixed
114
     */
115 48
    public function __call($method, $arguments)
116
    {
117 48
        if (isset(self::IS_TYPE_OF[$method])) {
118 37
            return (bool) $this->{self::IS_TYPE_OF[$method]};
119
        }
120
121 22
        return $this->get(\strtolower((string) \preg_replace('/^get([A-Z]{1}[a-z]+)$/', '\1', $method, 1)));
122
    }
123
124
    /**
125
     * The method name generated for a service definition.
126
     */
127 20
    final public static function createMethod(string $id): string
128
    {
129 20
        return 'get' . \str_replace(['.', '_', '\\'], '', \ucwords($id, '._'));
130
    }
131
132
    /**
133
     * Attach the missing id and resolver to this definition.
134
     * NB: This method is used internally and should not be used directly.
135
     *
136
     * @internal
137
     */
138 90
    final public function withContainer(string $id, AbstractContainer $container): void
139
    {
140 90
        $this->id = $id;
141 90
        $this->container = $container;
142
143 90
        if ($container instanceof ContainerBuilder) {
144 48
            $this->builder = $container->getBuilder();
145
        }
146 90
    }
147
148
    /**
149
     * Get any of (id, entity, parameters, type, calls, extras, deprecation).
150
     *
151
     * @throws \BadMethodCallException if $name does not exist as property
152
     *
153
     * @return mixed
154
     */
155 57
    final public function get(string $name)
156
    {
157 57
        if ('deprecation' === $name) {
158 5
            $deprecation = $this->deprecated;
159
160 5
            if (isset($deprecation['message'])) {
161 5
                $deprecation['message'] = \sprintf($deprecation['message'], $this->id);
162
            }
163
164 5
            return $deprecation;
165
        }
166
167 54
        if (!isset(self::SUPPORTED_GET[$name])) {
168 1
            throw new \BadMethodCallException(\sprintf('Property call for %s invalid, %s::get(\'%1$s\') not supported.', $name, __CLASS__));
169
        }
170
171 54
        return $this->{$name};
172
    }
173
174
    /**
175
     * Replace existing entity to a new entity.
176
     *
177
     * NB: Using this method must be done before autowiring
178
     * else autowire manually.
179
     *
180
     * @param mixed $entity
181
     * @param bool  $if     rule matched
182
     *
183
     * @return $this
184
     */
185 116
    final public function replace($entity, bool $if): self
186
    {
187 116
        if ($entity instanceof RawDefinition) {
188 1
            throw new ServiceCreationException(\sprintf('An instance of %s is not a valid definition entity.', RawDefinition::class));
189
        }
190
191 116
        if ($if /* Replace if matches a rule */) {
192 116
            $this->entity = $entity;
193
        }
194
195 116
        return $this;
196
    }
197
198
    /**
199
     * Sets the arguments to pass to the service constructor/factory method.
200
     *
201
     * @return $this
202
     */
203 8
    final public function args(array $arguments): self
204
    {
205 8
        $this->parameters = $arguments;
206
207 8
        return $this;
208
    }
209
210
    /**
211
     * Sets/Replace one argument to pass to the service constructor/factory method.
212
     *
213
     * @param int|string $key
214
     * @param mixed      $value
215
     *
216
     * @return $this
217
     */
218 1
    final public function arg($key, $value): self
219
    {
220 1
        $this->parameters[$key] = $value;
221
222 1
        return $this;
223
    }
224
225
    /**
226
     * Sets method, property, Class|@Ref::Method or php code bindings.
227
     *
228
     * Binding map method name, property name, mixed type or php code that should be
229
     * injected in the definition's entity as assigned property, method or
230
     * extra code added in running that entity.
231
     *
232
     * @param string $nameOrMethod A parameter name, a method name, or self::EXTRA_BIND
233
     * @param mixed  $valueOrRef   The value, reference or statement to bind
234
     *
235
     * @return $this
236
     */
237 18
    final public function bind(string $nameOrMethod, $valueOrRef): self
238
    {
239 18
        if (self::EXTRA_BIND === $nameOrMethod) {
240 3
            $this->extras[] = $valueOrRef;
241
242 3
            return $this;
243
        }
244
245 18
        $this->calls[$nameOrMethod] = $valueOrRef;
246
247 18
        return $this;
248
    }
249
250
    /**
251
     * Enables autowiring.
252
     *
253
     * @return $this
254
     */
255 26
    final public function autowire(array $types = []): self
256
    {
257 26
        $this->autowired = true;
258 26
        $service = $this->entity;
259
260 26
        if ($service instanceof Statement) {
261 1
            $service = $service->value;
262
        }
263
264 26
        if ([] === $types && null !== $service) {
265 24
            $types = Resolver::autowireService($service);
266
        }
267
268 26
        $this->container->type($this->id, $types);
269
270 26
        return $this->typeOf($types);
271
    }
272
273
    /**
274
     * Represents a PHP type-hinted for this definition.
275
     *
276
     * @param string[]|string $types
277
     *
278
     * @return $this
279
     */
280 38
    final public function typeOf($types): self
281
    {
282 38
        if (\is_array($types) && (1 === \count($types) || \PHP_VERSION_ID < 80000)) {
283 17
            foreach ($types as $type) {
284 17
                if (\class_exists($type)) {
285 17
                    $types = $type;
286
287 17
                    break;
288
                }
289
            }
290
        }
291
292 38
        $this->type = $types;
293
294 38
        return $this;
295
    }
296
297
    /**
298
     * Whether this definition is deprecated, that means it should not be used anymore.
299
     *
300
     * @param string $package The name of the composer package that is triggering the deprecation
301
     * @param string $version The version of the package that introduced the deprecation
302
     * @param string $message The deprecation message to use
303
     *
304
     * @return $this
305
     */
306 6
    final public function deprecate(/* string $package, string $version, string $message */): self
307
    {
308 6
        $args = \func_get_args();
309
310 6
        $message = $args[2] ?? 'The "%s" service is deprecated. You should stop using it, as it will be removed in the future.';
311
312 6
        $this->deprecated['package'] = $args[0] ?? '';
313 6
        $this->deprecated['version'] = $args[1] ?? '';
314 6
        $this->deprecated['message'] = $message;
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 5
    public function resolve(): \PhpParser\Node\Expr
388
    {
389 5
        $resolved = $this->builder->methodCall($this->builder->var('this'), self::createMethod($this->id));
390
391 5
        if ($this->factory) {
392 2
            return $resolved;
393
        }
394
395 5
        return new BinaryOp\Coalesce(
396 5
            new ArrayDimFetch(
397 5
                new StaticPropertyFetch(new Name('self'), $this->public ? 'services' : 'privates'),
398 5
                new String_($this->id)
399
            ),
400
            $resolved
401
        );
402
    }
403
404
    /**
405
     * Build the definition service.
406
     *
407
     * @throws \ReflectionException
408
     */
409 20
    public function build(): \PhpParser\Builder\Method
410
    {
411 20
        $node = $this->builder->method(self::createMethod($this->id))->makeProtected();
412 20
        $factory = $this->resolveEntity($this->entity, $this->parameters);
413
414 13
        if ([] !== $deprecation = $this->deprecated) {
415 1
            $deprecation[] = $this->id;
416 1
            $node->addStmt($this->builder->funcCall('\trigger_deprecation', \array_values($deprecation)));
417
        }
418
419 13
        if (!empty($this->calls + $this->extras)) {
420 5
            $node->addStmt(new Assign($resolved = $this->builder->var($this->public ? 'service' : 'private'), $factory));
421 5
            $node = $this->resolveCalls($resolved, $factory, $node);
422
        }
423
424 13
        if (!empty($types = $this->type)) {
425 13
            $node->setReturnType(\is_array($types) ? new UnionType(\array_map(fn ($type) => new Name($type), $types)) : $types);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

425
            $node->setReturnType(/** @scrutinizer ignore-type */ \is_array($types) ? new UnionType(\array_map(fn ($type) => new Name($type), $types)) : $types);
Loading history...
426
        }
427
428 13
        if (!$this->factory) {
429 12
            $cached = new StaticPropertyFetch(new Name('self'), $this->public ? 'services' : 'privates');
430 12
            $resolved = new Assign(new ArrayDimFetch($cached, new String_($this->id)), $resolved ?? $factory);
431
        }
432
433 13
        return $node->addStmt(new Return_($resolved ?? $factory));
434
    }
435
}
436