Skip to content

Commit 1aa998f

Browse files
committed
Merge branch 'refactor-facade'
2 parents fd34399 + 4d138e6 commit 1aa998f

File tree

28 files changed

+893
-413
lines changed

28 files changed

+893
-413
lines changed

lk-util/Command/Generate/GenerateFacade.php

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ final class GenerateFacade extends GenerateCommand
2323
private const SKIP_METHODS = [
2424
'getReadable',
2525
'getWritable',
26-
'setFacade',
26+
'withFacade',
27+
'withoutFacade',
2728
// These are displaced by Facade
2829
'isLoaded',
2930
'load',
31+
'swap',
3032
'unload',
3133
'unloadAll',
3234
'getInstance',
@@ -123,10 +125,11 @@ protected function run(string ...$args)
123125
: $a->getName() <=> $b->getName())
124126
);
125127
$facadeMethods = [
126-
" * @method static $service load() Load and return an instance of the underlying $classClass class",
127-
" * @method static $service getInstance() Get the underlying $classClass instance",
128-
" * @method static bool isLoaded() True if an underlying $classClass instance has been loaded",
129-
" * @method static void unload() Clear the underlying $classClass instance",
128+
" * @method static bool isLoaded() True if the facade's underlying instance is loaded",
129+
" * @method static void load($service|null \$instance = null) Load the facade's underlying instance",
130+
" * @method static void swap($service \$instance) Replace the facade's underlying instance",
131+
" * @method static void unload() Remove the facade's underlying instance if loaded",
132+
" * @method static $service getInstance() Get the facade's underlying instance, loading it if necessary",
130133
];
131134
$methods = [];
132135
$toDeclare = [];
@@ -153,10 +156,7 @@ protected function run(string ...$args)
153156
$returnsVoid = false;
154157

155158
if ($_method->isConstructor()) {
156-
$method = 'load';
157-
$type = $service;
158-
$summary = "Load and return an instance of the underlying $classClass class";
159-
unset($facadeMethods[0]);
159+
continue;
160160
} else {
161161
if (isset($phpDoc->TagsByName['deprecated'])) {
162162
continue;
@@ -251,7 +251,7 @@ protected function run(string ...$args)
251251
);
252252
}
253253

254-
if (!$methods && !$_method->isConstructor()) {
254+
if (!$methods) {
255255
array_push($methods, ...$facadeMethods);
256256
}
257257

@@ -289,10 +289,6 @@ protected function run(string ...$args)
289289
. str_replace(\PHP_EOL, \PHP_EOL . ' * ', implode(', ', $params)) . ')'
290290
. " $summary";
291291
}
292-
293-
if ($_method->isConstructor()) {
294-
array_push($methods, ...$facadeMethods);
295-
}
296292
}
297293
$methods = implode(\PHP_EOL, $methods);
298294

@@ -338,7 +334,7 @@ protected function run(string ...$args)
338334
array_push(
339335
$lines,
340336
...$this->indent($this->generateGetter(
341-
'getServiceName',
337+
'getService',
342338
"$service::class",
343339
'@inheritDoc',
344340
'string',

src/Concept/Facade.php

Lines changed: 172 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,95 +2,88 @@
22

33
namespace Lkrms\Concept;
44

5+
use Lkrms\Concern\ResolvesServiceLists;
56
use Lkrms\Container\Event\GlobalContainerSetEvent;
67
use Lkrms\Container\Container;
7-
use Lkrms\Contract\IFacade;
8-
use Lkrms\Contract\ReceivesFacade;
8+
use Lkrms\Contract\FacadeAwareInterface;
9+
use Lkrms\Contract\FacadeInterface;
10+
use Lkrms\Contract\IContainer;
911
use Lkrms\Contract\Unloadable;
1012
use Lkrms\Facade\Event;
1113
use Lkrms\Support\EventDispatcher;
14+
use Lkrms\Utility\Get;
1215
use LogicException;
1316

1417
/**
1518
* Base class for facades
1619
*
17-
* @template TClass of object
18-
* @implements IFacade<TClass>
20+
* @template TService of object
21+
*
22+
* @implements FacadeInterface<TService>
1923
*/
20-
abstract class Facade implements IFacade
24+
abstract class Facade implements FacadeInterface
2125
{
26+
/** @use ResolvesServiceLists<TService> */
27+
use ResolvesServiceLists;
28+
2229
/**
23-
* Get the name of the underlying class
30+
* Get the facade's underlying class, or an array that maps its underlying
31+
* class to compatible implementations
32+
*
33+
* At least one of the values returned should be an instantiable class that
34+
* is guaranteed to exist.
2435
*
25-
* @return class-string<TClass>
36+
* @return class-string<TService>|array<class-string<TService>,class-string<TService>|array<class-string<TService>>>
2637
*/
27-
abstract protected static function getServiceName(): string;
38+
abstract protected static function getService();
2839

2940
/**
30-
* @var array<string,object>
41+
* @var array<class-string<static>,TService>
3142
*/
32-
private static $Instances = [];
43+
private static array $Instances = [];
3344

3445
/**
35-
* @var array<string,int>
46+
* @var array<class-string<static>,int>
3647
*/
37-
private static $ListenerIds = [];
48+
private static array $ListenerIds = [];
3849

3950
/**
40-
* @return TClass
51+
* @internal
4152
*/
42-
private static function _load()
53+
final public static function isLoaded(): bool
4354
{
44-
$service = static::getServiceName();
45-
46-
$container = Container::maybeGetGlobalContainer();
47-
if ($container) {
48-
$instance = $container
49-
->singletonIf($service)
50-
->get($service, func_get_args());
51-
} else {
52-
$instance = new $service(...func_get_args());
53-
}
55+
return isset(self::$Instances[static::class]);
56+
}
5457

55-
if ($instance instanceof ReceivesFacade) {
56-
$instance->setFacade(static::class);
58+
/**
59+
* @internal
60+
*/
61+
final public static function load(?object $instance = null): void
62+
{
63+
if (isset(self::$Instances[static::class])) {
64+
throw new LogicException(sprintf('Already loaded: %s', static::class));
5765
}
5866

59-
$dispatcher = $instance instanceof EventDispatcher
60-
? $instance
61-
: Event::getInstance();
62-
$id = $dispatcher->listen(
63-
function (GlobalContainerSetEvent $event) use ($service, $instance): void {
64-
$container = $event->container();
65-
if ($container) {
66-
$container->instanceIf($service, $instance);
67-
}
68-
}
69-
);
70-
self::$ListenerIds[static::class] = $id;
71-
72-
return self::$Instances[static::class] = $instance;
67+
self::$Instances[static::class] = self::doLoad($instance);
7368
}
7469

7570
/**
7671
* @internal
7772
*/
78-
final public static function isLoaded(): bool
73+
final public static function swap(object $instance): void
7974
{
80-
return isset(self::$Instances[static::class]);
75+
self::unload();
76+
self::$Instances[static::class] = self::doLoad($instance);
8177
}
8278

8379
/**
84-
* @internal
85-
* @return TClass
80+
* Remove the underlying instances of all facades
8681
*/
87-
final public static function load()
82+
final public static function unloadAll(): void
8883
{
89-
if (isset(self::$Instances[static::class])) {
90-
throw new LogicException(static::class . ' already loaded');
84+
foreach (array_keys(self::$Instances) as $class) {
85+
$class::unload();
9186
}
92-
93-
return self::_load(...func_get_args());
9487
}
9588

9689
/**
@@ -104,37 +97,46 @@ final public static function unload(): void
10497
unset(self::$ListenerIds[static::class]);
10598
}
10699

100+
$instance = self::$Instances[static::class] ?? null;
101+
if (!$instance) {
102+
return;
103+
}
104+
107105
$container = Container::maybeGetGlobalContainer();
108106
if ($container) {
109-
$container->unbind(static::getServiceName());
107+
$serviceName = self::getServiceName();
108+
if (
109+
$container->hasInstance($serviceName) &&
110+
$container->get($serviceName) === $instance
111+
) {
112+
$container->unbind($serviceName);
113+
}
110114
}
111115

112-
$instance = self::$Instances[static::class] ?? null;
113-
if ($instance) {
114-
if ($instance instanceof Unloadable) {
115-
$instance->unload();
116-
}
117-
unset(self::$Instances[static::class]);
116+
if ($instance instanceof FacadeAwareInterface) {
117+
$instance = $instance->withoutFacade(static::class, true);
118118
}
119-
}
120119

121-
/**
122-
* Clear the underlying instances of all facades
123-
*/
124-
final public static function unloadAll(): void
125-
{
126-
foreach (array_keys(self::$Instances) as $class) {
127-
$class::unload();
120+
if ($instance instanceof Unloadable) {
121+
$instance->unload();
128122
}
123+
124+
unset(self::$Instances[static::class]);
129125
}
130126

131127
/**
132128
* @internal
133-
* @return TClass
134129
*/
135130
final public static function getInstance()
136131
{
137-
return self::$Instances[static::class] ?? self::_load();
132+
$instance = self::$Instances[static::class]
133+
??= self::doLoad();
134+
135+
if ($instance instanceof FacadeAwareInterface) {
136+
return $instance->withoutFacade(static::class, false);
137+
}
138+
139+
return $instance;
138140
}
139141

140142
/**
@@ -144,6 +146,109 @@ final public static function getInstance()
144146
*/
145147
final public static function __callStatic(string $name, array $arguments)
146148
{
147-
return (self::$Instances[static::class] ?? self::_load())->$name(...$arguments);
149+
return (self::$Instances[static::class]
150+
??= self::doLoad())->$name(...$arguments);
151+
}
152+
153+
/**
154+
* @param TService|null $instance
155+
* @return TService
156+
*/
157+
private static function doLoad($instance = null)
158+
{
159+
$serviceName = self::getServiceName();
160+
161+
if ($instance !== null && (
162+
!is_object($instance) || !is_a($instance, $serviceName)
163+
)) {
164+
throw new LogicException(sprintf(
165+
'%s does not inherit %s',
166+
Get::type($instance),
167+
$serviceName,
168+
));
169+
}
170+
171+
$container = Container::maybeGetGlobalContainer();
172+
173+
$instance ??= $container
174+
? self::getInstanceFromContainer($container, $serviceName)
175+
: self::createInstance();
176+
177+
if ($container) {
178+
$container->instanceIf($serviceName, $instance);
179+
}
180+
181+
$dispatcher = $instance instanceof EventDispatcher
182+
? $instance
183+
: Event::getInstance();
184+
185+
$id = $dispatcher->listen(
186+
static function (GlobalContainerSetEvent $event) use ($serviceName, $instance): void {
187+
$container = $event->container();
188+
if ($container) {
189+
$container->instanceIf($serviceName, $instance);
190+
}
191+
}
192+
);
193+
self::$ListenerIds[static::class] = $id;
194+
195+
if ($instance instanceof FacadeAwareInterface) {
196+
$instance = $instance->withFacade(static::class);
197+
}
198+
199+
return $instance;
200+
}
201+
202+
/**
203+
* @return TService
204+
*/
205+
private static function getInstanceFromContainer(
206+
IContainer $container,
207+
string $serviceName
208+
) {
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()
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+
));
148253
}
149254
}

0 commit comments

Comments
 (0)