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