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