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