1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core\Facade;
4:
5: use Salient\Container\Container;
6: use Salient\Contract\Container\Event\BeforeGlobalContainerSetEvent;
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: $result = $instance->$name(...$arguments);
109: // @phpstan-ignore nullCoalesce.offset
110: return $result === (self::$Instances[static::class] ?? null)
111: && $result instanceof FacadeAwareInterface
112: ? $result->withoutFacade(static::class, false)
113: : $result;
114: }
115:
116: /**
117: * @param TService|null $instance
118: * @return TService
119: */
120: private static function doLoad(?object $instance = null): object
121: {
122: [$name] = self::getNormalisedService();
123:
124: if ($instance && !is_a($instance, $name)) {
125: throw new LogicException(sprintf(
126: '%s does not inherit %s',
127: get_class($instance),
128: $name,
129: ));
130: }
131:
132: $containerExists = class_exists(Container::class);
133: $container = $containerExists && Container::hasGlobalContainer()
134: ? Container::getGlobalContainer()
135: : null;
136:
137: $instance ??= $container
138: ? self::getInstanceFromContainer($container)
139: : self::createInstance() ?? (
140: $containerExists
141: ? self::getInstanceFromContainer(new Container())
142: : null
143: );
144:
145: if (!$instance) {
146: throw new LogicException(sprintf(
147: 'Service not instantiable: %s::getService()',
148: static::class,
149: ));
150: }
151:
152: /** @var EventDispatcher */
153: // @phpstan-ignore identical.alwaysFalse
154: $dispatcher = static::class === Event::class
155: ? $instance
156: : Event::getInstance();
157:
158: // @phpstan-ignore identical.alwaysFalse
159: if (static::class === App::class) {
160: /** @var ContainerInterface $instance */
161: if ($containerExists && !$container) {
162: Container::setGlobalContainer($instance);
163: }
164:
165: $listenerId = $dispatcher->listen(
166: static function (BeforeGlobalContainerSetEvent $event): void {
167: if ($container = $event->getContainer()) {
168: App::swap($container);
169: }
170: }
171: );
172: } else {
173: if ($container && !$container->hasInstance($name)) {
174: $container->instance($name, $instance);
175: }
176:
177: $listenerId = $dispatcher->listen(
178: static function (BeforeGlobalContainerSetEvent $event) use ($name, $instance): void {
179: if (($container = $event->getContainer()) && !$container->hasInstance($name)) {
180: $container->instance($name, $instance);
181: }
182: }
183: );
184: }
185:
186: self::$ListenerIds[static::class] = $listenerId;
187:
188: if ($instance instanceof FacadeAwareInterface) {
189: $instance = $instance->withFacade(static::class);
190: }
191:
192: return $instance;
193: }
194:
195: /**
196: * @return TService|null
197: */
198: private static function getInstanceFromContainer(ContainerInterface $container): ?object
199: {
200: [$name, $list] = self::getNormalisedService();
201:
202: foreach (Arr::extend([$name], ...$list) as $service) {
203: if ($container->has($service)) {
204: $instance = $service === $name
205: ? $container->get($name)
206: : $container->getAs($service, $name);
207: if (!is_a($instance, $name)) {
208: throw new LogicException(sprintf(
209: '%s does not inherit %s',
210: get_class($instance),
211: $name,
212: ));
213: }
214: return $instance;
215: }
216: }
217:
218: $service = self::getInstantiableService();
219: return $service !== null
220: ? $container->getAs($service, $name)
221: : null;
222: }
223:
224: /**
225: * @return TService|null
226: */
227: private static function createInstance(): ?object
228: {
229: $service = self::getInstantiableService();
230: return $service !== null
231: ? new $service()
232: : null;
233: }
234:
235: private static function doUnload(): void
236: {
237: // @phpstan-ignore identical.alwaysFalse
238: if (static::class === Event::class) {
239: $loaded = array_keys(self::$Instances);
240: if ($loaded && $loaded !== [Event::class]) {
241: throw new LogicException(sprintf(
242: '%s cannot be unloaded before other facades',
243: Event::class,
244: ));
245: }
246: }
247:
248: $id = self::$ListenerIds[static::class] ?? null;
249: if ($id !== null) {
250: Event::removeListener($id);
251: unset(self::$ListenerIds[static::class]);
252: }
253:
254: $instance = self::$Instances[static::class] ?? null;
255: if (!$instance) {
256: return;
257: }
258:
259: // @phpstan-ignore notIdentical.alwaysTrue
260: if (static::class !== App::class) {
261: $container = class_exists(Container::class)
262: && Container::hasGlobalContainer()
263: ? Container::getGlobalContainer()
264: : null;
265:
266: if ($container) {
267: [$name] = self::getNormalisedService();
268: if (
269: $container->hasInstance($name)
270: && $container->get($name) === $instance
271: ) {
272: $container->removeInstance($name);
273: }
274: }
275: }
276:
277: if ($instance instanceof FacadeAwareInterface) {
278: $instance = $instance->withoutFacade(static::class, true);
279: }
280:
281: if ($instance instanceof Unloadable) {
282: $instance->unload();
283: }
284:
285: unset(self::$Instances[static::class]);
286: }
287: }
288: