Skip to content

Commit a2a1dff

Browse files
committed
Fix AbstractFacade::unload() issues and improve documentation
- Fix issue where `HasFacade` doesn't remove references to cloned instances when unloaded - Fix issue where service container bindings are forgotten when a facade's underlying instance is removed from a container - Add service container methods `hasSingleton()` and `unbindInstance()` - Leave `unload()`ed containers in a usable state - Add tests
1 parent a805cbf commit a2a1dff

File tree

8 files changed

+202
-35
lines changed

8 files changed

+202
-35
lines changed

src/Toolkit/Core/AbstractFacade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ final public static function unload(): void
129129
$container->hasInstance($serviceName) &&
130130
$container->get($serviceName) === $instance
131131
) {
132-
$container->unbind($serviceName);
132+
$container->unbindInstance($serviceName);
133133
}
134134
}
135135
}

src/Toolkit/Core/Concern/HasFacade.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ final public function withoutFacade(string $facade, bool $unloading)
8989

9090
// Keep a copy of the cloned instance for future reuse if the facade is
9191
// not being unloaded
92-
if (!$unloading) {
92+
if ($unloading) {
93+
$instance->InstanceWithoutFacade = null;
94+
$instance->InstanceWithFacade = null;
95+
} else {
9396
$instance->InstanceWithoutFacade = $instance;
9497
$instance->InstanceWithFacade = $this;
9598
}

src/Toolkit/Core/Contract/FacadeInterface.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
/**
88
* Provides a static interface to an underlying instance
99
*
10-
* Underlying instances that implement {@see FacadeAwareInterface} are replaced
11-
* with the object returned by {@see FacadeAwareInterface::withFacade()}, and
12-
* its {@see FacadeAwareInterface::withoutFacade()} method is used to service
13-
* the facade's {@see swap()}, {@see unload()} and {@see getInstance()} methods.
14-
*
1510
* @api
1611
*
1712
* @template TService of object
@@ -28,6 +23,10 @@ public static function isLoaded(): bool;
2823
*
2924
* If `$instance` is `null`, the facade creates a new underlying instance.
3025
*
26+
* Then, if the instance implements {@see FacadeAwareInterface}, it is
27+
* replaced with the return value of
28+
* {@see FacadeAwareInterface::withFacade()}.
29+
*
3130
* @param TService|null $instance
3231
* @throws LogicException if the facade's underlying instance is already
3332
* loaded.
@@ -37,12 +36,22 @@ public static function load(?object $instance = null): void;
3736
/**
3837
* Replace the facade's underlying instance
3938
*
39+
* Equivalent to calling {@see unload()} before passing `$instance` to
40+
* {@see load()}.
41+
*
4042
* @param TService $instance
4143
*/
4244
public static function swap(object $instance): void;
4345

4446
/**
4547
* Remove the facade's underlying instance if loaded
48+
*
49+
* If the underlying instance implements {@see FacadeAwareInterface}, it is
50+
* replaced with the return value of
51+
* {@see FacadeAwareInterface::withoutFacade()}.
52+
*
53+
* Then, if the instance implements {@see Unloadable}, its
54+
* {@see Unloadable::unload()} method is called.
4655
*/
4756
public static function unload(): void;
4857

src/Util/Container/Container.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@ public function unload(): void
7777
{
7878
if (self::$GlobalContainer === $this) {
7979
self::setGlobalContainer(null);
80-
$this->unloadFacades();
8180
}
8281

83-
unset($this->Dice);
82+
$this->unloadFacades();
83+
84+
$this->Dice = new Dice();
85+
$this->bindContainer();
8486
}
8587

8688
private function bindContainer(): void
@@ -258,6 +260,17 @@ final public function has(string $id): bool
258260
return $this->Dice->hasRule($id) || $this->Dice->hasShared($id);
259261
}
260262

263+
/**
264+
* @inheritDoc
265+
*/
266+
final public function hasSingleton(string $id): bool
267+
{
268+
return $this->Dice->hasShared($id) || (
269+
$this->Dice->hasRule($id) &&
270+
($this->Dice->getRule($id)['shared'] ?? false)
271+
);
272+
}
273+
261274
/**
262275
* @inheritDoc
263276
*/
@@ -647,6 +660,25 @@ final public function unbind(string $id): self
647660
return $this;
648661
}
649662

663+
/**
664+
* @inheritDoc
665+
*/
666+
final public function unbindInstance(string $id): self
667+
{
668+
if (!$this->Dice->hasShared($id)) {
669+
return $this;
670+
}
671+
672+
if ($this->Dice->hasRule($id)) {
673+
// Reapplying the rule removes the instance
674+
$this->Dice = $this->Dice->addRule($id, $this->Dice->getRule($id));
675+
return $this;
676+
}
677+
678+
$this->Dice = $this->Dice->removeRule($id);
679+
return $this;
680+
}
681+
650682
/**
651683
* 0 if another container has the same bindings, otherwise 1
652684
*

src/Util/Container/ContainerInterface.php

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
interface ContainerInterface extends PsrContainerInterface, Unloadable
4242
{
4343
/**
44-
* Creates a new service container object
44+
* Creates a new service container
4545
*/
4646
public function __construct();
4747

4848
/**
49-
* True if the global container is set
49+
* Check if the global container is set
5050
*/
5151
public static function hasGlobalContainer(): bool;
5252

@@ -117,28 +117,35 @@ public function getAs(string $id, string $service, array $args = []): object;
117117
public function getName(string $id): string;
118118

119119
/**
120-
* True if a service is bound to the container
120+
* Check if a service is bound to the container
121121
*
122122
* @param class-string $id
123123
*/
124124
public function has(string $id): bool;
125125

126126
/**
127-
* True if a service provider is registered with the container
127+
* Check if a service provider is registered with the container
128128
*
129129
* @param class-string $id
130130
*/
131131
public function hasProvider(string $id): bool;
132132

133133
/**
134-
* True if a service resolves to a shared instance
134+
* Check if a shared service is bound to the container
135+
*
136+
* @param class-string $id
137+
*/
138+
public function hasSingleton(string $id): bool;
139+
140+
/**
141+
* Check if a service resolves to a shared instance
135142
*
136143
* @param class-string $id
137144
*/
138145
public function hasInstance(string $id): bool;
139146

140147
/**
141-
* Register a binding with the container
148+
* Bind a service to the container
142149
*
143150
* Subsequent requests for `$id` resolve to `$class`.
144151
*
@@ -162,7 +169,7 @@ public function bind(
162169
): self;
163170

164171
/**
165-
* Register a binding with the container if it isn't already registered
172+
* Bind a service to the container if it isn't already bound
166173
*
167174
* @template TService of object
168175
* @template T of TService
@@ -179,7 +186,7 @@ public function bindIf(
179186
): self;
180187

181188
/**
182-
* Register a shared binding with the container
189+
* Bind a shared service to the container
183190
*
184191
* Subsequent requests for `$id` resolve to the instance of `$class` created
185192
* when `$id` is first requested.
@@ -199,8 +206,7 @@ public function singleton(
199206
): self;
200207

201208
/**
202-
* Register a shared binding with the container if it isn't already
203-
* registered
209+
* Bind a shared service to the container if it isn't already bound
204210
*
205211
* @template TService of object
206212
* @template T of TService
@@ -296,7 +302,7 @@ public function providers(
296302
public function getProviders(): array;
297303

298304
/**
299-
* Register an object with the container as a shared binding
305+
* Bind a shared instance to the container
300306
*
301307
* @template TService of object
302308
* @template T of TService
@@ -308,8 +314,7 @@ public function getProviders(): array;
308314
public function instance(string $id, $instance): self;
309315

310316
/**
311-
* Register an object with the container as a shared binding if it isn't
312-
* already registered
317+
* Bind a shared instance to the container if it isn't already bound
313318
*
314319
* @template TService of object
315320
* @template T of TService
@@ -327,4 +332,12 @@ public function instanceIf(string $id, $instance): self;
327332
* @return $this
328333
*/
329334
public function unbind(string $id): self;
335+
336+
/**
337+
* Remove a shared instance from the container
338+
*
339+
* @param class-string $id
340+
* @return $this
341+
*/
342+
public function unbindInstance(string $id): self;
330343
}

src/Util/Facade/App.php

Lines changed: 12 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/unit/Util/Concept/FacadeTest.php renamed to tests/unit/Toolkit/Core/AbstractFacadeTest.php

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<?php declare(strict_types=1);
22

3-
namespace Lkrms\Tests\Concept;
3+
namespace Salient\Tests\Core;
44

55
use Lkrms\Container\Container;
6+
use Lkrms\Facade\App;
67
use Lkrms\Facade\Event;
78
use Lkrms\Tests\Concept\Facade\MyBrokenFacade;
89
use Lkrms\Tests\Concept\Facade\MyClassFacade;
@@ -14,11 +15,12 @@
1415
use Lkrms\Tests\TestCase;
1516
use Salient\Core\AbstractFacade;
1617
use LogicException;
18+
use stdClass;
1719

1820
/**
19-
* @covers \Lkrms\Concept\Facade
21+
* @covers \Salient\Core\AbstractFacade
2022
*/
21-
final class FacadeTest extends TestCase
23+
final class AbstractFacadeTest extends TestCase
2224
{
2325
public function testBrokenFacade(): void
2426
{
@@ -139,6 +141,66 @@ public function testLoadWithContainerBindings(): void
139141
$this->assertNotSame($instance1, $instance3);
140142
}
141143

144+
public function testLoadWithInvalidBinding(): void
145+
{
146+
$container = Container::getGlobalContainer();
147+
$container->bind(MyServiceInterface::class, stdClass::class);
148+
$this->expectException(LogicException::class);
149+
$this->expectExceptionMessage(' does not inherit ');
150+
MyInterfaceFacade::load();
151+
}
152+
153+
public function testGlobalContainerIsSetAndUnloaded(): void
154+
{
155+
$this->assertFalse(Container::hasGlobalContainer());
156+
$this->assertFalse(App::isLoaded());
157+
App::get(stdClass::class);
158+
$this->assertTrue(Container::hasGlobalContainer());
159+
$this->assertTrue(App::isLoaded());
160+
$container = Container::getGlobalContainer();
161+
$this->assertSame($container, App::getInstance());
162+
App::unload();
163+
$this->assertFalse(Container::hasGlobalContainer());
164+
$this->assertFalse(App::isLoaded());
165+
}
166+
167+
public function testGlobalContainerIsNotReplaced(): void
168+
{
169+
$container = Container::getGlobalContainer();
170+
$this->assertFalse($container->has(MyServiceClass::class));
171+
MyClassFacade::load();
172+
$this->assertTrue($container->has(MyServiceClass::class));
173+
$this->assertFalse(App::isLoaded());
174+
App::get(stdClass::class);
175+
$this->assertTrue(App::isLoaded());
176+
$this->assertSame($container, App::getInstance());
177+
App::unload();
178+
$this->assertFalse(Container::hasGlobalContainer());
179+
$this->assertFalse(App::isLoaded());
180+
$this->assertFalse($container->has(MyServiceClass::class));
181+
}
182+
183+
public function testGlobalContainerBindingsAreMaintained(): void
184+
{
185+
MyClassFacade::load();
186+
$this->assertFalse(Container::hasGlobalContainer());
187+
$container = Container::getGlobalContainer();
188+
$container2 = new Container();
189+
$this->assertTrue($container->has(MyServiceClass::class));
190+
$this->assertFalse($container2->has(MyServiceClass::class));
191+
192+
$this->assertFalse(App::isLoaded());
193+
App::get(stdClass::class);
194+
$this->assertTrue(App::isLoaded());
195+
$this->assertSame($container, App::getInstance());
196+
197+
Container::setGlobalContainer($container2);
198+
$this->assertTrue(App::isLoaded());
199+
$this->assertSame($container2, App::getInstance());
200+
$this->assertTrue($container2->has(MyServiceClass::class));
201+
$this->assertFalse($container->has(MyServiceClass::class));
202+
}
203+
142204
protected function tearDown(): void
143205
{
144206
AbstractFacade::unloadAll();

0 commit comments

Comments
 (0)