Skip to content
Draft
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
39 changes: 38 additions & 1 deletion docs/1-essentials/05-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ final readonly class Client
}
```

Furthermore, an initializer method can be annotated as a `#[Singleton]`, meaning its return object will only ever be resolved once:
Furthermore, an initializer method can be annotated as a `#[Singleton]`, meaning its return object will be reused for the singleton’s lifetime:

```php app/MarkdownInitializer.php
use Tempest\Console\ConsoleCommand;
Expand All @@ -241,6 +241,43 @@ final readonly class MarkdownInitializer implements Initializer
}
```

### Singleton lifetimes

By default, singletons use {`Tempest\Container\Lifetime::PROCESS`} as their lifetime: the container keeps the same instance for the lifetime of the process, including across requests in long-running applications.

For objects that hold request-specific state, you can specify `Lifetime::REQUEST` instead:

```php
use Tempest\Container\Lifetime;
use Tempest\Container\Singleton;

#[Singleton(lifetime: Lifetime::REQUEST)]
final class RequestContext
{
public ?string $tenantId = null;
}
```

The container reuses this instance within a request. When the container is reset, it clears the instance so that the next request receives a fresh one, this happens after every request in long-running applications.

Note that you can also declare the lifetime via initializers:

```php
use Tempest\Container\Container;
use Tempest\Container\Initializer;
use Tempest\Container\Lifetime;
use Tempest\Container\Singleton;

final class RequestContextInitializer implements Initializer
{
#[Singleton(lifetime: Lifetime::REQUEST)]
public function initialize(Container $container): RequestContext
{
return new RequestContext();
}
}
```

### Tagged singletons

In some cases, you want more control over singleton definitions.
Expand Down
1 change: 1 addition & 0 deletions packages/console/tests/TerminalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public function cursor_is_hidden_while_redrawing_content(): void
$terminal = new Terminal($console);
$terminal->disableTty();
$terminal->cursor = $cursor;

$events = [];

iterator_to_array($terminal->render(new TextInputComponent(label: 'Name')));
Expand Down
33 changes: 30 additions & 3 deletions packages/container/src/GenericContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Tempest\Reflection\TypeReflector;
use Throwable;
use UnitEnum;
use WeakMap;

use const ARRAY_FILTER_USE_BOTH;

Expand All @@ -31,6 +32,9 @@ final class GenericContainer implements Container
/** @var array<class-string<DynamicInitializer>, DynamicInitializer> */
private array $resolvedDynamicInitializers = [];

/** @var WeakMap<object, Lifetime> */
private WeakMap $singletonLifetimes;

public function __construct(
/** @var ArrayIterator<array-key, mixed> $definitions */
private(set) ArrayIterator $definitions = new ArrayIterator(),
Expand All @@ -55,6 +59,7 @@ public function __construct(

private(set) ?DependencyChain $chain = null,
) {
$this->singletonLifetimes = new WeakMap();
$this->singleton(Container::class, $this);
$this->singleton(ContainerInterface::class, $this);
$this->singleton(GenericContainer::class, $this);
Expand Down Expand Up @@ -407,9 +412,12 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag
$initializer instanceof DynamicInitializer => $initializer->initialize($class, $tag, $this->clone()),
};

$singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
$singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass
->getMethod('initialize')
->getAttribute(Singleton::class) ?? $class->getAttribute(Singleton::class);

if ($singleton !== null) {
$this->singletonLifetimes[$object] = $singleton->lifetime;
$this->singleton($className, $object, $tag);
}

Expand Down Expand Up @@ -506,8 +514,9 @@ private function autowire(string $className, string|UnitEnum|null $tag, mixed ..
if (
! $classReflector->getType()->matches(Initializer::class)
&& ! $classReflector->getType()->matches(DynamicInitializer::class)
&& $classReflector->hasAttribute(Singleton::class)
&& ($singleton = $classReflector->getAttribute(Singleton::class)) !== null
) {
$this->singletonLifetimes[$instance] = $singleton->lifetime;
$this->singleton($className, $instance, $tag);
}

Expand Down Expand Up @@ -744,7 +753,6 @@ public function addResettable(string|ClassReflector $resettableClass): Container

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

foreach ($this->resettables as $resettableClass) {
Expand All @@ -754,6 +762,25 @@ public function reset(): self
$resettable->reset();
}

foreach ([$this->singletonDefinitions, $this->resolvedSingletons] as $singletons) {
foreach ($singletons->getArrayCopy() as $dependencyName => $instance) {
// Factories remain registered so the next request can create a fresh instance.
if (! is_object($instance)) {
continue;
}

if ($instance instanceof Closure) {
continue;
}

$this->singletonLifetimes[$instance] ??= new ClassReflector($instance)->getAttribute(Singleton::class)->lifetime ?? Lifetime::PROCESS;

if ($this->singletonLifetimes[$instance] === Lifetime::REQUEST) {
unset($singletons[$dependencyName]);
}
}
}

return $this;
}

Expand Down
14 changes: 14 additions & 0 deletions packages/container/src/Lifetime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Tempest\Container;

enum Lifetime
{
/** Keep a singleton alive as long as the process is running */
case PROCESS;

/** Keep a singleton alive within the lifetime of a request */
case REQUEST;
}
1 change: 1 addition & 0 deletions packages/container/src/Singleton.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
public function __construct(
public ?string $tag = null,
public bool $dynamicTags = false,
public Lifetime $lifetime = Lifetime::PROCESS,
) {}
}
Loading
Loading