1: <?php declare(strict_types=1);
2:
3: namespace Salient\Container;
4:
5: use Dice\Dice;
6: use Dice\DiceException;
7: use Psr\Container\ContainerInterface as PsrContainerInterface;
8: use Psr\Log\LoggerInterface;
9: use Salient\Cache\CacheStore;
10: use Salient\Console\Console;
11: use Salient\Console\ConsoleLogger;
12: use Salient\Container\Event\BeforeGlobalContainerSetEvent;
13: use Salient\Container\Exception\ArgumentsNotUsedException;
14: use Salient\Container\Exception\InvalidServiceException;
15: use Salient\Container\Exception\ServiceNotFoundException;
16: use Salient\Contract\Cache\CacheInterface;
17: use Salient\Contract\Console\ConsoleInterface;
18: use Salient\Contract\Container\ContainerAwareInterface;
19: use Salient\Contract\Container\ContainerInterface;
20: use Salient\Contract\Container\HasBindings;
21: use Salient\Contract\Container\HasContextualBindings;
22: use Salient\Contract\Container\HasServices;
23: use Salient\Contract\Container\ServiceAwareInterface;
24: use Salient\Contract\Container\ServiceLifetime;
25: use Salient\Contract\Container\SingletonInterface;
26: use Salient\Contract\Core\Event\EventDispatcherInterface;
27: use Salient\Contract\Core\Facade\FacadeAwareInterface;
28: use Salient\Contract\Sync\SyncStoreInterface;
29: use Salient\Core\Concern\ChainableTrait;
30: use Salient\Core\Concern\FacadeAwareTrait;
31: use Salient\Core\Event\EventDispatcher;
32: use Salient\Core\Facade\Event;
33: use Salient\Sync\SyncStore;
34: use Closure;
35: use InvalidArgumentException;
36: use LogicException;
37: use ReflectionClass;
38: use ReflectionNamedType;
39: use ReflectionParameter;
40:
41: /**
42: * A service container with contextual bindings
43: *
44: * @implements FacadeAwareInterface<ContainerInterface>
45: */
46: class Container implements ContainerInterface, FacadeAwareInterface
47: {
48: /** @use FacadeAwareTrait<ContainerInterface> */
49: use FacadeAwareTrait;
50: use ChainableTrait;
51:
52: private const SERVICE_PROVIDER_INTERFACES = [
53: ContainerAwareInterface::class,
54: ServiceAwareInterface::class,
55: SingletonInterface::class,
56: HasServices::class,
57: HasBindings::class,
58: HasContextualBindings::class,
59: ];
60:
61: private const DEFAULT_SERVICES = [
62: CacheInterface::class => [CacheStore::class, ServiceLifetime::SINGLETON],
63: EventDispatcherInterface::class => [EventDispatcher::class, ServiceLifetime::SINGLETON],
64: ConsoleInterface::class => [Console::class, ServiceLifetime::SINGLETON],
65: LoggerInterface::class => [ConsoleLogger::class, ServiceLifetime::INHERIT],
66: SyncStoreInterface::class => [SyncStore::class, ServiceLifetime::SINGLETON],
67: ];
68:
69: private static ?ContainerInterface $GlobalContainer = null;
70: private Dice $Dice;
71: /** @var array<class-string,true> */
72: private array $Providers = [];
73: /** @var array<class-string,class-string> */
74: private array $GetAsServiceMap = [];
75:
76: /**
77: * @inheritDoc
78: */
79: public function __construct()
80: {
81: $this->Dice = new Dice();
82: $this->bindContainer();
83: }
84:
85: /**
86: * @inheritDoc
87: */
88: public function unload(): void
89: {
90: if (self::$GlobalContainer === $this) {
91: self::setGlobalContainer(null);
92: }
93:
94: $this->unloadFacades();
95:
96: $this->Dice = new Dice();
97: $this->bindContainer();
98: }
99:
100: private function bindContainer(): void
101: {
102: $class = new ReflectionClass(static::class);
103:
104: // Bind interfaces that extend Psr\Container\ContainerInterface
105: /** @var class-string $name */
106: foreach ($class->getInterfaces() as $name => $interface) {
107: if ($interface->implementsInterface(PsrContainerInterface::class)) {
108: $this->instance($name, $this);
109: }
110: }
111:
112: // Also bind classes between self and static
113: do {
114: $this->instance($class->getName(), $this);
115: } while (
116: $class->isSubclassOf(self::class)
117: && ($class = $class->getParentClass())
118: );
119:
120: $this->Dice = $this->Dice->addCallback(
121: '*',
122: Closure::fromCallable([$this, 'callback']),
123: __METHOD__,
124: );
125: }
126:
127: /**
128: * @param class-string $name
129: */
130: private function callback(object $instance, string $name): object
131: {
132: if ($instance instanceof ContainerAwareInterface) {
133: $instance->setContainer($this);
134: }
135:
136: if ($instance instanceof ServiceAwareInterface) {
137: $instance->setService($this->GetAsServiceMap[$name] ?? $name);
138: }
139:
140: return $instance;
141: }
142:
143: /**
144: * @inheritDoc
145: */
146: final public static function hasGlobalContainer(): bool
147: {
148: return self::$GlobalContainer !== null;
149: }
150:
151: /**
152: * @inheritDoc
153: */
154: final public static function getGlobalContainer(): ContainerInterface
155: {
156: if (self::$GlobalContainer === null) {
157: $container = new static();
158: self::setGlobalContainer($container);
159: return $container;
160: }
161:
162: return self::$GlobalContainer;
163: }
164:
165: /**
166: * @inheritDoc
167: */
168: final public static function setGlobalContainer(?ContainerInterface $container): void
169: {
170: if (self::$GlobalContainer === $container) {
171: return;
172: }
173:
174: Event::dispatch(new BeforeGlobalContainerSetEvent($container));
175:
176: self::$GlobalContainer = $container;
177: }
178:
179: /**
180: * @inheritDoc
181: */
182: final public function get(string $id, array $args = []): object
183: {
184: return $this->_get($id, $id, $args);
185: }
186:
187: /**
188: * @inheritDoc
189: */
190: final public function getAs(string $id, string $service, array $args = []): object
191: {
192: return $this->_get($id, $service, $args);
193: }
194:
195: /**
196: * @template T
197: * @template TService
198: *
199: * @param class-string<T> $id
200: * @param class-string<TService> $service
201: * @param mixed[] $args
202: * @return T&TService&object
203: */
204: private function _get(string $id, string $service, array $args): object
205: {
206: $hasInstance = $this->Dice->hasShared($id);
207: if ($hasInstance && $args) {
208: throw new ArgumentsNotUsedException(sprintf(
209: 'Cannot apply arguments to shared instance: %s',
210: $id,
211: ));
212: }
213:
214: if ($hasInstance) {
215: $instance = $this->Dice->create($id);
216:
217: if ($instance instanceof ServiceAwareInterface) {
218: $instance->setService($service);
219: }
220:
221: /** @var T&TService&object */
222: return $instance;
223: }
224:
225: if ($service !== $id) {
226: $this->GetAsServiceMap[$id] = $service;
227: }
228:
229: if (isset(self::DEFAULT_SERVICES[$id]) && !$this->Dice->hasRule($id)) {
230: $this->bindDefaultService($id);
231: }
232:
233: try {
234: do {
235: try {
236: /** @var T&TService&object */
237: return $this->Dice->create($id, $args);
238: } catch (DiceException $ex) {
239: /** @var class-string|null */
240: $failed = $ex->getClass();
241: if (
242: $failed !== null
243: && isset(self::DEFAULT_SERVICES[$failed])
244: && !$this->has($failed)
245: ) {
246: $this->bindDefaultService($failed);
247: continue;
248: }
249: throw new ServiceNotFoundException($ex->getMessage(), $ex);
250: }
251: } while (true);
252: } finally {
253: if ($service !== $id) {
254: unset($this->GetAsServiceMap[$id]);
255: }
256: }
257: }
258:
259: /**
260: * @param class-string $id
261: */
262: private function bindDefaultService(string $id): void
263: {
264: $defaultService = self::DEFAULT_SERVICES[$id];
265: /** @var class-string */
266: $class = $defaultService[0];
267: /** @var ServiceLifetime::* */
268: $lifetime = $defaultService[1];
269: if (
270: $lifetime === ServiceLifetime::SINGLETON || (
271: $lifetime === ServiceLifetime::INHERIT
272: && is_a($class, SingletonInterface::class, true)
273: )
274: ) {
275: $this->singleton($id, $class);
276: } else {
277: $this->bind($id, $class);
278: }
279: }
280:
281: /**
282: * @inheritDoc
283: */
284: final public function getName(string $id): string
285: {
286: return $this->Dice->getRule($id)['instanceOf'] ?? $id;
287: }
288:
289: /**
290: * @inheritDoc
291: */
292: final public function has(string $id): bool
293: {
294: return $this->Dice->hasRule($id) || $this->Dice->hasShared($id);
295: }
296:
297: /**
298: * @inheritDoc
299: */
300: final public function hasSingleton(string $id): bool
301: {
302: return $this->Dice->hasShared($id) || (
303: $this->Dice->hasRule($id)
304: && ($this->Dice->getRule($id)['shared'] ?? false)
305: );
306: }
307:
308: /**
309: * @inheritDoc
310: */
311: final public function hasInstance(string $id): bool
312: {
313: return $this->Dice->hasShared($id);
314: }
315:
316: /**
317: * @param array<string,mixed> $rule
318: */
319: private function addRule(string $id, array $rule): void
320: {
321: $this->Dice = $this->Dice->addRule($id, $rule);
322: }
323:
324: /**
325: * @return static
326: */
327: final public function inContextOf(string $id): ContainerInterface
328: {
329: $clone = clone $this;
330:
331: // If not already registered, register $id as a service provider without
332: // binding services that may be bound to other providers
333: if (!isset($this->Providers[$id])) {
334: $clone->applyService($id, []);
335: $clone->Providers[$id] = true;
336:
337: // If nothing changed, skip `applyService()` in future
338: if (!$this->compareBindingsWith($clone)) {
339: $this->Providers[$id] = true;
340: }
341: }
342:
343: if (!$clone->Dice->hasRule($id)) {
344: return $this;
345: }
346:
347: $subs = $clone->Dice->getRule($id)['substitutions'] ?? null;
348: if (!$subs) {
349: return $this;
350: }
351:
352: $clone->applyBindings($subs);
353:
354: if (!$this->compareBindingsWith($clone)) {
355: return $this;
356: }
357:
358: $clone->bindContainer();
359:
360: return $clone;
361: }
362:
363: /**
364: * @param array<class-string,string|object|array<string,mixed>> $subs
365: */
366: private function applyBindings(array $subs): void
367: {
368: foreach ($subs as $key => $value) {
369: if (is_string($value)) {
370: if (strcasecmp($this->Dice->getRule($key)['instanceOf'] ?? '', $value)) {
371: $this->addRule($key, ['instanceOf' => $value]);
372: }
373: continue;
374: }
375: if (is_object($value)) {
376: if (!$this->Dice->hasShared($key) || $this->get($key) !== $value) {
377: $this->Dice = $this->Dice->addShared($key, $value);
378: }
379: continue;
380: }
381: $rule = $this->Dice->getDefaultRule();
382: // If this substitution can't be converted to a standalone rule,
383: // apply it via the default rule
384: if (($rule['substitutions'][ltrim($key, '\\')] ?? null) !== $value) {
385: $this->Dice = $this->Dice->addSubstitution($key, $value);
386: }
387: }
388: }
389:
390: /**
391: * @template TService
392: * @template T of TService
393: *
394: * @param class-string<TService> $id
395: * @param class-string<T>|null $class
396: * @param mixed[] $args
397: * @param array<string,mixed> $rule
398: * @return $this
399: */
400: private function _bind(
401: string $id,
402: ?string $class,
403: array $args,
404: array $rule = []
405: ): ContainerInterface {
406: if ($class !== null) {
407: $rule['instanceOf'] = $class;
408: }
409:
410: if ($args) {
411: $rule['constructParams'] = $args;
412: }
413:
414: $this->addRule($id, $rule);
415:
416: return $this;
417: }
418:
419: /**
420: * @return $this
421: */
422: final public function bind(
423: string $id,
424: ?string $class = null,
425: array $args = []
426: ): ContainerInterface {
427: return $this->_bind($id, $class, $args);
428: }
429:
430: /**
431: * @return $this
432: */
433: final public function bindIf(
434: string $id,
435: ?string $class = null,
436: array $args = []
437: ): ContainerInterface {
438: if ($this->has($id)) {
439: return $this;
440: }
441:
442: return $this->_bind($id, $class, $args);
443: }
444:
445: /**
446: * @return $this
447: */
448: final public function singleton(
449: string $id,
450: ?string $class = null,
451: array $args = []
452: ): ContainerInterface {
453: return $this->_bind($id, $class, $args, ['shared' => true]);
454: }
455:
456: /**
457: * @return $this
458: */
459: final public function singletonIf(
460: string $id,
461: ?string $class = null,
462: array $args = []
463: ): ContainerInterface {
464: if ($this->has($id)) {
465: return $this;
466: }
467:
468: return $this->_bind($id, $class, $args, ['shared' => true]);
469: }
470:
471: /**
472: * @inheritDoc
473: */
474: final public function hasProvider(string $id): bool
475: {
476: return isset($this->Providers[$id]);
477: }
478:
479: /**
480: * @return $this
481: */
482: final public function provider(
483: string $id,
484: ?array $services = null,
485: array $exceptServices = [],
486: int $lifetime = ServiceLifetime::INHERIT
487: ): ContainerInterface {
488: $this->applyService($id, $services, $exceptServices, $lifetime);
489: $this->Providers[$id] = true;
490: return $this;
491: }
492:
493: /**
494: * @param class-string $id
495: * @param class-string[]|null $services
496: * @param class-string[] $exceptServices
497: * @param ServiceLifetime::* $lifetime
498: */
499: private function applyService(
500: string $id,
501: ?array $services = null,
502: array $exceptServices = [],
503: int $lifetime = ServiceLifetime::INHERIT
504: ): void {
505: if ($lifetime === ServiceLifetime::INHERIT) {
506: $lifetime = is_a($id, SingletonInterface::class, true)
507: ? ServiceLifetime::SINGLETON
508: : ServiceLifetime::TRANSIENT;
509: }
510:
511: $rule = [];
512: if ($lifetime === ServiceLifetime::SINGLETON) {
513: $rule['shared'] = true;
514: }
515:
516: if (
517: is_a($id, HasContextualBindings::class, true)
518: && ($bindings = $id::getContextualBindings())
519: ) {
520: $rule['substitutions'] = $bindings;
521: }
522:
523: if ($rule) {
524: $this->addRule($id, $rule);
525: }
526:
527: if (is_a($id, HasBindings::class, true)) {
528: $bindings = $id::getBindings();
529: foreach ($bindings as $service => $class) {
530: $this->bind($service, $class);
531: }
532:
533: $singletons = $id::getSingletons();
534: foreach ($singletons as $service => $class) {
535: if (is_int($service)) {
536: $service = $class;
537: }
538: $this->singleton($service, $class);
539: }
540: }
541:
542: if (is_a($id, HasServices::class, true)) {
543: $bind = $id::getServices();
544: } else {
545: $bind = array_diff(
546: (new ReflectionClass($id))->getInterfaceNames(),
547: self::SERVICE_PROVIDER_INTERFACES,
548: );
549: }
550:
551: if ($services !== null) {
552: $services = array_unique($services);
553: $bind = array_intersect($bind, $services);
554: if (count($bind) < count($services)) {
555: // @codeCoverageIgnoreStart
556: throw new InvalidServiceException(sprintf(
557: '%s does not implement: %s',
558: $id,
559: implode(', ', array_diff($services, $bind)),
560: ));
561: // @codeCoverageIgnoreEnd
562: }
563: }
564:
565: if ($exceptServices) {
566: $bind = array_diff($bind, $exceptServices);
567: }
568:
569: if (!$bind) {
570: return;
571: }
572:
573: $rule = [
574: 'instanceOf' => $id
575: ];
576: foreach ($bind as $service) {
577: $this->addRule($service, $rule);
578: }
579: }
580:
581: /**
582: * @return $this
583: */
584: final public function addContextualBinding($context, string $dependency, $value): ContainerInterface
585: {
586: if (is_array($context)) {
587: foreach ($context as $_context) {
588: $this->addContextualBinding($_context, $dependency, $value);
589: }
590: return $this;
591: }
592:
593: if ($dependency === '') {
594: // @codeCoverageIgnoreStart
595: throw new InvalidArgumentException('Argument #2 ($dependency) must be a non-empty string');
596: // @codeCoverageIgnoreEnd
597: }
598:
599: $rule = $this->Dice->hasRule($context)
600: ? $this->Dice->getRule($context)
601: : [];
602:
603: if (is_callable($value)) {
604: $value = [Dice::INSTANCE => fn() => $value($this)];
605: }
606:
607: if (
608: $dependency[0] === '$' && (
609: !($type = (new ReflectionParameter([$context, '__construct'], substr($dependency, 1)))->getType())
610: || !$type instanceof ReflectionNamedType
611: || $type->isBuiltin()
612: )
613: ) {
614: $rule['constructParams'][] = $value;
615: } else {
616: $rule['substitutions'][$dependency] = $value;
617: }
618:
619: $this->addRule($context, $rule);
620:
621: return $this;
622: }
623:
624: /**
625: * @return $this
626: */
627: final public function instance(string $id, $instance): ContainerInterface
628: {
629: $this->Dice = $this->Dice->addShared($id, $instance);
630:
631: return $this;
632: }
633:
634: /**
635: * @return $this
636: */
637: final public function providers(
638: array $serviceMap,
639: int $lifetime = ServiceLifetime::INHERIT
640: ): ContainerInterface {
641: $idMap = [];
642: foreach ($serviceMap as $id => $class) {
643: if (!class_exists($class)) {
644: throw new LogicException(sprintf(
645: 'Not a class: %s',
646: $class,
647: ));
648: }
649: if (!is_a($class, $id, true)) {
650: throw new LogicException(sprintf(
651: '%s does not inherit %s',
652: $class,
653: $id,
654: ));
655: }
656: if (is_a($id, $class, true)) {
657: // Don't add classes mapped to themselves to their service list
658: $idMap[$class] ??= [];
659: continue;
660: }
661: $idMap[$class][] = $id;
662: }
663:
664: foreach ($idMap as $class => $services) {
665: $this->provider($class, $services, [], $lifetime);
666: }
667:
668: return $this;
669: }
670:
671: /**
672: * @inheritDoc
673: */
674: final public function getProviders(): array
675: {
676: return array_keys($this->Providers);
677: }
678:
679: /**
680: * @return $this
681: */
682: final public function unbind(string $id): ContainerInterface
683: {
684: $this->Dice = $this->Dice->removeRule($id);
685:
686: return $this;
687: }
688:
689: /**
690: * @return $this
691: */
692: final public function removeInstance(string $id): ContainerInterface
693: {
694: if (!$this->Dice->hasShared($id)) {
695: return $this;
696: }
697:
698: if ($this->Dice->hasRule($id)) {
699: // Reapplying the rule removes the instance
700: $this->Dice = $this->Dice->addRule($id, $this->Dice->getRule($id));
701: return $this;
702: }
703:
704: $this->Dice = $this->Dice->removeRule($id);
705: return $this;
706: }
707:
708: /**
709: * 0 if another container has the same bindings, otherwise 1
710: *
711: * @param static $container
712: */
713: private function compareBindingsWith($container): int
714: {
715: return $this->Dice === $container->Dice ? 0 : 1;
716: }
717: }
718: