├── AbstractRouteCollection.php ├── CallableDispatcher.php ├── CompiledRouteCollection.php ├── Console ├── ControllerMakeCommand.php ├── MiddlewareMakeCommand.php └── stubs │ ├── controller.api.stub │ ├── controller.invokable.stub │ ├── controller.model.api.stub │ ├── controller.model.stub │ ├── controller.nested.api.stub │ ├── controller.nested.singleton.api.stub │ ├── controller.nested.singleton.stub │ ├── controller.nested.stub │ ├── controller.plain.stub │ ├── controller.singleton.api.stub │ ├── controller.singleton.stub │ ├── controller.stub │ └── middleware.stub ├── Contracts ├── CallableDispatcher.php └── ControllerDispatcher.php ├── Controller.php ├── ControllerDispatcher.php ├── ControllerMiddlewareOptions.php ├── Controllers ├── HasMiddleware.php └── Middleware.php ├── CreatesRegularExpressionRouteConstraints.php ├── Events ├── PreparingResponse.php ├── ResponsePrepared.php ├── RouteMatched.php └── Routing.php ├── Exceptions ├── BackedEnumCaseNotFoundException.php ├── InvalidSignatureException.php ├── MissingRateLimiterException.php ├── StreamedResponseException.php └── UrlGenerationException.php ├── FiltersControllerMiddleware.php ├── ImplicitRouteBinding.php ├── LICENSE.md ├── Matching ├── HostValidator.php ├── MethodValidator.php ├── SchemeValidator.php ├── UriValidator.php └── ValidatorInterface.php ├── Middleware ├── SubstituteBindings.php ├── ThrottleRequests.php ├── ThrottleRequestsWithRedis.php └── ValidateSignature.php ├── MiddlewareNameResolver.php ├── PendingResourceRegistration.php ├── PendingSingletonResourceRegistration.php ├── Pipeline.php ├── RedirectController.php ├── Redirector.php ├── ResolvesRouteDependencies.php ├── ResourceRegistrar.php ├── ResponseFactory.php ├── Route.php ├── RouteAction.php ├── RouteBinding.php ├── RouteCollection.php ├── RouteCollectionInterface.php ├── RouteDependencyResolverTrait.php ├── RouteFileRegistrar.php ├── RouteGroup.php ├── RouteParameterBinder.php ├── RouteRegistrar.php ├── RouteSignatureParameters.php ├── RouteUri.php ├── RouteUrlGenerator.php ├── Router.php ├── RoutingServiceProvider.php ├── SortedMiddleware.php ├── UrlGenerator.php ├── ViewController.php └── composer.json /AbstractRouteCollection.php: -------------------------------------------------------------------------------- 1 | bind($request); 34 | } 35 | 36 | // If no route was found we will now check if a matching route is specified by 37 | // another HTTP verb. If it is we will need to throw a MethodNotAllowed and 38 | // inform the user agent of which HTTP verb it should use for this route. 39 | $others = $this->checkForAlternateVerbs($request); 40 | 41 | if (count($others) > 0) { 42 | return $this->getRouteForMethods($request, $others); 43 | } 44 | 45 | throw new NotFoundHttpException(sprintf( 46 | 'The route %s could not be found.', 47 | $request->path() 48 | )); 49 | } 50 | 51 | /** 52 | * Determine if any routes match on another HTTP verb. 53 | * 54 | * @param \Illuminate\Http\Request $request 55 | * @return array 56 | */ 57 | protected function checkForAlternateVerbs($request) 58 | { 59 | $methods = array_diff(Router::$verbs, [$request->getMethod()]); 60 | 61 | // Here we will spin through all verbs except for the current request verb and 62 | // check to see if any routes respond to them. If they do, we will return a 63 | // proper error response with the correct headers on the response string. 64 | return array_values(array_filter( 65 | $methods, 66 | function ($method) use ($request) { 67 | return ! is_null($this->matchAgainstRoutes($this->get($method), $request, false)); 68 | } 69 | )); 70 | } 71 | 72 | /** 73 | * Determine if a route in the array matches the request. 74 | * 75 | * @param \Illuminate\Routing\Route[] $routes 76 | * @param \Illuminate\Http\Request $request 77 | * @param bool $includingMethod 78 | * @return \Illuminate\Routing\Route|null 79 | */ 80 | protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true) 81 | { 82 | [$fallbacks, $routes] = (new Collection($routes))->partition(function ($route) { 83 | return $route->isFallback; 84 | }); 85 | 86 | return $routes->merge($fallbacks)->first( 87 | fn (Route $route) => $route->matches($request, $includingMethod) 88 | ); 89 | } 90 | 91 | /** 92 | * Get a route (if necessary) that responds when other available methods are present. 93 | * 94 | * @param \Illuminate\Http\Request $request 95 | * @param string[] $methods 96 | * @return \Illuminate\Routing\Route 97 | * 98 | * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException 99 | */ 100 | protected function getRouteForMethods($request, array $methods) 101 | { 102 | if ($request->isMethod('OPTIONS')) { 103 | return (new Route('OPTIONS', $request->path(), function () use ($methods) { 104 | return new Response('', 200, ['Allow' => implode(',', $methods)]); 105 | }))->bind($request); 106 | } 107 | 108 | $this->requestMethodNotAllowed($request, $methods, $request->method()); 109 | } 110 | 111 | /** 112 | * Throw a method not allowed HTTP exception. 113 | * 114 | * @param \Illuminate\Http\Request $request 115 | * @param array $others 116 | * @param string $method 117 | * @return never 118 | * 119 | * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException 120 | */ 121 | protected function requestMethodNotAllowed($request, array $others, $method) 122 | { 123 | throw new MethodNotAllowedHttpException( 124 | $others, 125 | sprintf( 126 | 'The %s method is not supported for route %s. Supported methods: %s.', 127 | $method, 128 | $request->path(), 129 | implode(', ', $others) 130 | ) 131 | ); 132 | } 133 | 134 | /** 135 | * Throw a method not allowed HTTP exception. 136 | * 137 | * @param array $others 138 | * @param string $method 139 | * @return void 140 | * 141 | * @deprecated use requestMethodNotAllowed 142 | * 143 | * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException 144 | */ 145 | protected function methodNotAllowed(array $others, $method) 146 | { 147 | throw new MethodNotAllowedHttpException( 148 | $others, 149 | sprintf( 150 | 'The %s method is not supported for this route. Supported methods: %s.', 151 | $method, 152 | implode(', ', $others) 153 | ) 154 | ); 155 | } 156 | 157 | /** 158 | * Compile the routes for caching. 159 | * 160 | * @return array 161 | */ 162 | public function compile() 163 | { 164 | $compiled = $this->dumper()->getCompiledRoutes(); 165 | 166 | $attributes = []; 167 | 168 | foreach ($this->getRoutes() as $route) { 169 | $attributes[$route->getName()] = [ 170 | 'methods' => $route->methods(), 171 | 'uri' => $route->uri(), 172 | 'action' => $route->getAction(), 173 | 'fallback' => $route->isFallback, 174 | 'defaults' => $route->defaults, 175 | 'wheres' => $route->wheres, 176 | 'bindingFields' => $route->bindingFields(), 177 | 'lockSeconds' => $route->locksFor(), 178 | 'waitSeconds' => $route->waitsFor(), 179 | 'withTrashed' => $route->allowsTrashedBindings(), 180 | ]; 181 | } 182 | 183 | return compact('compiled', 'attributes'); 184 | } 185 | 186 | /** 187 | * Return the CompiledUrlMatcherDumper instance for the route collection. 188 | * 189 | * @return \Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper 190 | */ 191 | public function dumper() 192 | { 193 | return new CompiledUrlMatcherDumper($this->toSymfonyRouteCollection()); 194 | } 195 | 196 | /** 197 | * Convert the collection to a Symfony RouteCollection instance. 198 | * 199 | * @return \Symfony\Component\Routing\RouteCollection 200 | */ 201 | public function toSymfonyRouteCollection() 202 | { 203 | $symfonyRoutes = new SymfonyRouteCollection; 204 | 205 | $routes = $this->getRoutes(); 206 | 207 | foreach ($routes as $route) { 208 | if (! $route->isFallback) { 209 | $symfonyRoutes = $this->addToSymfonyRoutesCollection($symfonyRoutes, $route); 210 | } 211 | } 212 | 213 | foreach ($routes as $route) { 214 | if ($route->isFallback) { 215 | $symfonyRoutes = $this->addToSymfonyRoutesCollection($symfonyRoutes, $route); 216 | } 217 | } 218 | 219 | return $symfonyRoutes; 220 | } 221 | 222 | /** 223 | * Add a route to the SymfonyRouteCollection instance. 224 | * 225 | * @param \Symfony\Component\Routing\RouteCollection $symfonyRoutes 226 | * @param \Illuminate\Routing\Route $route 227 | * @return \Symfony\Component\Routing\RouteCollection 228 | * 229 | * @throws \LogicException 230 | */ 231 | protected function addToSymfonyRoutesCollection(SymfonyRouteCollection $symfonyRoutes, Route $route) 232 | { 233 | $name = $route->getName(); 234 | 235 | if ( 236 | ! is_null($name) 237 | && str_ends_with($name, '.') 238 | && ! is_null($symfonyRoutes->get($name)) 239 | ) { 240 | $name = null; 241 | } 242 | 243 | if (! $name) { 244 | $route->name($this->generateRouteName()); 245 | 246 | $this->add($route); 247 | } elseif (! is_null($symfonyRoutes->get($name))) { 248 | throw new LogicException("Unable to prepare route [{$route->uri}] for serialization. Another route has already been assigned name [{$name}]."); 249 | } 250 | 251 | $symfonyRoutes->add($route->getName(), $route->toSymfonyRoute()); 252 | 253 | return $symfonyRoutes; 254 | } 255 | 256 | /** 257 | * Get a randomly generated route name. 258 | * 259 | * @return string 260 | */ 261 | protected function generateRouteName() 262 | { 263 | return 'generated::'.Str::random(); 264 | } 265 | 266 | /** 267 | * Get an iterator for the items. 268 | * 269 | * @return \ArrayIterator 270 | */ 271 | public function getIterator(): Traversable 272 | { 273 | return new ArrayIterator($this->getRoutes()); 274 | } 275 | 276 | /** 277 | * Count the number of items in the collection. 278 | * 279 | * @return int 280 | */ 281 | public function count(): int 282 | { 283 | return count($this->getRoutes()); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /CallableDispatcher.php: -------------------------------------------------------------------------------- 1 | container = $container; 28 | } 29 | 30 | /** 31 | * Dispatch a request to a given callable. 32 | * 33 | * @param \Illuminate\Routing\Route $route 34 | * @param callable $callable 35 | * @return mixed 36 | */ 37 | public function dispatch(Route $route, $callable) 38 | { 39 | return $callable(...array_values($this->resolveParameters($route, $callable))); 40 | } 41 | 42 | /** 43 | * Resolve the parameters for the callable. 44 | * 45 | * @param \Illuminate\Routing\Route $route 46 | * @param callable $callable 47 | * @return array 48 | */ 49 | protected function resolveParameters(Route $route, $callable) 50 | { 51 | return $this->resolveMethodDependencies($route->parametersWithoutNulls(), new ReflectionFunction($callable)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CompiledRouteCollection.php: -------------------------------------------------------------------------------- 1 | compiled = $compiled; 61 | $this->attributes = $attributes; 62 | $this->routes = new RouteCollection; 63 | } 64 | 65 | /** 66 | * Add a Route instance to the collection. 67 | * 68 | * @param \Illuminate\Routing\Route $route 69 | * @return \Illuminate\Routing\Route 70 | */ 71 | public function add(Route $route) 72 | { 73 | return $this->routes->add($route); 74 | } 75 | 76 | /** 77 | * Refresh the name look-up table. 78 | * 79 | * This is done in case any names are fluently defined or if routes are overwritten. 80 | * 81 | * @return void 82 | */ 83 | public function refreshNameLookups() 84 | { 85 | // 86 | } 87 | 88 | /** 89 | * Refresh the action look-up table. 90 | * 91 | * This is done in case any actions are overwritten with new controllers. 92 | * 93 | * @return void 94 | */ 95 | public function refreshActionLookups() 96 | { 97 | // 98 | } 99 | 100 | /** 101 | * Find the first route matching a given request. 102 | * 103 | * @param \Illuminate\Http\Request $request 104 | * @return \Illuminate\Routing\Route 105 | * 106 | * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException 107 | * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException 108 | */ 109 | public function match(Request $request) 110 | { 111 | $matcher = new CompiledUrlMatcher( 112 | $this->compiled, (new RequestContext)->fromRequest( 113 | $trimmedRequest = $this->requestWithoutTrailingSlash($request) 114 | ) 115 | ); 116 | 117 | $route = null; 118 | 119 | try { 120 | if ($result = $matcher->matchRequest($trimmedRequest)) { 121 | $route = $this->getByName($result['_route']); 122 | } 123 | } catch (ResourceNotFoundException|MethodNotAllowedException) { 124 | try { 125 | return $this->routes->match($request); 126 | } catch (NotFoundHttpException) { 127 | // 128 | } 129 | } 130 | 131 | if ($route && $route->isFallback) { 132 | try { 133 | $dynamicRoute = $this->routes->match($request); 134 | 135 | if (! $dynamicRoute->isFallback) { 136 | $route = $dynamicRoute; 137 | } 138 | } catch (NotFoundHttpException|MethodNotAllowedHttpException) { 139 | // 140 | } 141 | } 142 | 143 | return $this->handleMatchedRoute($request, $route); 144 | } 145 | 146 | /** 147 | * Get a cloned instance of the given request without any trailing slash on the URI. 148 | * 149 | * @param \Illuminate\Http\Request $request 150 | * @return \Illuminate\Http\Request 151 | */ 152 | protected function requestWithoutTrailingSlash(Request $request) 153 | { 154 | $trimmedRequest = $request->duplicate(); 155 | 156 | $parts = explode('?', $request->server->get('REQUEST_URI'), 2); 157 | 158 | $trimmedRequest->server->set( 159 | 'REQUEST_URI', rtrim($parts[0], '/').(isset($parts[1]) ? '?'.$parts[1] : '') 160 | ); 161 | 162 | return $trimmedRequest; 163 | } 164 | 165 | /** 166 | * Get routes from the collection by method. 167 | * 168 | * @param string|null $method 169 | * @return \Illuminate\Routing\Route[] 170 | */ 171 | public function get($method = null) 172 | { 173 | return $this->getRoutesByMethod()[$method] ?? []; 174 | } 175 | 176 | /** 177 | * Determine if the route collection contains a given named route. 178 | * 179 | * @param string $name 180 | * @return bool 181 | */ 182 | public function hasNamedRoute($name) 183 | { 184 | return isset($this->attributes[$name]) || $this->routes->hasNamedRoute($name); 185 | } 186 | 187 | /** 188 | * Get a route instance by its name. 189 | * 190 | * @param string $name 191 | * @return \Illuminate\Routing\Route|null 192 | */ 193 | public function getByName($name) 194 | { 195 | if (isset($this->attributes[$name])) { 196 | return $this->newRoute($this->attributes[$name]); 197 | } 198 | 199 | return $this->routes->getByName($name); 200 | } 201 | 202 | /** 203 | * Get a route instance by its controller action. 204 | * 205 | * @param string $action 206 | * @return \Illuminate\Routing\Route|null 207 | */ 208 | public function getByAction($action) 209 | { 210 | $attributes = (new Collection($this->attributes))->first(function (array $attributes) use ($action) { 211 | if (isset($attributes['action']['controller'])) { 212 | return trim($attributes['action']['controller'], '\\') === $action; 213 | } 214 | 215 | return $attributes['action']['uses'] === $action; 216 | }); 217 | 218 | if ($attributes) { 219 | return $this->newRoute($attributes); 220 | } 221 | 222 | return $this->routes->getByAction($action); 223 | } 224 | 225 | /** 226 | * Get all of the routes in the collection. 227 | * 228 | * @return \Illuminate\Routing\Route[] 229 | */ 230 | public function getRoutes() 231 | { 232 | return (new Collection($this->attributes)) 233 | ->map(function (array $attributes) { 234 | return $this->newRoute($attributes); 235 | }) 236 | ->merge($this->routes->getRoutes()) 237 | ->values() 238 | ->all(); 239 | } 240 | 241 | /** 242 | * Get all of the routes keyed by their HTTP verb / method. 243 | * 244 | * @return array 245 | */ 246 | public function getRoutesByMethod() 247 | { 248 | return (new Collection($this->getRoutes())) 249 | ->groupBy(function (Route $route) { 250 | return $route->methods(); 251 | }) 252 | ->map(function (Collection $routes) { 253 | return $routes->mapWithKeys(function (Route $route) { 254 | return [$route->getDomain().$route->uri => $route]; 255 | })->all(); 256 | }) 257 | ->all(); 258 | } 259 | 260 | /** 261 | * Get all of the routes keyed by their name. 262 | * 263 | * @return \Illuminate\Routing\Route[] 264 | */ 265 | public function getRoutesByName() 266 | { 267 | return (new Collection($this->getRoutes())) 268 | ->keyBy(function (Route $route) { 269 | return $route->getName(); 270 | }) 271 | ->all(); 272 | } 273 | 274 | /** 275 | * Resolve an array of attributes to a Route instance. 276 | * 277 | * @param array $attributes 278 | * @return \Illuminate\Routing\Route 279 | */ 280 | protected function newRoute(array $attributes) 281 | { 282 | if (empty($attributes['action']['prefix'] ?? '')) { 283 | $baseUri = $attributes['uri']; 284 | } else { 285 | $prefix = trim($attributes['action']['prefix'], '/'); 286 | 287 | $baseUri = trim(implode( 288 | '/', array_slice( 289 | explode('/', trim($attributes['uri'], '/')), 290 | count($prefix !== '' ? explode('/', $prefix) : []) 291 | ) 292 | ), '/'); 293 | } 294 | 295 | return $this->router->newRoute($attributes['methods'], $baseUri === '' ? '/' : $baseUri, $attributes['action']) 296 | ->setFallback($attributes['fallback']) 297 | ->setDefaults($attributes['defaults']) 298 | ->setWheres($attributes['wheres']) 299 | ->setBindingFields($attributes['bindingFields']) 300 | ->block($attributes['lockSeconds'] ?? null, $attributes['waitSeconds'] ?? null) 301 | ->withTrashed($attributes['withTrashed'] ?? false); 302 | } 303 | 304 | /** 305 | * Set the router instance on the route. 306 | * 307 | * @param \Illuminate\Routing\Router $router 308 | * @return $this 309 | */ 310 | public function setRouter(Router $router) 311 | { 312 | $this->router = $router; 313 | 314 | return $this; 315 | } 316 | 317 | /** 318 | * Set the container instance on the route. 319 | * 320 | * @param \Illuminate\Container\Container $container 321 | * @return $this 322 | */ 323 | public function setContainer(Container $container) 324 | { 325 | $this->container = $container; 326 | 327 | return $this; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Console/ControllerMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('type')) { 53 | $stub = "/stubs/controller.{$type}.stub"; 54 | } elseif ($this->option('parent')) { 55 | $stub = $this->option('singleton') 56 | ? '/stubs/controller.nested.singleton.stub' 57 | : '/stubs/controller.nested.stub'; 58 | } elseif ($this->option('model')) { 59 | $stub = '/stubs/controller.model.stub'; 60 | } elseif ($this->option('invokable')) { 61 | $stub = '/stubs/controller.invokable.stub'; 62 | } elseif ($this->option('singleton')) { 63 | $stub = '/stubs/controller.singleton.stub'; 64 | } elseif ($this->option('resource')) { 65 | $stub = '/stubs/controller.stub'; 66 | } 67 | 68 | if ($this->option('api') && is_null($stub)) { 69 | $stub = '/stubs/controller.api.stub'; 70 | } elseif ($this->option('api') && ! is_null($stub) && ! $this->option('invokable')) { 71 | $stub = str_replace('.stub', '.api.stub', $stub); 72 | } 73 | 74 | $stub ??= '/stubs/controller.plain.stub'; 75 | 76 | return $this->resolveStubPath($stub); 77 | } 78 | 79 | /** 80 | * Resolve the fully-qualified path to the stub. 81 | * 82 | * @param string $stub 83 | * @return string 84 | */ 85 | protected function resolveStubPath($stub) 86 | { 87 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 88 | ? $customPath 89 | : __DIR__.$stub; 90 | } 91 | 92 | /** 93 | * Get the default namespace for the class. 94 | * 95 | * @param string $rootNamespace 96 | * @return string 97 | */ 98 | protected function getDefaultNamespace($rootNamespace) 99 | { 100 | return $rootNamespace.'\Http\Controllers'; 101 | } 102 | 103 | /** 104 | * Build the class with the given name. 105 | * 106 | * Remove the base controller import if we are already in the base namespace. 107 | * 108 | * @param string $name 109 | * @return string 110 | */ 111 | protected function buildClass($name) 112 | { 113 | $rootNamespace = $this->rootNamespace(); 114 | $controllerNamespace = $this->getNamespace($name); 115 | 116 | $replace = []; 117 | 118 | if ($this->option('parent')) { 119 | $replace = $this->buildParentReplacements(); 120 | } 121 | 122 | if ($this->option('model')) { 123 | $replace = $this->buildModelReplacements($replace); 124 | } 125 | 126 | if ($this->option('creatable')) { 127 | $replace['abort(404);'] = '//'; 128 | } 129 | 130 | $baseControllerExists = file_exists($this->getPath("{$rootNamespace}Http\Controllers\Controller")); 131 | 132 | if ($baseControllerExists) { 133 | $replace["use {$controllerNamespace}\Controller;\n"] = ''; 134 | } else { 135 | $replace[' extends Controller'] = ''; 136 | $replace["use {$rootNamespace}Http\Controllers\Controller;\n"] = ''; 137 | } 138 | 139 | return str_replace( 140 | array_keys($replace), array_values($replace), parent::buildClass($name) 141 | ); 142 | } 143 | 144 | /** 145 | * Build the replacements for a parent controller. 146 | * 147 | * @return array 148 | */ 149 | protected function buildParentReplacements() 150 | { 151 | $parentModelClass = $this->parseModel($this->option('parent')); 152 | 153 | if (! class_exists($parentModelClass) && 154 | confirm("A {$parentModelClass} model does not exist. Do you want to generate it?", default: true)) { 155 | $this->call('make:model', ['name' => $parentModelClass]); 156 | } 157 | 158 | return [ 159 | 'ParentDummyFullModelClass' => $parentModelClass, 160 | '{{ namespacedParentModel }}' => $parentModelClass, 161 | '{{namespacedParentModel}}' => $parentModelClass, 162 | 'ParentDummyModelClass' => class_basename($parentModelClass), 163 | '{{ parentModel }}' => class_basename($parentModelClass), 164 | '{{parentModel}}' => class_basename($parentModelClass), 165 | 'ParentDummyModelVariable' => lcfirst(class_basename($parentModelClass)), 166 | '{{ parentModelVariable }}' => lcfirst(class_basename($parentModelClass)), 167 | '{{parentModelVariable}}' => lcfirst(class_basename($parentModelClass)), 168 | ]; 169 | } 170 | 171 | /** 172 | * Build the model replacement values. 173 | * 174 | * @param array $replace 175 | * @return array 176 | */ 177 | protected function buildModelReplacements(array $replace) 178 | { 179 | $modelClass = $this->parseModel($this->option('model')); 180 | 181 | if (! class_exists($modelClass) && confirm("A {$modelClass} model does not exist. Do you want to generate it?", default: true)) { 182 | $this->call('make:model', ['name' => $modelClass]); 183 | } 184 | 185 | $replace = $this->buildFormRequestReplacements($replace, $modelClass); 186 | 187 | return array_merge($replace, [ 188 | 'DummyFullModelClass' => $modelClass, 189 | '{{ namespacedModel }}' => $modelClass, 190 | '{{namespacedModel}}' => $modelClass, 191 | 'DummyModelClass' => class_basename($modelClass), 192 | '{{ model }}' => class_basename($modelClass), 193 | '{{model}}' => class_basename($modelClass), 194 | 'DummyModelVariable' => lcfirst(class_basename($modelClass)), 195 | '{{ modelVariable }}' => lcfirst(class_basename($modelClass)), 196 | '{{modelVariable}}' => lcfirst(class_basename($modelClass)), 197 | ]); 198 | } 199 | 200 | /** 201 | * Get the fully-qualified model class name. 202 | * 203 | * @param string $model 204 | * @return string 205 | * 206 | * @throws \InvalidArgumentException 207 | */ 208 | protected function parseModel($model) 209 | { 210 | if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) { 211 | throw new InvalidArgumentException('Model name contains invalid characters.'); 212 | } 213 | 214 | return $this->qualifyModel($model); 215 | } 216 | 217 | /** 218 | * Build the model replacement values. 219 | * 220 | * @param array $replace 221 | * @param string $modelClass 222 | * @return array 223 | */ 224 | protected function buildFormRequestReplacements(array $replace, $modelClass) 225 | { 226 | [$namespace, $storeRequestClass, $updateRequestClass] = [ 227 | 'Illuminate\\Http', 'Request', 'Request', 228 | ]; 229 | 230 | if ($this->option('requests')) { 231 | $namespace = 'App\\Http\\Requests'; 232 | 233 | [$storeRequestClass, $updateRequestClass] = $this->generateFormRequests( 234 | $modelClass, $storeRequestClass, $updateRequestClass 235 | ); 236 | } 237 | 238 | $namespacedRequests = $namespace.'\\'.$storeRequestClass.';'; 239 | 240 | if ($storeRequestClass !== $updateRequestClass) { 241 | $namespacedRequests .= PHP_EOL.'use '.$namespace.'\\'.$updateRequestClass.';'; 242 | } 243 | 244 | return array_merge($replace, [ 245 | '{{ storeRequest }}' => $storeRequestClass, 246 | '{{storeRequest}}' => $storeRequestClass, 247 | '{{ updateRequest }}' => $updateRequestClass, 248 | '{{updateRequest}}' => $updateRequestClass, 249 | '{{ namespacedStoreRequest }}' => $namespace.'\\'.$storeRequestClass, 250 | '{{namespacedStoreRequest}}' => $namespace.'\\'.$storeRequestClass, 251 | '{{ namespacedUpdateRequest }}' => $namespace.'\\'.$updateRequestClass, 252 | '{{namespacedUpdateRequest}}' => $namespace.'\\'.$updateRequestClass, 253 | '{{ namespacedRequests }}' => $namespacedRequests, 254 | '{{namespacedRequests}}' => $namespacedRequests, 255 | ]); 256 | } 257 | 258 | /** 259 | * Generate the form requests for the given model and classes. 260 | * 261 | * @param string $modelClass 262 | * @param string $storeRequestClass 263 | * @param string $updateRequestClass 264 | * @return array 265 | */ 266 | protected function generateFormRequests($modelClass, $storeRequestClass, $updateRequestClass) 267 | { 268 | $storeRequestClass = 'Store'.class_basename($modelClass).'Request'; 269 | 270 | $this->call('make:request', [ 271 | 'name' => $storeRequestClass, 272 | ]); 273 | 274 | $updateRequestClass = 'Update'.class_basename($modelClass).'Request'; 275 | 276 | $this->call('make:request', [ 277 | 'name' => $updateRequestClass, 278 | ]); 279 | 280 | return [$storeRequestClass, $updateRequestClass]; 281 | } 282 | 283 | /** 284 | * Get the console command options. 285 | * 286 | * @return array 287 | */ 288 | protected function getOptions() 289 | { 290 | return [ 291 | ['api', null, InputOption::VALUE_NONE, 'Exclude the create and edit methods from the controller'], 292 | ['type', null, InputOption::VALUE_REQUIRED, 'Manually specify the controller stub file to use'], 293 | ['force', null, InputOption::VALUE_NONE, 'Create the class even if the controller already exists'], 294 | ['invokable', 'i', InputOption::VALUE_NONE, 'Generate a single method, invokable controller class'], 295 | ['model', 'm', InputOption::VALUE_OPTIONAL, 'Generate a resource controller for the given model'], 296 | ['parent', 'p', InputOption::VALUE_OPTIONAL, 'Generate a nested resource controller class'], 297 | ['resource', 'r', InputOption::VALUE_NONE, 'Generate a resource controller class'], 298 | ['requests', 'R', InputOption::VALUE_NONE, 'Generate FormRequest classes for store and update'], 299 | ['singleton', 's', InputOption::VALUE_NONE, 'Generate a singleton resource controller class'], 300 | ['creatable', null, InputOption::VALUE_NONE, 'Indicate that a singleton resource should be creatable'], 301 | ]; 302 | } 303 | 304 | /** 305 | * Interact further with the user if they were prompted for missing arguments. 306 | * 307 | * @param \Symfony\Component\Console\Input\InputInterface $input 308 | * @param \Symfony\Component\Console\Output\OutputInterface $output 309 | * @return void 310 | */ 311 | protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) 312 | { 313 | if ($this->didReceiveOptions($input)) { 314 | return; 315 | } 316 | 317 | $type = select('Which type of controller would you like?', [ 318 | 'empty' => 'Empty', 319 | 'resource' => 'Resource', 320 | 'singleton' => 'Singleton', 321 | 'api' => 'API', 322 | 'invokable' => 'Invokable', 323 | ]); 324 | 325 | if ($type !== 'empty') { 326 | $input->setOption($type, true); 327 | } 328 | 329 | if (in_array($type, ['api', 'resource', 'singleton'])) { 330 | $model = suggest( 331 | "What model should this $type controller be for? (Optional)", 332 | $this->possibleModels() 333 | ); 334 | 335 | if ($model) { 336 | $input->setOption('model', $model); 337 | } 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /Console/MiddlewareMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveStubPath('/stubs/middleware.stub'); 43 | } 44 | 45 | /** 46 | * Resolve the fully-qualified path to the stub. 47 | * 48 | * @param string $stub 49 | * @return string 50 | */ 51 | protected function resolveStubPath($stub) 52 | { 53 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 54 | ? $customPath 55 | : __DIR__.$stub; 56 | } 57 | 58 | /** 59 | * Get the default namespace for the class. 60 | * 61 | * @param string $rootNamespace 62 | * @return string 63 | */ 64 | protected function getDefaultNamespace($rootNamespace) 65 | { 66 | return $rootNamespace.'\Http\Middleware'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Console/stubs/controller.api.stub: -------------------------------------------------------------------------------- 1 | middleware[] = [ 27 | 'middleware' => $m, 28 | 'options' => &$options, 29 | ]; 30 | } 31 | 32 | return new ControllerMiddlewareOptions($options); 33 | } 34 | 35 | /** 36 | * Get the middleware assigned to the controller. 37 | * 38 | * @return array 39 | */ 40 | public function getMiddleware() 41 | { 42 | return $this->middleware; 43 | } 44 | 45 | /** 46 | * Execute an action on the controller. 47 | * 48 | * @param string $method 49 | * @param array $parameters 50 | * @return \Symfony\Component\HttpFoundation\Response 51 | */ 52 | public function callAction($method, $parameters) 53 | { 54 | return $this->{$method}(...array_values($parameters)); 55 | } 56 | 57 | /** 58 | * Handle calls to missing methods on the controller. 59 | * 60 | * @param string $method 61 | * @param array $parameters 62 | * @return mixed 63 | * 64 | * @throws \BadMethodCallException 65 | */ 66 | public function __call($method, $parameters) 67 | { 68 | throw new BadMethodCallException(sprintf( 69 | 'Method %s::%s does not exist.', static::class, $method 70 | )); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ControllerDispatcher.php: -------------------------------------------------------------------------------- 1 | container = $container; 28 | } 29 | 30 | /** 31 | * Dispatch a request to a given controller and method. 32 | * 33 | * @param \Illuminate\Routing\Route $route 34 | * @param mixed $controller 35 | * @param string $method 36 | * @return mixed 37 | */ 38 | public function dispatch(Route $route, $controller, $method) 39 | { 40 | $parameters = $this->resolveParameters($route, $controller, $method); 41 | 42 | if (method_exists($controller, 'callAction')) { 43 | return $controller->callAction($method, $parameters); 44 | } 45 | 46 | return $controller->{$method}(...array_values($parameters)); 47 | } 48 | 49 | /** 50 | * Resolve the parameters for the controller. 51 | * 52 | * @param \Illuminate\Routing\Route $route 53 | * @param mixed $controller 54 | * @param string $method 55 | * @return array 56 | */ 57 | protected function resolveParameters(Route $route, $controller, $method) 58 | { 59 | return $this->resolveClassMethodDependencies( 60 | $route->parametersWithoutNulls(), $controller, $method 61 | ); 62 | } 63 | 64 | /** 65 | * Get the middleware for the controller instance. 66 | * 67 | * @param \Illuminate\Routing\Controller $controller 68 | * @param string $method 69 | * @return array 70 | */ 71 | public function getMiddleware($controller, $method) 72 | { 73 | if (! method_exists($controller, 'getMiddleware')) { 74 | return []; 75 | } 76 | 77 | return (new Collection($controller->getMiddleware()))->reject(function ($data) use ($method) { 78 | return static::methodExcludedByOptions($method, $data['options']); 79 | })->pluck('middleware')->all(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /ControllerMiddlewareOptions.php: -------------------------------------------------------------------------------- 1 | options = &$options; 22 | } 23 | 24 | /** 25 | * Set the controller methods the middleware should apply to. 26 | * 27 | * @param array|string|mixed $methods 28 | * @return $this 29 | */ 30 | public function only($methods) 31 | { 32 | $this->options['only'] = is_array($methods) ? $methods : func_get_args(); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Set the controller methods the middleware should exclude. 39 | * 40 | * @param array|string|mixed $methods 41 | * @return $this 42 | */ 43 | public function except($methods) 44 | { 45 | $this->options['except'] = is_array($methods) ? $methods : func_get_args(); 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Controllers/HasMiddleware.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public static function middleware(); 13 | } 14 | -------------------------------------------------------------------------------- /Controllers/Middleware.php: -------------------------------------------------------------------------------- 1 | only = Arr::wrap($only); 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Specify the controller methods the middleware should not apply to. 34 | * 35 | * @param array|string $except 36 | * @return $this 37 | */ 38 | public function except(array|string $except) 39 | { 40 | $this->except = Arr::wrap($except); 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CreatesRegularExpressionRouteConstraints.php: -------------------------------------------------------------------------------- 1 | assignExpressionToParameters($parameters, '[a-zA-Z]+'); 20 | } 21 | 22 | /** 23 | * Specify that the given route parameters must be alphanumeric. 24 | * 25 | * @param array|string $parameters 26 | * @return $this 27 | */ 28 | public function whereAlphaNumeric($parameters) 29 | { 30 | return $this->assignExpressionToParameters($parameters, '[a-zA-Z0-9]+'); 31 | } 32 | 33 | /** 34 | * Specify that the given route parameters must be numeric. 35 | * 36 | * @param array|string $parameters 37 | * @return $this 38 | */ 39 | public function whereNumber($parameters) 40 | { 41 | return $this->assignExpressionToParameters($parameters, '[0-9]+'); 42 | } 43 | 44 | /** 45 | * Specify that the given route parameters must be ULIDs. 46 | * 47 | * @param array|string $parameters 48 | * @return $this 49 | */ 50 | public function whereUlid($parameters) 51 | { 52 | return $this->assignExpressionToParameters($parameters, '[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}'); 53 | } 54 | 55 | /** 56 | * Specify that the given route parameters must be UUIDs. 57 | * 58 | * @param array|string $parameters 59 | * @return $this 60 | */ 61 | public function whereUuid($parameters) 62 | { 63 | return $this->assignExpressionToParameters($parameters, '[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}'); 64 | } 65 | 66 | /** 67 | * Specify that the given route parameters must be one of the given values. 68 | * 69 | * @param array|string $parameters 70 | * @param array $values 71 | * @return $this 72 | */ 73 | public function whereIn($parameters, array $values) 74 | { 75 | return $this->assignExpressionToParameters( 76 | $parameters, 77 | (new Collection($values)) 78 | ->map(fn ($value) => enum_value($value)) 79 | ->implode('|') 80 | ); 81 | } 82 | 83 | /** 84 | * Apply the given regular expression to the given parameters. 85 | * 86 | * @param array|string $parameters 87 | * @param string $expression 88 | * @return $this 89 | */ 90 | protected function assignExpressionToParameters($parameters, $expression) 91 | { 92 | return $this->where(Collection::wrap($parameters) 93 | ->mapWithKeys(fn ($parameter) => [$parameter => $expression]) 94 | ->all()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Events/PreparingResponse.php: -------------------------------------------------------------------------------- 1 | originalException = $originalException; 26 | 27 | parent::__construct($originalException->getMessage()); 28 | } 29 | 30 | /** 31 | * Render the exception. 32 | * 33 | * @return \Illuminate\Http\Response 34 | */ 35 | public function render() 36 | { 37 | return new Response(''); 38 | } 39 | 40 | /** 41 | * Get the actual exception thrown during the stream. 42 | * 43 | * @return \Throwable 44 | */ 45 | public function getInnerException() 46 | { 47 | return $this->originalException; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Exceptions/UrlGenerationException.php: -------------------------------------------------------------------------------- 1 | getName(), 26 | $route->uri() 27 | ); 28 | 29 | if (count($parameters) > 0) { 30 | $message .= sprintf(' [Missing %s: %s]', $parameterLabel, implode(', ', $parameters)); 31 | } 32 | 33 | $message .= '.'; 34 | 35 | return new static($message); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /FiltersControllerMiddleware.php: -------------------------------------------------------------------------------- 1 | 22 | * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException 23 | */ 24 | public static function resolveForRoute($container, $route) 25 | { 26 | $parameters = $route->parameters(); 27 | 28 | $route = static::resolveBackedEnumsForRoute($route, $parameters); 29 | 30 | foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) { 31 | if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) { 32 | continue; 33 | } 34 | 35 | $parameterValue = $parameters[$parameterName]; 36 | 37 | if ($parameterValue instanceof UrlRoutable) { 38 | continue; 39 | } 40 | 41 | $instance = $container->make(Reflector::getParameterClassName($parameter)); 42 | 43 | $parent = $route->parentOfParameter($parameterName); 44 | 45 | $routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance)) 46 | ? 'resolveSoftDeletableRouteBinding' 47 | : 'resolveRouteBinding'; 48 | 49 | if ($parent instanceof UrlRoutable && 50 | ! $route->preventsScopedBindings() && 51 | ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) { 52 | $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance)) 53 | ? 'resolveSoftDeletableChildRouteBinding' 54 | : 'resolveChildRouteBinding'; 55 | 56 | if (! $model = $parent->{$childRouteBindingMethod}( 57 | $parameterName, $parameterValue, $route->bindingFieldFor($parameterName) 58 | )) { 59 | throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); 60 | } 61 | } elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) { 62 | throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); 63 | } 64 | 65 | $route->setParameter($parameterName, $model); 66 | } 67 | } 68 | 69 | /** 70 | * Resolve the Backed Enums route bindings for the route. 71 | * 72 | * @param \Illuminate\Routing\Route $route 73 | * @param array $parameters 74 | * @return \Illuminate\Routing\Route 75 | * 76 | * @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException 77 | */ 78 | protected static function resolveBackedEnumsForRoute($route, $parameters) 79 | { 80 | foreach ($route->signatureParameters(['backedEnum' => true]) as $parameter) { 81 | if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) { 82 | continue; 83 | } 84 | 85 | $parameterValue = $parameters[$parameterName]; 86 | 87 | if ($parameterValue === null) { 88 | continue; 89 | } 90 | 91 | $backedEnumClass = $parameter->getType()?->getName(); 92 | 93 | $backedEnum = $parameterValue instanceof $backedEnumClass 94 | ? $parameterValue 95 | : $backedEnumClass::tryFrom((string) $parameterValue); 96 | 97 | if (is_null($backedEnum)) { 98 | throw new BackedEnumCaseNotFoundException($backedEnumClass, $parameterValue); 99 | } 100 | 101 | $route->setParameter($parameterName, $backedEnum); 102 | } 103 | 104 | return $route; 105 | } 106 | 107 | /** 108 | * Return the parameter name if it exists in the given parameters. 109 | * 110 | * @param string $name 111 | * @param array $parameters 112 | * @return string|null 113 | */ 114 | protected static function getParameterName($name, $parameters) 115 | { 116 | if (array_key_exists($name, $parameters)) { 117 | return $name; 118 | } 119 | 120 | if (array_key_exists($snakedName = Str::snake($name), $parameters)) { 121 | return $snakedName; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Matching/HostValidator.php: -------------------------------------------------------------------------------- 1 | getCompiled()->getHostRegex(); 20 | 21 | if (is_null($hostRegex)) { 22 | return true; 23 | } 24 | 25 | return preg_match($hostRegex, $request->getHost()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Matching/MethodValidator.php: -------------------------------------------------------------------------------- 1 | getMethod(), $route->methods()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Matching/SchemeValidator.php: -------------------------------------------------------------------------------- 1 | httpOnly()) { 20 | return ! $request->secure(); 21 | } elseif ($route->secure()) { 22 | return $request->secure(); 23 | } 24 | 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Matching/UriValidator.php: -------------------------------------------------------------------------------- 1 | getPathInfo(), '/') ?: '/'; 20 | 21 | return preg_match($route->getCompiled()->getRegex(), rawurldecode($path)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Matching/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 | router = $router; 26 | } 27 | 28 | /** 29 | * Handle an incoming request. 30 | * 31 | * @param \Illuminate\Http\Request $request 32 | * @param \Closure $next 33 | * @return mixed 34 | */ 35 | public function handle($request, Closure $next) 36 | { 37 | $route = $request->route(); 38 | 39 | try { 40 | $this->router->substituteBindings($route); 41 | $this->router->substituteImplicitBindings($route); 42 | } catch (ModelNotFoundException $exception) { 43 | if ($route->getMissing()) { 44 | return $route->getMissing()($request, $exception); 45 | } 46 | 47 | throw $exception; 48 | } 49 | 50 | return $next($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Middleware/ThrottleRequests.php: -------------------------------------------------------------------------------- 1 | limiter = $limiter; 42 | } 43 | 44 | /** 45 | * Specify the named rate limiter to use for the middleware. 46 | * 47 | * @param string $name 48 | * @return string 49 | */ 50 | public static function using($name) 51 | { 52 | return static::class.':'.$name; 53 | } 54 | 55 | /** 56 | * Specify the rate limiter configuration for the middleware. 57 | * 58 | * @param int $maxAttempts 59 | * @param int $decayMinutes 60 | * @param string $prefix 61 | * @return string 62 | * 63 | * @named-arguments-supported 64 | */ 65 | public static function with($maxAttempts = 60, $decayMinutes = 1, $prefix = '') 66 | { 67 | return static::class.':'.implode(',', func_get_args()); 68 | } 69 | 70 | /** 71 | * Handle an incoming request. 72 | * 73 | * @param \Illuminate\Http\Request $request 74 | * @param \Closure $next 75 | * @param int|string $maxAttempts 76 | * @param float|int $decayMinutes 77 | * @param string $prefix 78 | * @return \Symfony\Component\HttpFoundation\Response 79 | * 80 | * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException 81 | * @throws \Illuminate\Routing\Exceptions\MissingRateLimiterException 82 | */ 83 | public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '') 84 | { 85 | if (is_string($maxAttempts) 86 | && func_num_args() === 3 87 | && ! is_null($limiter = $this->limiter->limiter($maxAttempts))) { 88 | return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter); 89 | } 90 | 91 | return $this->handleRequest( 92 | $request, 93 | $next, 94 | [ 95 | (object) [ 96 | 'key' => $prefix.$this->resolveRequestSignature($request), 97 | 'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts), 98 | 'decaySeconds' => 60 * $decayMinutes, 99 | 'responseCallback' => null, 100 | ], 101 | ] 102 | ); 103 | } 104 | 105 | /** 106 | * Handle an incoming request. 107 | * 108 | * @param \Illuminate\Http\Request $request 109 | * @param \Closure $next 110 | * @param string $limiterName 111 | * @param \Closure $limiter 112 | * @return \Symfony\Component\HttpFoundation\Response 113 | * 114 | * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException 115 | */ 116 | protected function handleRequestUsingNamedLimiter($request, Closure $next, $limiterName, Closure $limiter) 117 | { 118 | $limiterResponse = $limiter($request); 119 | 120 | if ($limiterResponse instanceof Response) { 121 | return $limiterResponse; 122 | } elseif ($limiterResponse instanceof Unlimited) { 123 | return $next($request); 124 | } 125 | 126 | return $this->handleRequest( 127 | $request, 128 | $next, 129 | Collection::wrap($limiterResponse)->map(function ($limit) use ($limiterName) { 130 | return (object) [ 131 | 'key' => self::$shouldHashKeys ? md5($limiterName.$limit->key) : $limiterName.':'.$limit->key, 132 | 'maxAttempts' => $limit->maxAttempts, 133 | 'decaySeconds' => $limit->decaySeconds, 134 | 'responseCallback' => $limit->responseCallback, 135 | ]; 136 | })->all() 137 | ); 138 | } 139 | 140 | /** 141 | * Handle an incoming request. 142 | * 143 | * @param \Illuminate\Http\Request $request 144 | * @param \Closure $next 145 | * @param array $limits 146 | * @return \Symfony\Component\HttpFoundation\Response 147 | * 148 | * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException 149 | */ 150 | protected function handleRequest($request, Closure $next, array $limits) 151 | { 152 | foreach ($limits as $limit) { 153 | if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) { 154 | throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback); 155 | } 156 | 157 | $this->limiter->hit($limit->key, $limit->decaySeconds); 158 | } 159 | 160 | $response = $next($request); 161 | 162 | foreach ($limits as $limit) { 163 | $response = $this->addHeaders( 164 | $response, 165 | $limit->maxAttempts, 166 | $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts) 167 | ); 168 | } 169 | 170 | return $response; 171 | } 172 | 173 | /** 174 | * Resolve the number of attempts if the user is authenticated or not. 175 | * 176 | * @param \Illuminate\Http\Request $request 177 | * @param int|string $maxAttempts 178 | * @return int 179 | * 180 | * @throws \Illuminate\Routing\Exceptions\MissingRateLimiterException 181 | */ 182 | protected function resolveMaxAttempts($request, $maxAttempts) 183 | { 184 | if (str_contains($maxAttempts, '|')) { 185 | $maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0]; 186 | } 187 | 188 | if (! is_numeric($maxAttempts) && 189 | $request->user()?->hasAttribute($maxAttempts) 190 | ) { 191 | $maxAttempts = $request->user()->{$maxAttempts}; 192 | } 193 | 194 | // If we still don't have a numeric value, there was no matching rate limiter... 195 | if (! is_numeric($maxAttempts)) { 196 | is_null($request->user()) 197 | ? throw MissingRateLimiterException::forLimiter($maxAttempts) 198 | : throw MissingRateLimiterException::forLimiterAndUser($maxAttempts, get_class($request->user())); 199 | } 200 | 201 | return (int) $maxAttempts; 202 | } 203 | 204 | /** 205 | * Resolve request signature. 206 | * 207 | * @param \Illuminate\Http\Request $request 208 | * @return string 209 | * 210 | * @throws \RuntimeException 211 | */ 212 | protected function resolveRequestSignature($request) 213 | { 214 | if ($user = $request->user()) { 215 | return $this->formatIdentifier($user->getAuthIdentifier()); 216 | } elseif ($route = $request->route()) { 217 | return $this->formatIdentifier($route->getDomain().'|'.$request->ip()); 218 | } 219 | 220 | throw new RuntimeException('Unable to generate the request signature. Route unavailable.'); 221 | } 222 | 223 | /** 224 | * Create a 'too many attempts' exception. 225 | * 226 | * @param \Illuminate\Http\Request $request 227 | * @param string $key 228 | * @param int $maxAttempts 229 | * @param callable|null $responseCallback 230 | * @return \Illuminate\Http\Exceptions\ThrottleRequestsException|\Illuminate\Http\Exceptions\HttpResponseException 231 | */ 232 | protected function buildException($request, $key, $maxAttempts, $responseCallback = null) 233 | { 234 | $retryAfter = $this->getTimeUntilNextRetry($key); 235 | 236 | $headers = $this->getHeaders( 237 | $maxAttempts, 238 | $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter), 239 | $retryAfter 240 | ); 241 | 242 | return is_callable($responseCallback) 243 | ? new HttpResponseException($responseCallback($request, $headers)) 244 | : new ThrottleRequestsException('Too Many Attempts.', null, $headers); 245 | } 246 | 247 | /** 248 | * Get the number of seconds until the next retry. 249 | * 250 | * @param string $key 251 | * @return int 252 | */ 253 | protected function getTimeUntilNextRetry($key) 254 | { 255 | return $this->limiter->availableIn($key); 256 | } 257 | 258 | /** 259 | * Add the limit header information to the given response. 260 | * 261 | * @param \Symfony\Component\HttpFoundation\Response $response 262 | * @param int $maxAttempts 263 | * @param int $remainingAttempts 264 | * @param int|null $retryAfter 265 | * @return \Symfony\Component\HttpFoundation\Response 266 | */ 267 | protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null) 268 | { 269 | $response->headers->add( 270 | $this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter, $response) 271 | ); 272 | 273 | return $response; 274 | } 275 | 276 | /** 277 | * Get the limit headers information. 278 | * 279 | * @param int $maxAttempts 280 | * @param int $remainingAttempts 281 | * @param int|null $retryAfter 282 | * @param \Symfony\Component\HttpFoundation\Response|null $response 283 | * @return array 284 | */ 285 | protected function getHeaders($maxAttempts, 286 | $remainingAttempts, 287 | $retryAfter = null, 288 | ?Response $response = null) 289 | { 290 | if ($response && 291 | ! is_null($response->headers->get('X-RateLimit-Remaining')) && 292 | (int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) { 293 | return []; 294 | } 295 | 296 | $headers = [ 297 | 'X-RateLimit-Limit' => $maxAttempts, 298 | 'X-RateLimit-Remaining' => $remainingAttempts, 299 | ]; 300 | 301 | if (! is_null($retryAfter)) { 302 | $headers['Retry-After'] = $retryAfter; 303 | $headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter); 304 | } 305 | 306 | return $headers; 307 | } 308 | 309 | /** 310 | * Calculate the number of remaining attempts. 311 | * 312 | * @param string $key 313 | * @param int $maxAttempts 314 | * @param int|null $retryAfter 315 | * @return int 316 | */ 317 | protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null) 318 | { 319 | return is_null($retryAfter) ? $this->limiter->retriesLeft($key, $maxAttempts) : 0; 320 | } 321 | 322 | /** 323 | * Format the given identifier based on the configured hashing settings. 324 | * 325 | * @param string $value 326 | * @return string 327 | */ 328 | private function formatIdentifier($value) 329 | { 330 | return self::$shouldHashKeys ? sha1($value) : $value; 331 | } 332 | 333 | /** 334 | * Specify whether rate limiter keys should be hashed. 335 | * 336 | * @param bool $shouldHashKeys 337 | * @return void 338 | */ 339 | public static function shouldHashKeys(bool $shouldHashKeys = true) 340 | { 341 | self::$shouldHashKeys = $shouldHashKeys; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Middleware/ThrottleRequestsWithRedis.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 44 | } 45 | 46 | /** 47 | * Handle an incoming request. 48 | * 49 | * @param \Illuminate\Http\Request $request 50 | * @param \Closure $next 51 | * @param array $limits 52 | * @return \Symfony\Component\HttpFoundation\Response 53 | * 54 | * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException 55 | */ 56 | protected function handleRequest($request, Closure $next, array $limits) 57 | { 58 | foreach ($limits as $limit) { 59 | if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) { 60 | throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback); 61 | } 62 | } 63 | 64 | $response = $next($request); 65 | 66 | foreach ($limits as $limit) { 67 | $response = $this->addHeaders( 68 | $response, 69 | $limit->maxAttempts, 70 | $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts) 71 | ); 72 | } 73 | 74 | return $response; 75 | } 76 | 77 | /** 78 | * Determine if the given key has been "accessed" too many times. 79 | * 80 | * @param string $key 81 | * @param int $maxAttempts 82 | * @param int $decaySeconds 83 | * @return mixed 84 | */ 85 | protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) 86 | { 87 | $limiter = new DurationLimiter( 88 | $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds 89 | ); 90 | 91 | return tap(! $limiter->acquire(), function () use ($key, $limiter) { 92 | [$this->decaysAt[$key], $this->remaining[$key]] = [ 93 | $limiter->decaysAt, $limiter->remaining, 94 | ]; 95 | }); 96 | } 97 | 98 | /** 99 | * Calculate the number of remaining attempts. 100 | * 101 | * @param string $key 102 | * @param int $maxAttempts 103 | * @param int|null $retryAfter 104 | * @return int 105 | */ 106 | protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null) 107 | { 108 | return is_null($retryAfter) ? $this->remaining[$key] : 0; 109 | } 110 | 111 | /** 112 | * Get the number of seconds until the lock is released. 113 | * 114 | * @param string $key 115 | * @return int 116 | */ 117 | protected function getTimeUntilNextRetry($key) 118 | { 119 | return $this->decaysAt[$key] - $this->currentTime(); 120 | } 121 | 122 | /** 123 | * Get the Redis connection that should be used for throttling. 124 | * 125 | * @return \Illuminate\Redis\Connections\Connection 126 | */ 127 | protected function getRedisConnection() 128 | { 129 | return $this->redis->connection(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $ignore = [ 17 | // 18 | ]; 19 | 20 | /** 21 | * The globally ignored parameters. 22 | * 23 | * @var array 24 | */ 25 | protected static $neverValidate = []; 26 | 27 | /** 28 | * Specify that the URL signature is for a relative URL. 29 | * 30 | * @param array|string $ignore 31 | * @return string 32 | */ 33 | public static function relative($ignore = []) 34 | { 35 | $ignore = Arr::wrap($ignore); 36 | 37 | return static::class.':'.implode(',', empty($ignore) ? ['relative'] : ['relative', ...$ignore]); 38 | } 39 | 40 | /** 41 | * Specify that the URL signature is for an absolute URL. 42 | * 43 | * @param array|string $ignore 44 | * @return class-string 45 | */ 46 | public static function absolute($ignore = []) 47 | { 48 | $ignore = Arr::wrap($ignore); 49 | 50 | return empty($ignore) 51 | ? static::class 52 | : static::class.':'.implode(',', $ignore); 53 | } 54 | 55 | /** 56 | * Handle an incoming request. 57 | * 58 | * @param \Illuminate\Http\Request $request 59 | * @param \Closure $next 60 | * @param array|null $args 61 | * @return \Illuminate\Http\Response 62 | * 63 | * @throws \Illuminate\Routing\Exceptions\InvalidSignatureException 64 | */ 65 | public function handle($request, Closure $next, ...$args) 66 | { 67 | [$relative, $ignore] = $this->parseArguments($args); 68 | 69 | if ($request->hasValidSignatureWhileIgnoring($ignore, ! $relative)) { 70 | return $next($request); 71 | } 72 | 73 | throw new InvalidSignatureException; 74 | } 75 | 76 | /** 77 | * Parse the additional arguments given to the middleware. 78 | * 79 | * @param array $args 80 | * @return array 81 | */ 82 | protected function parseArguments(array $args) 83 | { 84 | $relative = ! empty($args) && $args[0] === 'relative'; 85 | 86 | if ($relative) { 87 | array_shift($args); 88 | } 89 | 90 | $ignore = array_merge( 91 | property_exists($this, 'except') ? $this->except : $this->ignore, 92 | $args 93 | ); 94 | 95 | return [$relative, array_merge($ignore, static::$neverValidate)]; 96 | } 97 | 98 | /** 99 | * Indicate that the given parameters should be ignored during signature validation. 100 | * 101 | * @param array|string $parameters 102 | * @return void 103 | */ 104 | public static function except($parameters) 105 | { 106 | static::$neverValidate = array_values(array_unique( 107 | array_merge(static::$neverValidate, Arr::wrap($parameters)) 108 | )); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MiddlewareNameResolver.php: -------------------------------------------------------------------------------- 1 | name = $name; 58 | $this->options = $options; 59 | $this->registrar = $registrar; 60 | $this->controller = $controller; 61 | } 62 | 63 | /** 64 | * Set the methods the controller should apply to. 65 | * 66 | * @param array|string|mixed $methods 67 | * @return \Illuminate\Routing\PendingResourceRegistration 68 | */ 69 | public function only($methods) 70 | { 71 | $this->options['only'] = is_array($methods) ? $methods : func_get_args(); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Set the methods the controller should exclude. 78 | * 79 | * @param array|string|mixed $methods 80 | * @return \Illuminate\Routing\PendingResourceRegistration 81 | */ 82 | public function except($methods) 83 | { 84 | $this->options['except'] = is_array($methods) ? $methods : func_get_args(); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Set the route names for controller actions. 91 | * 92 | * @param array|string $names 93 | * @return \Illuminate\Routing\PendingResourceRegistration 94 | */ 95 | public function names($names) 96 | { 97 | $this->options['names'] = $names; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Set the route name for a controller action. 104 | * 105 | * @param string $method 106 | * @param string $name 107 | * @return \Illuminate\Routing\PendingResourceRegistration 108 | */ 109 | public function name($method, $name) 110 | { 111 | $this->options['names'][$method] = $name; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Override the route parameter names. 118 | * 119 | * @param array|string $parameters 120 | * @return \Illuminate\Routing\PendingResourceRegistration 121 | */ 122 | public function parameters($parameters) 123 | { 124 | $this->options['parameters'] = $parameters; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Override a route parameter's name. 131 | * 132 | * @param string $previous 133 | * @param string $new 134 | * @return \Illuminate\Routing\PendingResourceRegistration 135 | */ 136 | public function parameter($previous, $new) 137 | { 138 | $this->options['parameters'][$previous] = $new; 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Add middleware to the resource routes. 145 | * 146 | * @param mixed $middleware 147 | * @return \Illuminate\Routing\PendingResourceRegistration 148 | */ 149 | public function middleware($middleware) 150 | { 151 | $middleware = Arr::wrap($middleware); 152 | 153 | foreach ($middleware as $key => $value) { 154 | $middleware[$key] = (string) $value; 155 | } 156 | 157 | $this->options['middleware'] = $middleware; 158 | 159 | if (isset($this->options['middleware_for'])) { 160 | foreach ($this->options['middleware_for'] as $method => $value) { 161 | $this->options['middleware_for'][$method] = Router::uniqueMiddleware(array_merge( 162 | Arr::wrap($value), 163 | $middleware 164 | )); 165 | } 166 | } 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Specify middleware that should be added to the specified resource routes. 173 | * 174 | * @param array|string $methods 175 | * @param array|string $middleware 176 | * @return $this 177 | */ 178 | public function middlewareFor($methods, $middleware) 179 | { 180 | $methods = Arr::wrap($methods); 181 | $middleware = Arr::wrap($middleware); 182 | 183 | if (isset($this->options['middleware'])) { 184 | $middleware = Router::uniqueMiddleware(array_merge( 185 | $this->options['middleware'], 186 | $middleware 187 | )); 188 | } 189 | 190 | foreach ($methods as $method) { 191 | $this->options['middleware_for'][$method] = $middleware; 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Specify middleware that should be removed from the resource routes. 199 | * 200 | * @param array|string $middleware 201 | * @return $this|array 202 | */ 203 | public function withoutMiddleware($middleware) 204 | { 205 | $this->options['excluded_middleware'] = array_merge( 206 | (array) ($this->options['excluded_middleware'] ?? []), Arr::wrap($middleware) 207 | ); 208 | 209 | return $this; 210 | } 211 | 212 | /** 213 | * Specify middleware that should be removed from the specified resource routes. 214 | * 215 | * @param array|string $methods 216 | * @param array|string $middleware 217 | * @return $this 218 | */ 219 | public function withoutMiddlewareFor($methods, $middleware) 220 | { 221 | $methods = Arr::wrap($methods); 222 | $middleware = Arr::wrap($middleware); 223 | 224 | foreach ($methods as $method) { 225 | $this->options['excluded_middleware_for'][$method] = $middleware; 226 | } 227 | 228 | return $this; 229 | } 230 | 231 | /** 232 | * Add "where" constraints to the resource routes. 233 | * 234 | * @param mixed $wheres 235 | * @return \Illuminate\Routing\PendingResourceRegistration 236 | */ 237 | public function where($wheres) 238 | { 239 | $this->options['wheres'] = $wheres; 240 | 241 | return $this; 242 | } 243 | 244 | /** 245 | * Indicate that the resource routes should have "shallow" nesting. 246 | * 247 | * @param bool $shallow 248 | * @return \Illuminate\Routing\PendingResourceRegistration 249 | */ 250 | public function shallow($shallow = true) 251 | { 252 | $this->options['shallow'] = $shallow; 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * Define the callable that should be invoked on a missing model exception. 259 | * 260 | * @param callable $callback 261 | * @return $this 262 | */ 263 | public function missing($callback) 264 | { 265 | $this->options['missing'] = $callback; 266 | 267 | return $this; 268 | } 269 | 270 | /** 271 | * Indicate that the resource routes should be scoped using the given binding fields. 272 | * 273 | * @param array $fields 274 | * @return \Illuminate\Routing\PendingResourceRegistration 275 | */ 276 | public function scoped(array $fields = []) 277 | { 278 | $this->options['bindingFields'] = $fields; 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * Define which routes should allow "trashed" models to be retrieved when resolving implicit model bindings. 285 | * 286 | * @param array $methods 287 | * @return \Illuminate\Routing\PendingResourceRegistration 288 | */ 289 | public function withTrashed(array $methods = []) 290 | { 291 | $this->options['trashed'] = $methods; 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Register the resource route. 298 | * 299 | * @return \Illuminate\Routing\RouteCollection 300 | */ 301 | public function register() 302 | { 303 | $this->registered = true; 304 | 305 | return $this->registrar->register( 306 | $this->name, $this->controller, $this->options 307 | ); 308 | } 309 | 310 | /** 311 | * Handle the object's destruction. 312 | * 313 | * @return void 314 | */ 315 | public function __destruct() 316 | { 317 | if (! $this->registered) { 318 | $this->register(); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /PendingSingletonResourceRegistration.php: -------------------------------------------------------------------------------- 1 | name = $name; 58 | $this->options = $options; 59 | $this->registrar = $registrar; 60 | $this->controller = $controller; 61 | } 62 | 63 | /** 64 | * Set the methods the controller should apply to. 65 | * 66 | * @param array|string|mixed $methods 67 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 68 | */ 69 | public function only($methods) 70 | { 71 | $this->options['only'] = is_array($methods) ? $methods : func_get_args(); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Set the methods the controller should exclude. 78 | * 79 | * @param array|string|mixed $methods 80 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 81 | */ 82 | public function except($methods) 83 | { 84 | $this->options['except'] = is_array($methods) ? $methods : func_get_args(); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Indicate that the resource should have creation and storage routes. 91 | * 92 | * @return $this 93 | */ 94 | public function creatable() 95 | { 96 | $this->options['creatable'] = true; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Indicate that the resource should have a deletion route. 103 | * 104 | * @return $this 105 | */ 106 | public function destroyable() 107 | { 108 | $this->options['destroyable'] = true; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Set the route names for controller actions. 115 | * 116 | * @param array|string $names 117 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 118 | */ 119 | public function names($names) 120 | { 121 | $this->options['names'] = $names; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Set the route name for a controller action. 128 | * 129 | * @param string $method 130 | * @param string $name 131 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 132 | */ 133 | public function name($method, $name) 134 | { 135 | $this->options['names'][$method] = $name; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Override the route parameter names. 142 | * 143 | * @param array|string $parameters 144 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 145 | */ 146 | public function parameters($parameters) 147 | { 148 | $this->options['parameters'] = $parameters; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Override a route parameter's name. 155 | * 156 | * @param string $previous 157 | * @param string $new 158 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 159 | */ 160 | public function parameter($previous, $new) 161 | { 162 | $this->options['parameters'][$previous] = $new; 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Add middleware to the resource routes. 169 | * 170 | * @param mixed $middleware 171 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 172 | */ 173 | public function middleware($middleware) 174 | { 175 | $middleware = Arr::wrap($middleware); 176 | 177 | foreach ($middleware as $key => $value) { 178 | $middleware[$key] = (string) $value; 179 | } 180 | 181 | $this->options['middleware'] = $middleware; 182 | 183 | if (isset($this->options['middleware_for'])) { 184 | foreach ($this->options['middleware_for'] as $method => $value) { 185 | $this->options['middleware_for'][$method] = Router::uniqueMiddleware(array_merge( 186 | Arr::wrap($value), 187 | $middleware 188 | )); 189 | } 190 | } 191 | 192 | return $this; 193 | } 194 | 195 | /** 196 | * Specify middleware that should be added to the specified resource routes. 197 | * 198 | * @param array|string $methods 199 | * @param array|string $middleware 200 | * @return $this 201 | */ 202 | public function middlewareFor($methods, $middleware) 203 | { 204 | $methods = Arr::wrap($methods); 205 | $middleware = Arr::wrap($middleware); 206 | 207 | if (isset($this->options['middleware'])) { 208 | $middleware = Router::uniqueMiddleware(array_merge( 209 | $this->options['middleware'], 210 | $middleware 211 | )); 212 | } 213 | 214 | foreach ($methods as $method) { 215 | $this->options['middleware_for'][$method] = $middleware; 216 | } 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * Specify middleware that should be removed from the resource routes. 223 | * 224 | * @param array|string $middleware 225 | * @return $this|array 226 | */ 227 | public function withoutMiddleware($middleware) 228 | { 229 | $this->options['excluded_middleware'] = array_merge( 230 | (array) ($this->options['excluded_middleware'] ?? []), Arr::wrap($middleware) 231 | ); 232 | 233 | return $this; 234 | } 235 | 236 | /** 237 | * Specify middleware that should be removed from the specified resource routes. 238 | * 239 | * @param array|string $methods 240 | * @param array|string $middleware 241 | * @return $this 242 | */ 243 | public function withoutMiddlewareFor($methods, $middleware) 244 | { 245 | $methods = Arr::wrap($methods); 246 | $middleware = Arr::wrap($middleware); 247 | 248 | foreach ($methods as $method) { 249 | $this->options['excluded_middleware_for'][$method] = $middleware; 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Add "where" constraints to the resource routes. 257 | * 258 | * @param mixed $wheres 259 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 260 | */ 261 | public function where($wheres) 262 | { 263 | $this->options['wheres'] = $wheres; 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Register the singleton resource route. 270 | * 271 | * @return \Illuminate\Routing\RouteCollection 272 | */ 273 | public function register() 274 | { 275 | $this->registered = true; 276 | 277 | return $this->registrar->singleton( 278 | $this->name, $this->controller, $this->options 279 | ); 280 | } 281 | 282 | /** 283 | * Handle the object's destruction. 284 | * 285 | * @return void 286 | */ 287 | public function __destruct() 288 | { 289 | if (! $this->registered) { 290 | $this->register(); 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Pipeline.php: -------------------------------------------------------------------------------- 1 | toResponse($this->getContainer()->make(Request::class)) 28 | : $carry; 29 | } 30 | 31 | /** 32 | * Handle the given exception. 33 | * 34 | * @param mixed $passable 35 | * @param \Throwable $e 36 | * @return mixed 37 | * 38 | * @throws \Throwable 39 | */ 40 | protected function handleException($passable, Throwable $e) 41 | { 42 | if (! $this->container->bound(ExceptionHandler::class) || 43 | ! $passable instanceof Request) { 44 | throw $e; 45 | } 46 | 47 | $handler = $this->container->make(ExceptionHandler::class); 48 | 49 | $handler->report($e); 50 | 51 | $response = $handler->render($passable, $e); 52 | 53 | if (is_object($response) && method_exists($response, 'withException')) { 54 | $response->withException($e); 55 | } 56 | 57 | return $this->handleCarry($response); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RedirectController.php: -------------------------------------------------------------------------------- 1 | route()->parameters()); 22 | 23 | $status = $parameters->get('status'); 24 | 25 | $destination = $parameters->get('destination'); 26 | 27 | $parameters->forget('status')->forget('destination'); 28 | 29 | $route = (new Route('GET', $destination, [ 30 | 'as' => 'laravel_route_redirect_destination', 31 | ]))->bind($request); 32 | 33 | $parameters = $parameters->only( 34 | $route->getCompiled()->getPathVariables() 35 | )->all(); 36 | 37 | $url = $url->toRoute($route, $parameters, false); 38 | 39 | if (! str_starts_with($destination, '/') && str_starts_with($url, '/')) { 40 | $url = Str::after($url, '/'); 41 | } 42 | 43 | return new RedirectResponse($url, $status); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Redirector.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 35 | } 36 | 37 | /** 38 | * Create a new redirect response to the previous location. 39 | * 40 | * @param int $status 41 | * @param array $headers 42 | * @param mixed $fallback 43 | * @return \Illuminate\Http\RedirectResponse 44 | */ 45 | public function back($status = 302, $headers = [], $fallback = false) 46 | { 47 | return $this->createRedirect($this->generator->previous($fallback), $status, $headers); 48 | } 49 | 50 | /** 51 | * Create a new redirect response to the current URI. 52 | * 53 | * @param int $status 54 | * @param array $headers 55 | * @return \Illuminate\Http\RedirectResponse 56 | */ 57 | public function refresh($status = 302, $headers = []) 58 | { 59 | return $this->to($this->generator->getRequest()->path(), $status, $headers); 60 | } 61 | 62 | /** 63 | * Create a new redirect response, while putting the current URL in the session. 64 | * 65 | * @param string $path 66 | * @param int $status 67 | * @param array $headers 68 | * @param bool|null $secure 69 | * @return \Illuminate\Http\RedirectResponse 70 | */ 71 | public function guest($path, $status = 302, $headers = [], $secure = null) 72 | { 73 | $request = $this->generator->getRequest(); 74 | 75 | $intended = $request->isMethod('GET') && $request->route() && ! $request->expectsJson() 76 | ? $this->generator->full() 77 | : $this->generator->previous(); 78 | 79 | if ($intended) { 80 | $this->setIntendedUrl($intended); 81 | } 82 | 83 | return $this->to($path, $status, $headers, $secure); 84 | } 85 | 86 | /** 87 | * Create a new redirect response to the previously intended location. 88 | * 89 | * @param mixed $default 90 | * @param int $status 91 | * @param array $headers 92 | * @param bool|null $secure 93 | * @return \Illuminate\Http\RedirectResponse 94 | */ 95 | public function intended($default = '/', $status = 302, $headers = [], $secure = null) 96 | { 97 | $path = $this->session->pull('url.intended', $default); 98 | 99 | return $this->to($path, $status, $headers, $secure); 100 | } 101 | 102 | /** 103 | * Create a new redirect response to the given path. 104 | * 105 | * @param string $path 106 | * @param int $status 107 | * @param array $headers 108 | * @param bool|null $secure 109 | * @return \Illuminate\Http\RedirectResponse 110 | */ 111 | public function to($path, $status = 302, $headers = [], $secure = null) 112 | { 113 | return $this->createRedirect($this->generator->to($path, [], $secure), $status, $headers); 114 | } 115 | 116 | /** 117 | * Create a new redirect response to an external URL (no validation). 118 | * 119 | * @param string $path 120 | * @param int $status 121 | * @param array $headers 122 | * @return \Illuminate\Http\RedirectResponse 123 | */ 124 | public function away($path, $status = 302, $headers = []) 125 | { 126 | return $this->createRedirect($path, $status, $headers); 127 | } 128 | 129 | /** 130 | * Create a new redirect response to the given HTTPS path. 131 | * 132 | * @param string $path 133 | * @param int $status 134 | * @param array $headers 135 | * @return \Illuminate\Http\RedirectResponse 136 | */ 137 | public function secure($path, $status = 302, $headers = []) 138 | { 139 | return $this->to($path, $status, $headers, true); 140 | } 141 | 142 | /** 143 | * Create a new redirect response to a named route. 144 | * 145 | * @param \BackedEnum|string $route 146 | * @param mixed $parameters 147 | * @param int $status 148 | * @param array $headers 149 | * @return \Illuminate\Http\RedirectResponse 150 | */ 151 | public function route($route, $parameters = [], $status = 302, $headers = []) 152 | { 153 | return $this->to($this->generator->route($route, $parameters), $status, $headers); 154 | } 155 | 156 | /** 157 | * Create a new redirect response to a signed named route. 158 | * 159 | * @param \BackedEnum|string $route 160 | * @param mixed $parameters 161 | * @param \DateTimeInterface|\DateInterval|int|null $expiration 162 | * @param int $status 163 | * @param array $headers 164 | * @return \Illuminate\Http\RedirectResponse 165 | */ 166 | public function signedRoute($route, $parameters = [], $expiration = null, $status = 302, $headers = []) 167 | { 168 | return $this->to($this->generator->signedRoute($route, $parameters, $expiration), $status, $headers); 169 | } 170 | 171 | /** 172 | * Create a new redirect response to a signed named route. 173 | * 174 | * @param \BackedEnum|string $route 175 | * @param \DateTimeInterface|\DateInterval|int|null $expiration 176 | * @param mixed $parameters 177 | * @param int $status 178 | * @param array $headers 179 | * @return \Illuminate\Http\RedirectResponse 180 | */ 181 | public function temporarySignedRoute($route, $expiration, $parameters = [], $status = 302, $headers = []) 182 | { 183 | return $this->to($this->generator->temporarySignedRoute($route, $expiration, $parameters), $status, $headers); 184 | } 185 | 186 | /** 187 | * Create a new redirect response to a controller action. 188 | * 189 | * @param string|array $action 190 | * @param mixed $parameters 191 | * @param int $status 192 | * @param array $headers 193 | * @return \Illuminate\Http\RedirectResponse 194 | */ 195 | public function action($action, $parameters = [], $status = 302, $headers = []) 196 | { 197 | return $this->to($this->generator->action($action, $parameters), $status, $headers); 198 | } 199 | 200 | /** 201 | * Create a new redirect response. 202 | * 203 | * @param string $path 204 | * @param int $status 205 | * @param array $headers 206 | * @return \Illuminate\Http\RedirectResponse 207 | */ 208 | protected function createRedirect($path, $status, $headers) 209 | { 210 | return tap(new RedirectResponse($path, $status, $headers), function ($redirect) { 211 | if (isset($this->session)) { 212 | $redirect->setSession($this->session); 213 | } 214 | 215 | $redirect->setRequest($this->generator->getRequest()); 216 | }); 217 | } 218 | 219 | /** 220 | * Get the URL generator instance. 221 | * 222 | * @return \Illuminate\Routing\UrlGenerator 223 | */ 224 | public function getUrlGenerator() 225 | { 226 | return $this->generator; 227 | } 228 | 229 | /** 230 | * Set the active session store. 231 | * 232 | * @param \Illuminate\Session\Store $session 233 | * @return void 234 | */ 235 | public function setSession(SessionStore $session) 236 | { 237 | $this->session = $session; 238 | } 239 | 240 | /** 241 | * Get the "intended" URL from the session. 242 | * 243 | * @return string|null 244 | */ 245 | public function getIntendedUrl() 246 | { 247 | return $this->session->get('url.intended'); 248 | } 249 | 250 | /** 251 | * Set the "intended" URL in the session. 252 | * 253 | * @param string $url 254 | * @return $this 255 | */ 256 | public function setIntendedUrl($url) 257 | { 258 | $this->session->put('url.intended', $url); 259 | 260 | return $this; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /ResolvesRouteDependencies.php: -------------------------------------------------------------------------------- 1 | resolveMethodDependencies( 31 | $parameters, new ReflectionMethod($instance, $method) 32 | ); 33 | } 34 | 35 | /** 36 | * Resolve the given method's type-hinted dependencies. 37 | * 38 | * @param array $parameters 39 | * @param \ReflectionFunctionAbstract $reflector 40 | * @return array 41 | */ 42 | public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector) 43 | { 44 | $instanceCount = 0; 45 | 46 | $values = array_values($parameters); 47 | 48 | $skippableValue = new stdClass; 49 | 50 | foreach ($reflector->getParameters() as $key => $parameter) { 51 | $instance = $this->transformDependency($parameter, $parameters, $skippableValue); 52 | 53 | if ($instance !== $skippableValue) { 54 | $instanceCount++; 55 | 56 | $this->spliceIntoParameters($parameters, $key, $instance); 57 | } elseif (! isset($values[$key - $instanceCount]) && 58 | $parameter->isDefaultValueAvailable()) { 59 | $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); 60 | } 61 | 62 | $this->container->fireAfterResolvingAttributeCallbacks($parameter->getAttributes(), $instance); 63 | } 64 | 65 | return $parameters; 66 | } 67 | 68 | /** 69 | * Attempt to transform the given parameter into a class instance. 70 | * 71 | * @param \ReflectionParameter $parameter 72 | * @param array $parameters 73 | * @param object $skippableValue 74 | * @return mixed 75 | */ 76 | protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue) 77 | { 78 | if ($attribute = Util::getContextualAttributeFromDependency($parameter)) { 79 | return $this->container->resolveFromAttribute($attribute); 80 | } 81 | 82 | $className = Reflector::getParameterClassName($parameter); 83 | 84 | // If the parameter has a type-hinted class, we will check to see if it is already in 85 | // the list of parameters. If it is we will just skip it as it is probably a model 86 | // binding and we do not want to mess with those; otherwise, we resolve it here. 87 | if ($className && ! $this->alreadyInParameters($className, $parameters)) { 88 | $isEnum = (new ReflectionClass($className))->isEnum(); 89 | 90 | return $parameter->isDefaultValueAvailable() 91 | ? ($isEnum ? $parameter->getDefaultValue() : null) 92 | : $this->container->make($className); 93 | } 94 | 95 | return $skippableValue; 96 | } 97 | 98 | /** 99 | * Determine if an object of the given class is in a list of parameters. 100 | * 101 | * @param string $class 102 | * @param array $parameters 103 | * @return bool 104 | */ 105 | protected function alreadyInParameters($class, array $parameters) 106 | { 107 | return ! is_null(Arr::first($parameters, fn ($value) => $value instanceof $class)); 108 | } 109 | 110 | /** 111 | * Splice the given value into the parameter list. 112 | * 113 | * @param array $parameters 114 | * @param string $offset 115 | * @param mixed $value 116 | * @return void 117 | */ 118 | protected function spliceIntoParameters(array &$parameters, $offset, $value) 119 | { 120 | array_splice( 121 | $parameters, $offset, 0, [$value] 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /ResponseFactory.php: -------------------------------------------------------------------------------- 1 | view = $view; 47 | $this->redirector = $redirector; 48 | } 49 | 50 | /** 51 | * Create a new response instance. 52 | * 53 | * @param mixed $content 54 | * @param int $status 55 | * @param array $headers 56 | * @return \Illuminate\Http\Response 57 | */ 58 | public function make($content = '', $status = 200, array $headers = []) 59 | { 60 | return new Response($content, $status, $headers); 61 | } 62 | 63 | /** 64 | * Create a new "no content" response. 65 | * 66 | * @param int $status 67 | * @param array $headers 68 | * @return \Illuminate\Http\Response 69 | */ 70 | public function noContent($status = 204, array $headers = []) 71 | { 72 | return $this->make('', $status, $headers); 73 | } 74 | 75 | /** 76 | * Create a new response for a given view. 77 | * 78 | * @param string|array $view 79 | * @param array $data 80 | * @param int $status 81 | * @param array $headers 82 | * @return \Illuminate\Http\Response 83 | */ 84 | public function view($view, $data = [], $status = 200, array $headers = []) 85 | { 86 | if (is_array($view)) { 87 | return $this->make($this->view->first($view, $data), $status, $headers); 88 | } 89 | 90 | return $this->make($this->view->make($view, $data), $status, $headers); 91 | } 92 | 93 | /** 94 | * Create a new JSON response instance. 95 | * 96 | * @param mixed $data 97 | * @param int $status 98 | * @param array $headers 99 | * @param int $options 100 | * @return \Illuminate\Http\JsonResponse 101 | */ 102 | public function json($data = [], $status = 200, array $headers = [], $options = 0) 103 | { 104 | return new JsonResponse($data, $status, $headers, $options); 105 | } 106 | 107 | /** 108 | * Create a new JSONP response instance. 109 | * 110 | * @param string $callback 111 | * @param mixed $data 112 | * @param int $status 113 | * @param array $headers 114 | * @param int $options 115 | * @return \Illuminate\Http\JsonResponse 116 | */ 117 | public function jsonp($callback, $data = [], $status = 200, array $headers = [], $options = 0) 118 | { 119 | return $this->json($data, $status, $headers, $options)->setCallback($callback); 120 | } 121 | 122 | /** 123 | * Create a new event stream response. 124 | * 125 | * @param \Closure $callback 126 | * @param array $headers 127 | * @param \Illuminate\Http\StreamedEvent|string|null $endStreamWith 128 | * @return \Symfony\Component\HttpFoundation\StreamedResponse 129 | */ 130 | public function eventStream(Closure $callback, array $headers = [], StreamedEvent|string|null $endStreamWith = '') 131 | { 132 | return $this->stream(function () use ($callback, $endStreamWith) { 133 | foreach ($callback() as $message) { 134 | if (connection_aborted()) { 135 | break; 136 | } 137 | 138 | $event = 'update'; 139 | 140 | if ($message instanceof StreamedEvent) { 141 | $event = $message->event; 142 | $message = $message->data; 143 | } 144 | 145 | if (! is_string($message) && ! is_numeric($message)) { 146 | $message = Js::encode($message); 147 | } 148 | 149 | echo "event: $event\n"; 150 | echo 'data: '.$message; 151 | echo "\n\n"; 152 | 153 | if (ob_get_level() > 0) { 154 | ob_flush(); 155 | } 156 | 157 | flush(); 158 | } 159 | 160 | if (filled($endStreamWith)) { 161 | $endEvent = 'update'; 162 | 163 | if ($endStreamWith instanceof StreamedEvent) { 164 | $endEvent = $endStreamWith->event; 165 | $endStreamWith = $endStreamWith->data; 166 | } 167 | 168 | echo "event: $endEvent\n"; 169 | echo 'data: '.$endStreamWith; 170 | echo "\n\n"; 171 | 172 | if (ob_get_level() > 0) { 173 | ob_flush(); 174 | } 175 | 176 | flush(); 177 | } 178 | }, 200, array_merge($headers, [ 179 | 'Content-Type' => 'text/event-stream', 180 | 'Cache-Control' => 'no-cache', 181 | 'X-Accel-Buffering' => 'no', 182 | ])); 183 | } 184 | 185 | /** 186 | * Create a new streamed response instance. 187 | * 188 | * @param callable $callback 189 | * @param int $status 190 | * @param array $headers 191 | * @return \Symfony\Component\HttpFoundation\StreamedResponse 192 | */ 193 | public function stream($callback, $status = 200, array $headers = []) 194 | { 195 | return new StreamedResponse($callback, $status, $headers); 196 | } 197 | 198 | /** 199 | * Create a new streamed JSON response instance. 200 | * 201 | * @param array $data 202 | * @param int $status 203 | * @param array $headers 204 | * @param int $encodingOptions 205 | * @return \Symfony\Component\HttpFoundation\StreamedJsonResponse 206 | */ 207 | public function streamJson($data, $status = 200, $headers = [], $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS) 208 | { 209 | return new StreamedJsonResponse($data, $status, $headers, $encodingOptions); 210 | } 211 | 212 | /** 213 | * Create a new streamed response instance as a file download. 214 | * 215 | * @param callable $callback 216 | * @param string|null $name 217 | * @param array $headers 218 | * @param string|null $disposition 219 | * @return \Symfony\Component\HttpFoundation\StreamedResponse 220 | * 221 | * @throws \Illuminate\Routing\Exceptions\StreamedResponseException 222 | */ 223 | public function streamDownload($callback, $name = null, array $headers = [], $disposition = 'attachment') 224 | { 225 | $withWrappedException = function () use ($callback) { 226 | try { 227 | $callback(); 228 | } catch (Throwable $e) { 229 | throw new StreamedResponseException($e); 230 | } 231 | }; 232 | 233 | $response = new StreamedResponse($withWrappedException, 200, $headers); 234 | 235 | if (! is_null($name)) { 236 | $response->headers->set('Content-Disposition', $response->headers->makeDisposition( 237 | $disposition, 238 | $name, 239 | $this->fallbackName($name) 240 | )); 241 | } 242 | 243 | return $response; 244 | } 245 | 246 | /** 247 | * Create a new file download response. 248 | * 249 | * @param \SplFileInfo|string $file 250 | * @param string|null $name 251 | * @param array $headers 252 | * @param string|null $disposition 253 | * @return \Symfony\Component\HttpFoundation\BinaryFileResponse 254 | */ 255 | public function download($file, $name = null, array $headers = [], $disposition = 'attachment') 256 | { 257 | $response = new BinaryFileResponse($file, 200, $headers, true, $disposition); 258 | 259 | if (! is_null($name)) { 260 | return $response->setContentDisposition($disposition, $name, $this->fallbackName($name)); 261 | } 262 | 263 | return $response; 264 | } 265 | 266 | /** 267 | * Convert the string to ASCII characters that are equivalent to the given name. 268 | * 269 | * @param string $name 270 | * @return string 271 | */ 272 | protected function fallbackName($name) 273 | { 274 | return str_replace('%', '', Str::ascii($name)); 275 | } 276 | 277 | /** 278 | * Return the raw contents of a binary file. 279 | * 280 | * @param \SplFileInfo|string $file 281 | * @param array $headers 282 | * @return \Symfony\Component\HttpFoundation\BinaryFileResponse 283 | */ 284 | public function file($file, array $headers = []) 285 | { 286 | return new BinaryFileResponse($file, 200, $headers); 287 | } 288 | 289 | /** 290 | * Create a new redirect response to the given path. 291 | * 292 | * @param string $path 293 | * @param int $status 294 | * @param array $headers 295 | * @param bool|null $secure 296 | * @return \Illuminate\Http\RedirectResponse 297 | */ 298 | public function redirectTo($path, $status = 302, $headers = [], $secure = null) 299 | { 300 | return $this->redirector->to($path, $status, $headers, $secure); 301 | } 302 | 303 | /** 304 | * Create a new redirect response to a named route. 305 | * 306 | * @param \BackedEnum|string $route 307 | * @param mixed $parameters 308 | * @param int $status 309 | * @param array $headers 310 | * @return \Illuminate\Http\RedirectResponse 311 | */ 312 | public function redirectToRoute($route, $parameters = [], $status = 302, $headers = []) 313 | { 314 | return $this->redirector->route($route, $parameters, $status, $headers); 315 | } 316 | 317 | /** 318 | * Create a new redirect response to a controller action. 319 | * 320 | * @param array|string $action 321 | * @param mixed $parameters 322 | * @param int $status 323 | * @param array $headers 324 | * @return \Illuminate\Http\RedirectResponse 325 | */ 326 | public function redirectToAction($action, $parameters = [], $status = 302, $headers = []) 327 | { 328 | return $this->redirector->action($action, $parameters, $status, $headers); 329 | } 330 | 331 | /** 332 | * Create a new redirect response, while putting the current URL in the session. 333 | * 334 | * @param string $path 335 | * @param int $status 336 | * @param array $headers 337 | * @param bool|null $secure 338 | * @return \Illuminate\Http\RedirectResponse 339 | */ 340 | public function redirectGuest($path, $status = 302, $headers = [], $secure = null) 341 | { 342 | return $this->redirector->guest($path, $status, $headers, $secure); 343 | } 344 | 345 | /** 346 | * Create a new redirect response to the previously intended location. 347 | * 348 | * @param string $default 349 | * @param int $status 350 | * @param array $headers 351 | * @param bool|null $secure 352 | * @return \Illuminate\Http\RedirectResponse 353 | */ 354 | public function redirectToIntended($default = '/', $status = 302, $headers = [], $secure = null) 355 | { 356 | return $this->redirector->intended($default, $status, $headers, $secure); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /RouteAction.php: -------------------------------------------------------------------------------- 1 | $action] : [ 34 | 'uses' => $action[0].'@'.$action[1], 35 | 'controller' => $action[0].'@'.$action[1], 36 | ]; 37 | } 38 | 39 | // If no "uses" property has been set, we will dig through the array to find a 40 | // Closure instance within this list. We will set the first Closure we come 41 | // across into the "uses" property that will get fired off by this route. 42 | elseif (! isset($action['uses'])) { 43 | $action['uses'] = static::findCallable($action); 44 | } 45 | 46 | if (! static::containsSerializedClosure($action) && is_string($action['uses']) && ! str_contains($action['uses'], '@')) { 47 | $action['uses'] = static::makeInvokable($action['uses']); 48 | } 49 | 50 | return $action; 51 | } 52 | 53 | /** 54 | * Get an action for a route that has no action. 55 | * 56 | * @param string $uri 57 | * @return array 58 | * 59 | * @throws \LogicException 60 | */ 61 | protected static function missingAction($uri) 62 | { 63 | return ['uses' => function () use ($uri) { 64 | throw new LogicException("Route for [{$uri}] has no action."); 65 | }]; 66 | } 67 | 68 | /** 69 | * Find the callable in an action array. 70 | * 71 | * @param array $action 72 | * @return callable 73 | */ 74 | protected static function findCallable(array $action) 75 | { 76 | return Arr::first($action, function ($value, $key) { 77 | return Reflector::isCallable($value) && is_numeric($key); 78 | }); 79 | } 80 | 81 | /** 82 | * Make an action for an invokable controller. 83 | * 84 | * @param string $action 85 | * @return string 86 | * 87 | * @throws \UnexpectedValueException 88 | */ 89 | protected static function makeInvokable($action) 90 | { 91 | if (! method_exists($action, '__invoke')) { 92 | throw new UnexpectedValueException("Invalid route action: [{$action}]."); 93 | } 94 | 95 | return $action.'@__invoke'; 96 | } 97 | 98 | /** 99 | * Determine if the given array actions contain a serialized Closure. 100 | * 101 | * @param array $action 102 | * @return bool 103 | */ 104 | public static function containsSerializedClosure(array $action) 105 | { 106 | return is_string($action['uses']) && Str::startsWith($action['uses'], [ 107 | 'O:47:"Laravel\\SerializableClosure\\SerializableClosure', 108 | 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure', 109 | ]) !== false; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /RouteBinding.php: -------------------------------------------------------------------------------- 1 | make($class), $method]; 44 | 45 | return $callable($value, $route); 46 | }; 47 | } 48 | 49 | /** 50 | * Create a Route model binding for a model. 51 | * 52 | * @param \Illuminate\Container\Container $container 53 | * @param string $class 54 | * @param \Closure|null $callback 55 | * @return \Closure 56 | * 57 | * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model> 58 | */ 59 | public static function forModel($container, $class, $callback = null) 60 | { 61 | return function ($value, $route = null) use ($container, $class, $callback) { 62 | if (is_null($value)) { 63 | return; 64 | } 65 | 66 | // For model binders, we will attempt to retrieve the models using the first 67 | // method on the model instance. If we cannot retrieve the models we'll 68 | // throw a not found exception otherwise we will return the instance. 69 | $instance = $container->make($class); 70 | 71 | $routeBindingMethod = $route?->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance)) 72 | ? 'resolveSoftDeletableRouteBinding' 73 | : 'resolveRouteBinding'; 74 | 75 | if ($model = $instance->{$routeBindingMethod}($value)) { 76 | return $model; 77 | } 78 | 79 | // If a callback was supplied to the method we will call that to determine 80 | // what we should do when the model is not found. This just gives these 81 | // developer a little greater flexibility to decide what will happen. 82 | if ($callback instanceof Closure) { 83 | return $callback($value); 84 | } 85 | 86 | throw (new ModelNotFoundException)->setModel($class); 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /RouteCollection.php: -------------------------------------------------------------------------------- 1 | addToCollections($route); 47 | 48 | $this->addLookups($route); 49 | 50 | return $route; 51 | } 52 | 53 | /** 54 | * Add the given route to the arrays of routes. 55 | * 56 | * @param \Illuminate\Routing\Route $route 57 | * @return void 58 | */ 59 | protected function addToCollections($route) 60 | { 61 | $methods = $route->methods(); 62 | $domainAndUri = $route->getDomain().$route->uri(); 63 | 64 | foreach ($methods as $method) { 65 | $this->routes[$method][$domainAndUri] = $route; 66 | } 67 | 68 | $this->allRoutes[implode('|', $methods).$domainAndUri] = $route; 69 | } 70 | 71 | /** 72 | * Add the route to any look-up tables if necessary. 73 | * 74 | * @param \Illuminate\Routing\Route $route 75 | * @return void 76 | */ 77 | protected function addLookups($route) 78 | { 79 | // If the route has a name, we will add it to the name look-up table, so that we 80 | // will quickly be able to find the route associated with a name and not have 81 | // to iterate through every route every time we need to find a named route. 82 | if ($name = $route->getName()) { 83 | $this->nameList[$name] = $route; 84 | } 85 | 86 | // When the route is routing to a controller we will also store the action that 87 | // is used by the route. This will let us reverse route to controllers while 88 | // processing a request and easily generate URLs to the given controllers. 89 | $action = $route->getAction(); 90 | 91 | if (isset($action['controller'])) { 92 | $this->addToActionList($action, $route); 93 | } 94 | } 95 | 96 | /** 97 | * Add a route to the controller action dictionary. 98 | * 99 | * @param array $action 100 | * @param \Illuminate\Routing\Route $route 101 | * @return void 102 | */ 103 | protected function addToActionList($action, $route) 104 | { 105 | $this->actionList[trim($action['controller'], '\\')] = $route; 106 | } 107 | 108 | /** 109 | * Refresh the name look-up table. 110 | * 111 | * This is done in case any names are fluently defined or if routes are overwritten. 112 | * 113 | * @return void 114 | */ 115 | public function refreshNameLookups() 116 | { 117 | $this->nameList = []; 118 | 119 | foreach ($this->allRoutes as $route) { 120 | if ($route->getName()) { 121 | $this->nameList[$route->getName()] = $route; 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * Refresh the action look-up table. 128 | * 129 | * This is done in case any actions are overwritten with new controllers. 130 | * 131 | * @return void 132 | */ 133 | public function refreshActionLookups() 134 | { 135 | $this->actionList = []; 136 | 137 | foreach ($this->allRoutes as $route) { 138 | if (isset($route->getAction()['controller'])) { 139 | $this->addToActionList($route->getAction(), $route); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Find the first route matching a given request. 146 | * 147 | * @param \Illuminate\Http\Request $request 148 | * @return \Illuminate\Routing\Route 149 | * 150 | * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException 151 | * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException 152 | */ 153 | public function match(Request $request) 154 | { 155 | $routes = $this->get($request->getMethod()); 156 | 157 | // First, we will see if we can find a matching route for this current request 158 | // method. If we can, great, we can just return it so that it can be called 159 | // by the consumer. Otherwise we will check for routes with another verb. 160 | $route = $this->matchAgainstRoutes($routes, $request); 161 | 162 | return $this->handleMatchedRoute($request, $route); 163 | } 164 | 165 | /** 166 | * Get routes from the collection by method. 167 | * 168 | * @param string|null $method 169 | * @return \Illuminate\Routing\Route[] 170 | */ 171 | public function get($method = null) 172 | { 173 | return is_null($method) ? $this->getRoutes() : ($this->routes[$method] ?? []); 174 | } 175 | 176 | /** 177 | * Determine if the route collection contains a given named route. 178 | * 179 | * @param string $name 180 | * @return bool 181 | */ 182 | public function hasNamedRoute($name) 183 | { 184 | return ! is_null($this->getByName($name)); 185 | } 186 | 187 | /** 188 | * Get a route instance by its name. 189 | * 190 | * @param string $name 191 | * @return \Illuminate\Routing\Route|null 192 | */ 193 | public function getByName($name) 194 | { 195 | return $this->nameList[$name] ?? null; 196 | } 197 | 198 | /** 199 | * Get a route instance by its controller action. 200 | * 201 | * @param string $action 202 | * @return \Illuminate\Routing\Route|null 203 | */ 204 | public function getByAction($action) 205 | { 206 | return $this->actionList[$action] ?? null; 207 | } 208 | 209 | /** 210 | * Get all of the routes in the collection. 211 | * 212 | * @return \Illuminate\Routing\Route[] 213 | */ 214 | public function getRoutes() 215 | { 216 | return array_values($this->allRoutes); 217 | } 218 | 219 | /** 220 | * Get all of the routes keyed by their HTTP verb / method. 221 | * 222 | * @return array 223 | */ 224 | public function getRoutesByMethod() 225 | { 226 | return $this->routes; 227 | } 228 | 229 | /** 230 | * Get all of the routes keyed by their name. 231 | * 232 | * @return \Illuminate\Routing\Route[] 233 | */ 234 | public function getRoutesByName() 235 | { 236 | return $this->nameList; 237 | } 238 | 239 | /** 240 | * Convert the collection to a Symfony RouteCollection instance. 241 | * 242 | * @return \Symfony\Component\Routing\RouteCollection 243 | */ 244 | public function toSymfonyRouteCollection() 245 | { 246 | $symfonyRoutes = parent::toSymfonyRouteCollection(); 247 | 248 | $this->refreshNameLookups(); 249 | 250 | return $symfonyRoutes; 251 | } 252 | 253 | /** 254 | * Convert the collection to a CompiledRouteCollection instance. 255 | * 256 | * @param \Illuminate\Routing\Router $router 257 | * @param \Illuminate\Container\Container $container 258 | * @return \Illuminate\Routing\CompiledRouteCollection 259 | */ 260 | public function toCompiledRouteCollection(Router $router, Container $container) 261 | { 262 | ['compiled' => $compiled, 'attributes' => $attributes] = $this->compile(); 263 | 264 | return (new CompiledRouteCollection($compiled, $attributes)) 265 | ->setRouter($router) 266 | ->setContainer($container); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /RouteCollectionInterface.php: -------------------------------------------------------------------------------- 1 | router = $router; 22 | } 23 | 24 | /** 25 | * Require the given routes file. 26 | * 27 | * @param string $routes 28 | * @return void 29 | */ 30 | public function register($routes) 31 | { 32 | $router = $this->router; 33 | 34 | require $routes; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /RouteGroup.php: -------------------------------------------------------------------------------- 1 | static::formatNamespace($new, $old), 29 | 'prefix' => static::formatPrefix($new, $old, $prependExistingPrefix), 30 | 'where' => static::formatWhere($new, $old), 31 | ]); 32 | 33 | return array_merge_recursive(Arr::except( 34 | $old, ['namespace', 'prefix', 'where', 'as'] 35 | ), $new); 36 | } 37 | 38 | /** 39 | * Format the namespace for the new group attributes. 40 | * 41 | * @param array $new 42 | * @param array $old 43 | * @return string|null 44 | */ 45 | protected static function formatNamespace($new, $old) 46 | { 47 | if (isset($new['namespace'])) { 48 | return isset($old['namespace']) && ! str_starts_with($new['namespace'], '\\') 49 | ? trim($old['namespace'], '\\').'\\'.trim($new['namespace'], '\\') 50 | : trim($new['namespace'], '\\'); 51 | } 52 | 53 | return $old['namespace'] ?? null; 54 | } 55 | 56 | /** 57 | * Format the prefix for the new group attributes. 58 | * 59 | * @param array $new 60 | * @param array $old 61 | * @param bool $prependExistingPrefix 62 | * @return string|null 63 | */ 64 | protected static function formatPrefix($new, $old, $prependExistingPrefix = true) 65 | { 66 | $old = $old['prefix'] ?? ''; 67 | 68 | if ($prependExistingPrefix) { 69 | return isset($new['prefix']) ? trim($old, '/').'/'.trim($new['prefix'], '/') : $old; 70 | } 71 | 72 | return isset($new['prefix']) ? trim($new['prefix'], '/').'/'.trim($old, '/') : $old; 73 | } 74 | 75 | /** 76 | * Format the "wheres" for the new group attributes. 77 | * 78 | * @param array $new 79 | * @param array $old 80 | * @return array 81 | */ 82 | protected static function formatWhere($new, $old) 83 | { 84 | return array_merge( 85 | $old['where'] ?? [], 86 | $new['where'] ?? [] 87 | ); 88 | } 89 | 90 | /** 91 | * Format the "as" clause of the new group attributes. 92 | * 93 | * @param array $new 94 | * @param array $old 95 | * @return array 96 | */ 97 | protected static function formatAs($new, $old) 98 | { 99 | if (isset($old['as'])) { 100 | $new['as'] = $old['as'].($new['as'] ?? ''); 101 | } 102 | 103 | return $new; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /RouteParameterBinder.php: -------------------------------------------------------------------------------- 1 | route = $route; 24 | } 25 | 26 | /** 27 | * Get the parameters for the route. 28 | * 29 | * @param \Illuminate\Http\Request $request 30 | * @return array 31 | */ 32 | public function parameters($request) 33 | { 34 | $parameters = $this->bindPathParameters($request); 35 | 36 | // If the route has a regular expression for the host part of the URI, we will 37 | // compile that and get the parameter matches for this domain. We will then 38 | // merge them into this parameters array so that this array is completed. 39 | if (! is_null($this->route->compiled->getHostRegex())) { 40 | $parameters = $this->bindHostParameters( 41 | $request, $parameters 42 | ); 43 | } 44 | 45 | return $this->replaceDefaults($parameters); 46 | } 47 | 48 | /** 49 | * Get the parameter matches for the path portion of the URI. 50 | * 51 | * @param \Illuminate\Http\Request $request 52 | * @return array 53 | */ 54 | protected function bindPathParameters($request) 55 | { 56 | $path = '/'.ltrim($request->decodedPath(), '/'); 57 | 58 | preg_match($this->route->compiled->getRegex(), $path, $matches); 59 | 60 | return $this->matchToKeys(array_slice($matches, 1)); 61 | } 62 | 63 | /** 64 | * Extract the parameter list from the host part of the request. 65 | * 66 | * @param \Illuminate\Http\Request $request 67 | * @param array $parameters 68 | * @return array 69 | */ 70 | protected function bindHostParameters($request, $parameters) 71 | { 72 | preg_match($this->route->compiled->getHostRegex(), $request->getHost(), $matches); 73 | 74 | return array_merge($this->matchToKeys(array_slice($matches, 1)), $parameters); 75 | } 76 | 77 | /** 78 | * Combine a set of parameter matches with the route's keys. 79 | * 80 | * @param array $matches 81 | * @return array 82 | */ 83 | protected function matchToKeys(array $matches) 84 | { 85 | if (empty($parameterNames = $this->route->parameterNames())) { 86 | return []; 87 | } 88 | 89 | $parameters = array_intersect_key($matches, array_flip($parameterNames)); 90 | 91 | return array_filter($parameters, function ($value) { 92 | return is_string($value) && strlen($value) > 0; 93 | }); 94 | } 95 | 96 | /** 97 | * Replace null parameters with their defaults. 98 | * 99 | * @param array $parameters 100 | * @return array 101 | */ 102 | protected function replaceDefaults(array $parameters) 103 | { 104 | foreach ($parameters as $key => $value) { 105 | $parameters[$key] = $value ?? Arr::get($this->route->defaults, $key); 106 | } 107 | 108 | foreach ($this->route->defaults as $key => $value) { 109 | if (! isset($parameters[$key])) { 110 | $parameters[$key] = $value; 111 | } 112 | } 113 | 114 | return $parameters; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /RouteRegistrar.php: -------------------------------------------------------------------------------- 1 | 'as', 89 | 'scopeBindings' => 'scope_bindings', 90 | 'withoutScopedBindings' => 'scope_bindings', 91 | 'withoutMiddleware' => 'excluded_middleware', 92 | ]; 93 | 94 | /** 95 | * Create a new route registrar instance. 96 | * 97 | * @param \Illuminate\Routing\Router $router 98 | */ 99 | public function __construct(Router $router) 100 | { 101 | $this->router = $router; 102 | } 103 | 104 | /** 105 | * Set the value for a given attribute. 106 | * 107 | * @param string $key 108 | * @param mixed $value 109 | * @return $this 110 | * 111 | * @throws \InvalidArgumentException 112 | */ 113 | public function attribute($key, $value) 114 | { 115 | if (! in_array($key, $this->allowedAttributes)) { 116 | throw new InvalidArgumentException("Attribute [{$key}] does not exist."); 117 | } 118 | 119 | if ($key === 'middleware') { 120 | foreach ($value as $index => $middleware) { 121 | $value[$index] = (string) $middleware; 122 | } 123 | } 124 | 125 | $attributeKey = Arr::get($this->aliases, $key, $key); 126 | 127 | if ($key === 'withoutMiddleware') { 128 | $value = array_merge( 129 | (array) ($this->attributes[$attributeKey] ?? []), Arr::wrap($value) 130 | ); 131 | } 132 | 133 | if ($key === 'withoutScopedBindings') { 134 | $value = false; 135 | } 136 | 137 | if ($value instanceof BackedEnum && ! is_string($value = $value->value)) { 138 | throw new InvalidArgumentException("Attribute [{$key}] expects a string backed enum."); 139 | } 140 | 141 | $this->attributes[$attributeKey] = $value; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Route a resource to a controller. 148 | * 149 | * @param string $name 150 | * @param string $controller 151 | * @param array $options 152 | * @return \Illuminate\Routing\PendingResourceRegistration 153 | */ 154 | public function resource($name, $controller, array $options = []) 155 | { 156 | return $this->router->resource($name, $controller, $this->attributes + $options); 157 | } 158 | 159 | /** 160 | * Route an API resource to a controller. 161 | * 162 | * @param string $name 163 | * @param string $controller 164 | * @param array $options 165 | * @return \Illuminate\Routing\PendingResourceRegistration 166 | */ 167 | public function apiResource($name, $controller, array $options = []) 168 | { 169 | return $this->router->apiResource($name, $controller, $this->attributes + $options); 170 | } 171 | 172 | /** 173 | * Route a singleton resource to a controller. 174 | * 175 | * @param string $name 176 | * @param string $controller 177 | * @param array $options 178 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 179 | */ 180 | public function singleton($name, $controller, array $options = []) 181 | { 182 | return $this->router->singleton($name, $controller, $this->attributes + $options); 183 | } 184 | 185 | /** 186 | * Route an API singleton resource to a controller. 187 | * 188 | * @param string $name 189 | * @param string $controller 190 | * @param array $options 191 | * @return \Illuminate\Routing\PendingSingletonResourceRegistration 192 | */ 193 | public function apiSingleton($name, $controller, array $options = []) 194 | { 195 | return $this->router->apiSingleton($name, $controller, $this->attributes + $options); 196 | } 197 | 198 | /** 199 | * Create a route group with shared attributes. 200 | * 201 | * @param \Closure|array|string $callback 202 | * @return $this 203 | */ 204 | public function group($callback) 205 | { 206 | $this->router->group($this->attributes, $callback); 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Register a new route with the given verbs. 213 | * 214 | * @param array|string $methods 215 | * @param string $uri 216 | * @param \Closure|array|string|null $action 217 | * @return \Illuminate\Routing\Route 218 | */ 219 | public function match($methods, $uri, $action = null) 220 | { 221 | return $this->router->match($methods, $uri, $this->compileAction($action)); 222 | } 223 | 224 | /** 225 | * Register a new route with the router. 226 | * 227 | * @param string $method 228 | * @param string $uri 229 | * @param \Closure|array|string|null $action 230 | * @return \Illuminate\Routing\Route 231 | */ 232 | protected function registerRoute($method, $uri, $action = null) 233 | { 234 | if (! is_array($action)) { 235 | $action = array_merge($this->attributes, $action ? ['uses' => $action] : []); 236 | } 237 | 238 | return $this->router->{$method}($uri, $this->compileAction($action)); 239 | } 240 | 241 | /** 242 | * Compile the action into an array including the attributes. 243 | * 244 | * @param \Closure|array|string|null $action 245 | * @return array 246 | */ 247 | protected function compileAction($action) 248 | { 249 | if (is_null($action)) { 250 | return $this->attributes; 251 | } 252 | 253 | if (is_string($action) || $action instanceof Closure) { 254 | $action = ['uses' => $action]; 255 | } 256 | 257 | if (is_array($action) && 258 | array_is_list($action) && 259 | Reflector::isCallable($action)) { 260 | if (strncmp($action[0], '\\', 1)) { 261 | $action[0] = '\\'.$action[0]; 262 | } 263 | $action = [ 264 | 'uses' => $action[0].'@'.$action[1], 265 | 'controller' => $action[0].'@'.$action[1], 266 | ]; 267 | } 268 | 269 | return array_merge($this->attributes, $action); 270 | } 271 | 272 | /** 273 | * Dynamically handle calls into the route registrar. 274 | * 275 | * @param string $method 276 | * @param array $parameters 277 | * @return \Illuminate\Routing\Route|$this 278 | * 279 | * @throws \BadMethodCallException 280 | */ 281 | public function __call($method, $parameters) 282 | { 283 | if (in_array($method, $this->passthru)) { 284 | return $this->registerRoute($method, ...$parameters); 285 | } 286 | 287 | if (in_array($method, $this->allowedAttributes)) { 288 | if ($method === 'middleware') { 289 | return $this->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); 290 | } 291 | 292 | return $this->attribute($method, array_key_exists(0, $parameters) ? $parameters[0] : true); 293 | } 294 | 295 | throw new BadMethodCallException(sprintf( 296 | 'Method %s::%s does not exist.', static::class, $method 297 | )); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /RouteSignatureParameters.php: -------------------------------------------------------------------------------- 1 | getClosure() 23 | : $action['uses']; 24 | 25 | $parameters = is_string($callback) 26 | ? static::fromClassMethodString($callback) 27 | : (new ReflectionFunction($callback))->getParameters(); 28 | 29 | return match (true) { 30 | ! empty($conditions['subClass']) => array_filter($parameters, fn ($p) => Reflector::isParameterSubclassOf($p, $conditions['subClass'])), 31 | ! empty($conditions['backedEnum']) => array_filter($parameters, fn ($p) => Reflector::isParameterBackedEnumWithStringBackingType($p)), 32 | default => $parameters, 33 | }; 34 | } 35 | 36 | /** 37 | * Get the parameters for the given class / method by string. 38 | * 39 | * @param string $uses 40 | * @return array 41 | */ 42 | protected static function fromClassMethodString($uses) 43 | { 44 | [$class, $method] = Str::parseCallback($uses); 45 | 46 | if (! method_exists($class, $method) && Reflector::isCallable($class, $method)) { 47 | return []; 48 | } 49 | 50 | return (new ReflectionMethod($class, $method))->getParameters(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RouteUri.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 30 | $this->bindingFields = $bindingFields; 31 | } 32 | 33 | /** 34 | * Parse the given URI. 35 | * 36 | * @param string $uri 37 | * @return static 38 | */ 39 | public static function parse($uri) 40 | { 41 | preg_match_all('/\{([\w\:]+?)\??\}/', $uri, $matches); 42 | 43 | $bindingFields = []; 44 | 45 | foreach ($matches[0] as $match) { 46 | if (! str_contains($match, ':')) { 47 | continue; 48 | } 49 | 50 | $segments = explode(':', trim($match, '{}?')); 51 | 52 | $bindingFields[$segments[0]] = $segments[1]; 53 | 54 | $uri = str_contains($match, '?') 55 | ? str_replace($match, '{'.$segments[0].'?}', $uri) 56 | : str_replace($match, '{'.$segments[0].'}', $uri); 57 | } 58 | 59 | return new static($uri, $bindingFields); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /RouteUrlGenerator.php: -------------------------------------------------------------------------------- 1 | '/', 41 | '%40' => '@', 42 | '%3A' => ':', 43 | '%3B' => ';', 44 | '%2C' => ',', 45 | '%3D' => '=', 46 | '%2B' => '+', 47 | '%21' => '!', 48 | '%2A' => '*', 49 | '%7C' => '|', 50 | '%3F' => '?', 51 | '%26' => '&', 52 | '%23' => '#', 53 | '%25' => '%', 54 | ]; 55 | 56 | /** 57 | * Create a new Route URL generator. 58 | * 59 | * @param \Illuminate\Routing\UrlGenerator $url 60 | * @param \Illuminate\Http\Request $request 61 | */ 62 | public function __construct($url, $request) 63 | { 64 | $this->url = $url; 65 | $this->request = $request; 66 | } 67 | 68 | /** 69 | * Generate a URL for the given route. 70 | * 71 | * @param \Illuminate\Routing\Route $route 72 | * @param array $parameters 73 | * @param bool $absolute 74 | * @return string 75 | * 76 | * @throws \Illuminate\Routing\Exceptions\UrlGenerationException 77 | */ 78 | public function to($route, $parameters = [], $absolute = false) 79 | { 80 | $parameters = $this->formatParameters($route, $parameters); 81 | 82 | $domain = $this->getRouteDomain($route, $parameters); 83 | 84 | // First we will construct the entire URI including the root and query string. Once it 85 | // has been constructed, we'll make sure we don't have any missing parameters or we 86 | // will need to throw the exception to let the developers know one was not given. 87 | $uri = $this->addQueryString($this->url->format( 88 | $root = $this->replaceRootParameters($route, $domain, $parameters), 89 | $this->replaceRouteParameters($route->uri(), $parameters), 90 | $route 91 | ), $parameters); 92 | 93 | if (preg_match_all('/{(.*?)}/', $uri, $matchedMissingParameters)) { 94 | throw UrlGenerationException::forMissingParameters($route, $matchedMissingParameters[1]); 95 | } 96 | 97 | // Once we have ensured that there are no missing parameters in the URI we will encode 98 | // the URI and prepare it for returning to the developer. If the URI is supposed to 99 | // be absolute, we will return it as-is. Otherwise we will remove the URL's root. 100 | $uri = strtr(rawurlencode($uri), $this->dontEncode); 101 | 102 | if (! $absolute) { 103 | $uri = preg_replace('#^(//|[^/?])+#', '', $uri); 104 | 105 | if ($base = $this->request->getBaseUrl()) { 106 | $uri = preg_replace('#^'.$base.'#i', '', $uri); 107 | } 108 | 109 | return '/'.ltrim($uri, '/'); 110 | } 111 | 112 | return $uri; 113 | } 114 | 115 | /** 116 | * Get the formatted domain for a given route. 117 | * 118 | * @param \Illuminate\Routing\Route $route 119 | * @param array $parameters 120 | * @return string 121 | */ 122 | protected function getRouteDomain($route, &$parameters) 123 | { 124 | return $route->getDomain() ? $this->formatDomain($route, $parameters) : null; 125 | } 126 | 127 | /** 128 | * Format the domain and port for the route and request. 129 | * 130 | * @param \Illuminate\Routing\Route $route 131 | * @param array $parameters 132 | * @return string 133 | */ 134 | protected function formatDomain($route, &$parameters) 135 | { 136 | return $this->addPortToDomain( 137 | $this->getRouteScheme($route).$route->getDomain() 138 | ); 139 | } 140 | 141 | /** 142 | * Get the scheme for the given route. 143 | * 144 | * @param \Illuminate\Routing\Route $route 145 | * @return string 146 | */ 147 | protected function getRouteScheme($route) 148 | { 149 | if ($route->httpOnly()) { 150 | return 'http://'; 151 | } elseif ($route->httpsOnly()) { 152 | return 'https://'; 153 | } 154 | 155 | return $this->url->formatScheme(); 156 | } 157 | 158 | /** 159 | * Add the port to the domain if necessary. 160 | * 161 | * @param string $domain 162 | * @return string 163 | */ 164 | protected function addPortToDomain($domain) 165 | { 166 | $secure = $this->request->isSecure(); 167 | 168 | $port = (int) $this->request->getPort(); 169 | 170 | return ($secure && $port === 443) || (! $secure && $port === 80) 171 | ? $domain 172 | : $domain.':'.$port; 173 | } 174 | 175 | /** 176 | * Format the array of route parameters. 177 | * 178 | * @param \Illuminate\Routing\Route $route 179 | * @param mixed $parameters 180 | * @return array 181 | */ 182 | protected function formatParameters(Route $route, $parameters) 183 | { 184 | $parameters = Arr::wrap($parameters); 185 | 186 | $namedParameters = []; 187 | $namedQueryParameters = []; 188 | $requiredRouteParametersWithoutDefaultsOrNamedParameters = []; 189 | 190 | $routeParameters = $route->parameterNames(); 191 | $optionalParameters = $route->getOptionalParameterNames(); 192 | 193 | foreach ($routeParameters as $name) { 194 | if (isset($parameters[$name])) { 195 | // Named parameters don't need any special handling... 196 | $namedParameters[$name] = $parameters[$name]; 197 | unset($parameters[$name]); 198 | 199 | continue; 200 | } else { 201 | $bindingField = $route->bindingFieldFor($name); 202 | $defaultParameterKey = $bindingField ? "$name:$bindingField" : $name; 203 | 204 | if (! isset($this->defaultParameters[$defaultParameterKey]) && ! isset($optionalParameters[$name])) { 205 | // No named parameter or default value for a required parameter, try to match to positional parameter below... 206 | array_push($requiredRouteParametersWithoutDefaultsOrNamedParameters, $name); 207 | } 208 | } 209 | 210 | $namedParameters[$name] = ''; 211 | } 212 | 213 | // Named parameters that don't have route parameters will be used for query string... 214 | foreach ($parameters as $key => $value) { 215 | if (is_string($key)) { 216 | $namedQueryParameters[$key] = $value; 217 | 218 | unset($parameters[$key]); 219 | } 220 | } 221 | 222 | // Match positional parameters to the route parameters that didn't have a value in order... 223 | if (count($parameters) == count($requiredRouteParametersWithoutDefaultsOrNamedParameters)) { 224 | foreach (array_reverse($requiredRouteParametersWithoutDefaultsOrNamedParameters) as $name) { 225 | if (count($parameters) === 0) { 226 | break; 227 | } 228 | 229 | $namedParameters[$name] = array_pop($parameters); 230 | } 231 | } 232 | 233 | $offset = 0; 234 | $emptyParameters = array_filter($namedParameters, static fn ($val) => $val === ''); 235 | 236 | if (count($requiredRouteParametersWithoutDefaultsOrNamedParameters) !== 0 && 237 | count($parameters) !== count($emptyParameters)) { 238 | // Find the index of the first required parameter... 239 | $offset = array_search($requiredRouteParametersWithoutDefaultsOrNamedParameters[0], array_keys($namedParameters)); 240 | 241 | // If more empty parameters remain, adjust the offset... 242 | $remaining = count($emptyParameters) - $offset - count($parameters); 243 | 244 | if ($remaining < 0) { 245 | // Effectively subtract the remaining count since it's negative... 246 | $offset += $remaining; 247 | } 248 | 249 | // Correct offset if it goes below zero... 250 | if ($offset < 0) { 251 | $offset = 0; 252 | } 253 | } elseif (count($requiredRouteParametersWithoutDefaultsOrNamedParameters) === 0 && count($parameters) !== 0) { 254 | // Handle the case where all passed parameters are for parameters that have default values... 255 | $remainingCount = count($parameters); 256 | 257 | // Loop over empty parameters backwards and stop when we run out of passed parameters... 258 | for ($i = count($namedParameters) - 1; $i >= 0; $i--) { 259 | if ($namedParameters[array_keys($namedParameters)[$i]] === '') { 260 | $offset = $i; 261 | $remainingCount--; 262 | 263 | if ($remainingCount === 0) { 264 | // If there are no more passed parameters, we stop here... 265 | break; 266 | } 267 | } 268 | } 269 | } 270 | 271 | // Starting from the offset, match any passed parameters from left to right... 272 | for ($i = $offset; $i < count($namedParameters); $i++) { 273 | $key = array_keys($namedParameters)[$i]; 274 | 275 | if ($namedParameters[$key] !== '') { 276 | continue; 277 | } elseif (! empty($parameters)) { 278 | $namedParameters[$key] = array_shift($parameters); 279 | } 280 | } 281 | 282 | // Fill leftmost parameters with defaults if the loop above was offset... 283 | foreach ($namedParameters as $key => $value) { 284 | $bindingField = $route->bindingFieldFor($key); 285 | $defaultParameterKey = $bindingField ? "$key:$bindingField" : $key; 286 | 287 | if ($value === '' && isset($this->defaultParameters[$defaultParameterKey])) { 288 | $namedParameters[$key] = $this->defaultParameters[$defaultParameterKey]; 289 | } 290 | } 291 | 292 | // Any remaining values in $parameters are unnamed query string parameters... 293 | $parameters = array_merge($namedParameters, $namedQueryParameters, $parameters); 294 | 295 | $parameters = Collection::wrap($parameters)->map(function ($value, $key) use ($route) { 296 | return $value instanceof UrlRoutable && $route->bindingFieldFor($key) 297 | ? $value->{$route->bindingFieldFor($key)} 298 | : $value; 299 | })->all(); 300 | 301 | array_walk_recursive($parameters, function (&$item) { 302 | if ($item instanceof BackedEnum) { 303 | $item = $item->value; 304 | } 305 | }); 306 | 307 | return $this->url->formatParameters($parameters); 308 | } 309 | 310 | /** 311 | * Replace the parameters on the root path. 312 | * 313 | * @param \Illuminate\Routing\Route $route 314 | * @param string $domain 315 | * @param array $parameters 316 | * @return string 317 | */ 318 | protected function replaceRootParameters($route, $domain, &$parameters) 319 | { 320 | $scheme = $this->getRouteScheme($route); 321 | 322 | return $this->replaceRouteParameters( 323 | $this->url->formatRoot($scheme, $domain), $parameters 324 | ); 325 | } 326 | 327 | /** 328 | * Replace all of the wildcard parameters for a route path. 329 | * 330 | * @param string $path 331 | * @param array $parameters 332 | * @return string 333 | */ 334 | protected function replaceRouteParameters($path, array &$parameters) 335 | { 336 | $path = $this->replaceNamedParameters($path, $parameters); 337 | 338 | $path = preg_replace_callback('/\{.*?\}/', function ($match) use (&$parameters) { 339 | // Reset only the numeric keys... 340 | $parameters = array_merge($parameters); 341 | 342 | return (! isset($parameters[0]) && ! str_ends_with($match[0], '?}')) 343 | ? $match[0] 344 | : Arr::pull($parameters, 0); 345 | }, $path); 346 | 347 | return trim(preg_replace('/\{.*?\?\}/', '', $path), '/'); 348 | } 349 | 350 | /** 351 | * Replace all of the named parameters in the path. 352 | * 353 | * @param string $path 354 | * @param array $parameters 355 | * @return string 356 | */ 357 | protected function replaceNamedParameters($path, &$parameters) 358 | { 359 | return preg_replace_callback('/\{(.*?)(\?)?\}/', function ($m) use (&$parameters) { 360 | if (isset($parameters[$m[1]]) && $parameters[$m[1]] !== '') { 361 | return Arr::pull($parameters, $m[1]); 362 | } elseif (isset($this->defaultParameters[$m[1]])) { 363 | return $this->defaultParameters[$m[1]]; 364 | } elseif (isset($parameters[$m[1]])) { 365 | Arr::pull($parameters, $m[1]); 366 | } 367 | 368 | return $m[0]; 369 | }, $path); 370 | } 371 | 372 | /** 373 | * Add a query string to the URI. 374 | * 375 | * @param string $uri 376 | * @param array $parameters 377 | * @return mixed|string 378 | */ 379 | protected function addQueryString($uri, array $parameters) 380 | { 381 | // If the URI has a fragment we will move it to the end of this URI since it will 382 | // need to come after any query string that may be added to the URL else it is 383 | // not going to be available. We will remove it then append it back on here. 384 | if (! is_null($fragment = parse_url($uri, PHP_URL_FRAGMENT))) { 385 | $uri = preg_replace('/#.*/', '', $uri); 386 | } 387 | 388 | $uri .= $this->getRouteQueryString($parameters); 389 | 390 | return is_null($fragment) ? $uri : $uri."#{$fragment}"; 391 | } 392 | 393 | /** 394 | * Get the query string for a given route. 395 | * 396 | * @param array $parameters 397 | * @return string 398 | */ 399 | protected function getRouteQueryString(array $parameters) 400 | { 401 | // First we will get all of the string parameters that are remaining after we 402 | // have replaced the route wildcards. We'll then build a query string from 403 | // these string parameters then use it as a starting point for the rest. 404 | if (count($parameters) === 0) { 405 | return ''; 406 | } 407 | 408 | $query = Arr::query( 409 | $keyed = $this->getStringParameters($parameters) 410 | ); 411 | 412 | // Lastly, if there are still parameters remaining, we will fetch the numeric 413 | // parameters that are in the array and add them to the query string or we 414 | // will make the initial query string if it wasn't started with strings. 415 | if (count($keyed) < count($parameters)) { 416 | $query .= '&'.implode( 417 | '&', $this->getNumericParameters($parameters) 418 | ); 419 | } 420 | 421 | $query = trim($query, '&'); 422 | 423 | return $query === '' ? '' : "?{$query}"; 424 | } 425 | 426 | /** 427 | * Get the string parameters from a given list. 428 | * 429 | * @param array $parameters 430 | * @return array 431 | */ 432 | protected function getStringParameters(array $parameters) 433 | { 434 | return array_filter($parameters, 'is_string', ARRAY_FILTER_USE_KEY); 435 | } 436 | 437 | /** 438 | * Get the numeric parameters from a given list. 439 | * 440 | * @param array $parameters 441 | * @return array 442 | */ 443 | protected function getNumericParameters(array $parameters) 444 | { 445 | return array_filter($parameters, 'is_numeric', ARRAY_FILTER_USE_KEY); 446 | } 447 | 448 | /** 449 | * Set the default named parameters used by the URL generator. 450 | * 451 | * @param array $defaults 452 | * @return void 453 | */ 454 | public function defaults(array $defaults) 455 | { 456 | $this->defaultParameters = array_merge( 457 | $this->defaultParameters, $defaults 458 | ); 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /RoutingServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRouter(); 27 | $this->registerUrlGenerator(); 28 | $this->registerRedirector(); 29 | $this->registerPsrRequest(); 30 | $this->registerPsrResponse(); 31 | $this->registerResponseFactory(); 32 | $this->registerCallableDispatcher(); 33 | $this->registerControllerDispatcher(); 34 | } 35 | 36 | /** 37 | * Register the router instance. 38 | * 39 | * @return void 40 | */ 41 | protected function registerRouter() 42 | { 43 | $this->app->singleton('router', function ($app) { 44 | return new Router($app['events'], $app); 45 | }); 46 | } 47 | 48 | /** 49 | * Register the URL generator service. 50 | * 51 | * @return void 52 | */ 53 | protected function registerUrlGenerator() 54 | { 55 | $this->app->singleton('url', function ($app) { 56 | $routes = $app['router']->getRoutes(); 57 | 58 | // The URL generator needs the route collection that exists on the router. 59 | // Keep in mind this is an object, so we're passing by references here 60 | // and all the registered routes will be available to the generator. 61 | $app->instance('routes', $routes); 62 | 63 | return new UrlGenerator( 64 | $routes, $app->rebinding( 65 | 'request', $this->requestRebinder() 66 | ), $app['config']['app.asset_url'] 67 | ); 68 | }); 69 | 70 | $this->app->extend('url', function (UrlGeneratorContract $url, $app) { 71 | // Next we will set a few service resolvers on the URL generator so it can 72 | // get the information it needs to function. This just provides some of 73 | // the convenience features to this URL generator like "signed" URLs. 74 | $url->setSessionResolver(function () { 75 | return $this->app['session'] ?? null; 76 | }); 77 | 78 | $url->setKeyResolver(function () { 79 | $config = $this->app->make('config'); 80 | 81 | return [$config->get('app.key'), ...($config->get('app.previous_keys') ?? [])]; 82 | }); 83 | 84 | // If the route collection is "rebound", for example, when the routes stay 85 | // cached for the application, we will need to rebind the routes on the 86 | // URL generator instance so it has the latest version of the routes. 87 | $app->rebinding('routes', function ($app, $routes) { 88 | $app['url']->setRoutes($routes); 89 | }); 90 | 91 | return $url; 92 | }); 93 | } 94 | 95 | /** 96 | * Get the URL generator request rebinder. 97 | * 98 | * @return \Closure 99 | */ 100 | protected function requestRebinder() 101 | { 102 | return function ($app, $request) { 103 | $app['url']->setRequest($request); 104 | }; 105 | } 106 | 107 | /** 108 | * Register the Redirector service. 109 | * 110 | * @return void 111 | */ 112 | protected function registerRedirector() 113 | { 114 | $this->app->singleton('redirect', function ($app) { 115 | $redirector = new Redirector($app['url']); 116 | 117 | // If the session is set on the application instance, we'll inject it into 118 | // the redirector instance. This allows the redirect responses to allow 119 | // for the quite convenient "with" methods that flash to the session. 120 | if (isset($app['session.store'])) { 121 | $redirector->setSession($app['session.store']); 122 | } 123 | 124 | return $redirector; 125 | }); 126 | } 127 | 128 | /** 129 | * Register a binding for the PSR-7 request implementation. 130 | * 131 | * @return void 132 | */ 133 | protected function registerPsrRequest() 134 | { 135 | $this->app->bind(ServerRequestInterface::class, function ($app) { 136 | if (class_exists(PsrHttpFactory::class)) { 137 | return with((new PsrHttpFactory) 138 | ->createRequest($illuminateRequest = $app->make('request')), function (ServerRequestInterface $request) use ($illuminateRequest) { 139 | if ($illuminateRequest->getContentTypeFormat() !== 'json' && $illuminateRequest->request->count() === 0) { 140 | return $request; 141 | } 142 | 143 | return $request->withParsedBody( 144 | array_merge($request->getParsedBody() ?? [], $illuminateRequest->getPayload()->all()) 145 | ); 146 | }); 147 | } 148 | 149 | throw new BindingResolutionException('Unable to resolve PSR request. Please install the "symfony/psr-http-message-bridge" package.'); 150 | }); 151 | } 152 | 153 | /** 154 | * Register a binding for the PSR-7 response implementation. 155 | * 156 | * @return void 157 | */ 158 | protected function registerPsrResponse() 159 | { 160 | $this->app->bind(ResponseInterface::class, function () { 161 | if (class_exists(PsrHttpFactory::class)) { 162 | return (new PsrHttpFactory)->createResponse(new Response); 163 | } 164 | 165 | throw new BindingResolutionException('Unable to resolve PSR response. Please install the "symfony/psr-http-message-bridge" package.'); 166 | }); 167 | } 168 | 169 | /** 170 | * Register the response factory implementation. 171 | * 172 | * @return void 173 | */ 174 | protected function registerResponseFactory() 175 | { 176 | $this->app->singleton(ResponseFactoryContract::class, function ($app) { 177 | return new ResponseFactory($app[ViewFactoryContract::class], $app['redirect']); 178 | }); 179 | } 180 | 181 | /** 182 | * Register the callable dispatcher. 183 | * 184 | * @return void 185 | */ 186 | protected function registerCallableDispatcher() 187 | { 188 | $this->app->singleton(CallableDispatcherContract::class, function ($app) { 189 | return new CallableDispatcher($app); 190 | }); 191 | } 192 | 193 | /** 194 | * Register the controller dispatcher. 195 | * 196 | * @return void 197 | */ 198 | protected function registerControllerDispatcher() 199 | { 200 | $this->app->singleton(ControllerDispatcherContract::class, function ($app) { 201 | return new ControllerDispatcher($app); 202 | }); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /SortedMiddleware.php: -------------------------------------------------------------------------------- 1 | all(); 19 | } 20 | 21 | $this->items = $this->sortMiddleware($priorityMap, $middlewares); 22 | } 23 | 24 | /** 25 | * Sort the middlewares by the given priority map. 26 | * 27 | * Each call to this method makes one discrete middleware movement if necessary. 28 | * 29 | * @param array $priorityMap 30 | * @param array $middlewares 31 | * @return array 32 | */ 33 | protected function sortMiddleware($priorityMap, $middlewares) 34 | { 35 | $lastIndex = 0; 36 | 37 | foreach ($middlewares as $index => $middleware) { 38 | if (! is_string($middleware)) { 39 | continue; 40 | } 41 | 42 | $priorityIndex = $this->priorityMapIndex($priorityMap, $middleware); 43 | 44 | if (! is_null($priorityIndex)) { 45 | // This middleware is in the priority map. If we have encountered another middleware 46 | // that was also in the priority map and was at a lower priority than the current 47 | // middleware, we will move this middleware to be above the previous encounter. 48 | if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) { 49 | return $this->sortMiddleware( 50 | $priorityMap, array_values($this->moveMiddleware($middlewares, $index, $lastIndex)) 51 | ); 52 | } 53 | 54 | // This middleware is in the priority map; but, this is the first middleware we have 55 | // encountered from the map thus far. We'll save its current index plus its index 56 | // from the priority map so we can compare against them on the next iterations. 57 | $lastIndex = $index; 58 | 59 | $lastPriorityIndex = $priorityIndex; 60 | } 61 | } 62 | 63 | return Router::uniqueMiddleware($middlewares); 64 | } 65 | 66 | /** 67 | * Calculate the priority map index of the middleware. 68 | * 69 | * @param array $priorityMap 70 | * @param string $middleware 71 | * @return int|null 72 | */ 73 | protected function priorityMapIndex($priorityMap, $middleware) 74 | { 75 | foreach ($this->middlewareNames($middleware) as $name) { 76 | $priorityIndex = array_search($name, $priorityMap); 77 | 78 | if ($priorityIndex !== false) { 79 | return $priorityIndex; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Resolve the middleware names to look for in the priority array. 86 | * 87 | * @param string $middleware 88 | * @return \Generator 89 | */ 90 | protected function middlewareNames($middleware) 91 | { 92 | $stripped = head(explode(':', $middleware)); 93 | 94 | yield $stripped; 95 | 96 | $interfaces = @class_implements($stripped); 97 | 98 | if ($interfaces !== false) { 99 | foreach ($interfaces as $interface) { 100 | yield $interface; 101 | } 102 | } 103 | 104 | $parents = @class_parents($stripped); 105 | 106 | if ($parents !== false) { 107 | foreach ($parents as $parent) { 108 | yield $parent; 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Splice a middleware into a new position and remove the old entry. 115 | * 116 | * @param array $middlewares 117 | * @param int $from 118 | * @param int $to 119 | * @return array 120 | */ 121 | protected function moveMiddleware($middlewares, $from, $to) 122 | { 123 | array_splice($middlewares, $to, 0, $middlewares[$from]); 124 | 125 | unset($middlewares[$from + 1]); 126 | 127 | return $middlewares; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /ViewController.php: -------------------------------------------------------------------------------- 1 | response = $response; 24 | } 25 | 26 | /** 27 | * Invoke the controller method. 28 | * 29 | * @param mixed ...$args 30 | * @return \Illuminate\Http\Response 31 | */ 32 | public function __invoke(...$args) 33 | { 34 | $routeParameters = array_filter($args, function ($key) { 35 | return ! in_array($key, ['view', 'data', 'status', 'headers']); 36 | }, ARRAY_FILTER_USE_KEY); 37 | 38 | $args['data'] = array_merge($args['data'], $routeParameters); 39 | 40 | return $this->response->view( 41 | $args['view'], 42 | $args['data'], 43 | $args['status'], 44 | $args['headers'] 45 | ); 46 | } 47 | 48 | /** 49 | * Execute an action on the controller. 50 | * 51 | * @param string $method 52 | * @param array $parameters 53 | * @return \Symfony\Component\HttpFoundation\Response 54 | */ 55 | public function callAction($method, $parameters) 56 | { 57 | return $this->{$method}(...$parameters); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminate/routing", 3 | "description": "The Illuminate Routing package.", 4 | "license": "MIT", 5 | "homepage": "https://laravel.com", 6 | "support": { 7 | "issues": "https://github.com/laravel/framework/issues", 8 | "source": "https://github.com/laravel/framework" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.3", 18 | "ext-filter": "*", 19 | "ext-hash": "*", 20 | "illuminate/collections": "^13.0", 21 | "illuminate/container": "^13.0", 22 | "illuminate/contracts": "^13.0", 23 | "illuminate/http": "^13.0", 24 | "illuminate/macroable": "^13.0", 25 | "illuminate/pipeline": "^13.0", 26 | "illuminate/session": "^13.0", 27 | "illuminate/support": "^13.0", 28 | "symfony/http-foundation": "^7.2.0", 29 | "symfony/http-kernel": "^7.2.0", 30 | "symfony/routing": "^7.2.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Illuminate\\Routing\\": "" 35 | } 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "13.0.x-dev" 40 | } 41 | }, 42 | "suggest": { 43 | "illuminate/console": "Required to use the make commands (^13.0).", 44 | "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", 45 | "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." 46 | }, 47 | "config": { 48 | "sort-packages": true, 49 | "allow-plugins": { 50 | "php-http/discovery": false 51 | } 52 | }, 53 | "minimum-stability": "dev" 54 | } 55 | --------------------------------------------------------------------------------