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