1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core\Facade;
4:
5: use Salient\Container\Container;
6: use Salient\Contract\Container\BeforeGlobalContainerSetEventInterface;
7: use Salient\Contract\Container\ContainerInterface;
8: use Salient\Contract\Core\Facade\FacadeAwareInterface;
9: use Salient\Contract\Core\Facade\FacadeInterface;
10: use Salient\Contract\Core\Instantiable;
11: use Salient\Contract\Core\Unloadable;
12: use Salient\Core\Event\EventDispatcher;
13: use Salient\Core\Internal\HasUnderlyingService;
14: use Salient\Utility\Arr;
15: use LogicException;
16:
17: /**
18: * @api
19: *
20: * @template TService of Instantiable
21: *
22: * @implements FacadeInterface<TService>
23: */
24: abstract class Facade implements FacadeInterface
25: {
26: /** @use HasUnderlyingService<TService> */
27: use HasUnderlyingService;
28:
29: /** @var array<class-string<self<*>>,TService> */
30: private static array $Instances = [];
31: /** @var array<class-string<self<*>>,int> */
32: private static array $ListenerIds = [];
33:
34: /**
35: * Get the facade's underlying class, or an array with the facade's
36: * underlying class at index 0 and an inheritor or list thereof at index 1
37: *
38: * @return class-string<TService>|array{class-string<TService>,class-string<TService>[]|class-string<TService>}
39: */
40: abstract protected static function getService();
41:
42: /**
43: * @inheritDoc
44: */
45: final public static function isLoaded(): bool
46: {
47: return isset(self::$Instances[static::class]);
48: }
49:
50: /**
51: * @inheritDoc
52: */
53: final public static function load(?object $instance = null): void
54: {
55: if (isset(self::$Instances[static::class])) {
56: throw new LogicException(sprintf('Already loaded: %s', static::class));
57: }
58: self::$Instances[static::class] = self::doLoad($instance);
59: }
60:
61: /**
62: * @inheritDoc
63: */
64: final public static function swap(object $instance): void
65: {
66: self::doUnload();
67: self::$Instances[static::class] = self::doLoad($instance);
68: }
69:
70: /**
71: * @inheritDoc
72: */
73: final public static function unload(): void
74: {
75: self::doUnload();
76: }
77:
78: /**
79: * Remove the underlying instances of all facades
80: */
81: final public static function unloadAll(): void
82: {
83: foreach (array_keys(self::$Instances) as $class) {
84: if ($class !== Event::class) {
85: $class::doUnload();
86: }
87: }
88: Event::doUnload();
89: }
90:
91: /**
92: * @inheritDoc
93: */
94: final public static function getInstance(): object
95: {
96: $instance = self::$Instances[static::class] ??= self::doLoad();
97: return $instance instanceof FacadeAwareInterface
98: ? $instance->withoutFacade(static::class, false)
99: : $instance;
100: }
101:
102: /**
103: * @inheritDoc
104: */
105: final public static function __callStatic(string $name, array $arguments)
106: {
107: $instance = self::$Instances[static::class] ??= self::doLoad();
108: return $instance->$name(...$arguments);
109: }
110:
111: /**
112: * @param TService|null $instance
113: * @return TService
114: */
115: private static function doLoad(?object $instance = null): object
116: {
117: [$name] = self::getNormalisedService();
118:
119: if ($instance && !is_a($instance, $name)) {
120: throw new LogicException(sprintf(
121: '%s does not inherit %s',
122: get_class($instance),
123: $name,
124: ));
125: }
126:
127: $containerExists = class_exists(Container::class);
128: $container = $containerExists && Container::hasGlobalContainer()
129: ? Container::getGlobalContainer()
130: : null;
131:
132: $instance ??= $container
133: ? self::getInstanceFromContainer($container)
134: : self::createInstance() ?? (
135: $containerExists
136: ? self::getInstanceFromContainer(new Container())
137: : null
138: );
139:
140: if (!$instance) {
141: throw new LogicException(sprintf(
142: 'Service not instantiable: %s::getService()',
143: static::class,
144: ));
145: }
146:
147: /** @var EventDispatcher */
148: // @phpstan-ignore identical.alwaysFalse
149: $dispatcher = static::class === Event::class
150: ? $instance
151: : Event::getInstance();
152:
153: // @phpstan-ignore identical.alwaysFalse
154: if (static::class === App::class) {
155: /** @var ContainerInterface $instance */
156: if ($containerExists && !$container) {
157: Container::setGlobalContainer($instance);
158: }
159:
160: $listenerId = $dispatcher->listen(
161: static function (BeforeGlobalContainerSetEventInterface $event): void {
162: if ($container = $event->getContainer()) {
163: App::swap($container);
164: }
165: }
166: );
167: } else {
168: if ($container && !$container->hasInstance($name)) {
169: $container->instance($name, $instance);
170: }
171:
172: $listenerId = $dispatcher->listen(
173: static function (BeforeGlobalContainerSetEventInterface $event) use ($name, $instance): void {
174: if (($container = $event->getContainer()) && !$container->hasInstance($name)) {
175: $container->instance($name, $instance);
176: }
177: }
178: );
179: }
180:
181: self::$ListenerIds[static::class] = $listenerId;
182:
183: if ($instance instanceof FacadeAwareInterface) {
184: $instance = $instance->withFacade(static::class);
185: }
186:
187: return $instance;
188: }
189:
190: /**
191: * @return TService|null
192: */
193: private static function getInstanceFromContainer(ContainerInterface $container): ?object
194: {
195: [$name, $list] = self::getNormalisedService();
196:
197: foreach (Arr::extend([$name], ...$list) as $service) {
198: if ($container->has($service)) {
199: $instance = $service === $name
200: ? $container->get($name)
201: : $container->getAs($service, $name);
202: if (!is_a($instance, $name)) {
203: throw new LogicException(sprintf(
204: '%s does not inherit %s',
205: get_class($instance),
206: $name,
207: ));
208: }
209: return $instance;
210: }
211: }
212:
213: $service = self::getInstantiableService();
214: return $service !== null
215: ? $container->getAs($service, $name)
216: : null;
217: }
218:
219: /**
220: * @return TService|null
221: */
222: private static function createInstance(): ?object
223: {
224: $service = self::getInstantiableService();
225: return $service !== null
226: ? new $service()
227: : null;
228: }
229:
230: private static function doUnload(): void
231: {
232: // @phpstan-ignore identical.alwaysFalse
233: if (static::class === Event::class) {
234: $loaded = array_keys(self::$Instances);
235: if ($loaded && $loaded !== [Event::class]) {
236: throw new LogicException(sprintf(
237: '%s cannot be unloaded before other facades',
238: Event::class,
239: ));
240: }
241: }
242:
243: $id = self::$ListenerIds[static::class] ?? null;
244: if ($id !== null) {
245: Event::removeListener($id);
246: unset(self::$ListenerIds[static::class]);
247: }
248:
249: $instance = self::$Instances[static::class] ?? null;
250: if (!$instance) {
251: return;
252: }
253:
254: // @phpstan-ignore notIdentical.alwaysTrue
255: if (static::class !== App::class) {
256: $container = class_exists(Container::class)
257: && Container::hasGlobalContainer()
258: ? Container::getGlobalContainer()
259: : null;
260:
261: if ($container) {
262: [$name] = self::getNormalisedService();
263: if (
264: $container->hasInstance($name)
265: && $container->get($name) === $instance
266: ) {
267: $container->removeInstance($name);
268: }
269: }
270: }
271:
272: if ($instance instanceof FacadeAwareInterface) {
273: $instance = $instance->withoutFacade(static::class, true);
274: }
275:
276: if ($instance instanceof Unloadable) {
277: $instance->unload();
278: }
279:
280: unset(self::$Instances[static::class]);
281: }
282: }
283: