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: /** @var array{class-string,ServiceLifetime::*} */
263: $defaultService = self::DEFAULT_SERVICES[$id];
264: [$class, $lifetime] = $defaultService;
265: if (
266: $lifetime === ServiceLifetime::SINGLETON || (
267: $lifetime === ServiceLifetime::INHERIT
268: && is_a($class, SingletonInterface::class, true)
269: )
270: ) {
271: $this->singleton($id, $class);
272: } else {
273: $this->bind($id, $class);
274: }
275: }
276:
277: /**
278: * @inheritDoc
279: */
280: final public function getName(string $id): string
281: {
282: return $this->Dice->getRule($id)['instanceOf'] ?? $id;
283: }
284:
285: /**
286: * @inheritDoc
287: */
288: final public function has(string $id): bool
289: {
290: return $this->Dice->hasRule($id) || $this->Dice->hasShared($id);
291: }
292:
293: /**
294: * @inheritDoc
295: */
296: final public function hasSingleton(string $id): bool
297: {
298: return $this->Dice->hasShared($id) || (
299: $this->Dice->hasRule($id)
300: && ($this->Dice->getRule($id)['shared'] ?? false)
301: );
302: }
303:
304: /**
305: * @inheritDoc
306: */
307: final public function hasInstance(string $id): bool
308: {
309: return $this->Dice->hasShared($id);
310: }
311:
312: /**
313: * @param array<string,mixed> $rule
314: */
315: private function addRule(string $id, array $rule): void
316: {
317: $this->Dice = $this->Dice->addRule($id, $rule);
318: }
319:
320: /**
321: * @return static
322: */
323: final public function inContextOf(string $id): ContainerInterface
324: {
325: $clone = clone $this;
326:
327: // If not already registered, register $id as a service provider without
328: // binding services that may be bound to other providers
329: if (!isset($this->Providers[$id])) {
330: $clone->applyService($id, []);
331: $clone->Providers[$id] = true;
332:
333: // If nothing changed, skip `applyService()` in future
334: if (!$this->compareBindingsWith($clone)) {
335: $this->Providers[$id] = true;
336: }
337: }
338:
339: if (!$clone->Dice->hasRule($id)) {
340: return $this;
341: }
342:
343: $subs = $clone->Dice->getRule($id)['substitutions'] ?? null;
344: if (!$subs) {
345: return $this;
346: }
347:
348: $clone->applyBindings($subs);
349:
350: if (!$this->compareBindingsWith($clone)) {
351: return $this;
352: }
353:
354: $clone->bindContainer();
355:
356: return $clone;
357: }
358:
359: /**
360: * @param array<class-string,string|object|array<string,mixed>> $subs
361: */
362: private function applyBindings(array $subs): void
363: {
364: foreach ($subs as $key => $value) {
365: if (is_string($value)) {
366: if (strcasecmp($this->Dice->getRule($key)['instanceOf'] ?? '', $value)) {
367: $this->addRule($key, ['instanceOf' => $value]);
368: }
369: continue;
370: }
371: if (is_object($value)) {
372: if (!$this->Dice->hasShared($key) || $this->get($key) !== $value) {
373: $this->Dice = $this->Dice->addShared($key, $value);
374: }
375: continue;
376: }
377: $rule = $this->Dice->getDefaultRule();
378: // If this substitution can't be converted to a standalone rule,
379: // apply it via the default rule
380: if (($rule['substitutions'][ltrim($key, '\\')] ?? null) !== $value) {
381: $this->Dice = $this->Dice->addSubstitution($key, $value);
382: }
383: }
384: }
385:
386: /**
387: * @template TService
388: * @template T of TService
389: *
390: * @param class-string<TService> $id
391: * @param class-string<T>|null $class
392: * @param mixed[] $args
393: * @param array<string,mixed> $rule
394: * @return $this
395: */
396: private function _bind(
397: string $id,
398: ?string $class,
399: array $args,
400: array $rule = []
401: ): ContainerInterface {
402: if ($class !== null) {
403: $rule['instanceOf'] = $class;
404: }
405:
406: if ($args) {
407: $rule['constructParams'] = $args;
408: }
409:
410: $this->addRule($id, $rule);
411:
412: return $this;
413: }
414:
415: /**
416: * @return $this
417: */
418: final public function bind(
419: string $id,
420: ?string $class = null,
421: array $args = []
422: ): ContainerInterface {
423: return $this->_bind($id, $class, $args);
424: }
425:
426: /**
427: * @return $this
428: */
429: final public function bindIf(
430: string $id,
431: ?string $class = null,
432: array $args = []
433: ): ContainerInterface {
434: if ($this->has($id)) {
435: return $this;
436: }
437:
438: return $this->_bind($id, $class, $args);
439: }
440:
441: /**
442: * @return $this
443: */
444: final public function singleton(
445: string $id,
446: ?string $class = null,
447: array $args = []
448: ): ContainerInterface {
449: return $this->_bind($id, $class, $args, ['shared' => true]);
450: }
451:
452: /**
453: * @return $this
454: */
455: final public function singletonIf(
456: string $id,
457: ?string $class = null,
458: array $args = []
459: ): ContainerInterface {
460: if ($this->has($id)) {
461: return $this;
462: }
463:
464: return $this->_bind($id, $class, $args, ['shared' => true]);
465: }
466:
467: /**
468: * @inheritDoc
469: */
470: final public function hasProvider(string $id): bool
471: {
472: return isset($this->Providers[$id]);
473: }
474:
475: /**
476: * @return $this
477: */
478: final public function provider(
479: string $id,
480: ?array $services = null,
481: array $exceptServices = [],
482: int $lifetime = ServiceLifetime::INHERIT
483: ): ContainerInterface {
484: $this->applyService($id, $services, $exceptServices, $lifetime);
485: $this->Providers[$id] = true;
486: return $this;
487: }
488:
489: /**
490: * @param class-string $id
491: * @param class-string[]|null $services
492: * @param class-string[] $exceptServices
493: * @param ServiceLifetime::* $lifetime
494: */
495: private function applyService(
496: string $id,
497: ?array $services = null,
498: array $exceptServices = [],
499: int $lifetime = ServiceLifetime::INHERIT
500: ): void {
501: if ($lifetime === ServiceLifetime::INHERIT) {
502: $lifetime = is_a($id, SingletonInterface::class, true)
503: ? ServiceLifetime::SINGLETON
504: : ServiceLifetime::TRANSIENT;
505: }
506:
507: $rule = [];
508: if ($lifetime === ServiceLifetime::SINGLETON) {
509: $rule['shared'] = true;
510: }
511:
512: if (
513: is_a($id, HasContextualBindings::class, true)
514: && ($bindings = $id::getContextualBindings())
515: ) {
516: $rule['substitutions'] = $bindings;
517: }
518:
519: if ($rule) {
520: $this->addRule($id, $rule);
521: }
522:
523: if (is_a($id, HasBindings::class, true)) {
524: $bindings = $id::getBindings();
525: foreach ($bindings as $service => $class) {
526: $this->bind($service, $class);
527: }
528:
529: $singletons = $id::getSingletons();
530: foreach ($singletons as $service => $class) {
531: if (is_int($service)) {
532: $service = $class;
533: }
534: $this->singleton($service, $class);
535: }
536: }
537:
538: if (is_a($id, HasServices::class, true)) {
539: $bind = $id::getServices();
540: } else {
541: $bind = array_diff(
542: (new ReflectionClass($id))->getInterfaceNames(),
543: self::SERVICE_PROVIDER_INTERFACES,
544: );
545: }
546:
547: if ($services !== null) {
548: $services = array_unique($services);
549: $bind = array_intersect($bind, $services);
550: if (count($bind) < count($services)) {
551: // @codeCoverageIgnoreStart
552: throw new InvalidServiceException(sprintf(
553: '%s does not implement: %s',
554: $id,
555: implode(', ', array_diff($services, $bind)),
556: ));
557: // @codeCoverageIgnoreEnd
558: }
559: }
560:
561: if ($exceptServices) {
562: $bind = array_diff($bind, $exceptServices);
563: }
564:
565: if (!$bind) {
566: return;
567: }
568:
569: $rule = [
570: 'instanceOf' => $id
571: ];
572: foreach ($bind as $service) {
573: $this->addRule($service, $rule);
574: }
575: }
576:
577: /**
578: * @return $this
579: */
580: final public function addContextualBinding($context, string $dependency, $value): ContainerInterface
581: {
582: if (is_array($context)) {
583: foreach ($context as $_context) {
584: $this->addContextualBinding($_context, $dependency, $value);
585: }
586: return $this;
587: }
588:
589: if ($dependency === '') {
590: // @codeCoverageIgnoreStart
591: throw new InvalidArgumentException('Argument #2 ($dependency) must be a non-empty string');
592: // @codeCoverageIgnoreEnd
593: }
594:
595: $rule = $this->Dice->hasRule($context)
596: ? $this->Dice->getRule($context)
597: : [];
598:
599: if (is_callable($value)) {
600: $value = [Dice::INSTANCE => fn() => $value($this)];
601: }
602:
603: if (
604: $dependency[0] === '$' && (
605: !($type = (new ReflectionParameter([$context, '__construct'], substr($dependency, 1)))->getType())
606: || !$type instanceof ReflectionNamedType
607: || $type->isBuiltin()
608: )
609: ) {
610: $rule['constructParams'][] = $value;
611: } else {
612: $rule['substitutions'][$dependency] = $value;
613: }
614:
615: $this->addRule($context, $rule);
616:
617: return $this;
618: }
619:
620: /**
621: * @return $this
622: */
623: final public function instance(string $id, $instance): ContainerInterface
624: {
625: $this->Dice = $this->Dice->addShared($id, $instance);
626:
627: return $this;
628: }
629:
630: /**
631: * @return $this
632: */
633: final public function providers(
634: array $serviceMap,
635: int $lifetime = ServiceLifetime::INHERIT
636: ): ContainerInterface {
637: $idMap = [];
638: foreach ($serviceMap as $id => $class) {
639: if (!class_exists($class)) {
640: throw new LogicException(sprintf(
641: 'Not a class: %s',
642: $class,
643: ));
644: }
645: if (!is_a($class, $id, true)) {
646: throw new LogicException(sprintf(
647: '%s does not inherit %s',
648: $class,
649: $id,
650: ));
651: }
652: if (is_a($id, $class, true)) {
653: // Don't add classes mapped to themselves to their service list
654: $idMap[$class] ??= [];
655: continue;
656: }
657: $idMap[$class][] = $id;
658: }
659:
660: foreach ($idMap as $class => $services) {
661: $this->provider($class, $services, [], $lifetime);
662: }
663:
664: return $this;
665: }
666:
667: /**
668: * @inheritDoc
669: */
670: final public function getProviders(): array
671: {
672: return array_keys($this->Providers);
673: }
674:
675: /**
676: * @return $this
677: */
678: final public function unbind(string $id): ContainerInterface
679: {
680: $this->Dice = $this->Dice->removeRule($id);
681:
682: return $this;
683: }
684:
685: /**
686: * @return $this
687: */
688: final public function removeInstance(string $id): ContainerInterface
689: {
690: if (!$this->Dice->hasShared($id)) {
691: return $this;
692: }
693:
694: if ($this->Dice->hasRule($id)) {
695: // Reapplying the rule removes the instance
696: $this->Dice = $this->Dice->addRule($id, $this->Dice->getRule($id));
697: return $this;
698: }
699:
700: $this->Dice = $this->Dice->removeRule($id);
701: return $this;
702: }
703:
704: /**
705: * 0 if another container has the same bindings, otherwise 1
706: *
707: * @param static $container
708: */
709: private function compareBindingsWith($container): int
710: {
711: return $this->Dice === $container->Dice ? 0 : 1;
712: }
713: }
714: