diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index b8f08af95560..057e996cb763 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -250,6 +250,11 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static $isBroadcasting = true; + /** + * Indicates if query exceptions during implicit route model binding should be reported. + */ + protected static bool $reportRouteModelBindingExceptions = true; + /** * The Eloquent query builder class to use for the model. * @@ -633,6 +638,14 @@ public static function handleMissingAttributeViolationUsing(?callable $callback) static::$missingAttributeViolationCallback = $callback; } + /** + * Report query exceptions during implicit route model binding. + */ + public static function reportRouteModelBindingExceptions(bool $value = true): void + { + static::$reportRouteModelBindingExceptions = $value; + } + /** * Execute a callback without broadcasting any model events for all model types. * @@ -2447,6 +2460,14 @@ public static function preventsAccessingMissingAttributes() return static::$modelsShouldPreventAccessingMissingAttributes; } + /** + * Determine if query exceptions during implicit route model binding should be reported. + */ + public static function reportsRouteModelBindingExceptions(): bool + { + return static::$reportRouteModelBindingExceptions; + } + /** * Get the broadcast channel route definition that is associated with the given entity. * diff --git a/src/Illuminate/Routing/ImplicitRouteBinding.php b/src/Illuminate/Routing/ImplicitRouteBinding.php index e7b81e48545f..39aaa42d1882 100644 --- a/src/Illuminate/Routing/ImplicitRouteBinding.php +++ b/src/Illuminate/Routing/ImplicitRouteBinding.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\QueryException; use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException; use Illuminate\Support\Reflector; use Illuminate\Support\Str; @@ -45,19 +46,24 @@ public static function resolveForRoute($container, $route) ? 'resolveSoftDeletableRouteBinding' : 'resolveRouteBinding'; - if ($parent instanceof UrlRoutable && - ! $route->preventsScopedBindings() && - ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) { - $childRouteBindingMethod = $route->allowsTrashedBindings() && $instance::isSoftDeletable() - ? 'resolveSoftDeletableChildRouteBinding' - : 'resolveChildRouteBinding'; - - if (! $model = $parent->{$childRouteBindingMethod}( - $parameterName, $parameterValue, $route->bindingFieldFor($parameterName) - )) { + try { + if ($parent instanceof UrlRoutable && + ! $route->preventsScopedBindings() && + ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) { + $childRouteBindingMethod = $route->allowsTrashedBindings() && $instance::isSoftDeletable() + ? 'resolveSoftDeletableChildRouteBinding' + : 'resolveChildRouteBinding'; + + if (! $model = $parent->{$childRouteBindingMethod}( + $parameterName, $parameterValue, $route->bindingFieldFor($parameterName) + )) { + throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); + } + } elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) { throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); } - } elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) { + } catch (QueryException $e) { + report_if($instance::reportsRouteModelBindingExceptions(), $e); throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); } diff --git a/tests/Routing/ImplicitRouteBindingTest.php b/tests/Routing/ImplicitRouteBindingTest.php index b6db381be902..111ebc4a5824 100644 --- a/tests/Routing/ImplicitRouteBindingTest.php +++ b/tests/Routing/ImplicitRouteBindingTest.php @@ -2,17 +2,31 @@ namespace Illuminate\Tests\Routing; +use Exception; use Illuminate\Container\Container; +use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\QueryException; use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException; use Illuminate\Routing\ImplicitRouteBinding; use Illuminate\Routing\Route; +use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; include_once 'Enums.php'; class ImplicitRouteBindingTest extends TestCase { + protected function tearDown(): void + { + Container::setInstance(null); + Model::reportRouteModelBindingExceptions(); + + parent::tearDown(); + } + public function test_it_can_resolve_the_implicit_backed_enum_route_bindings_for_the_given_route() { $action = ['uses' => function (CategoryBackedEnum $category) { @@ -126,9 +140,49 @@ public function test_it_can_resolve_the_implicit_model_route_bindings_for_the_gi ImplicitRouteBinding::resolveForRoute($container, $route); } + + #[DataProvider('shouldReportProvider')] + public function testItThrowsModelNotFoundExceptionOnQueryException(bool $shouldReport): void + { + Model::reportRouteModelBindingExceptions($shouldReport); + + $mock = m::mock(ExceptionHandler::class); + + if ($shouldReport) { + $mock->shouldReceive('report')->once()->with(m::type(QueryException::class)); + } else { + $mock->shouldReceive('report')->never(); + } + + $container = Container::getInstance(); + $container->instance(ExceptionHandler::class, $mock); + + $action = ['uses' => fn (ImplicitRouteBindingUserWithQueryException $user) => $user]; + + $route = new Route('GET', '/test', $action); + $route->parameters = ['user' => 'invalid-value']; + $route->prepareForSerialization(); + $this->expectException(ModelNotFoundException::class); + + ImplicitRouteBinding::resolveForRoute($container, $route); + } + + /** @return list */ + public static function shouldReportProvider(): array + { + return [[true], [false]]; + } } class ImplicitRouteBindingUser extends Model { // } + +class ImplicitRouteBindingUserWithQueryException extends Model +{ + public function resolveRouteBinding($value, $field = null): void + { + throw new QueryException('mysql', 'select * from users where id = ?', [$value], new Exception('Out of range value')); + } +}