From 2281d686039d21027f8f281de081b3a79e77ee1f Mon Sep 17 00:00:00 2001 From: Neil Carlo Sucuangco Date: Tue, 18 Nov 2025 03:48:08 +0800 Subject: [PATCH] feat: resolve Eloquent models in AsObject::run() --- src/Concerns/AsObject.php | 50 +++++++++++++++++- .../AsObjectWithModelBindingNullableTest.php | 33 ++++++++++++ ...bjectWithModelBindingOptionalParamTest.php | 42 +++++++++++++++ tests/AsObjectWithModelBindingTest.php | 52 +++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 tests/AsObjectWithModelBindingNullableTest.php create mode 100644 tests/AsObjectWithModelBindingOptionalParamTest.php create mode 100644 tests/AsObjectWithModelBindingTest.php diff --git a/src/Concerns/AsObject.php b/src/Concerns/AsObject.php index ce46fa7..0ee9a7c 100644 --- a/src/Concerns/AsObject.php +++ b/src/Concerns/AsObject.php @@ -2,10 +2,15 @@ namespace Lorisleiva\Actions\Concerns; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Routing\RouteDependencyResolverTrait; use Illuminate\Support\Fluent; +use ReflectionMethod; trait AsObject { + use RouteDependencyResolverTrait; + /** * @return static */ @@ -19,7 +24,10 @@ public static function make() */ public static function run(mixed ...$arguments): mixed { - return static::make()->handle(...$arguments); + $instance = static::make(); + $arguments = static::resolveModelBindings($instance, 'handle', $arguments); + + return app()->call([$instance, 'handle'], $arguments); } public static function runIf(bool $boolean, mixed ...$arguments): mixed @@ -31,4 +39,44 @@ public static function runUnless(bool $boolean, mixed ...$arguments): mixed { return static::runIf(! $boolean, ...$arguments); } + + protected static function resolveModelBindings(object $instance, string $method, array $arguments): array + { + if (! method_exists($instance, $method)) { + return $arguments; + } + + $reflection = new ReflectionMethod($instance, $method); + $parameters = $reflection->getParameters(); + $resolved = []; + + foreach ($parameters as $index => $parameter) { + if (! array_key_exists($index, $arguments)) { + continue; + } + + $value = $arguments[$index]; + + if (is_object($value)) { + $resolved[$parameter->getName()] = $value; + continue; + } + + $type = $parameter->getType(); + if ($type && ! $type->isBuiltin() && class_exists($type->getName())) { + $typeName = $type->getName(); + + if (is_subclass_of($typeName, Model::class)) { + if (is_scalar($value) && $value !== null) { + $resolved[$parameter->getName()] = $typeName::findOrFail($value); + continue; + } + } + } + + $resolved[$parameter->getName()] = $value; + } + + return $resolved; + } } diff --git a/tests/AsObjectWithModelBindingNullableTest.php b/tests/AsObjectWithModelBindingNullableTest.php new file mode 100644 index 0000000..e61bc2f --- /dev/null +++ b/tests/AsObjectWithModelBindingNullableTest.php @@ -0,0 +1,33 @@ +toBeNull(); +}); + +it('handles nullable model parameters with scalar value', function () { + loadMigrations(); + createUser(['id' => 1, 'name' => 'John']); + + $result = AsObjectWithModelBindingNullableTest::run(1); + + expect($result)->toBeInstanceOf(User::class); + expect($result->id)->toBe(1); +}); + diff --git a/tests/AsObjectWithModelBindingOptionalParamTest.php b/tests/AsObjectWithModelBindingOptionalParamTest.php new file mode 100644 index 0000000..36b4da6 --- /dev/null +++ b/tests/AsObjectWithModelBindingOptionalParamTest.php @@ -0,0 +1,42 @@ + $user, + 'status' => $status, + ]; + } +} + +it('handles optional parameters correctly', function () { + loadMigrations(); + createUser(['id' => 1, 'name' => 'John']); + + $result = AsObjectWithModelBindingOptionalParamTest::run(1); + + expect($result['user'])->toBeInstanceOf(User::class); + expect($result['user']->id)->toBe(1); + expect($result['status'])->toBe('active'); +}); + +it('handles optional parameters with provided value', function () { + loadMigrations(); + createUser(['id' => 1, 'name' => 'John']); + + $result = AsObjectWithModelBindingOptionalParamTest::run(1, 'inactive'); + + expect($result['user'])->toBeInstanceOf(User::class); + expect($result['user']->id)->toBe(1); + expect($result['status'])->toBe('inactive'); +}); + diff --git a/tests/AsObjectWithModelBindingTest.php b/tests/AsObjectWithModelBindingTest.php new file mode 100644 index 0000000..23249c5 --- /dev/null +++ b/tests/AsObjectWithModelBindingTest.php @@ -0,0 +1,52 @@ + 42, + 'name' => 'John Doe', + ]); + + $result = AsObjectWithModelBindingTest::run(42); + + expect($result)->toBeInstanceOf(User::class); + expect($result->id)->toBe(42); + expect($result->name)->toBe('John Doe'); +}); + +it('works when passing model instance directly', function () { + loadMigrations(); + $user = createUser([ + 'id' => 42, + 'name' => 'John Doe', + ]); + + $result = AsObjectWithModelBindingTest::run($user); + + expect($result)->toBe($user); + expect($result->id)->toBe(42); +}); + +it('throws exception when model not found', function () { + loadMigrations(); + + expect(fn() => AsObjectWithModelBindingTest::run(999)) + ->toThrow(ModelNotFoundException::class); +}); +