Issues (3641)

src/Spryker/Service/Container/Container.php (1 issue)

1
<?php
2
3
/**
4
 * Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
5
 * Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
6
 */
7
8
namespace Spryker\Service\Container;
9
10
use ArrayAccess;
11
use SplObjectStorage;
12
use Spryker\Service\Container\Exception\AliasException;
13
use Spryker\Service\Container\Exception\ContainerException;
14
use Spryker\Service\Container\Exception\FrozenServiceException;
15
use Spryker\Service\Container\Exception\NotFoundException;
16
use Symfony\Component\OptionsResolver\Options;
17
use Symfony\Component\OptionsResolver\OptionsResolver;
18
19
/**
20
 * @implements \ArrayAccess<string, mixed>
21
 */
22
class Container implements ContainerInterface, ArrayAccess
23
{
24
    /**
25
     * @var string
26
     */
27
    public const TRIGGER_ERROR = 'container_trigger_error';
28
29
    /**
30
     * @var bool|null
31
     */
32
    protected $isTriggerErrorEnabled;
33
34
    /**
35
     * @var array
36
     */
37
    protected $services = [];
38
39
    /**
40
     * @var array<mixed>
41
     */
42
    protected $raw = [];
43
44
    /**
45
     * @var array<bool>
46
     */
47
    protected $serviceIdentifier = [];
48
49
    /**
50
     * @var array
51
     */
52
    protected static $globalServices = [];
53
54
    /**
55
     * @var array
56
     */
57
    protected static $globalServiceIdentifier = [];
58
59
    /**
60
     * @var array<bool>
61
     */
62
    protected static $globalFrozenServices = [];
63
64
    /**
65
     * @var \SplObjectStorage<object, mixed>
66
     */
67
    protected $factoryServices;
68
69
    /**
70
     * @var \SplObjectStorage<object, mixed>
71
     */
72
    protected $protectedServices;
73
74
    /**
75
     * @var array<bool>
76
     */
77
    protected $frozenServices = [];
78
79
    /**
80
     * This is a storage for services which should be extended, but at the point where extend was called the service was not found.
81
     *
82
     * @var array<array<\Closure>>
83
     */
84
    protected $toBeExtended = [];
85
86
    /**
87
     * @var string|null
88
     */
89
    protected $currentlyExtending;
90
91
    /**
92
     * @var string|null
93
     */
94
    protected $currentExtendingHash;
95
96
    /**
97
     * @var array<bool>
98
     */
99
    protected $sharedServiceHashes = [];
100
101
    /**
102
     * @var \Symfony\Component\OptionsResolver\OptionsResolver|null
103
     */
104
    protected $configurationResolver;
105
106
    /**
107
     * @var array<string>
108
     */
109
    protected static $aliases = [];
110
111
    /**
112
     * @param array $services
113
     */
114
    public function __construct(array $services = [])
115
    {
116
        if ($this->factoryServices === null) {
117
            $this->factoryServices = new SplObjectStorage();
118
        }
119
120
        if ($this->protectedServices === null) {
121
            $this->protectedServices = new SplObjectStorage();
122
        }
123
124
        foreach ($services as $key => $value) {
125
            $this->set($key, $value);
126
        }
127
    }
128
129
    /**
130
     * @param string $id
131
     * @param mixed $service
132
     *
133
     * @throws \Spryker\Service\Container\Exception\FrozenServiceException
134
     *
135
     * @return void
136
     */
137
    public function set(string $id, $service): void
138
    {
139
        if (isset($this->frozenServices[$id])) {
140
            throw new FrozenServiceException(sprintf('The service "%s" is frozen (already in use) and can not be changed at this point anymore.', $id));
141
        }
142
143
        $this->services[$id] = $service;
144
        $this->serviceIdentifier[$id] = true;
145
146
        if ($this->currentlyExtending === $id) {
147
            return;
148
        }
149
150
        $this->extendService($id, $service);
151
    }
152
153
    /**
154
     * @param string $id
155
     * @param callable|string $service
156
     *
157
     * @throws \Spryker\Service\Container\Exception\FrozenServiceException
158
     *
159
     * @return void
160
     */
161
    public function setGlobal(string $id, $service): void
162
    {
163
        if (isset(static::$globalFrozenServices[$id])) {
164
            throw new FrozenServiceException(sprintf('The global service "%s" is frozen (already in use) and can not be changed at this point anymore.', $id));
165
        }
166
167
        static::$globalServices[$id] = $service;
168
        static::$globalServiceIdentifier[$id] = true;
169
    }
170
171
    /**
172
     * @param string $id
173
     *
174
     * @return bool
175
     */
176
    public function has($id): bool
177
    {
178
        $id = $this->getServiceIdentifier($id);
179
180
        return ($this->hasService($id) || $this->hasGlobalService($id));
181
    }
182
183
    /**
184
     * @param string $id
185
     *
186
     * @return bool
187
     */
188
    protected function hasService(string $id): bool
189
    {
190
        return isset($this->serviceIdentifier[$id]);
191
    }
192
193
    /**
194
     * @param string $id
195
     *
196
     * @return bool
197
     */
198
    protected function hasGlobalService(string $id): bool
199
    {
200
        return isset(static::$globalServiceIdentifier[$id]);
201
    }
202
203
    /**
204
     * @param string $id
205
     *
206
     * @return mixed
207
     */
208
    public function get($id)
209
    {
210
        $id = $this->getServiceIdentifier($id);
211
212
        if ($this->hasGlobalService($id)) {
213
            return $this->getGlobalService($id);
214
        }
215
216
        return $this->getService($id);
217
    }
218
219
    /**
220
     * @param string $id
221
     *
222
     * @return string
223
     */
224
    protected function getServiceIdentifier(string $id): string
225
    {
226
        if (isset(static::$aliases[$id])) {
227
            return static::$aliases[$id];
228
        }
229
230
        return $id;
231
    }
232
233
    /**
234
     * @param string $id
235
     *
236
     * @return mixed
237
     */
238
    protected function getGlobalService(string $id)
239
    {
240
        if (!is_object(static::$globalServices[$id]) || !method_exists(static::$globalServices[$id], '__invoke')) {
241
            return static::$globalServices[$id];
242
        }
243
244
        $rawService = static::$globalServices[$id];
245
        $resolvedService = static::$globalServices[$id] = $rawService($this);
246
247
        static::$globalFrozenServices[$id] = true;
248
249
        return $resolvedService;
250
    }
251
252
    /**
253
     * @param string $id
254
     *
255
     * @throws \Spryker\Service\Container\Exception\NotFoundException
256
     *
257
     * @return mixed
258
     */
259
    protected function getService(string $id)
260
    {
261
        if (!$this->hasService($id)) {
262
            throw new NotFoundException(sprintf('The requested service "%s" was not found in the container!', $id));
263
        }
264
265
        if (
266
            isset($this->raw[$id])
267
            || !is_object($this->services[$id])
268
            || isset($this->protectedServices[$this->services[$id]])
269
            || !method_exists($this->services[$id], '__invoke')
270
        ) {
271
            return $this->services[$id];
272
        }
273
274
        if (isset($this->factoryServices[$this->services[$id]])) {
275
            return $this->services[$id]($this);
276
        }
277
278
        $rawService = $this->services[$id];
279
        $resolvedService = $this->services[$id] = $rawService($this);
280
        $this->raw[$id] = $rawService;
281
282
        $this->frozenServices[$id] = true;
283
284
        return $resolvedService;
285
    }
286
287
    /**
288
     * @param string $id
289
     * @param array<string, mixed> $configuration
290
     *
291
     * @throws \Spryker\Service\Container\Exception\NotFoundException
292
     *
293
     * @return void
294
     */
295
    public function configure(string $id, array $configuration): void
296
    {
297
        if (!$this->hasService($id)) {
298
            throw new NotFoundException(sprintf('Only services which are added to the container can be configured! The service "%s" was not found.', $id));
299
        }
300
301
        $configuration = $this->getConfigurationResolver()
302
            ->resolve($configuration);
303
304
        if ($configuration['isGlobal']) {
305
            $this->makeGlobal($id);
306
        }
307
308
        $this->addAliases($id, $configuration['alias']);
309
    }
310
311
    /**
312
     * @return \Symfony\Component\OptionsResolver\OptionsResolver
313
     */
314
    protected function getConfigurationResolver(): OptionsResolver
315
    {
316
        if ($this->configurationResolver === null) {
317
            $this->configurationResolver = new OptionsResolver();
318
            $this->configurationResolver
319
                ->setDefaults([
320
                    'isGlobal' => false,
321
                    'alias' => [],
322
                ])
323
                ->setAllowedTypes('isGlobal', ['bool'])
324
                ->setAllowedTypes('alias', ['array', 'string'])
325
                ->setNormalizer('alias', function (Options $options, $value) {
326
                    return (array)$value;
327
                });
328
        }
329
330
        return $this->configurationResolver;
331
    }
332
333
    /**
334
     * @param string $id
335
     *
336
     * @return void
337
     */
338
    protected function makeGlobal(string $id): void
339
    {
340
        if (!$this->hasGlobalService($id)) {
341
            $rawService = $this->services[$id];
342
            $this->setGlobal($id, $rawService);
343
        }
344
    }
345
346
    /**
347
     * @param string $id
348
     * @param array<string> $aliases
349
     *
350
     * @return void
351
     */
352
    protected function addAliases(string $id, array $aliases): void
353
    {
354
        foreach ($aliases as $alias) {
355
            $this->addAlias($id, $alias);
356
        }
357
    }
358
359
    /**
360
     * @param string $id
361
     * @param string $alias
362
     *
363
     * @throws \Spryker\Service\Container\Exception\AliasException
364
     *
365
     * @return void
366
     */
367
    protected function addAlias(string $id, string $alias): void
368
    {
369
        if (isset(static::$aliases[$alias]) && static::$aliases[$alias] !== $id) {
370
            throw new AliasException(sprintf(
371
                'The alias "%s" is already in use for the "%s" service and can\'t be reused for the service "%s".',
372
                $alias,
373
                static::$aliases[$alias],
374
                $id,
375
            ));
376
        }
377
378
        static::$aliases[$alias] = $id;
379
    }
380
381
    /**
382
     * Do not set the returned callable to the Container, this is done automatically.
383
     *
384
     * @param string $id
385
     * @param \Closure $service
386
     *
387
     * @throws \Spryker\Service\Container\Exception\ContainerException
388
     * @throws \Spryker\Service\Container\Exception\FrozenServiceException
389
     *
390
     * @return \Closure
391
     */
392
    public function extend(string $id, $service)
393
    {
394
        if ($this->hasGlobalService($id)) {
395
            return $this->extendGlobalService($id, $service);
396
        }
397
398
        if (!$this->hasService($id)) {
399
            // For BC reasons we will not throw exception here until everything is migrated.
400
            // We store the extension until the service is set and do the extension than.
401
            $this->extendLater($id, $service);
402
403
            $this->currentExtendingHash = spl_object_hash($service);
404
405
            return $service;
406
        }
407
408
        if (!is_object($service) || !method_exists($service, '__invoke')) {
409
            throw new ContainerException('The passed service for extension is not a closure and is not invokable.');
410
        }
411
412
        if (isset($this->frozenServices[$id])) {
413
            throw new FrozenServiceException(sprintf('The service "%s" is marked as frozen an can\'t be extended at this point.', $id));
414
        }
415
416
        if (!is_object($this->services[$id]) || !method_exists($this->services[$id], '__invoke')) {
417
            throw new ContainerException(sprintf('The requested service "%s" is not an object and is not invokable.', $id));
418
        }
419
420
        if (isset($this->protectedServices[$this->services[$id]])) {
421
            throw new ContainerException(sprintf('The requested service "%s" is protected and can\'t be extended.', $id));
422
        }
423
424
        $factory = $this->services[$id];
425
426
        $extended = function ($container) use ($service, $factory) {
427
            return $service($factory($container), $container);
428
        };
429
430
        if (isset($this->factoryServices[$factory])) {
431
            $this->factoryServices->detach($factory);
432
            $this->factoryServices->attach($extended);
433
        }
434
435
        $this->set($id, $extended);
436
437
        return $extended;
438
    }
439
440
    /**
441
     * Do not set the returned callable to the Container, this is done automatically.
442
     *
443
     * @param string $id
444
     * @param \Closure $service
445
     *
446
     * @throws \Spryker\Service\Container\Exception\ContainerException
447
     * @throws \Spryker\Service\Container\Exception\FrozenServiceException
448
     *
449
     * @return \Closure
450
     */
451
    protected function extendGlobalService(string $id, $service)
452
    {
453
        if (!is_object($service) || !method_exists($service, '__invoke')) {
454
            throw new ContainerException('The passed service for extension is not a closure and is not invokable.');
455
        }
456
457
        if (isset(static::$globalFrozenServices[$id])) {
458
            throw new FrozenServiceException(sprintf('The global service "%s" is marked as frozen an can\'t be extended at this point.', $id));
459
        }
460
461
        if (!is_object(static::$globalServices[$id]) || !method_exists(static::$globalServices[$id], '__invoke')) {
462
            throw new ContainerException(sprintf('The requested service "%s" is not an object and is not invokable.', $id));
463
        }
464
465
        $factory = static::$globalServices[$id];
466
467
        $extended = function ($container) use ($service, $factory) {
468
            return $service($factory($container), $container);
469
        };
470
471
        $this->setGlobal($id, $extended);
472
473
        return $extended;
474
    }
475
476
    /**
477
     * @param string $id
478
     *
479
     * @return void
480
     */
481
    public function remove(string $id): void
482
    {
483
        $this->removeAliases($id);
484
        $this->removeService($id);
485
        $this->removeGlobalService($id);
486
    }
487
488
    /**
489
     * @param string $id
490
     *
491
     * @return void
492
     */
493
    protected function removeAliases(string $id): void
494
    {
495
        foreach ($this->getAliasesForService($id) as $alias) {
496
            unset(static::$aliases[$alias]);
497
        }
498
499
        unset(static::$aliases[$id]);
500
    }
501
502
    /**
503
     * @param string $id
504
     *
505
     * @return array
506
     */
507
    protected function getAliasesForService(string $id): array
508
    {
509
        return array_keys(static::$aliases, $id);
510
    }
511
512
    /**
513
     * @param string $id
514
     *
515
     * @return void
516
     */
517
    protected function removeService(string $id): void
518
    {
519
        if ($this->hasService($id)) {
520
            unset(
521
                $this->services[$id],
522
                $this->frozenServices[$id],
523
                $this->serviceIdentifier[$id],
524
            );
525
        }
526
    }
527
528
    /**
529
     * @param string $id
530
     *
531
     * @return void
532
     */
533
    protected function removeGlobalService(string $id): void
534
    {
535
        if ($this->hasGlobalService($id)) {
536
            unset(
537
                static::$globalServices[$id],
538
                static::$globalFrozenServices[$id],
539
                static::$globalServiceIdentifier[$id],
540
            );
541
        }
542
    }
543
544
    /**
545
     * @deprecated Do not use this method anymore. All services are shared by default now.
546
     *
547
     * @param \Closure|mixed|object $service
548
     *
549
     * @return \Closure|mixed|object
550
     */
551
    public function share($service)
552
    {
553
        if (method_exists($service, '__invoke')) {
554
            $serviceHash = spl_object_hash($service);
555
556
            $this->sharedServiceHashes[$serviceHash] = true;
557
        }
558
559
        return $service;
560
    }
561
562
    /**
563
     * @param \Closure|object $service
564
     *
565
     * @throws \Spryker\Service\Container\Exception\ContainerException
566
     *
567
     * @return \Closure|object
568
     */
569
    public function protect($service)
570
    {
571
        if (!method_exists($service, '__invoke')) {
572
            throw new ContainerException('The passed service is not a closure and is not invokable.');
573
        }
574
575
        $this->protectedServices->attach($service);
576
577
        return $service;
578
    }
579
580
    /**
581
     * @param \Closure|object $service
582
     *
583
     * @throws \Spryker\Service\Container\Exception\ContainerException
584
     *
585
     * @return \Closure|object
586
     */
587
    public function factory($service)
588
    {
589
        if (!method_exists($service, '__invoke')) {
590
            throw new ContainerException('The passed service is not a closure and is not invokable.');
591
        }
592
593
        $this->factoryServices->attach($service);
594
595
        return $service;
596
    }
597
598
    /**
599
     * @deprecated Use {@link \Spryker\Service\Container\ContainerInterface::has()} instead.
600
     *
601
     * @param mixed $offset
602
     *
603
     * @return bool
604
     */
605
    public function offsetExists($offset): bool
606
    {
607
        $this->triggerError(sprintf('ArrayAccess for the container in Spryker (e.g. `isset($container[\'%s\'])`) is no longer supported! Please use `ContainerInterface:has()` instead.', $offset));
608
609
        return $this->has($offset);
610
    }
611
612
    /**
613
     * @deprecated Use {@link \Spryker\Service\Container\ContainerInterface::get()} instead.
614
     *
615
     * @param mixed $offset
616
     *
617
     * @return mixed
618
     */
619
    #[\ReturnTypeWillChange]
620
    public function offsetGet($offset)
621
    {
622
        $this->triggerError(sprintf('ArrayAccess for the container in Spryker (e.g. `$foo = $container[\'%s\']`) is no longer supported! Please use `ContainerInterface:get()` instead.', $offset));
623
624
        return $this->get($offset);
625
    }
626
627
    /**
628
     * @deprecated Use {@link \Spryker\Service\Container\ContainerInterface::set()} instead.
629
     *
630
     * @param mixed $offset
631
     * @param mixed $value
632
     *
633
     * @return void
634
     */
635
    public function offsetSet($offset, $value): void
636
    {
637
        $this->triggerError(sprintf('ArrayAccess for the container in Spryker (e.g. `$container[\'%s\'] = $foo`) is no longer supported! Please use `ContainerInterface:set()` instead.', $offset));
638
639
        // When extend is called for a service which is not registered so far, we store the extension and wait for the service to be added.
640
        // For BC reasons code like `$container['service'] = $container->extend('service', callable)` is valid and still needs to be supported
641
        // and we need to make sure that the returned to be extended service is added now.
642
        if (($this->currentExtendingHash !== null && is_object($value)) && spl_object_hash($value) === $this->currentExtendingHash) {
643
            $this->currentExtendingHash = null;
644
645
            return;
646
        }
647
648
        if ($value && (is_string($value) || is_object($value)) && method_exists($value, '__invoke') && !isset($this->sharedServiceHashes[spl_object_hash($value)])) {
649
            $value = $this->factory($value);
650
        }
651
652
        $this->set($offset, $value);
653
    }
654
655
    /**
656
     * @deprecated Use {@link \Spryker\Service\Container\ContainerInterface::remove()} instead.
657
     *
658
     * @param mixed $offset
659
     *
660
     * @return void
661
     */
662
    public function offsetUnset($offset): void
663
    {
664
        $this->triggerError(sprintf('ArrayAccess for the container in Spryker (e.g. `unset($container[\'%s\'])`) is no longer supported! Please use `ContainerInterface:remove()` instead.', $offset));
665
666
        $this->remove($offset);
667
    }
668
669
    /**
670
     * @param string $message
671
     *
672
     * @return void
673
     */
674
    protected function triggerError(string $message): void
675
    {
676
        if ($this->isTriggerErrorEnabled()) {
677
            // phpcs:ignore
678
            @trigger_error($message, E_USER_DEPRECATED);
679
        }
680
    }
681
682
    /**
683
     * @return bool
684
     */
685
    protected function isTriggerErrorEnabled(): bool
686
    {
687
        if ($this->isTriggerErrorEnabled === null) {
688
            $this->isTriggerErrorEnabled = ($this->has(static::TRIGGER_ERROR)
689
                ? $this->get(static::TRIGGER_ERROR)
690
                : false);
691
        }
692
693
        return $this->isTriggerErrorEnabled;
694
    }
695
696
    /**
697
     * This method (currently) exists only for BC reasons.
698
     *
699
     * @param string $id
700
     * @param \Closure $service
701
     *
702
     * @return void
703
     */
704
    protected function extendLater(string $id, $service): void
705
    {
706
        if (!isset($this->toBeExtended[$id])) {
707
            $this->toBeExtended[$id] = [];
708
        }
709
710
        $this->toBeExtended[$id][] = $service;
711
    }
712
713
    /**
714
     * @param string $id
715
     * @param \Closure $service
716
     *
717
     * @return void
718
     */
719
    protected function extendService(string $id, $service): void
720
    {
721
        if (isset($this->toBeExtended[$id])) {
722
            $this->currentlyExtending = $id;
723
724
            foreach ($this->toBeExtended[$id] as $service) {
0 ignored issues
show
$service is overwriting one of the parameters of this function.
Loading history...
725
                $service = $this->extend($id, $service);
726
            }
727
728
            unset($this->toBeExtended[$id]);
729
            $this->currentlyExtending = null;
730
731
            $this->set($id, $service);
732
        }
733
    }
734
}
735