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
3 changes: 3 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
->exclude('src/testbench/workbench/storage')
->exclude('vendor')
->notPath('#^bin/#')
// These deliberately omit strict_types so PHP applies its native weak scalar conversion.
->notPath('src/container/src/NativeInvoker.php')
->notPath('src/data/src/Support/Creation/NativeScalar.php')
->notPath('tests/Data/Fixtures/PhpDocTypeContext.php')
->notPath('tests/Foundation/Fixtures/fake-compiled-view.php')
->name('hypervel-test-profile')
Expand Down
1 change: 1 addition & 0 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@

## Validation

- Remove pipe-separated validation rule strings in favor of arrays of rules across both `hypervel/validation` and `hypervel/data`, including Data's rule normalizer, denormalizer and `#[Rule]` handling. Update callers, tests and documentation together; preserve pipes inside individual regex patterns.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This would remove a documented public API, not just change internal normalization: #[Rule('required|string')] is explicitly supported. Preserve string-rule compatibility at public boundaries and normalize internally, or specify a versioned deprecation and migration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/todo.md, line 73:

<comment>This would remove a documented public API, not just change internal normalization: `#[Rule('required|string')]` is explicitly supported. Preserve string-rule compatibility at public boundaries and normalize internally, or specify a versioned deprecation and migration.</comment>

<file context>
@@ -70,6 +70,7 @@
 
 ## Validation
 
+- Remove pipe-separated validation rule strings in favor of arrays of rules across both `hypervel/validation` and `hypervel/data`, including Data's rule normalizer, denormalizer and `#[Rule]` handling. Update callers, tests and documentation together; preserve pipes inside individual regex patterns.
 - Adopt Brick Math 0.20's bounded parsing API for untrusted numeric validation inputs once the supported dependency graph permits it. Ramsey UUID currently limits Brick Math to 0.18, which has neither bounded parsing nor 0.20's parser-backtracking fix, so an intermediate upgrade would not address this issue. Preserve the configurable exponent-range behavior and cover delegated and compiled rules, oversized mantissas and exponents, malformed input, and `multiple_of`; do not add a local parsing workaround or disguise an incompatible Brick version with a Composer alias.
 
</file context>
Suggested change
- Remove pipe-separated validation rule strings in favor of arrays of rules across both `hypervel/validation` and `hypervel/data`, including Data's rule normalizer, denormalizer and `#[Rule]` handling. Update callers, tests and documentation together; preserve pipes inside individual regex patterns.
- Normalize pipe-separated validation rules to arrays internally across `hypervel/validation` and `hypervel/data`, including Data's rule normalizer, denormalizer and `#[Rule]` handling. Preserve public string-rule compatibility and pipes inside regex patterns; update callers, tests and documentation together.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The entry records a planned breaking change, and this PR doesn't make it. When it's made, callers, tests and documentation change with it, as the entry says.

- Adopt Brick Math 0.20's bounded parsing API for untrusted numeric validation inputs once the supported dependency graph permits it. Ramsey UUID currently limits Brick Math to 0.18, which has neither bounded parsing nor 0.20's parser-backtracking fix, so an intermediate upgrade would not address this issue. Preserve the configurable exponent-range behavior and cover delegated and compiled rules, oversized mantissas and exponents, malformed input, and `multiple_of`; do not add a local parsing workaround or disguise an incompatible Brick version with a Composer alias.

## Notifications
Expand Down
23 changes: 12 additions & 11 deletions docs/upstream-sync/sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,23 +126,23 @@ laravel/tinker:

laravel/wayfinder:
branch: main
checked_through: null
last_reviewed_pr: null
sync_date: null
checked_through: dd454ed0a7fba5debbcbd8f3da06a2b4a7fae6b8
last_reviewed_pr: 295
sync_date: '2026-10-03'
notes: Track main for applicable fixes. Wayfinder next and its Surveyor, Ranger and Structure Discoverer dependency ports are deferred until Wayfinder v1. At each sync, check whether v1 has been released; if so, notify the owner and obtain approval before changing the tracked branch or beginning those ports.

laravel/docs:
branch: 13.x
checked_through: null
last_reviewed_pr: null
sync_date: null
checked_through: 156fc7fde114548640e13c39aa79b991291c3f91
last_reviewed_pr: 11395
sync_date: '2026-10-03'
notes: Shared documentation for the framework and Laravel packages. Reconcile upstream changes with existing Hypervel documentation, including independently written coverage. Replace custom wording for equivalent content with upstream wording, adapting it for Hypervel and preserving enhancements and intentional differences absent upstream. Do not duplicate existing coverage.

inertiajs/inertia-laravel:
branch: 3.x
checked_through: null
last_reviewed_pr: null
sync_date: null
checked_through: 4da52b72da39396cad1d3ec8523b6c649009456e
last_reviewed_pr: 918
sync_date: '2026-10-03'

inertiajs/docs:
branch: main
Expand All @@ -153,9 +153,10 @@ inertiajs/docs:

spatie/laravel-data:
branch: main
checked_through: null
checked_through: ce296f22861dc1237ce468754cc7f46d3ac34ad5
last_reviewed_pr: null
sync_date: null
sync_date: '2026-10-03'
notes: Hypervel replaces upstream's data pipes and resolvers with a fixed engine. The pipes and DataFromSomethingResolver map to Support/Creation/DataCreator and its per-operation ConstructionState; the validation resolvers and built-in rule inferrers map to Support/Validation/DataValidationCompiler and DataValidator; TransformedDataResolver and VisibleDataFieldsResolver map to Support/Transformation/DataTransformer and PartialTree. DataCreator's direct construction from DataClass creation recipes and DataTransformer's bulk copy are deliberate fast paths, so apply upstream behavior changes to them as well as to the general paths, and compare tests/Benchmarks/Data/benchmark.php results before and after. Upstream tests for those replaced classes live in the matching upstream feature tests or in the owning engine's tests under tests/Data/Support.

spatie/laravel-permission:
branch: main
Expand Down
12 changes: 11 additions & 1 deletion src/container/src/Attributes/Give.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@
namespace Hypervel\Container\Attributes;

use Attribute;
use Hypervel\Container\Attributes\Concerns\ExtractsPropertyValue;
use Hypervel\Contracts\Container\Container;
use Hypervel\Contracts\Container\ContextualAttribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Give implements ContextualAttribute
{
use ExtractsPropertyValue;

/**
* Provide a concrete class implementation for dependency injection.
*
* Property paths use data_get() and may invoke object accessors or lazy-load
* Eloquent relationships.
*/
public function __construct(
public string $class,
public array $params = [],
public ?string $property = null,
) {
}

Expand All @@ -25,6 +32,9 @@ public function __construct(
*/
public static function resolve(self $attribute, Container $container): mixed
{
return $container->make($attribute->class, $attribute->params);
return $attribute->extractPropertyValue(
$container->make($attribute->class, $attribute->params),
$attribute->property,
);
}
}
6 changes: 5 additions & 1 deletion src/container/src/BoundMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ public static function call(Container $container, callable|string $callback, arr
}

return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters, $reflection) {
return $callback(...array_values(static::getMethodDependencies($container, $callback, $parameters, $reflection)));
// A function name is resolved before its dependencies, so a missing one fails as a direct call would.
return NativeInvoker::call(
is_string($callback) ? $callback(...) : $callback,
array_values(static::getMethodDependencies($container, $callback, $parameters, $reflection)),
);
});
}

Expand Down
54 changes: 53 additions & 1 deletion src/container/src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,55 @@ public function buildWith(Closure|string $concrete, array $parameters = []): mix
}
}

/**
* Resolve the contextual constructor parameters of a class within its build context.
*
* Contextual bindings for the class apply to dependencies the attributes resolve, and each
* parameter's attribute callbacks fire here. Pass the values to buildWith() as overrides
* so they are not resolved again. Variadic parameters are skipped, because an override
* is passed as one argument; the container resolves them when the class is built.
*
* @param class-string $concrete
* @param null|list<string> $names the parameters to resolve, or null for every contextual parameter
* @return array<string, mixed>
*
* @throws BindingResolutionException
*/
public function resolveContextualParameters(string $concrete, ?array $names = null): array
{
$recipe = $this->getBuildRecipe($concrete);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: An unknown class produces an empty recipe here, so this method silently returns [] instead of the BindingResolutionException raised by build() for the same target. Check classExists before iterating so invalid names do not look like classes without contextual parameters.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/container/src/Container.php, line 1775:

<comment>An unknown class produces an empty recipe here, so this method silently returns `[]` instead of the `BindingResolutionException` raised by `build()` for the same target. Check `classExists` before iterating so invalid names do not look like classes without contextual parameters.</comment>

<file context>
@@ -1756,6 +1756,55 @@ public function buildWith(Closure|string $concrete, array $parameters = []): mix
+     */
+    public function resolveContextualParameters(string $concrete, ?array $names = null): array
+    {
+        $recipe = $this->getBuildRecipe($concrete);
+        $resolutionState = $this->getOrCreateResolutionState();
+        $resolutionState->buildStack[] = $concrete;
</file context>
Suggested change
$recipe = $this->getBuildRecipe($concrete);
$recipe = $this->getBuildRecipe($concrete);
if (! $recipe->classExists) {
throw new BindingResolutionException("Target class [{$concrete}] does not exist.");
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The values are resolved for a class that is then built with buildWith(), and buildWith() throws BindingResolutionException for a class that doesn't exist. A check here would report the same error one call earlier.

$resolutionState = $this->getOrCreateResolutionState();
$resolutionState->buildStack[] = $concrete;

try {
$values = [];

foreach ($recipe->parameters as $paramRecipe) {
if ($paramRecipe->contextualAttribute === null
|| $paramRecipe->isVariadic
|| ($names !== null && ! in_array($paramRecipe->name, $names, true))
) {
continue;
}

$value = $this->resolveFromAttribute(
$paramRecipe->contextualAttribute,
$paramRecipe->getReflectionParameter(),
);

if ($paramRecipe->attributes !== []) {
$this->fireAfterResolvingAttributeCallbacks($paramRecipe->attributes, $value);
}

$values[$paramRecipe->name] = $value;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

return $values;
} finally {
array_pop($resolutionState->buildStack);
}
}

/**
* Instantiate a concrete instance of the given type.
*
Expand Down Expand Up @@ -1824,7 +1873,10 @@ public function build(Closure|string $concrete): mixed
array_pop($resolutionState->buildStack);
}

$instance = new $concrete(...$instances);
// Arguments go through the native invoker so scalars convert as they do in Laravel.
$instance = $instances === []
? new $concrete
: NativeInvoker::construct($concrete, $instances);

if ($recipe->classAttributes !== []) {
$this->fireAfterResolvingAttributeCallbacks(
Expand Down
38 changes: 38 additions & 0 deletions src/container/src/NativeInvoker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

// This file deliberately omits strict_types. Route parameters arrive as strings, and Laravel's
// dispatchers and container are not strict, so PHP's weak typing turns '123' into 123 for an
// `int $id` action or constructor parameter. Calling application code from here keeps that
// Laravel-compatible behavior, including PHP's own TypeError for a value such as 'abc'.

namespace Hypervel\Container;

/**
* @internal
*/
final class NativeInvoker
{
/**
* Call application code with PHP's weak scalar conversion.
*
* @param list<mixed> $arguments
*/
public static function call(callable $callback, array $arguments): mixed
{
return $callback(...$arguments);
}

/**
* Construct a class with PHP's weak scalar conversion.
*
* @template TClass of object
*
* @param class-string<TClass> $class
* @param list<mixed> $arguments
* @return TClass
*/
public static function construct(string $class, array $arguments): object
{
return new $class(...$arguments);
}
}
15 changes: 15 additions & 0 deletions src/contracts/src/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ public function build(Closure|string $concrete): mixed;
*/
public function buildWith(Closure|string $concrete, array $parameters = []): mixed;

/**
* Resolve the contextual constructor parameters of a class within its build context.
*
* Pass the values to buildWith() as overrides so they are not resolved again. Variadic
* parameters are skipped, because an override is passed as one argument; the container
* resolves them when the class is built.
*
* @param class-string $concrete
* @param null|list<string> $names the parameters to resolve, or null for every contextual parameter
* @return array<string, mixed>
*
* @throws BindingResolutionException
*/
public function resolveContextualParameters(string $concrete, ?array $names = null): array;

/**
* Call the given Closure / class@method and inject its dependencies.
*/
Expand Down
Loading
Loading