Context switching
Service lifetimes: Singleton, Scoped, Ephemeral
A service lives for the worker, for one request, or for one use. Choose by whose data it holds.
Dependency-injection containers name three lifetimes. Under PHP-FPM a singleton lived as long as the request, so the first two were the same. In a worker they are not.
| Lifetime | Lives for | In PHP | Typical services |
|---|---|---|---|
| Singleton | the worker | a static, a container singleton, an object built in swerve.php |
configuration, the router's table, a logger, a template engine, a connection pool |
| Scoped | one request | a WeakMap keyed by Scope::getCurrent() |
the current user, the request, the session, a unit of work, a per-request cache |
| Ephemeral | one use | new, a factory |
value objects, query builders, DTOs |
Choose by whose data the service holds. If it holds nothing but what it was built with, it is a singleton. If it holds anything that belongs to a request, it is scoped.
A scope helper#
phasync has no scope container. The pattern from per-request state is short enough to write once for your own services:
final class Scope
{
/** @var WeakMap<object, ArrayObject> */
private static WeakMap $scopes;
/** The instance of $class that belongs to the current request; $make creates it on first use. */
public static function get(string $class, ?Closure $make = null): object
{
self::$scopes ??= new WeakMap();
$scope = self::$scopes[Scope::getCurrent()] ??= new ArrayObject();
return $scope[$class] ??= ($make ?? throw new LogicException("No $class in this request"))();
}
}
Scope::get(CurrentUser::class, fn () => new CurrentUser('ann')); // at the start of the request
Scope::get(CurrentUser::class)->name; // anywhere later in itIn the frameworks#
Each framework spells the lifetimes its own way: Laravel's scoped(), Symfony's services and kernel.reset, Yii's reset callbacks. What the adapter does with them is on the framework's page: Laravel, Symfony, Yii.
Previous: Per-request state ยท Next: Captive dependencies