Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9492a69
Consolidate reflection metadata caching
binaryfire Jul 13, 2026
5a29d36
Correct closure type inference
binaryfire Jul 13, 2026
b4f928e
Harden reflector callable contracts
binaryfire Jul 13, 2026
8487a32
Complete lazy and proxy helper behavior
binaryfire Jul 13, 2026
90894b0
Verify reflection helper type inference
binaryfire Jul 13, 2026
0c012b9
Document lazy and proxy helpers
binaryfire Jul 13, 2026
c0b198a
Document reflection failure contracts
binaryfire Jul 13, 2026
e5aad36
Record completion of the reflection audit
binaryfire Jul 13, 2026
2db63d9
Merge branch '0.4' into audit/reflection-config-container
binaryfire Jul 14, 2026
16079c8
Clarify audit compatibility and complexity boundaries
binaryfire Jul 14, 2026
ea83be9
Harden configuration repository mutation boundaries
binaryfire Jul 14, 2026
c13fca7
Preserve configuration identity across worker reloads
binaryfire Jul 14, 2026
57a445a
Remove the Reverb config tracking workaround
binaryfire Jul 14, 2026
40b2c50
Record the completed Config audit
binaryfire Jul 14, 2026
9ebd59e
Harden container resolution and lifecycle semantics
binaryfire Jul 14, 2026
fb24b92
Cover container lifecycle and recipe regressions
binaryfire Jul 14, 2026
3fd8b11
Cover concurrent shared container resolution
binaryfire Jul 14, 2026
4c17098
Preserve container state when resolving callbacks fail
binaryfire Jul 14, 2026
a587d63
Correct callable dependency resolution
binaryfire Jul 14, 2026
bb2890c
Support enum authentication identifiers
binaryfire Jul 14, 2026
e2f2ebf
Support enum cache identifiers
binaryfire Jul 14, 2026
53def00
Preserve named logger behavior
binaryfire Jul 14, 2026
1f157ad
Complete contextual attribute resolution
binaryfire Jul 14, 2026
23462b9
Document current container capabilities
binaryfire Jul 14, 2026
aa9e035
Record the completed Container audit
binaryfire Jul 14, 2026
65fea4b
Merge branch '0.4' into audit/core-reflection-config-container
binaryfire Jul 14, 2026
f954a4b
Clarify configuration lifecycle boundaries
binaryfire Jul 14, 2026
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

Large diffs are not rendered by default.

56 changes: 39 additions & 17 deletions docs/plans/2026-07-12-framework-coroutine-state-lifecycle-audit.md

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions src/auth/src/AuthManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
use Hypervel\Contracts\Auth\StatefulGuard;
use Hypervel\Contracts\Container\Container;
use InvalidArgumentException;
use UnitEnum;

use function Hypervel\Support\enum_value;

/**
* @mixin Guard
Expand Down Expand Up @@ -59,8 +62,12 @@ public function __construct(
/**
* Attempt to get the guard from the local cache.
*/
public function guard(?string $name = null): Guard|StatefulGuard
public function guard(UnitEnum|string|null $name = null): Guard|StatefulGuard
{
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

$name ??= $this->getDefaultDriver();

return $this->guards[$name] ??= $this->resolve($name);
Expand Down Expand Up @@ -177,8 +184,12 @@ public function getDefaultDriver(): string
/**
* Set the default guard driver the factory should serve.
*/
public function shouldUse(?string $name): void
public function shouldUse(UnitEnum|string|null $name): void
{
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

$name ??= $this->getDefaultDriver();

$this->setDefaultDriver($name);
Expand All @@ -191,8 +202,12 @@ public function shouldUse(?string $name): void
*
* Uses coroutine Context so one request's override doesn't affect others.
*/
public function setDefaultDriver(string $name): void
public function setDefaultDriver(UnitEnum|string $name): void
{
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

CoroutineContext::set(self::DEFAULT_GUARD_CONTEXT_KEY, $name);
}

Expand Down
20 changes: 16 additions & 4 deletions src/boost/docs/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,21 +460,25 @@ class PhotoController extends Controller
{
public function __construct(
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Cache('redis', memo: true)] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[Context('uuid')] protected string $uuid,
#[Context('ulid', hidden: true)] protected string $ulid,
#[DB('mysql')] protected Connection $connection,
#[Give(DatabaseRepository::class)] protected UserRepository $users,
#[Log('daily')] protected LoggerInterface $log,
#[RouteParameter('photo')] protected Photo $photo,
#[Log('daily', name: 'photos')] protected LoggerInterface $log,
#[RouteParameter] protected Photo $photo,
#[Tag('reports')] protected iterable $reports,
) {
// ...
}
}
```

The `RouteParameter` attribute resolves the route parameter matching the variable name. If needed, you may specify the route parameter name explicitly: `#[RouteParameter('photo')]`.

Pass `memo: true` to `Cache` to inject a request-scoped memoized repository. The optional `name` argument on `Log` creates a named logger and is supported by Monolog-backed channels. Driver identifiers accepted by `Auth`, `Authenticated`, `Cache`, `Log`, and `Storage` may also be unit or backed enum cases; Hypervel uses the unit case name or backed value as the identifier.

Furthermore, Hypervel provides `CurrentUser` and `Authenticated` attributes for injecting the currently authenticated user into a given route or class. `CurrentUser` requires that a user is authenticated; `Authenticated` returns `null` when no user is authenticated, which is useful for optional auth:

```php
Expand Down Expand Up @@ -505,6 +509,7 @@ namespace App\Attributes;
use Attribute;
use Hypervel\Contracts\Container\Container;
use Hypervel\Contracts\Container\ContextualAttribute;
use ReflectionParameter;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
Expand All @@ -521,15 +526,22 @@ class Config implements ContextualAttribute
*
* @param self $attribute
* @param \Hypervel\Contracts\Container\Container $container
* @param \ReflectionParameter $parameter
* @return mixed
*/
public static function resolve(self $attribute, Container $container)
public static function resolve(
self $attribute,
Container $container,
ReflectionParameter $parameter,
): mixed
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}
```

The current `ReflectionParameter` is passed as the third argument, allowing custom attributes to inspect the parameter name or type when resolving a value.

<a name="binding-primitives"></a>
### Binding Primitives

Expand Down
87 changes: 87 additions & 0 deletions src/boost/docs/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Hypervel includes a variety of global "helper" PHP functions. Many of these func
[fake](#method-fake)
[filled](#method-filled)
[info](#method-info)
[lazy](#method-lazy)
[literal](#method-literal)
[logger](#method-logger)
[method_field](#method-method-field)
Expand All @@ -212,6 +213,7 @@ Hypervel includes a variety of global "helper" PHP functions. Many of these func
[once](#method-once)
[optional](#method-optional)
[policy](#method-policy)
[proxy](#method-proxy)
[redirect](#method-redirect)
[report](#method-report)
[report_if](#method-report-if)
Expand Down Expand Up @@ -2778,6 +2780,58 @@ An array of contextual data may also be passed to the function:
info('User login attempt failed.', ['id' => $user->id]);
```

<a name="method-lazy"></a>
#### `lazy()` {.collection-method}

The `lazy` function creates a lazy ghost object. The object has the requested class immediately, but its initializer does not run until a non-eager property is first accessed:

```php
class Report
{
public function __construct(
public string $path,
public string $format = 'csv',
) {
}
}

$report = lazy(Report::class, fn () => [
'path' => storage_path('reports/monthly.csv'),
'format' => 'csv',
]);

// The initializer runs when the property is first accessed...
$path = $report->path;
```

When the initializer returns an array, its values are passed to the class constructor as positional or named arguments. Alternatively, the initializer may construct the lazy instance directly:

```php
$report = lazy(Report::class, function (Report $report) {
$report->__construct(storage_path('reports/monthly.csv'));
});
```

The class name may be inferred from the initializer's first parameter type:

```php
$report = lazy(fn (Report $report) => [
storage_path('reports/monthly.csv'),
]);
```

Properties passed through the `eager` argument are available without triggering initialization:

```php
$report = lazy(
Report::class,
fn () => [storage_path('reports/monthly.csv')],
eager: ['format' => 'csv'],
);

$format = $report->format; // Does not initialize the object...
```

<a name="method-literal"></a>
#### `literal()` {.collection-method}

Expand Down Expand Up @@ -2924,6 +2978,39 @@ The `policy` method retrieves a [policy](/docs/{{version}}/authorization#creatin
$policy = policy(App\Models\User::class);
```

<a name="method-proxy"></a>
#### `proxy()` {.collection-method}

The `proxy` function creates a lazy proxy. Unlike a [lazy ghost](#method-lazy), which initializes the same object, a proxy's factory returns the real object that replaces the proxy when it is first used:

```php
$report = proxy(Report::class, function (Report $proxy) {
return new Report(storage_path('reports/monthly.csv'));
});

// The factory runs and returns the real Report instance...
$path = $report->path;
```

The class may be inferred from the callback's return type or, when no usable return type is declared, its first parameter type:

```php
$report = proxy(fn (): Report => new Report(
storage_path('reports/monthly.csv'),
));
```

As with `lazy`, the optional `eager` argument makes selected proxy properties available before initialization. The factory receives those values as its second argument so it may copy them to the real object when required:

```php
$report = proxy(Report::class, function (Report $proxy, array $eager) {
return new Report(
storage_path('reports/monthly.csv'),
$eager['format'],
);
}, eager: ['format' => 'csv']);
```

<a name="method-redirect"></a>
#### `redirect()` {.collection-method}

Expand Down
39 changes: 31 additions & 8 deletions src/cache/src/CacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
use InvalidArgumentException;
use Mockery;
use Mockery\LegacyMockInterface;
use UnitEnum;

use function Hypervel\Support\enum_value;

/**
* @mixin \Hypervel\Contracts\Cache\Repository
Expand Down Expand Up @@ -51,17 +54,21 @@ public function __construct(
/**
* Get a cache store instance by name, wrapped in a repository.
*/
public function store(?string $name = null): CacheRepository
public function store(UnitEnum|string|null $name = null): CacheRepository
{
$name = $name ?: $this->getDefaultDriver();
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

$name ??= $this->getDefaultDriver();

return $this->stores[$name] ??= $this->resolve($name);
}

/**
* Get a cache driver instance.
*/
public function driver(?string $driver = null): CacheRepository
public function driver(UnitEnum|string|null $driver = null): CacheRepository
{
return $this->store($driver);
}
Expand All @@ -72,8 +79,12 @@ public function driver(?string $driver = null): CacheRepository
* The memoized repository is isolated to the current coroutine and resets
* when the coroutine ends.
*/
public function memo(?string $driver = null): CacheRepository
public function memo(UnitEnum|string|null $driver = null): CacheRepository
{
if ($driver instanceof UnitEnum) {
$driver = (string) enum_value($driver);
}

$driver = $driver ?? $this->getDefaultDriver();

// Laravel uses a scoped container binding here. Hypervel stores this
Expand Down Expand Up @@ -401,8 +412,12 @@ public function getDefaultDriver(): string
*
* Boot-only. Mutates process-global config; per-request use races across coroutines.
*/
public function setDefaultDriver(string $name): void
public function setDefaultDriver(UnitEnum|string $name): void
{
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

$this->app['config']['cache.default'] = $name;
}

Expand All @@ -413,11 +428,15 @@ public function setDefaultDriver(string $name): void
* coroutines may already hold a reference to the store and next resolution
* will rebuild with fresh connections.
*/
public function forgetDriver(array|string|null $name = null): static
public function forgetDriver(array|UnitEnum|string|null $name = null): static
{
$name ??= $this->getDefaultDriver();

foreach ((array) $name as $cacheName) {
foreach (is_array($name) ? $name : [$name] as $cacheName) {
if ($cacheName instanceof UnitEnum) {
$cacheName = (string) enum_value($cacheName);
}

if (isset($this->stores[$cacheName])) {
unset($this->stores[$cacheName]);
}
Expand All @@ -433,8 +452,12 @@ public function forgetDriver(array|string|null $name = null): static
* coroutines may already hold a reference to the store and next resolution
* will rebuild with fresh connections.
*/
public function purge(?string $name = null): void
public function purge(UnitEnum|string|null $name = null): void
{
if ($name instanceof UnitEnum) {
$name = (string) enum_value($name);
}

$name ??= $this->getDefaultDriver();

unset($this->stores[$name]);
Expand Down
3 changes: 1 addition & 2 deletions src/config/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"require": {
"php": "^8.4",
"hypervel/collections": "^0.4",
"hypervel/container": "^0.4",
"hypervel/contracts": "^0.4",
"hypervel/macroable": "^0.4",
"hypervel/support": "^0.4"
Expand All @@ -44,4 +43,4 @@
"dev-main": "0.4-dev"
}
}
}
}
Loading