From 61ab22e41419dd18b52e9c10ff546c64eb91ba97 Mon Sep 17 00:00:00 2001 From: Mehmet Onur AKKAYA Date: Thu, 11 Apr 2024 22:49:17 +0300 Subject: [PATCH] adds support for int backed enums to implicit enum route binding --- .../Foundation/Exceptions/Handler.php | 3 ++ .../BackedEnumCaseWrongTypeException.php | 20 +++++++++ .../Routing/ImplicitRouteBinding.php | 10 ++++- .../Routing/RouteSignatureParameters.php | 2 +- src/Illuminate/Routing/Router.php | 2 + src/Illuminate/Support/Reflector.php | 6 +-- tests/Routing/Enums.php | 6 +++ tests/Routing/ImplicitRouteBindingTest.php | 42 +++++++++++++++++++ 8 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/Illuminate/Routing/Exceptions/BackedEnumCaseWrongTypeException.php diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 7d049553737e..19343cae5b4a 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -23,6 +23,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Response; use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException; +use Illuminate\Routing\Exceptions\BackedEnumCaseWrongTypeException; use Illuminate\Routing\Router; use Illuminate\Session\TokenMismatchException; use Illuminate\Support\Arr; @@ -139,6 +140,7 @@ class Handler implements ExceptionHandlerContract AuthenticationException::class, AuthorizationException::class, BackedEnumCaseNotFoundException::class, + BackedEnumCaseWrongTypeException::class, HttpException::class, HttpResponseException::class, ModelNotFoundException::class, @@ -625,6 +627,7 @@ protected function prepareException(Throwable $e) { return match (true) { $e instanceof BackedEnumCaseNotFoundException => new NotFoundHttpException($e->getMessage(), $e), + $e instanceof BackedEnumCaseWrongTypeException => new NotFoundHttpException($e->getMessage(), $e), $e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e), $e instanceof AuthorizationException && $e->hasStatus() => new HttpException( $e->status(), $e->response()?->message() ?: (Response::$statusTexts[$e->status()] ?? 'Whoops, looks like something went wrong.'), $e diff --git a/src/Illuminate/Routing/Exceptions/BackedEnumCaseWrongTypeException.php b/src/Illuminate/Routing/Exceptions/BackedEnumCaseWrongTypeException.php new file mode 100644 index 000000000000..d90935953dce --- /dev/null +++ b/src/Illuminate/Routing/Exceptions/BackedEnumCaseWrongTypeException.php @@ -0,0 +1,20 @@ + * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException + * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseWrongTypeException */ public static function resolveForRoute($container, $route) { @@ -74,6 +77,7 @@ public static function resolveForRoute($container, $route) * @return \Illuminate\Routing\Route * * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException + * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseWrongTypeException */ protected static function resolveBackedEnumsForRoute($route, $parameters) { @@ -86,7 +90,11 @@ protected static function resolveBackedEnumsForRoute($route, $parameters) $backedEnumClass = $parameter->getType()?->getName(); - $backedEnum = $backedEnumClass::tryFrom((string) $parameterValue); + try { + $backedEnum = $backedEnumClass::tryFrom($parameterValue); + } catch (TypeError) { + throw new BackedEnumCaseWrongTypeException($backedEnumClass, $parameterValue); + } if (is_null($backedEnum)) { throw new BackedEnumCaseNotFoundException($backedEnumClass, $parameterValue); diff --git a/src/Illuminate/Routing/RouteSignatureParameters.php b/src/Illuminate/Routing/RouteSignatureParameters.php index 7a4b6dadb996..239df49480ca 100644 --- a/src/Illuminate/Routing/RouteSignatureParameters.php +++ b/src/Illuminate/Routing/RouteSignatureParameters.php @@ -28,7 +28,7 @@ public static function fromAction(array $action, $conditions = []) return match (true) { ! empty($conditions['subClass']) => array_filter($parameters, fn ($p) => Reflector::isParameterSubclassOf($p, $conditions['subClass'])), - ! empty($conditions['backedEnum']) => array_filter($parameters, fn ($p) => Reflector::isParameterBackedEnumWithStringBackingType($p)), + ! empty($conditions['backedEnum']) => array_filter($parameters, fn ($p) => Reflector::isParameterBackedEnumWithValidBackingType($p)), default => $parameters, }; } diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index d7b90fc3dcc9..99ece5e7d4f8 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -933,6 +933,7 @@ public static function toResponse($request, $response) * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model> * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException + * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseWrongTypeException */ public function substituteBindings($route) { @@ -953,6 +954,7 @@ public function substituteBindings($route) * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model> * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException + * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseWrongTypeException */ public function substituteImplicitBindings($route) { diff --git a/src/Illuminate/Support/Reflector.php b/src/Illuminate/Support/Reflector.php index a767d5ea7073..83cb9c65c3c8 100644 --- a/src/Illuminate/Support/Reflector.php +++ b/src/Illuminate/Support/Reflector.php @@ -141,12 +141,12 @@ public static function isParameterSubclassOf($parameter, $className) } /** - * Determine if the parameter's type is a Backed Enum with a string backing type. + * Determine if the parameter's type is a Backed Enum with a string or int backing type. * * @param \ReflectionParameter $parameter * @return bool */ - public static function isParameterBackedEnumWithStringBackingType($parameter) + public static function isParameterBackedEnumWithValidBackingType($parameter) { if (! $parameter->getType() instanceof ReflectionNamedType) { return false; @@ -162,7 +162,7 @@ public static function isParameterBackedEnumWithStringBackingType($parameter) $reflectionBackedEnum = new ReflectionEnum($backedEnumClass); return $reflectionBackedEnum->isBacked() - && $reflectionBackedEnum->getBackingType()->getName() == 'string'; + && in_array($reflectionBackedEnum->getBackingType()->getName(), ['int', 'string']); } return false; diff --git a/tests/Routing/Enums.php b/tests/Routing/Enums.php index 6a97577a454c..ed2904a10d4b 100644 --- a/tests/Routing/Enums.php +++ b/tests/Routing/Enums.php @@ -13,3 +13,9 @@ enum CategoryBackedEnum: string case People = 'people'; case Fruits = 'fruits'; } + +enum CategoryIntBackedEnum: int +{ + case People = 1; + case Fruits = 2; +} diff --git a/tests/Routing/ImplicitRouteBindingTest.php b/tests/Routing/ImplicitRouteBindingTest.php index 18bb7a2ff2c1..9d48f5be7595 100644 --- a/tests/Routing/ImplicitRouteBindingTest.php +++ b/tests/Routing/ImplicitRouteBindingTest.php @@ -5,6 +5,7 @@ use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Model; use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException; +use Illuminate\Routing\Exceptions\BackedEnumCaseWrongTypeException; use Illuminate\Routing\ImplicitRouteBinding; use Illuminate\Routing\Route; use PHPUnit\Framework\TestCase; @@ -31,6 +32,24 @@ public function test_it_can_resolve_the_implicit_backed_enum_route_bindings_for_ $this->assertSame('fruits', $route->parameter('category')->value); } + public function test_it_can_resolve_the_implicit_int_backed_enum_route_bindings_for_the_given_route() + { + $action = ['uses' => function (CategoryIntBackedEnum $category) { + return $category->value; + }]; + + $route = new Route('GET', '/test', $action); + $route->parameters = ['category' => '1']; + + $route->prepareForSerialization(); + + $container = Container::getInstance(); + + ImplicitRouteBinding::resolveForRoute($container, $route); + + $this->assertSame(1, $route->parameter('category')->value); + } + public function test_it_can_resolve_the_implicit_backed_enum_route_bindings_for_the_given_route_with_optional_parameter() { $action = ['uses' => function (?CategoryBackedEnum $category = null) { @@ -91,6 +110,29 @@ public function test_implicit_backed_enum_internal_exception() ImplicitRouteBinding::resolveForRoute($container, $route); } + public function test_implicit_backed_enum_type_error_exception_for_invalid_type() + { + $action = ['uses' => function (CategoryIntBackedEnum $category) { + return $category->value; + }]; + + $route = new Route('GET', '/test', $action); + $route->parameters = ['category' => 'cars']; + + $route->prepareForSerialization(); + + $container = Container::getInstance(); + + $this->expectException(BackedEnumCaseWrongTypeException::class); + $this->expectExceptionMessage(sprintf( + 'Case [%s] is not a valid type for Backed Enum [%s].', + 'cars', + CategoryIntBackedEnum::class, + )); + + ImplicitRouteBinding::resolveForRoute($container, $route); + } + public function test_it_can_resolve_the_implicit_model_route_bindings_for_the_given_route() { $this->expectNotToPerformAssertions();