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