From 491d0b91992eef6ef2e61e537b432e353c8bcced Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:38:36 +0000 Subject: [PATCH 1/8] Add transient container lifetimes Hypervel auto-singletons unbound concrete classes for the worker lifetime. Add an inherited Transient marker for mutable class hierarchies whose unbound resolutions must remain fresh. Exclude transient concretes from both shared-resolution coordination and auto-singleton publication. Explicit singleton, scoped, bind, instance, attribute, alias, extender, callback, and parameterized-resolution behavior remains authoritative. --- src/container/src/Container.php | 11 +++++++---- src/contracts/src/Container/Transient.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/contracts/src/Container/Transient.php diff --git a/src/container/src/Container.php b/src/container/src/Container.php index 30c52356c6..8d58b59a84 100755 --- a/src/container/src/Container.php +++ b/src/container/src/Container.php @@ -16,6 +16,7 @@ use Hypervel\Contracts\Container\ContextualAttribute; use Hypervel\Contracts\Container\ContextualBindingBuilder as ContextualBindingBuilderContract; use Hypervel\Contracts\Container\SelfBuilding; +use Hypervel\Contracts\Container\Transient; use Hypervel\Support\ClassMetadataCache; use Hypervel\Support\Traits\ReflectsClosures; use InvalidArgumentException; @@ -1225,15 +1226,16 @@ protected function resolve(string $abstract, array $parameters = [], bool $raise } } elseif ($raiseEvents && ! isset($this->bindings[$abstract]) && is_string($concrete) && class_exists($concrete) && ! is_a($concrete, SelfBuilding::class, true) + && ! is_a($concrete, Transient::class, true) ) { // Auto-singleton: unbound concrete classes are cached for Swoole performance. // In Swoole's long-running process model, services are stateless singletons // by design. Re-creating them on every resolution wastes CPU and memory. // // Explicit bind() overrides this — bound classes follow their binding type. - // SelfBuilding classes are excluded — they control their own construction - // via newInstance() and typically read runtime state (config, request data) - // that may change between resolutions. Use explicit singleton() to opt in. + // SelfBuilding classes control their own construction, while Transient + // classes declare that every unbound resolution requires a fresh instance. + // Use an explicit singleton() binding to opt either lifetime into caching. // // Skipped when raiseEvents is false (internal binding resolution via getClosure) // so that concretes resolved as part of scoped/singleton bindings don't get @@ -1330,7 +1332,8 @@ protected function shouldCoordinateSharedResolution( && ! isset($this->bindings[$abstract]) && is_string($concrete) && class_exists($concrete) - && ! is_a($concrete, SelfBuilding::class, true); + && ! is_a($concrete, SelfBuilding::class, true) + && ! is_a($concrete, Transient::class, true); } /** diff --git a/src/contracts/src/Container/Transient.php b/src/contracts/src/Container/Transient.php new file mode 100644 index 0000000000..bf139098fd --- /dev/null +++ b/src/contracts/src/Container/Transient.php @@ -0,0 +1,12 @@ + Date: Tue, 25 Aug 2026 23:38:41 +0000 Subject: [PATCH 2/8] Cover transient container resolution Pin fresh make and PSR get behavior, inherited marker semantics, explicit singleton, scoped, and instance precedence, and per-instance extenders. Exercise concurrent transient construction through a yielding dependency to prove that transient misses neither coordinate nor converge on a worker-shared object. --- tests/Container/ContainerTest.php | 78 +++++++++++++++++++++++++ tests/Container/CoroutineSafetyTest.php | 48 +++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index ae1f59f549..062a3c00d0 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -15,6 +15,7 @@ use Hypervel\Contracts\Container\CircularDependencyException; use Hypervel\Contracts\Container\ContextualAttribute; use Hypervel\Contracts\Container\SelfBuilding; +use Hypervel\Contracts\Container\Transient; use Hypervel\Foundation\Application; use Hypervel\Tests\TestCase; use InvalidArgumentException; @@ -1480,6 +1481,74 @@ public function testAutoSingletonCachesUnboundConcreteClass() $this->assertSame($first, $second); } + public function testTransientClassIsNotAutoSingletoned(): void + { + $container = new Container; + + $first = $container->make(TransientStub::class); + $second = $container->get(TransientStub::class); + + $this->assertNotSame($first, $second); + } + + public function testTransientLifetimeIsInheritedBySubclasses(): void + { + $container = new Container; + + $first = $container->make(TransientChildStub::class); + $second = $container->make(TransientChildStub::class); + + $this->assertNotSame($first, $second); + } + + public function testTransientClassCanBeExplicitlySingletoned(): void + { + $container = new Container; + $container->singleton(TransientStub::class); + + $first = $container->make(TransientStub::class); + $second = $container->make(TransientStub::class); + + $this->assertSame($first, $second); + } + + public function testTransientClassCanBeExplicitlyScoped(): void + { + $container = new Container; + $container->scoped(TransientStub::class); + + $first = $container->make(TransientStub::class); + $second = $container->make(TransientStub::class); + + $this->assertSame($first, $second); + } + + public function testTransientClassCanBeRegisteredAsAnInstance(): void + { + $container = new Container; + $instance = new TransientStub; + $container->instance(TransientStub::class, $instance); + + $this->assertSame($instance, $container->make(TransientStub::class)); + } + + public function testTransientClassExtendersRunForEveryInstance(): void + { + $container = new Container; + $container->extend(TransientStub::class, function (TransientStub $instance): TransientStub { + $instance->marks[] = 'extended'; + + return $instance; + }); + + $first = $container->make(TransientStub::class); + $second = $container->make(TransientStub::class); + + $this->assertNotSame($first, $second); + $this->assertSame(['extended'], $first->marks); + $this->assertSame(['extended'], $second->marks); + } + public function testAutoSingletonSkippedWhenParametersProvided() { $container = new Container; @@ -1927,6 +1996,15 @@ public function __construct( } } +class TransientStub implements Transient +{ + public array $marks = []; +} + +class TransientChildStub extends TransientStub +{ +} + class SelfBuildingCounterStub implements SelfBuilding { public function __construct( diff --git a/tests/Container/CoroutineSafetyTest.php b/tests/Container/CoroutineSafetyTest.php index 5527f9a9ee..a3654067a8 100644 --- a/tests/Container/CoroutineSafetyTest.php +++ b/tests/Container/CoroutineSafetyTest.php @@ -8,6 +8,7 @@ use Hypervel\Container\SharedResolution; use Hypervel\Contracts\Container\BindingResolutionException; use Hypervel\Contracts\Container\CircularDependencyException; +use Hypervel\Contracts\Container\Transient; use Hypervel\Tests\TestCase; use RuntimeException; use stdClass; @@ -346,6 +347,43 @@ public function testConcurrentAutoSingletonConstructionConverges(): void $this->assertSame($results['owner'], $results['waiter']); } + public function testConcurrentTransientConstructionDoesNotCoordinateOrShare(): void + { + $container = new Container; + $dependencyEntered = new Channel(2); + $releaseDependency = new Channel(2); + + $container->bind(CoroutineCoordinatedDependency::class, function () use ($dependencyEntered, $releaseDependency) { + $dependencyEntered->push(true); + $releaseDependency->pop(); + + return new CoroutineCoordinatedDependency; + }); + CoroutineTransientService::$constructions = 0; + + try { + $results = parallel([ + 'first' => fn () => $container->make(CoroutineTransientService::class), + 'second' => fn () => $container->make(CoroutineTransientService::class), + 'release' => function () use ($dependencyEntered, $releaseDependency): bool { + $firstEntered = $dependencyEntered->pop(1); + $secondEntered = $dependencyEntered->pop(1); + $releaseDependency->push(true); + $releaseDependency->push(true); + + return $firstEntered === true && $secondEntered === true; + }, + ]); + $constructions = CoroutineTransientService::$constructions; + } finally { + CoroutineTransientService::$constructions = 0; + } + + $this->assertTrue($results['release']); + $this->assertSame(2, $constructions); + $this->assertNotSame($results['first'], $results['second']); + } + public function testConcurrentFailureFansOutAndAllowsRetry(): void { $container = new CoroutineInspectingContainer; @@ -685,3 +723,13 @@ public function __construct(public readonly CoroutineCoordinatedDependency $depe ++self::$constructions; } } + +class CoroutineTransientService implements Transient +{ + public static int $constructions = 0; + + public function __construct(public readonly CoroutineCoordinatedDependency $dependency) + { + ++self::$constructions; + } +} From 8b0f1d7f39292703830386a50f9feb6433d120d7 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:38:47 +0000 Subject: [PATCH 3/8] Keep injected Eloquent models fresh Make Eloquent Model implement the inherited Transient lifetime so application models resolved through the container cannot retain mutable state across requests or coroutines. Cover the ResolvesRouteDependencies path directly. Before this change, a controller-injected model could carry exists=true and prior attributes into the next request, allowing an intended insert to update the previous request's row. The regression also proves ordinary unbound services remain auto-singletoned. --- src/database/src/Eloquent/Model.php | 3 +- tests/Routing/RouteDependencyResolverTest.php | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/Routing/RouteDependencyResolverTest.php diff --git a/src/database/src/Eloquent/Model.php b/src/database/src/Eloquent/Model.php index b7678aa7e0..ebce9ccdf5 100644 --- a/src/database/src/Eloquent/Model.php +++ b/src/database/src/Eloquent/Model.php @@ -8,6 +8,7 @@ use Closure; use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Broadcasting\HasBroadcastChannel; +use Hypervel\Contracts\Container\Transient; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Queue\QueueableCollection; use Hypervel\Contracts\Queue\QueueableEntity; @@ -51,7 +52,7 @@ use function Hypervel\Support\enum_value; -abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, Stringable, UrlRoutable +abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, Stringable, Transient, UrlRoutable { use Concerns\HasAttributes; use Concerns\HasEvents; diff --git a/tests/Routing/RouteDependencyResolverTest.php b/tests/Routing/RouteDependencyResolverTest.php new file mode 100644 index 0000000000..697e4d01f0 --- /dev/null +++ b/tests/Routing/RouteDependencyResolverTest.php @@ -0,0 +1,41 @@ +exists || $model->getAttribute('leaked') !== null; + $model->exists = true; + $model->setAttribute('leaked', true); + + return [$modelWasDirty, ++$service->hits]; + }; + $route = new Route('GET', '/injected-model', $action); + $route->bind(Request::create('/injected-model')); + + $this->assertSame([false, 1], $dispatcher->dispatch($route, $action)); + $this->assertSame([false, 2], $dispatcher->dispatch($route, $action)); + } +} + +class InjectedRouteModel extends Model +{ +} + +class InjectedRouteService +{ + public int $hits = 0; +} From c123c5cfc9a839d13eb602269540f110708005cf Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:38:54 +0000 Subject: [PATCH 4/8] Cover fresh implicit model binding Exercise two implicit bindings through the same container with a stateful model receiver. Each binding must resolve a fresh model so custom route-binding state cannot leak into a later request. --- tests/Routing/ImplicitRouteBindingTest.php | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Routing/ImplicitRouteBindingTest.php b/tests/Routing/ImplicitRouteBindingTest.php index e1f0184771..4963f10312 100644 --- a/tests/Routing/ImplicitRouteBindingTest.php +++ b/tests/Routing/ImplicitRouteBindingTest.php @@ -14,6 +14,7 @@ use Hypervel\Tests\Routing\Fixtures\CategoryBackedEnum; use Hypervel\Tests\Routing\Fixtures\CategoryEnum; use Hypervel\Tests\Routing\RoutingTestCase; +use LogicException; use ReflectionProperty; use WeakMap; @@ -144,6 +145,24 @@ public function testItCanResolveTheImplicitModelRouteBindingsForTheGivenRoute(): ImplicitRouteBinding::resolveForRoute($container, $route); } + public function testItUsesAFreshModelForEachImplicitRouteBinding(): void + { + $container = Container::getInstance(); + + foreach ([1, 2] as $identifier) { + $action = ['uses' => function (FreshImplicitRouteBindingUser $user) { + return $user; + }]; + $route = new Route('GET', '/test/{user}', $action); + $route->bind(Request::create("/test/{$identifier}")); + $route->prepareForSerialization(); + + ImplicitRouteBinding::resolveForRoute($container, $route); + + $this->assertSame($identifier, $route->parameter('user')->getKey()); + } + } + public function testItResolvesInvokableObjectSignatureParameters(): void { $route = new Route( @@ -217,6 +236,22 @@ class ImplicitRouteBindingUser extends Model { } +class FreshImplicitRouteBindingUser extends Model +{ + private bool $resolved = false; + + public function resolveRouteBinding(mixed $value, ?string $field = null): ?self + { + if ($this->resolved) { + throw new LogicException('The route binding model was reused.'); + } + + $this->resolved = true; + + return (new static)->setAttribute($this->getRouteKeyName(), $value); + } +} + class EmptyParameterRoute extends Route { public int $signatureParameterCalls = 0; From 6ca2e5323eb438e71e06532deef5e2aa172a955b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:39:03 +0000 Subject: [PATCH 5/8] Cover fresh explicit model binding Exercise repeated RouteBinding::forModel calls through one container with a receiver that rejects reuse. This pins fresh Eloquent model construction for explicit model binders as well as implicit bindings and injected route dependencies. --- tests/Routing/RouteBindingTest.php | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Routing/RouteBindingTest.php b/tests/Routing/RouteBindingTest.php index 8f5e2d3e82..1393c45e4e 100644 --- a/tests/Routing/RouteBindingTest.php +++ b/tests/Routing/RouteBindingTest.php @@ -11,6 +11,7 @@ use Hypervel\Routing\Route; use Hypervel\Routing\RouteBinding; use Hypervel\Tests\Routing\RoutingTestCase; +use LogicException; class RouteBindingTest extends RoutingTestCase { @@ -25,6 +26,20 @@ public function testItCanResolveTheExplicitModelForTheGivenRoute() $this->assertInstanceOf(ExplicitRouteBindingUser::class, $callback(1, $route)); } + public function testItUsesAFreshModelForEachExplicitRouteBinding(): void + { + $container = Container::getInstance(); + $route = new Route('GET', '/users/{user}', function () { + }); + $callback = RouteBinding::forModel($container, FreshExplicitRouteBindingUser::class); + + $first = $callback(1, $route); + $second = $callback(2, $route); + + $this->assertSame(1, $first->getKey()); + $this->assertSame(2, $second->getKey()); + } + public function testItCannotResolveTheExplicitSoftDeletedModelForTheGivenRoute() { $container = Container::getInstance(); @@ -58,6 +73,22 @@ public function resolveRouteBinding(mixed $value, ?string $field = null): ?self } } +class FreshExplicitRouteBindingUser extends Model +{ + private bool $resolved = false; + + public function resolveRouteBinding(mixed $value, ?string $field = null): ?self + { + if ($this->resolved) { + throw new LogicException('The route binding model was reused.'); + } + + $this->resolved = true; + + return (new static)->setAttribute($this->getRouteKeyName(), $value); + } +} + class ExplicitRouteBindingSoftDeletableUser extends Model { use SoftDeletes; From 57d24761237b09c1882a4bf15a02684aefd60a11 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:39:14 +0000 Subject: [PATCH 6/8] Document transient container lifetimes Describe when to use the Transient marker, how explicit registrations retain precedence, and why Eloquent models inherit the lifetime without changing query hydration or metadata caches. Call out captive transient dependencies in longer-lived consumers and add the concise migration signal to the existing Laravel container-lifecycle guidance. --- src/docs/container.md | 37 +++++++++++++++++++++++++++----- src/docs/porting-from-laravel.md | 3 ++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/docs/container.md b/src/docs/container.md index ff1aba0b16..69a145c91a 100644 --- a/src/docs/container.md +++ b/src/docs/container.md @@ -18,6 +18,7 @@ - [Resolving](#resolving) - [The Make Method](#the-make-method) - [Forcing a Fresh Instance](#forcing-a-fresh-instance) + - [Transient Classes](#transient-classes) - [Self-Building Classes](#self-building-classes) - [Automatic Injection](#automatic-injection) - [Method Invocation and Injection](#method-invocation-and-injection) @@ -88,7 +89,7 @@ In this example, hitting your application's `/` route will automatically resolve Thankfully, many of the classes you will be writing when building a Hypervel application automatically receive their dependencies via the container, including [controllers](/docs/{{version}}/controllers), [event listeners](/docs/{{version}}/events), [middleware](/docs/{{version}}/middleware), and more. Additionally, you may type-hint dependencies in the `handle` method of [queued jobs](/docs/{{version}}/queues). Once you taste the power of automatic and zero configuration dependency injection it feels impossible to develop without it. > [!NOTE] -> Hypervel will auto-singleton (automatically cache) unbound concrete classes for the worker's lifetime — the first resolution of `Service` constructs an instance, and every subsequent resolution returns that same instance until the worker restarts. This is the right default for stateless services. Classes whose constructors capture per-call state should be [bound explicitly](#binding) or resolved with [`build()`](#forcing-a-fresh-instance). +> Hypervel will auto-singleton (automatically cache) unbound concrete classes for the worker's lifetime — the first resolution of `Service` constructs an instance, and every subsequent resolution returns that same instance until the worker restarts. This is the right default for stateless services. Classes whose constructors capture per-call state should be [bound explicitly](#binding), resolved with [`build()`](#forcing-a-fresh-instance), or marked as [transient](#transient-classes) when freshness is part of the class's design. ### When to Utilize the Container @@ -116,8 +117,9 @@ Because Hypervel runs inside a long-running Swoole worker, instance lifecycles a |---|---|---| | Fresh instance every call, ignoring all bindings and caches | `build($class)` | Always constructs a new instance. Nested constructor dependencies are still resolved through the container. | | Fresh instance with parameter overrides | `buildWith($class, $params)` | Same as `build()` but applies the given parameter overrides during construction. | +| Class hierarchy always requires a fresh instance | `implements Transient` | Builds a new instance for every unbound `make()` or `get()` call. Explicit bindings still determine the lifetime when present. | | Class declares its own factory | `implements SelfBuilding` + static `newInstance()` | Container invokes the static factory with DI on its parameters. Skips auto-singletoning by default; honors any explicit `singleton()` / `scoped()` binding. | -| Resolve respecting bindings and caching | `make($class)` | Honors `bind()` / `singleton()` / `scoped()`. Auto-singletons unbound concrete classes for the worker's lifetime. | +| Resolve respecting bindings and caching | `make($class)` | Honors `bind()` / `singleton()` / `scoped()`. Auto-singletons unbound concrete classes unless they implement `Transient` or `SelfBuilding`. | | Resolve with parameter overrides | `make($class, $params)` / `makeWith()` | Same as `make()` but contextual parameters bypass all caching. | | One instance per worker | `$app->singleton($abstract, ...)` or `#[Singleton]` | Cached for the worker's lifetime. Lives until the worker restarts. | | One instance per coroutine (per request / job) | `$app->scoped($abstract, ...)` or `#[Scoped]` | Cached in [CoroutineContext](/docs/{{version}}/coroutine-context) for the lifetime of the coroutine handling the request or job. | @@ -134,6 +136,7 @@ Most application code does not pick a lifecycle deliberately — it just type-hi - **Service that should hold per-request state** (request-derived caches, accumulated context): `scoped()`. The instance dies at the end of the coroutine (request / job). - **Service that should be built once and shared for the worker's lifetime** (heavy bootstrap, immutable configuration): `singleton()` is explicit; auto-singletoning achieves the same thing for unbound classes. - **Object that takes per-call inputs in its constructor** (builders, view components, form-request-style classes): `bind()` so each `make()` returns fresh, or skip the binding entirely and call `build()` / `buildWith()` at the resolution site. +- **Class hierarchy whose instances are always mutable and independent** (Eloquent models and similar value holders): implement `Transient` once on the base class so every unbound resolution is fresh. ### Per-Call State on Shared Instances @@ -158,11 +161,11 @@ $report = $this->app->make(ReportBuilder::class); $report = $this->app->build(ReportBuilder::class); ``` -Alternatively, mark the class with [`SelfBuilding`](#self-building-classes) and leave it unbound — every `make()` will then call `newInstance` and rebuild the instance from scratch. +When freshness is part of the class's design, implement [`Transient`](#transient-classes). If the class also needs to control how it is constructed, use [`SelfBuilding`](#self-building-classes) instead. The same caution applies to mutating state on a worker-lifetime singleton at runtime — anything you assign to `$this->foo` on a shared instance persists across every request that worker handles. For per-request state that lives on a shared service, use [CoroutineContext](/docs/{{version}}/coroutine-context) instead of instance properties. -Framework code that ships with Hypervel — view components, form requests, and so on — already routes through the fresh-instance path when needed, so you only need to think about this for your own classes. +Framework code that ships with Hypervel already chooses the required lifecycle. For example, every Eloquent model is transient, form requests are self-building, and view components use the fresh-instance path. ## Binding @@ -218,7 +221,7 @@ App::bind(function (Application $app): Transistor { ``` > [!NOTE] -> You don't need to bind classes the container can resolve via reflection. Hypervel will auto-singleton the resolved instance for the worker's lifetime, which is the right behavior for stateless services. Bind the class explicitly with `bind()` if you need a fresh instance per call, or call [`build()`](#forcing-a-fresh-instance) at the resolution site. +> You don't need to bind classes the container can resolve via reflection. Hypervel will auto-singleton the resolved instance for the worker's lifetime, which is the right behavior for stateless services. Bind the class explicitly with `bind()` if one registration needs a fresh instance per call, call [`build()`](#forcing-a-fresh-instance) at the resolution site, or implement [`Transient`](#transient-classes) when every unbound resolution of the class hierarchy must be fresh. #### Binding A Singleton @@ -747,6 +750,30 @@ Nested constructor dependencies are still resolved through the container, so the `buildWith` is the right choice when a class needs parameter overrides and must not be cached, for example a builder object or a class whose constructor captures per-call state. Internally, Hypervel uses this method to instantiate view components so each render gets a fresh instance even though the component class has no explicit binding. + +### Transient Classes + +When every instance of a class hierarchy is mutable and independent, implement the `Hypervel\Contracts\Container\Transient` marker interface on its base class. Unbound transient classes are constructed for every `make()` and `get()` call while retaining normal dependency injection, aliases, extenders, and resolving callbacks: + +```php + ### Self-Building Classes diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index 208392a656..e9279e6d86 100644 --- a/src/docs/porting-from-laravel.md +++ b/src/docs/porting-from-laravel.md @@ -405,10 +405,11 @@ Container lifecycles are adapted for Swoole: | One instance per request or job coroutine | `scoped()` | | One instance per worker | `singleton()` | | Fresh instance at the call site | `build()` or `buildWith()` | +| Fresh instance for every unbound resolution of a class hierarchy | Implement `Hypervel\Contracts\Container\Transient` on its base class | | Resolve using bindings and lifecycle rules | `make()` | > [!WARNING] -> Unbound concrete classes are automatically cached for the worker lifetime after their first resolution. If an unbound class captures the current user, tenant, request, or other mutable per-request data in its constructor, ordinary tests may pass while concurrent requests receive another request's state. Register the class with `bind()` for a fresh instance, use `scoped()` for one instance per request or job coroutine, or construct a fresh instance with `build()`. +> Unbound concrete classes are automatically cached for the worker lifetime after their first resolution. If an unbound class captures the current user, tenant, request, or other mutable per-request data in its constructor, ordinary tests may pass while concurrent requests receive another request's state. Register the class with `bind()` for a fresh instance, use `scoped()` for one instance per request or job coroutine, construct a fresh instance with `build()`, or implement `Transient` when every subclass must always be fresh. Eloquent models already implement `Transient`. ### Coroutine-Aware Dependencies From 1c2387fb39f38017d86ecfa80222fc922719d61c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:39:22 +0000 Subject: [PATCH 7/8] Clarify intrinsic transient lifetimes Teach future framework work that unbound Transient and SelfBuilding classes bypass auto-singletoning, while explicit registrations still decide their lifetime. Record that Eloquent models inherit Transient and include the marker in container binding and worker-state guidance so new mutable hierarchies use the lowest correct lifetime boundary. --- AGENTS.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1033661a75..82d1bd022b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -220,7 +220,7 @@ Hypervel's container keeps Laravel's named API surface — `bind()`, `singleton( ### Resolution semantics vs Laravel -The critical difference: **unbound concrete classes are auto-singletoned**. In Laravel, `make()` on a class with no binding builds a fresh instance every call. In Hypervel, the first resolution caches the instance (in `$autoSingletons`) for the worker lifetime — in Swoole's long-running process model services are stateless singletons by design, and re-creating them on every resolution wastes CPU and memory. Explicit bindings override this (bound classes follow their binding type), and `SelfBuilding` classes are excluded. +The critical difference: **unbound concrete classes are auto-singletoned**. In Laravel, `make()` on a class with no binding builds a fresh instance every call. In Hypervel, the first resolution caches the instance (in `$autoSingletons`) for the worker lifetime — in Swoole's long-running process model services are stateless singletons by design, and re-creating them on every resolution wastes CPU and memory. Explicit bindings override this (bound classes follow their binding type), and `SelfBuilding` and `Transient` classes are excluded. | Registration | Laravel | Hypervel | |---|---|---| @@ -231,19 +231,20 @@ The critical difference: **unbound concrete classes are auto-singletoned**. In L | `make($abstract, $params)` / `makeWith()` | Contextual build — never cached | Same — parameters bypass the singleton, scoped, and auto-singleton caches | | `build($concrete)` | Constructs the concrete directly, bypassing bindings and caches for the top-level class | Same; Hypervel adds `buildWith($concrete, $params)`. Nested constructor dependencies still resolve through the container | | `implements SelfBuilding` | Container calls the class's static `newInstance()` | Same, and it also skips auto-singletoning | +| `implements Transient` | No equivalent marker | Unbound resolutions are always fresh; explicit bindings still determine the lifetime. Eloquent models inherit this marker from `Model` | Rules that follow from this: -- **Classes that capture per-request data in their constructor or accumulate mutable state must not be auto-singletoned** — pick the correct lifetime: `scoped()` for one instance per coroutine/request, `bind()` for a fresh instance per resolution, `build()`/`buildWith()` for direct construction at the call site, or `SelfBuilding` for class-controlled construction. An existing class like that being auto-singletoned is a coroutine-safety bug — STOP and report it. +- **Classes that capture per-request data in their constructor or accumulate mutable state must not be auto-singletoned** — pick the correct lifetime: `scoped()` for one instance per coroutine/request, `bind()` for a fresh instance per resolution, `build()`/`buildWith()` for direct construction at the call site, `SelfBuilding` for class-controlled construction, or `Transient` when freshness is intrinsic to the whole class hierarchy. An existing class like that being auto-singletoned is a coroutine-safety bug — STOP and report it. - **Most classes are safe as auto-singletons:** services, middleware, listeners, factories, formatters — stateless or process-global by nature. - **Do not use `build()` as a drop-in freshness replacement for `make()`** when explicit bindings, test swaps, aliases, or resolving callbacks must be honored — it bypasses top-level binding lookups, aliases, and caches by design. ### Choosing a binding type - Stateless and shared for the worker lifetime → `singleton()`. -- Fresh mutable object per resolution → `bind()`. +- Fresh mutable object per resolution → `bind()`, or `Transient` when that lifetime is intrinsic to the whole class hierarchy. - State isolated per coroutine / request → `scoped()`. -- Concrete class with no separate abstract → don't bind it at all; auto-singletoning covers it. +- Concrete class with no separate abstract → don't bind it at all; auto-singletoning covers it unless the class implements `Transient` or `SelfBuilding`. ### Binding patterns @@ -264,7 +265,7 @@ $this->app->singleton('auth', fn ($app) => new AuthManager($app)); $this->app->singleton(FormatterInterface::class, DefaultFormatter::class); ``` -**3. Abstract and concrete are the same class — do not bind at all.** Hypervel's container auto-singletons unbound concrete classes on first resolution. An explicit `singleton(Foo::class)` is redundant: +**3. Abstract and concrete are the same class — do not bind merely to share it.** Hypervel's container auto-singletons unbound concrete classes on first resolution. An explicit `singleton(Foo::class)` is redundant unless the class declares an intrinsic fresh lifetime through `Transient` or `SelfBuilding`: ```php // Wrong: redundant; auto-singleton handles this. @@ -345,7 +346,7 @@ Decide where state lives before writing code: | Immutable metadata shared by all requests | Static property cache or worker-lifetime singleton | | Stateless service shared by all requests | `singleton()` or auto-singleton (see Container) | | Mutable state for one request, operation, or coroutine | `CoroutineContext` or a `scoped()` binding | -| Fresh mutable object per resolution | `bind()` or contextual parameters | +| Fresh mutable object per resolution | `bind()`, contextual parameters, or `Transient` for an intrinsically fresh class hierarchy | - **Use `Hypervel\Context\CoroutineContext` for invocation-scoped state** — anything that must not be visible to other concurrent coroutines in the same worker. Static properties and singleton fields leak across coroutines: whatever one coroutine sets becomes visible to all others in the worker. Use the established key-naming convention: `__.` value prefix, `_CONTEXT_KEY` / `_CONTEXT_KEY_PREFIX` constant suffixes, public only when other classes or tests reference the constant. Do not use `Hypervel\Support\Facades\Context` as the low-level coroutine store; it provides Laravel-style application context instead. - **Configure process-global values only during worker boot** — config is a process-global singleton, so `Config::set()` during request handling changes behavior for every concurrent request in the worker. Never mutate config for request-specific behavior; use `CoroutineContext` or middleware instead. Provider boot-time configuration is fine — it runs once per worker. From 8da81f14d68fb37da1729c16b7cc47460c03bdbd Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:48:48 +0000 Subject: [PATCH 8/8] Qualify transient model resolution Clarify that Eloquent models receive fresh unbound resolutions while explicit container registrations remain free to select singleton, scoped, bound, or instance lifetimes. This aligns the Eloquent example with the precedence rule documented immediately above it without changing container behavior. --- src/docs/container.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/container.md b/src/docs/container.md index 69a145c91a..99eaf85fba 100644 --- a/src/docs/container.md +++ b/src/docs/container.md @@ -772,7 +772,7 @@ Use `Transient` only when freshness belongs to the class itself and every subcla A transient dependency injected into a longer-lived service is retained by that service. If the service needs a fresh instance for each operation, resolve the transient dependency at the call site instead of injecting it through the constructor. -Hypervel's Eloquent `Model` implements `Transient`, so resolving an application model through the container always returns a fresh model. Query hydration and Eloquent's shared model metadata caches use their existing optimized paths. +Hypervel's Eloquent `Model` implements `Transient`, so application models use fresh unbound resolutions unless an explicit container registration selects another lifetime. Query hydration and Eloquent's shared model metadata caches use their existing optimized paths. ### Self-Building Classes