Issues (16)

src/Definition.php (2 issues)

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
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
    /** @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 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 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