Passed
Pull Request — master (#312)
by Melech
01:21
created

Container::getAliased()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
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;
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\InvalidArgumentException;
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
31
class Container implements Contract
32
{
33
    use ProvidersAwareTrait;
34
35
    /**
36
     * The aliases.
37
     *
38
     * @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...
39
     */
40
    protected array $aliases = [];
41
42
    /**
43
     * The instances.
44
     *
45
     * @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...
46
     */
47
    protected array $instances = [];
48
49
    /**
50
     * The services.
51
     *
52
     * @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...
53
     */
54
    protected array $services = [];
55
56
    /**
57
     * The service callables.
58
     *
59
     * @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...
60
     */
61
    protected array $callables = [];
62
63
    /**
64
     * The singletons.
65
     *
66
     * @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...
67
     */
68
    protected array $singletons = [];
69
70
    public function __construct(
71
        protected Data $data = new Data(),
72
        protected InvalidReferenceMode $mode = InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION,
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
     * @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...
258
     *
259
     * @psalm-suppress InvalidReturnType
260
     * @psalm-suppress InvalidReturnStatement
261
     * @psalm-suppress ImplementedReturnTypeMismatch
262
     * @psalm-suppress MoreSpecificImplementedParamType
263
     */
264
    #[Override]
265
    public function get(string $id, array $arguments = []): object|null
266
    {
267
        $this->publishUnpublishedProvided($id);
268
269
        return $this->getSingletonWithoutChecks($id)
270
            ?? $this->getCallableWithoutChecks($id, $arguments)
271
            ?? $this->getServiceWithoutChecks($id, $arguments)
272
            ?? $this->getAliasedWithoutChecks($id, $arguments)
273
            ?? $this->getFallback($id, $arguments);
274
    }
275
276
    /**
277
     * @inheritDoc
278
     *
279
     * @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...
280
     *
281
     * @psalm-suppress InvalidReturnType
282
     * @psalm-suppress InvalidReturnStatement
283
     * @psalm-suppress ImplementedReturnTypeMismatch
284
     */
285
    #[Override]
286
    public function getAliased(string $id, array $arguments = []): object|null
287
    {
288
        // @phpstan-ignore-next-line
289
        return $this->getAliasedWithoutChecks($id, $arguments);
290
    }
291
292
    /**
293
     * @inheritDoc
294
     *
295
     * @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...
296
     *
297
     * @psalm-suppress InvalidReturnType
298
     * @psalm-suppress InvalidReturnStatement
299
     * @psalm-suppress ImplementedReturnTypeMismatch
300
     */
301
    #[Override]
302
    public function getCallable(string $id, array $arguments = []): object|null
303
    {
304
        $this->publishUnpublishedProvided($id);
305
306
        // @phpstan-ignore-next-line
307
        return $this->getCallableWithoutChecks($id, $arguments);
308
    }
309
310
    /**
311
     * @inheritDoc
312
     *
313
     * @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...
314
     *
315
     * @psalm-suppress InvalidReturnType
316
     * @psalm-suppress InvalidReturnStatement
317
     * @psalm-suppress ImplementedReturnTypeMismatch
318
     */
319
    #[Override]
320
    public function getService(string $id, array $arguments = []): ServiceContract|null
321
    {
322
        $this->publishUnpublishedProvided($id);
323
324
        /** @var class-string<ServiceContract> $id */
325
326
        return $this->getServiceWithoutChecks($id, $arguments);
327
    }
328
329
    /**
330
     * @inheritDoc
331
     *
332
     * @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...
333
     *
334
     * @psalm-suppress InvalidReturnType
335
     * @psalm-suppress InvalidReturnStatement
336
     * @psalm-suppress ImplementedReturnTypeMismatch
337
     */
338
    #[Override]
339
    public function getSingleton(string $id): object|null
340
    {
341
        $this->publishUnpublishedProvided($id);
342
343
        // @phpstan-ignore-next-line
344
        return $this->getSingletonWithoutChecks($id);
345
    }
346
347
    /**
348
     * Get an aliased service from the container without trying to ensuring published.
349
     *
350
     * @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...
351
     * @param array<array-key, mixed> $arguments [optional] The arguments
352
     *
353
     * @return object|null
354
     */
355
    protected function getAliasedWithoutChecks(string $id, array $arguments = []): object|null
356
    {
357
        $aliased = $this->aliases[$id] ?? null;
358
359
        if ($aliased === null) {
360
            return null;
361
        }
362
363
        return $this->get($aliased, $arguments);
364
    }
365
366
    /**
367
     * Get a service bound to a callable from the container without trying to get an alias or ensuring published.
368
     *
369
     * @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...
370
     * @param array<array-key, mixed> $arguments [optional] The arguments
371
     *
372
     * @return object|null
373
     */
374
    protected function getCallableWithoutChecks(string $id, array $arguments = []): object|null
375
    {
376
        $closure = $this->callables[$id] ?? null;
377
378
        if ($closure === null) {
379
            return null;
380
        }
381
382
        return $closure($this, ...$arguments);
383
    }
384
385
    /**
386
     * Get a singleton from the container without trying to get an alias or ensuring published.
387
     *
388
     * @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...
389
     *
390
     * @return object|null
391
     */
392
    protected function getSingletonWithoutChecks(string $id): object|null
393
    {
394
        if (isset($this->instances[$id])) {
395
            return $this->instances[$id];
396
        }
397
398
        if (isset($this->singletons[$id])) {
399
            /** @var class-string<ServiceContract> $id */
400
            return $this->instances[$id] = $this->getServiceWithoutChecks($id);
401
        }
402
403
        return null;
404
    }
405
406
    /**
407
     * Get a service from the container without trying to get an alias or ensuring published.
408
     *
409
     * @param class-string<ServiceContract> $id        The service id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<ServiceContract> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<ServiceContract>.
Loading history...
410
     * @param array<array-key, mixed>       $arguments [optional] The arguments
411
     *
412
     * @return ServiceContract|null
413
     */
414
    protected function getServiceWithoutChecks(string $id, array $arguments = []): ServiceContract|null
415
    {
416
        $service = $this->services[$id] ?? null;
417
418
        if ($service === null) {
419
            return null;
420
        }
421
422
        // Make the object by dispatching the service
423
        return $service::make($this, $arguments);
424
    }
425
426
    /**
427
     * Fallback to the mode when a service is not found.
428
     *
429
     * @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...
430
     * @param array<array-key, mixed> $arguments [optional] The arguments
431
     *
432
     * @return object|null
433
     */
434
    protected function getFallback(string $id, array $arguments = []): object|null
435
    {
436
        return match ($this->mode) {
437
            InvalidReferenceMode::NULL                            => null,
438
            InvalidReferenceMode::THROW_EXCEPTION                 => throw new InvalidArgumentException("Provided $id does not exist"),
439
            InvalidReferenceMode::NEW_INSTANCE_OR_NULL,
440
            InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION => $this->newInstanceOrModeFallback($id, $arguments),
441
        };
442
    }
443
444
    /**
445
     * Fallback to create a new instance or return null/throw exception depending on mode.
446
     *
447
     * @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...
448
     * @param array<array-key, mixed> $arguments [optional] The arguments
449
     *
450
     * @return object|null
451
     */
452
    protected function newInstanceOrModeFallback(string $id, array $arguments = []): object|null
453
    {
454
        try {
455
            if (class_exists($id)) {
456
                /** @psalm-suppress MixedMethodCall The developer should have passed the proper arguments */
457
                // Return a new object with the arguments
458
                // @phpstan-ignore-next-line
459
                return new $id(...$arguments);
460
            }
461
        } catch (Throwable) {
462
        }
463
464
        if ($this->mode === InvalidReferenceMode::NEW_INSTANCE_OR_THROW_EXCEPTION) {
465
            throw new InvalidArgumentException("Provided $id does not exist");
466
        }
467
468
        return null;
469
    }
470
}
471