Container::getCallableWithoutChecks()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Container\Manager;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Throwable;
18
use Valkyrja\Container\Contract\ServiceContract;
19
use Valkyrja\Container\Data\Data;
20
use Valkyrja\Container\Enum\InvalidReferenceMode;
21
use Valkyrja\Container\Manager\Contract\ContainerContract as Contract;
22
use Valkyrja\Container\Manager\Trait\ProvidersAwareTrait;
23
use Valkyrja\Container\Throwable\Exception\InvalidReferenceException;
24
25
use function array_filter;
26
use function array_map;
27
use function array_merge;
28
use function assert;
29
use function is_a;
30
use function is_object;
31
32
class Container implements Contract
33
{
34
    use ProvidersAwareTrait;
35
36
    /**
37
     * The aliases.
38
     *
39
     * @var array<class-string, class-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, class-string> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, class-string>.
Loading history...
40
     */
41
    protected array $aliases = [];
42
43
    /**
44
     * The instances.
45
     *
46
     * @var array<class-string, object>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, object> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, object>.
Loading history...
47
     */
48
    protected array $instances = [];
49
50
    /**
51
     * The services.
52
     *
53
     * @var array<class-string<ServiceContract>, class-string<ServiceContract>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Servi...tring<ServiceContract>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<ServiceContract>, class-string<ServiceContract>>.
Loading history...
54
     */
55
    protected array $services = [];
56
57
    /**
58
     * The service callables.
59
     *
60
     * @var array<class-string, callable(Container, mixed...):object>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, call...iner, mixed...):object> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, callable(Container, mixed...):object>.
Loading history...
61
     */
62
    protected array $callables = [];
63
64
    /**
65
     * The singletons.
66
     *
67
     * @var array<class-string, class-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, class-string> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, class-string>.
Loading history...
68
     */
69
    protected array $singletons = [];
70
71
    public function __construct(
72
        protected Data $data = new Data()
73
    ) {
74
        $this->aliases          = $data->aliases;
75
        $this->deferred         = $data->deferred;
76
        $this->deferredCallback = $data->deferredCallback;
77
        $this->services         = $data->services;
78
        $this->singletons       = $data->singletons;
79
        $this->registered       = [];
80
    }
81
82
    /**
83
     * @inheritDoc
84
     */
85
    #[Override]
86
    public function getData(): Data
87
    {
88
        return new Data(
89
            aliases: $this->aliases,
90
            deferred: $this->deferred,
91
            deferredCallback: $this->deferredCallback,
92
            services: [],
93
            singletons: [],
94
            providers: array_filter($this->providers, static fn (string $provider): bool => ! $provider::deferred()),
95
        );
96
    }
97
98
    /**
99
     * @inheritDoc
100
     */
101
    #[Override]
102
    public function setFromData(Data $data): void
103
    {
104
        $this->aliases          = array_merge($this->aliases, $data->aliases);
105
        $this->deferred         = array_merge($this->deferred, $data->deferred);
106
        $this->deferredCallback = array_merge($this->deferredCallback, $data->deferredCallback);
107
        $this->services         = array_merge($this->services, $data->services);
108
        $this->singletons       = array_merge($this->singletons, $data->singletons);
109
110
        array_map(
111
            [$this, 'register'],
112
            $data->providers
113
        );
114
    }
115
116
    /**
117
     * @inheritDoc
118
     *
119
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
120
     *
121
     * @psalm-suppress MoreSpecificImplementedParamType
122
     */
123
    #[Override]
124
    public function has(string $id): bool
125
    {
126
        return $this->isDeferred($id)
127
            || $this->isSingleton($id)
128
            || $this->isService($id)
129
            || $this->isCallable($id)
130
            || $this->isAlias($id);
131
    }
132
133
    /**
134
     * @inheritDoc
135
     *
136
     * @param class-string                  $id      The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
137
     * @param class-string<ServiceContract> $service The service
138
     */
139
    #[Override]
140
    public function bind(string $id, string $service): static
141
    {
142
        assert(is_a($service, ServiceContract::class, true));
143
144
        /** @var class-string<ServiceContract> $id */
145
        $this->services[$id] = $service;
146
147
        return $this;
148
    }
149
150
    /**
151
     * @inheritDoc
152
     *
153
     * @param class-string $alias The alias
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
154
     * @param class-string $id    The service id to alias
155
     */
156
    #[Override]
157
    public function bindAlias(string $alias, string $id): static
158
    {
159
        $this->aliases[$alias] = $id;
160
161
        return $this;
162
    }
163
164
    /**
165
     * @inheritDoc
166
     *
167
     * @param class-string                  $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
168
     * @param class-string<ServiceContract> $singleton The singleton service
169
     */
170
    #[Override]
171
    public function bindSingleton(string $id, string $singleton): static
172
    {
173
        $this->singletons[$id] = $singleton;
174
175
        $this->bind($id, $singleton);
176
177
        return $this;
178
    }
179
180
    /**
181
     * @inheritDoc
182
     *
183
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
184
     */
185
    #[Override]
186
    public function setCallable(string $id, callable $callable): static
187
    {
188
        /** @var callable(Contract, mixed...):object $callable */
189
        $this->callables[$id] = $callable;
190
        $this->published[$id] = true;
191
192
        return $this;
193
    }
194
195
    /**
196
     * @inheritDoc
197
     *
198
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
199
     */
200
    #[Override]
201
    public function setSingleton(string $id, object $singleton): static
202
    {
203
        $this->singletons[$id] = $id;
204
        $this->instances[$id]  = $singleton;
205
        $this->published[$id]  = true;
206
207
        return $this;
208
    }
209
210
    /**
211
     * @inheritDoc
212
     *
213
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
214
     */
215
    #[Override]
216
    public function isAlias(string $id): bool
217
    {
218
        return isset($this->aliases[$id]);
219
    }
220
221
    /**
222
     * @inheritDoc
223
     *
224
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
225
     */
226
    #[Override]
227
    public function isCallable(string $id): bool
228
    {
229
        return isset($this->callables[$id]);
230
    }
231
232
    /**
233
     * @inheritDoc
234
     *
235
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
236
     */
237
    #[Override]
238
    public function isService(string $id): bool
239
    {
240
        return isset($this->services[$id]);
241
    }
242
243
    /**
244
     * @inheritDoc
245
     *
246
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
247
     */
248
    #[Override]
249
    public function isSingleton(string $id): bool
250
    {
251
        return isset($this->singletons[$id]);
252
    }
253
254
    /**
255
     * @inheritDoc
256
     *
257
     * @psalm-suppress InvalidReturnType
258
     * @psalm-suppress InvalidReturnStatement
259
     * @psalm-suppress ImplementedReturnTypeMismatch
260
     * @psalm-suppress MoreSpecificImplementedParamType
261
     */
262
    #[Override]
263
    public function get(string $id, array $arguments = [], InvalidReferenceMode $mode = InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION): object|null
264
    {
265
        $this->publishUnpublishedProvided($id);
266
267
        // @phpstan-ignore-next-line
268
        return $this->getSingletonWithoutChecks($id)
269
            ?? $this->getCallableWithoutChecks($id, $arguments)
270
            ?? $this->getServiceWithoutChecks($id, $arguments)
271
            ?? $this->getAliasedWithoutChecks($id, $arguments)
272
            ?? $this->getFallback($id, $arguments, $mode);
273
    }
274
275
    /**
276
     * @inheritDoc
277
     *
278
     * @psalm-suppress InvalidReturnType
279
     * @psalm-suppress InvalidReturnStatement
280
     * @psalm-suppress ImplementedReturnTypeMismatch
281
     */
282
    #[Override]
283
    public function getAliased(string $id, array $arguments = []): object
284
    {
285
        // @phpstan-ignore-next-line
286
        return $this->getAliasedWithoutChecks($id, $arguments)
287
            ?? throw new InvalidReferenceException($id);
288
    }
289
290
    /**
291
     * @inheritDoc
292
     *
293
     * @psalm-suppress InvalidReturnType
294
     * @psalm-suppress InvalidReturnStatement
295
     * @psalm-suppress ImplementedReturnTypeMismatch
296
     */
297
    #[Override]
298
    public function getCallable(string $id, array $arguments = []): object
299
    {
300
        $this->publishUnpublishedProvided($id);
301
302
        // @phpstan-ignore-next-line
303
        return $this->getCallableWithoutChecks($id, $arguments)
304
            ?? throw new InvalidReferenceException($id);
305
    }
306
307
    /**
308
     * @inheritDoc
309
     *
310
     * @psalm-suppress InvalidReturnType
311
     * @psalm-suppress InvalidReturnStatement
312
     * @psalm-suppress ImplementedReturnTypeMismatch
313
     */
314
    #[Override]
315
    public function getService(string $id, array $arguments = []): ServiceContract
316
    {
317
        $this->publishUnpublishedProvided($id);
318
319
        /** @var class-string<ServiceContract> $id */
320
321
        // @phpstan-ignore-next-line
322
        return $this->getServiceWithoutChecks($id, $arguments)
323
            ?? throw new InvalidReferenceException($id);
324
    }
325
326
    /**
327
     * @inheritDoc
328
     *
329
     * @psalm-suppress InvalidReturnType
330
     * @psalm-suppress InvalidReturnStatement
331
     * @psalm-suppress ImplementedReturnTypeMismatch
332
     */
333
    #[Override]
334
    public function getSingleton(string $id): object
335
    {
336
        $this->publishUnpublishedProvided($id);
337
338
        // @phpstan-ignore-next-line
339
        return $this->getSingletonWithoutChecks($id)
340
            ?? throw new InvalidReferenceException($id);
341
    }
342
343
    /**
344
     * Get an aliased service from the container without trying to ensuring published.
345
     *
346
     * @param class-string            $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
347
     * @param array<array-key, mixed> $arguments [optional] The arguments
348
     *
349
     * @return object|null
350
     */
351
    protected function getAliasedWithoutChecks(string $id, array $arguments = []): object|null
352
    {
353
        $aliased = $this->aliases[$id] ?? null;
354
355
        if ($aliased === null) {
356
            return null;
357
        }
358
359
        return $this->get($aliased, $arguments);
360
    }
361
362
    /**
363
     * Get a service bound to a callable from the container without trying to get an alias or ensuring published.
364
     *
365
     * @param class-string            $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
366
     * @param array<array-key, mixed> $arguments [optional] The arguments
367
     *
368
     * @return object|null
369
     */
370
    protected function getCallableWithoutChecks(string $id, array $arguments = []): object|null
371
    {
372
        $closure = $this->callables[$id] ?? null;
373
374
        if ($closure === null) {
375
            return null;
376
        }
377
378
        return $closure($this, ...$arguments);
379
    }
380
381
    /**
382
     * Get a singleton from the container without trying to get an alias or ensuring published.
383
     *
384
     * @param class-string $id The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
385
     *
386
     * @return object|null
387
     */
388
    protected function getSingletonWithoutChecks(string $id): object|null
389
    {
390
        if (isset($this->instances[$id])) {
391
            return $this->instances[$id];
392
        }
393
394
        if (! isset($this->singletons[$id])) {
395
            return null;
396
        }
397
398
        $singleton = $this->getServiceWithoutChecks($id);
399
400
        return is_object($singleton) ? $this->instances[$id] = $singleton : null;
401
    }
402
403
    /**
404
     * Get a service from the container without trying to get an alias or ensuring published.
405
     *
406
     * @param class-string            $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
407
     * @param array<array-key, mixed> $arguments [optional] The arguments
408
     *
409
     * @return ServiceContract|null
410
     */
411
    protected function getServiceWithoutChecks(string $id, array $arguments = []): ServiceContract|null
412
    {
413
        if (! is_a($id, ServiceContract::class, true)) {
414
            return null;
415
        }
416
417
        $service = $this->services[$id] ?? null;
418
419
        if ($service === null) {
420
            return null;
421
        }
422
423
        // Make the object by dispatching the service
424
        return $service::make($this, $arguments);
425
    }
426
427
    /**
428
     * Fallback to the mode when a service is not found.
429
     *
430
     * @template T of object
431
     * @template Mode of InvalidReferenceMode
432
     *
433
     * @param class-string<T>         $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
434
     * @param array<array-key, mixed> $arguments [optional] The arguments
435
     * @param Mode                    $mode      [optional] The invalid reference mode
436
     *
437
     * @return (Mode is InvalidReferenceMode::NEW_INSTANCE_OR_NULL|InvalidReferenceMode::NULL ? T|null : T)
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Mode at position 1 could not be parsed: Expected ')' at position 1, but found 'Mode'.
Loading history...
438
     */
439
    protected function getFallback(
440
        string $id,
441
        array $arguments = [],
442
        InvalidReferenceMode $mode = InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION
443
    ): object|null {
444
        return match ($mode) {
445
            InvalidReferenceMode::NULL                            => null,
446
            InvalidReferenceMode::THROW_EXCEPTION                 => throw new InvalidReferenceException($id),
447
            InvalidReferenceMode::NEW_INSTANCE_OR_NULL,
448
            InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION => $this->newInstanceOrModeFallback($id, $arguments, $mode),
449
        };
450
    }
451
452
    /**
453
     * Fallback to create a new instance or return null/throw exception depending on mode.
454
     *
455
     * @template T of object
456
     * @template Mode of InvalidReferenceMode
457
     *
458
     * @param class-string<T>         $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
459
     * @param array<array-key, mixed> $arguments [optional] The arguments
460
     * @param Mode                    $mode      [optional] The invalid reference mode
461
     *
462
     * @return (Mode is InvalidReferenceMode::NEW_INSTANCE_OR_NULL|InvalidReferenceMode::NULL ? T|null : T)
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Mode at position 1 could not be parsed: Expected ')' at position 1, but found 'Mode'.
Loading history...
463
     */
464
    protected function newInstanceOrModeFallback(
465
        string $id,
466
        array $arguments = [],
467
        InvalidReferenceMode $mode = InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION
468
    ): object|null {
469
        try {
470
            if (class_exists($id)) {
471
                /** @psalm-suppress MixedMethodCall The developer should have passed the proper arguments */
472
                // Return a new object with the arguments
473
                return new $id(...$arguments);
474
            }
475
        } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
476
        }
477
478
        if ($mode === InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION) {
479
            /** @var class-string $id */
480
            throw new InvalidReferenceException($id);
481
        }
482
483
        return null;
484
    }
485
}
486