Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@
],
"config": {
"allow-plugins": {
"carthage-software/mago": true
"carthage-software/mago": true,
"php-http/discovery": true
},
"audit": {
"ignore": {
Expand Down
8 changes: 8 additions & 0 deletions packages/container/src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public function invoke(ClassReflector|MethodReflector|FunctionReflector|callable
public function addInitializer(ClassReflector|string $initializerClass): self;

public function addDecorator(ClassReflector|string $decoratorClass, ClassReflector|string $decoratedClass): self;

/**
* @template T of \Tempest\Container\Resettable
* @param ClassReflector<T>|class-string<T> $resettableClass
*/
public function addResettable(ClassReflector|string $resettableClass): self;

public function reset(): self;
}
103 changes: 83 additions & 20 deletions packages/container/src/GenericContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Throwable;
use UnitEnum;

use const ARRAY_FILTER_USE_BOTH;

final class GenericContainer implements Container
{
use HasInstance;
Expand All @@ -30,8 +32,11 @@ public function __construct(
/** @var ArrayIterator<array-key, mixed> $definitions */
private(set) ArrayIterator $definitions = new ArrayIterator(),

/** @var ArrayIterator<array-key, mixed> $singletons */
private(set) ArrayIterator $singletons = new ArrayIterator(),
/** @var ArrayIterator<array-key, mixed> $singletonDefinitions */
private(set) ArrayIterator $singletonDefinitions = new ArrayIterator(),

/** @var ArrayIterator<array-key, object> $resolvedSingletons */
private(set) ArrayIterator $resolvedSingletons = new ArrayIterator(),

/** @var ArrayIterator<array-key, class-string> $initializers */
private(set) ArrayIterator $initializers = new ArrayIterator(),
Expand All @@ -41,6 +46,10 @@ public function __construct(

/** @var ArrayIterator<array-key, class-string[]> $decorators */
private(set) ArrayIterator $decorators = new ArrayIterator(),

/** @var ArrayIterator<array-key, class-string<\Tempest\Container\Resettable>> $resettables */
private(set) ArrayIterator $resettables = new ArrayIterator(),
Comment thread
brendt marked this conversation as resolved.

private(set) ?DependencyChain $chain = null,
) {
$this->singleton(Container::class, $this);
Expand All @@ -57,7 +66,7 @@ public function setDefinitions(array $definitions): self

public function setSingletons(array $singletons): self
{
$this->singletons = new ArrayIterator($singletons);
$this->singletonDefinitions = new ArrayIterator($singletons);

return $this;
}
Expand Down Expand Up @@ -93,7 +102,7 @@ public function getDefinitions(): array
*/
public function getSingletons(?string $interface = null): array
{
$singletons = $this->singletons->getArrayCopy();
$singletons = $this->singletonDefinitions->getArrayCopy();

if (is_null($interface)) {
return $singletons;
Expand All @@ -102,7 +111,7 @@ public function getSingletons(?string $interface = null): array
return array_filter(
array: $singletons,
callback: static fn (mixed $_, string $key) => str_starts_with($key, "{$interface}#") || $key === $interface,
mode: \ARRAY_FILTER_USE_BOTH,
mode: ARRAY_FILTER_USE_BOTH,
);
}

Expand Down Expand Up @@ -130,24 +139,34 @@ public function register(string $className, callable $definition): self

public function unregister(string $className, bool $tagged = false): self
{
unset($this->definitions[$className], $this->singletons[$className]);
unset($this->definitions[$className]);
unset($this->singletonDefinitions[$className]);
unset($this->resolvedSingletons[$className]);

if ($tagged) {
$singletons = array_filter(
array: $this->getSingletons(),
callback: static fn (mixed $_, string $key) => ! str_starts_with($key, "{$className}#"),
mode: \ARRAY_FILTER_USE_BOTH,
);
foreach ($this->singletonDefinitions as $key => $definition) {
if (! str_starts_with($key, "{$className}#")) {
continue;
}

unset($this->singletonDefinitions[$key]);
}

foreach ($this->resolvedSingletons as $key => $definition) {
if (! str_starts_with($key, "{$className}#")) {
continue;
}

$this->setSingletons($singletons);
unset($this->resolvedSingletons[$key]);
}
}

return $this;
}

public function has(string $className, null|string|UnitEnum $tag = null): bool
{
return isset($this->definitions[$className]) || isset($this->singletons[$this->resolveTaggedName($className, $tag)]);
return isset($this->definitions[$className]) || isset($this->singletonDefinitions[$this->resolveTaggedName($className, $tag)]);
}

public function singleton(string $className, mixed $definition, null|string|UnitEnum $tag = null): self
Expand All @@ -156,9 +175,10 @@ public function singleton(string $className, mixed $definition, null|string|Unit
$tag = $definition->tag;
}

$className = $this->resolveTaggedName($className, $tag);
$dependencyName = $this->resolveTaggedName($className, $tag);

$this->singletons[$className] = $definition;
$this->singletonDefinitions[$dependencyName] = $definition;
unset($this->resolvedSingletons[$dependencyName]);

return $this;
}
Expand Down Expand Up @@ -346,12 +366,18 @@ private function resolveDependency(string $className, null|string|UnitEnum $tag

$dependencyName = $this->resolveTaggedName($className, $tag);

// Check if a resolved singleton is present
if ($instance = $this->resolvedSingletons[$dependencyName] ?? null) {
$this->resolveChain()->add($class);

return $instance;
}

// Check if the class has been registered as a singleton.
if ($instance = $this->singletons[$dependencyName] ?? null) {
if ($instance instanceof Closure) {
$instance = $instance($this);
$this->singletons[$className] = $instance;
}
if ($singletonDefinition = $this->singletonDefinitions[$dependencyName] ?? null) {
$instance = $singletonDefinition instanceof Closure ? $singletonDefinition($this) : $singletonDefinition;

$this->resolvedSingletons[$dependencyName] = $instance;

$this->resolveChain()->add($class);

Expand Down Expand Up @@ -678,4 +704,41 @@ private function resolveDecorator(string $className, mixed $instance, null|strin

return $instance;
}

public function addResettable(string|ClassReflector $resettableClass): Container
{
if ($resettableClass instanceof ClassReflector) {
$resettableClass = $resettableClass->getName();
}

$this->resettables[$resettableClass] = $resettableClass;

return $this;
}

public function reset(): self
{
$this->resolvedSingletons = new ArrayIterator();

foreach ($this->resettables as $resettableClass) {
/** @var Resettable $resettable */
$resettable = $this->get($resettableClass);

$resettable->reset();
Comment thread
brendt marked this conversation as resolved.
}

return $this;
}

public function getResettables(): array
{
return $this->resettables->getArrayCopy();
}

public function setResettables(array $resettables): self
{
$this->resettables = new ArrayIterator($resettables);

return $this;
}
}
8 changes: 8 additions & 0 deletions packages/container/src/Resettable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Tempest\Container;

interface Resettable
{
public function reset(): void;
}
33 changes: 33 additions & 0 deletions packages/container/src/ResettableDiscovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Tempest\Container;

use Tempest\Discovery\Discovery;
use Tempest\Discovery\DiscoveryLocation;
use Tempest\Discovery\IsDiscovery;
use Tempest\Reflection\ClassReflector;

final class ResettableDiscovery implements Discovery
{
use IsDiscovery;

public function __construct(
private readonly Container $container,
) {}

public function discover(DiscoveryLocation $location, ClassReflector $class): void
{
if ($class->implements(Resettable::class)) {
$this->discoveryItems->add($location, $class);
}
}

public function apply(): void
{
foreach ($this->discoveryItems as $class) {
$this->container->addResettable($class);
}
}
}
27 changes: 27 additions & 0 deletions packages/container/tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
use Tempest\Container\Tests\Fixtures\InvokableClassWithDependencies;
use Tempest\Container\Tests\Fixtures\InvokableClassWithParameters;
use Tempest\Container\Tests\Fixtures\OptionalTypesClass;
use Tempest\Container\Tests\Fixtures\ResettableDependency;
use Tempest\Container\Tests\Fixtures\SingletonClass;
use Tempest\Container\Tests\Fixtures\SingletonInitializer;
use Tempest\Container\Tests\Fixtures\SlowDependency;
Expand All @@ -73,6 +74,12 @@
*/
final class ContainerTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
SingletonClass::$count = 0;
}

public function test_get_with_autowire(): void
{
$container = new GenericContainer();
Expand Down Expand Up @@ -690,4 +697,24 @@ public function test_returns_decorator_without_constructor(): void

$this->assertInstanceOf(DecoratorWithoutConstructor::class, $instance);
}

public function test_reset(): void
{
ResettableDependency::$reset = false;

$container = new GenericContainer();

$container->addResettable(ResettableDependency::class);
$container->singleton(SingletonClass::class, fn () => new SingletonClass());
$container->get(SingletonClass::class);
$container->get(SingletonClass::class);
$this->assertSame(1, SingletonClass::$count);

$container->reset();

$container->get(SingletonClass::class);
$container->get(SingletonClass::class);
$this->assertSame(2, SingletonClass::$count); // constructed twice, once before and once after reset
$this->assertTrue(ResettableDependency::$reset);
}
}
15 changes: 15 additions & 0 deletions packages/container/tests/Fixtures/ResettableDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Tempest\Container\Tests\Fixtures;

use Tempest\Container\Resettable;

final class ResettableDependency implements Resettable
{
public static bool $reset = false;

public function reset(): void
{
self::$reset = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function test_config_options(): void

$this->assertSame(
['cost' => 10],
(new BcryptConfig(cost: 10))->options,
new BcryptConfig(cost: 10)->options,
);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/discovery/src/BootDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tempest\Discovery;

use AssertionError;
use Pest\Exceptions\InvalidPestCommand;
use Psr\Container\ContainerInterface;
use Tempest\Container\GenericContainer;
use Tempest\Reflection\ClassReflector;
Expand Down Expand Up @@ -200,7 +201,7 @@ private function discoverPath(string $input, DiscoveryLocation $location, array
} elseif (class_exists($className)) {
$input = new ClassReflector($className);
}
} catch (AssertionError|\Pest\Exceptions\InvalidPestCommand) { // @phpstan-ignore class.notFound
} catch (AssertionError|InvalidPestCommand) { // @phpstan-ignore class.notFound
// Workaround for Pest test files autoloading.
// @mago-expect lint:no-empty-catch-clause
}
Expand Down
8 changes: 1 addition & 7 deletions packages/discovery/src/DiscoveryConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,7 @@ public function shouldSkip(string $input): bool
return true;
}

foreach ($this->skipUsing as $closure) {
if ($closure($input) === true) {
return true;
}
}

return false;
return array_any($this->skipUsing, fn ($closure) => $closure($input) === true);
}

/** @param (Closure(string): bool) $closure */
Expand Down
15 changes: 15 additions & 0 deletions tests/Integration/Container/Commands/ContainerShowCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Tempest\Container\Commands\ContainerShowCommand;
use Tempest\Container\Container;
use Tempest\Reflection\ClassReflector;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
use UnitEnum;

Expand Down Expand Up @@ -83,6 +84,20 @@ public function addDecorator(mixed $decoratorClass, mixed $decoratedClass): self

return $this;
}

public function addResettable(string|ClassReflector $resettableClass): Container
{
$this->container->addResettable($resettableClass);

return $this;
}

public function reset(): Container
{
$this->container->reset();

return $this;
}
},
);

Expand Down
Loading