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
13 changes: 7 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|
Expand All @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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: `__<package>.<key>` 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.
Expand Down
11 changes: 7 additions & 4 deletions src/container/src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/contracts/src/Container/Transient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Hypervel\Contracts\Container;

/**
* Mark a class as requiring a fresh instance for every unbound resolution.
*/
interface Transient
{
}
3 changes: 2 additions & 1 deletion src/database/src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 32 additions & 5 deletions src/docs/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

<a name="when-to-use-the-container"></a>
### When to Utilize the Container
Expand Down Expand Up @@ -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. |
Expand All @@ -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.

<a name="per-call-state-on-shared-instances"></a>
### Per-Call State on Shared Instances
Expand All @@ -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.

<a name="binding"></a>
## Binding
Expand Down Expand Up @@ -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.

<a name="binding-a-singleton"></a>
#### Binding A Singleton
Expand Down Expand Up @@ -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.

<a name="transient-classes"></a>
### 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
<?php

namespace App\Reports;

use Hypervel\Contracts\Container\Transient;

abstract class Report implements Transient
{
// ...
}
```

Use `Transient` only when freshness belongs to the class itself and every subclass should inherit that lifetime. For a single application registration, use `bind()` instead. Explicit `singleton()`, `scoped()`, `bind()`, and `instance()` registrations continue to control the lifetime of a transient class.

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 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.

<a name="self-building-classes"></a>
### Self-Building Classes

Expand Down
Loading