uri; preg_match_all('#\{(\w+?)}#', $uri, $params); $resourceRouteNames = [".index", ".show", ".update", ".destroy"]; $typeHintedEloquentModels = self::getTypeHintedEloquentModels($method); $routeName = $route->action['as'] ?? ''; if (Str::endsWith($routeName, $resourceRouteNames)) { // Note that resource routes can be nested eg users.posts.show $pluralResources = explode('.', $routeName); array_pop($pluralResources); // Remove the name of the action (eg `show`) $alreadyFoundResourceParam = false; foreach (array_reverse($pluralResources) as $pluralResource) { $singularResource = Str::singular($pluralResource); $singularResourceParam = str_replace('-', '_', $singularResource); // URL parameters are often declared with _ in Laravel but - outside $urlPatternsToSearchFor = [ "{$pluralResource}/{{$singularResourceParam}}", "{$pluralResource}/{{$singularResource}}", "{$pluralResource}/{{$singularResourceParam}?}", "{$pluralResource}/{{$singularResource}?}", ]; $binding = self::getRouteKeyForUrlParam( $route, $singularResource, $typeHintedEloquentModels, 'id' ); if (!$alreadyFoundResourceParam) { // This is the first resource param (from the end). // We set it to `params/{id}` (or whatever field it's bound to) $replaceWith = ["$pluralResource/{{$binding}}", "$pluralResource/{{$binding}?}"]; $alreadyFoundResourceParam = true; } else { // Other resource parameters will be `params/{_id}` $replaceWith = [ "{$pluralResource}/{{$singularResource}_{$binding}}", "{$pluralResource}/{{$singularResourceParam}_{$binding}}", "{$pluralResource}/{{$singularResource}_{$binding}?}", "{$pluralResource}/{{$singularResourceParam}_{$binding}?}", ]; } $uri = str_replace($urlPatternsToSearchFor, $replaceWith, $uri); } } foreach ($params[1] as $param) { // For non-resource parameters, if there's a field binding/type-hinted variable, replace that too: if ($binding = self::getRouteKeyForUrlParam($route, $param, $typeHintedEloquentModels)) { $urlPatternsToSearchFor = ["{{$param}}", "{{$param}?}"]; $replaceWith = ["{{$param}_{$binding}}", "{{$param}_{$binding}?}"]; $uri = str_replace($urlPatternsToSearchFor, $replaceWith, $uri); } } return $uri; } /** * Return the type-hinted method arguments in the action that are Eloquent models, * The arguments will be returned as an array of the form: [ => $instance] */ public static function getTypeHintedEloquentModels(ReflectionFunctionAbstract $method): array { $arguments = []; foreach ($method->getParameters() as $argument) { if (($instance = self::instantiateMethodArgument($argument)) && $instance instanceof Model) { $arguments[$argument->getName()] = $instance; } } return $arguments; } /** * Return the type-hinted method arguments in the action that are enums, * The arguments will be returned as an array of the form: [ => $instance] */ public static function getTypeHintedEnums(ReflectionFunctionAbstract $method): array { $arguments = []; foreach ($method->getParameters() as $argument) { $argumentType = $argument->getType(); if (!($argumentType instanceof \ReflectionNamedType)) continue; try { $reflectionEnum = new ReflectionEnum($argumentType->getName()); $arguments[$argument->getName()] = $reflectionEnum; } catch (ReflectionException) { continue; } } return $arguments; } /** * Given a URL that uses Eloquent model binding (for instance `/posts/{post}` -> `public function show(Post * $post)`), we need to figure out the field that Eloquent uses to retrieve the Post object. By default, this would * be `id`, but can be configured in a couple of ways: * * - Inline: `/posts/{post:slug}` * - `class Post { public function getRouteKeyName() { return 'slug'; } }` * * There are other ways, but they're dynamic and beyond our scope. * * @param \Illuminate\Routing\Route $route * @param string $paramName The name of the URL parameter * @param array $typeHintedEloquentModels * @param string|null $default Default field to use * * @return string|null */ protected static function getRouteKeyForUrlParam( Route $route, string $paramName, array $typeHintedEloquentModels = [], string $default = null ): ?string { if ($binding = self::getInlineRouteKey($route, $paramName)) { return $binding; } return self::getRouteKeyFromModel($paramName, $typeHintedEloquentModels) ?: $default; } /** * Return the `slug` in /posts/{post:slug} * * @param \Illuminate\Routing\Route $route * @param string $paramName * * @return string|null */ protected static function getInlineRouteKey(Route $route, string $paramName): ?string { // Was added in Laravel 7.x if (method_exists($route, 'bindingFieldFor')) { return $route->bindingFieldFor($paramName); } return null; } /** * Check if there's a type-hinted argument on the controller method matching the URL param name: * eg /posts/{post} -> public function show(Post $post) * If there is, check if it's an Eloquent model. * If it is, return it's `getRouteKeyName()`. * * @param string $paramName * @param Model[] $typeHintedEloquentModels * * @return string|null */ protected static function getRouteKeyFromModel(string $paramName, array $typeHintedEloquentModels): ?string { if (array_key_exists($paramName, $typeHintedEloquentModels)) { $argumentInstance = $typeHintedEloquentModels[$paramName]; return $argumentInstance->getRouteKeyName(); } return null; } /** * Instantiate an argument on a controller method via its typehint. For instance, $post in: * * public function show(Post $post) * * This method takes in a method argument and returns an instance, or null if it couldn't be instantiated safely. * Cases where instantiation may fail: * - the argument has no type (eg `public function show($postId)`) * - the argument has a primitive type (eg `public function show(string $postId)`) * - the argument is an injected dependency that itself needs other dependencies * (eg `public function show(PostsManager $manager)`) * * @param \ReflectionParameter $argument * * @return object|null */ protected static function instantiateMethodArgument(\ReflectionParameter $argument): ?object { $argumentType = $argument->getType(); // No type-hint, or primitive type if (!($argumentType instanceof \ReflectionNamedType)) return null; $argumentClassName = $argumentType->getName(); if (class_exists($argumentClassName)) { try { return new $argumentClassName; } catch (\Throwable $e) { return null; } } if (interface_exists($argumentClassName)) { return app($argumentClassName); } return null; } }