Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -0,0 +1,284 @@
<?php
namespace Illuminate\Routing;
use ArrayIterator;
use Countable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
use IteratorAggregate;
use LogicException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
use Symfony\Component\Routing\RouteCollection as SymfonyRouteCollection;
use Traversable;
abstract class AbstractRouteCollection implements Countable, IteratorAggregate, RouteCollectionInterface
{
/**
* Handle the matched route.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Routing\Route|null $route
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
protected function handleMatchedRoute(Request $request, $route)
{
if (! is_null($route)) {
return $route->bind($request);
}
// If no route was found we will now check if a matching route is specified by
// another HTTP verb. If it is we will need to throw a MethodNotAllowed and
// inform the user agent of which HTTP verb it should use for this route.
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException(sprintf(
'The route %s could not be found.',
$request->path()
));
}
/**
* Determine if any routes match on another HTTP verb.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function checkForAlternateVerbs($request)
{
$methods = array_diff(Router::$verbs, [$request->getMethod()]);
// Here we will spin through all verbs except for the current request verb and
// check to see if any routes respond to them. If they do, we will return a
// proper error response with the correct headers on the response string.
return array_values(array_filter(
$methods,
function ($method) use ($request) {
return ! is_null($this->matchAgainstRoutes($this->get($method), $request, false));
}
));
}
/**
* Determine if a route in the array matches the request.
*
* @param \Illuminate\Routing\Route[] $routes
* @param \Illuminate\Http\Request $request
* @param bool $includingMethod
* @return \Illuminate\Routing\Route|null
*/
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
return $routes->merge($fallbacks)->first(
fn (Route $route) => $route->matches($request, $includingMethod)
);
}
/**
* Get a route (if necessary) that responds when other available methods are present.
*
* @param \Illuminate\Http\Request $request
* @param string[] $methods
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
*/
protected function getRouteForMethods($request, array $methods)
{
if ($request->isMethod('OPTIONS')) {
return (new Route('OPTIONS', $request->path(), function () use ($methods) {
return new Response('', 200, ['Allow' => implode(',', $methods)]);
}))->bind($request);
}
$this->requestMethodNotAllowed($request, $methods, $request->method());
}
/**
* Throw a method not allowed HTTP exception.
*
* @param \Illuminate\Http\Request $request
* @param array $others
* @param string $method
* @return void
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
*/
protected function requestMethodNotAllowed($request, array $others, $method)
{
throw new MethodNotAllowedHttpException(
$others,
sprintf(
'The %s method is not supported for route %s. Supported methods: %s.',
$request->path(),
$method,
implode(', ', $others)
)
);
}
/**
* Throw a method not allowed HTTP exception.
*
* @param array $others
* @param string $method
* @return void
*
* @deprecated use requestMethodNotAllowed
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
*/
protected function methodNotAllowed(array $others, $method)
{
throw new MethodNotAllowedHttpException(
$others,
sprintf(
'The %s method is not supported for this route. Supported methods: %s.',
$method,
implode(', ', $others)
)
);
}
/**
* Compile the routes for caching.
*
* @return array
*/
public function compile()
{
$compiled = $this->dumper()->getCompiledRoutes();
$attributes = [];
foreach ($this->getRoutes() as $route) {
$attributes[$route->getName()] = [
'methods' => $route->methods(),
'uri' => $route->uri(),
'action' => $route->getAction(),
'fallback' => $route->isFallback,
'defaults' => $route->defaults,
'wheres' => $route->wheres,
'bindingFields' => $route->bindingFields(),
'lockSeconds' => $route->locksFor(),
'waitSeconds' => $route->waitsFor(),
'withTrashed' => $route->allowsTrashedBindings(),
];
}
return compact('compiled', 'attributes');
}
/**
* Return the CompiledUrlMatcherDumper instance for the route collection.
*
* @return \Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper
*/
public function dumper()
{
return new CompiledUrlMatcherDumper($this->toSymfonyRouteCollection());
}
/**
* Convert the collection to a Symfony RouteCollection instance.
*
* @return \Symfony\Component\Routing\RouteCollection
*/
public function toSymfonyRouteCollection()
{
$symfonyRoutes = new SymfonyRouteCollection;
$routes = $this->getRoutes();
foreach ($routes as $route) {
if (! $route->isFallback) {
$symfonyRoutes = $this->addToSymfonyRoutesCollection($symfonyRoutes, $route);
}
}
foreach ($routes as $route) {
if ($route->isFallback) {
$symfonyRoutes = $this->addToSymfonyRoutesCollection($symfonyRoutes, $route);
}
}
return $symfonyRoutes;
}
/**
* Add a route to the SymfonyRouteCollection instance.
*
* @param \Symfony\Component\Routing\RouteCollection $symfonyRoutes
* @param \Illuminate\Routing\Route $route
* @return \Symfony\Component\Routing\RouteCollection
*
* @throws \LogicException
*/
protected function addToSymfonyRoutesCollection(SymfonyRouteCollection $symfonyRoutes, Route $route)
{
$name = $route->getName();
if (
! is_null($name)
&& str_ends_with($name, '.')
&& ! is_null($symfonyRoutes->get($name))
) {
$name = null;
}
if (! $name) {
$route->name($this->generateRouteName());
$this->add($route);
} elseif (! is_null($symfonyRoutes->get($name))) {
throw new LogicException("Unable to prepare route [{$route->uri}] for serialization. Another route has already been assigned name [{$name}].");
}
$symfonyRoutes->add($route->getName(), $route->toSymfonyRoute());
return $symfonyRoutes;
}
/**
* Get a randomly generated route name.
*
* @return string
*/
protected function generateRouteName()
{
return 'generated::'.Str::random();
}
/**
* Get an iterator for the items.
*
* @return \ArrayIterator
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->getRoutes());
}
/**
* Count the number of items in the collection.
*
* @return int
*/
public function count(): int
{
return count($this->getRoutes());
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Container\Container;
use Illuminate\Routing\Contracts\CallableDispatcher as CallableDispatcherContract;
use ReflectionFunction;
class CallableDispatcher implements CallableDispatcherContract
{
use RouteDependencyResolverTrait;
/**
* The container instance.
*
* @var \Illuminate\Container\Container
*/
protected $container;
/**
* Create a new callable dispatcher instance.
*
* @param \Illuminate\Container\Container $container
* @return void
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Dispatch a request to a given callable.
*
* @param \Illuminate\Routing\Route $route
* @param callable $callable
* @return mixed
*/
public function dispatch(Route $route, $callable)
{
return $callable(...array_values($this->resolveParameters($route, $callable)));
}
/**
* Resolve the parameters for the callable.
*
* @param \Illuminate\Routing\Route $route
* @param callable $callable
* @return array
*/
protected function resolveParameters(Route $route, $callable)
{
return $this->resolveMethodDependencies($route->parametersWithoutNulls(), new ReflectionFunction($callable));
}
}

View File

@@ -0,0 +1,330 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Container\Container;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
use Symfony\Component\Routing\RequestContext;
class CompiledRouteCollection extends AbstractRouteCollection
{
/**
* The compiled routes collection.
*
* @var array
*/
protected $compiled = [];
/**
* An array of the route attributes keyed by name.
*
* @var array
*/
protected $attributes = [];
/**
* The dynamically added routes that were added after loading the cached, compiled routes.
*
* @var \Illuminate\Routing\RouteCollection|null
*/
protected $routes;
/**
* The router instance used by the route.
*
* @var \Illuminate\Routing\Router
*/
protected $router;
/**
* The container instance used by the route.
*
* @var \Illuminate\Container\Container
*/
protected $container;
/**
* Create a new CompiledRouteCollection instance.
*
* @param array $compiled
* @param array $attributes
* @return void
*/
public function __construct(array $compiled, array $attributes)
{
$this->compiled = $compiled;
$this->attributes = $attributes;
$this->routes = new RouteCollection;
}
/**
* Add a Route instance to the collection.
*
* @param \Illuminate\Routing\Route $route
* @return \Illuminate\Routing\Route
*/
public function add(Route $route)
{
return $this->routes->add($route);
}
/**
* Refresh the name look-up table.
*
* This is done in case any names are fluently defined or if routes are overwritten.
*
* @return void
*/
public function refreshNameLookups()
{
//
}
/**
* Refresh the action look-up table.
*
* This is done in case any actions are overwritten with new controllers.
*
* @return void
*/
public function refreshActionLookups()
{
//
}
/**
* Find the first route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function match(Request $request)
{
$matcher = new CompiledUrlMatcher(
$this->compiled, (new RequestContext)->fromRequest(
$trimmedRequest = $this->requestWithoutTrailingSlash($request)
)
);
$route = null;
try {
if ($result = $matcher->matchRequest($trimmedRequest)) {
$route = $this->getByName($result['_route']);
}
} catch (ResourceNotFoundException|MethodNotAllowedException $e) {
try {
return $this->routes->match($request);
} catch (NotFoundHttpException $e) {
//
}
}
if ($route && $route->isFallback) {
try {
$dynamicRoute = $this->routes->match($request);
if (! $dynamicRoute->isFallback) {
$route = $dynamicRoute;
}
} catch (NotFoundHttpException|MethodNotAllowedHttpException $e) {
//
}
}
return $this->handleMatchedRoute($request, $route);
}
/**
* Get a cloned instance of the given request without any trailing slash on the URI.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Request
*/
protected function requestWithoutTrailingSlash(Request $request)
{
$trimmedRequest = $request->duplicate();
$parts = explode('?', $request->server->get('REQUEST_URI'), 2);
$trimmedRequest->server->set(
'REQUEST_URI', rtrim($parts[0], '/').(isset($parts[1]) ? '?'.$parts[1] : '')
);
return $trimmedRequest;
}
/**
* Get routes from the collection by method.
*
* @param string|null $method
* @return \Illuminate\Routing\Route[]
*/
public function get($method = null)
{
return $this->getRoutesByMethod()[$method] ?? [];
}
/**
* Determine if the route collection contains a given named route.
*
* @param string $name
* @return bool
*/
public function hasNamedRoute($name)
{
return isset($this->attributes[$name]) || $this->routes->hasNamedRoute($name);
}
/**
* Get a route instance by its name.
*
* @param string $name
* @return \Illuminate\Routing\Route|null
*/
public function getByName($name)
{
if (isset($this->attributes[$name])) {
return $this->newRoute($this->attributes[$name]);
}
return $this->routes->getByName($name);
}
/**
* Get a route instance by its controller action.
*
* @param string $action
* @return \Illuminate\Routing\Route|null
*/
public function getByAction($action)
{
$attributes = collect($this->attributes)->first(function (array $attributes) use ($action) {
if (isset($attributes['action']['controller'])) {
return trim($attributes['action']['controller'], '\\') === $action;
}
return $attributes['action']['uses'] === $action;
});
if ($attributes) {
return $this->newRoute($attributes);
}
return $this->routes->getByAction($action);
}
/**
* Get all of the routes in the collection.
*
* @return \Illuminate\Routing\Route[]
*/
public function getRoutes()
{
return collect($this->attributes)
->map(function (array $attributes) {
return $this->newRoute($attributes);
})
->merge($this->routes->getRoutes())
->values()
->all();
}
/**
* Get all of the routes keyed by their HTTP verb / method.
*
* @return array
*/
public function getRoutesByMethod()
{
return collect($this->getRoutes())
->groupBy(function (Route $route) {
return $route->methods();
})
->map(function (Collection $routes) {
return $routes->mapWithKeys(function (Route $route) {
return [$route->getDomain().$route->uri => $route];
})->all();
})
->all();
}
/**
* Get all of the routes keyed by their name.
*
* @return \Illuminate\Routing\Route[]
*/
public function getRoutesByName()
{
return collect($this->getRoutes())
->keyBy(function (Route $route) {
return $route->getName();
})
->all();
}
/**
* Resolve an array of attributes to a Route instance.
*
* @param array $attributes
* @return \Illuminate\Routing\Route
*/
protected function newRoute(array $attributes)
{
if (empty($attributes['action']['prefix'] ?? '')) {
$baseUri = $attributes['uri'];
} else {
$prefix = trim($attributes['action']['prefix'], '/');
$baseUri = trim(implode(
'/', array_slice(
explode('/', trim($attributes['uri'], '/')),
count($prefix !== '' ? explode('/', $prefix) : [])
)
), '/');
}
return $this->router->newRoute($attributes['methods'], $baseUri === '' ? '/' : $baseUri, $attributes['action'])
->setFallback($attributes['fallback'])
->setDefaults($attributes['defaults'])
->setWheres($attributes['wheres'])
->setBindingFields($attributes['bindingFields'])
->block($attributes['lockSeconds'] ?? null, $attributes['waitSeconds'] ?? null)
->withTrashed($attributes['withTrashed'] ?? false);
}
/**
* Set the router instance on the route.
*
* @param \Illuminate\Routing\Router $router
* @return $this
*/
public function setRouter(Router $router)
{
$this->router = $router;
return $this;
}
/**
* Set the container instance on the route.
*
* @param \Illuminate\Container\Container $container
* @return $this
*/
public function setContainer(Container $container)
{
$this->container = $container;
return $this;
}
}

View File

@@ -2,13 +2,17 @@
namespace Illuminate\Routing\Console;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Console\GeneratorCommand;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'make:controller')]
class ControllerMakeCommand extends GeneratorCommand
{
use CreatesMatchingTest;
/**
* The console command name.
*
@@ -16,6 +20,17 @@ class ControllerMakeCommand extends GeneratorCommand
*/
protected $name = 'make:controller';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'make:controller';
/**
* The console command description.
*
@@ -37,15 +52,46 @@ class ControllerMakeCommand extends GeneratorCommand
*/
protected function getStub()
{
if ($this->option('parent')) {
return __DIR__.'/stubs/controller.nested.stub';
$stub = null;
if ($type = $this->option('type')) {
$stub = "/stubs/controller.{$type}.stub";
} elseif ($this->option('parent')) {
$stub = $this->option('singleton')
? '/stubs/controller.nested.singleton.stub'
: '/stubs/controller.nested.stub';
} elseif ($this->option('model')) {
return __DIR__.'/stubs/controller.model.stub';
$stub = '/stubs/controller.model.stub';
} elseif ($this->option('invokable')) {
$stub = '/stubs/controller.invokable.stub';
} elseif ($this->option('singleton')) {
$stub = '/stubs/controller.singleton.stub';
} elseif ($this->option('resource')) {
return __DIR__.'/stubs/controller.stub';
$stub = '/stubs/controller.stub';
}
return __DIR__.'/stubs/controller.plain.stub';
if ($this->option('api') && is_null($stub)) {
$stub = '/stubs/controller.api.stub';
} elseif ($this->option('api') && ! is_null($stub) && ! $this->option('invokable')) {
$stub = str_replace('.stub', '.api.stub', $stub);
}
$stub ??= '/stubs/controller.plain.stub';
return $this->resolveStubPath($stub);
}
/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.$stub;
}
/**
@@ -62,7 +108,7 @@ class ControllerMakeCommand extends GeneratorCommand
/**
* Build the class with the given name.
*
* Remove the base controller import if we are already in base namespace.
* Remove the base controller import if we are already in the base namespace.
*
* @param string $name
* @return string
@@ -81,6 +127,10 @@ class ControllerMakeCommand extends GeneratorCommand
$replace = $this->buildModelReplacements($replace);
}
if ($this->option('creatable')) {
$replace['abort(404);'] = '//';
}
$replace["use {$controllerNamespace}\Controller;\n"] = '';
return str_replace(
@@ -97,16 +147,21 @@ class ControllerMakeCommand extends GeneratorCommand
{
$parentModelClass = $this->parseModel($this->option('parent'));
if (! class_exists($parentModelClass)) {
if ($this->confirm("A {$parentModelClass} model does not exist. Do you want to generate it?", true)) {
$this->call('make:model', ['name' => $parentModelClass]);
}
if (! class_exists($parentModelClass) &&
$this->components->confirm("A {$parentModelClass} model does not exist. Do you want to generate it?", true)) {
$this->call('make:model', ['name' => $parentModelClass]);
}
return [
'ParentDummyFullModelClass' => $parentModelClass,
'{{ namespacedParentModel }}' => $parentModelClass,
'{{namespacedParentModel}}' => $parentModelClass,
'ParentDummyModelClass' => class_basename($parentModelClass),
'{{ parentModel }}' => class_basename($parentModelClass),
'{{parentModel}}' => class_basename($parentModelClass),
'ParentDummyModelVariable' => lcfirst(class_basename($parentModelClass)),
'{{ parentModelVariable }}' => lcfirst(class_basename($parentModelClass)),
'{{parentModelVariable}}' => lcfirst(class_basename($parentModelClass)),
];
}
@@ -120,16 +175,22 @@ class ControllerMakeCommand extends GeneratorCommand
{
$modelClass = $this->parseModel($this->option('model'));
if (! class_exists($modelClass)) {
if ($this->confirm("A {$modelClass} model does not exist. Do you want to generate it?", true)) {
$this->call('make:model', ['name' => $modelClass]);
}
if (! class_exists($modelClass) && $this->components->confirm("A {$modelClass} model does not exist. Do you want to generate it?", true)) {
$this->call('make:model', ['name' => $modelClass]);
}
$replace = $this->buildFormRequestReplacements($replace, $modelClass);
return array_merge($replace, [
'DummyFullModelClass' => $modelClass,
'{{ namespacedModel }}' => $modelClass,
'{{namespacedModel}}' => $modelClass,
'DummyModelClass' => class_basename($modelClass),
'{{ model }}' => class_basename($modelClass),
'{{model}}' => class_basename($modelClass),
'DummyModelVariable' => lcfirst(class_basename($modelClass)),
'{{ modelVariable }}' => lcfirst(class_basename($modelClass)),
'{{modelVariable}}' => lcfirst(class_basename($modelClass)),
]);
}
@@ -138,6 +199,8 @@ class ControllerMakeCommand extends GeneratorCommand
*
* @param string $model
* @return string
*
* @throws \InvalidArgumentException
*/
protected function parseModel($model)
{
@@ -145,13 +208,73 @@ class ControllerMakeCommand extends GeneratorCommand
throw new InvalidArgumentException('Model name contains invalid characters.');
}
$model = trim(str_replace('/', '\\', $model), '\\');
return $this->qualifyModel($model);
}
if (! Str::startsWith($model, $rootNamespace = $this->laravel->getNamespace())) {
$model = $rootNamespace.$model;
/**
* Build the model replacement values.
*
* @param array $replace
* @param string $modelClass
* @return array
*/
protected function buildFormRequestReplacements(array $replace, $modelClass)
{
[$namespace, $storeRequestClass, $updateRequestClass] = [
'Illuminate\\Http', 'Request', 'Request',
];
if ($this->option('requests')) {
$namespace = 'App\\Http\\Requests';
[$storeRequestClass, $updateRequestClass] = $this->generateFormRequests(
$modelClass, $storeRequestClass, $updateRequestClass
);
}
return $model;
$namespacedRequests = $namespace.'\\'.$storeRequestClass.';';
if ($storeRequestClass !== $updateRequestClass) {
$namespacedRequests .= PHP_EOL.'use '.$namespace.'\\'.$updateRequestClass.';';
}
return array_merge($replace, [
'{{ storeRequest }}' => $storeRequestClass,
'{{storeRequest}}' => $storeRequestClass,
'{{ updateRequest }}' => $updateRequestClass,
'{{updateRequest}}' => $updateRequestClass,
'{{ namespacedStoreRequest }}' => $namespace.'\\'.$storeRequestClass,
'{{namespacedStoreRequest}}' => $namespace.'\\'.$storeRequestClass,
'{{ namespacedUpdateRequest }}' => $namespace.'\\'.$updateRequestClass,
'{{namespacedUpdateRequest}}' => $namespace.'\\'.$updateRequestClass,
'{{ namespacedRequests }}' => $namespacedRequests,
'{{namespacedRequests}}' => $namespacedRequests,
]);
}
/**
* Generate the form requests for the given model and classes.
*
* @param string $modelClass
* @param string $storeRequestClass
* @param string $updateRequestClass
* @return array
*/
protected function generateFormRequests($modelClass, $storeRequestClass, $updateRequestClass)
{
$storeRequestClass = 'Store'.class_basename($modelClass).'Request';
$this->call('make:request', [
'name' => $storeRequestClass,
]);
$updateRequestClass = 'Update'.class_basename($modelClass).'Request';
$this->call('make:request', [
'name' => $updateRequestClass,
]);
return [$storeRequestClass, $updateRequestClass];
}
/**
@@ -162,11 +285,16 @@ class ControllerMakeCommand extends GeneratorCommand
protected function getOptions()
{
return [
['model', 'm', InputOption::VALUE_OPTIONAL, 'Generate a resource controller for the given model.'],
['resource', 'r', InputOption::VALUE_NONE, 'Generate a resource controller class.'],
['parent', 'p', InputOption::VALUE_OPTIONAL, 'Generate a nested resource controller class.'],
['api', null, InputOption::VALUE_NONE, 'Exclude the create and edit methods from the controller'],
['type', null, InputOption::VALUE_REQUIRED, 'Manually specify the controller stub file to use'],
['force', null, InputOption::VALUE_NONE, 'Create the class even if the controller already exists'],
['invokable', 'i', InputOption::VALUE_NONE, 'Generate a single method, invokable controller class'],
['model', 'm', InputOption::VALUE_OPTIONAL, 'Generate a resource controller for the given model'],
['parent', 'p', InputOption::VALUE_OPTIONAL, 'Generate a nested resource controller class'],
['resource', 'r', InputOption::VALUE_NONE, 'Generate a resource controller class'],
['requests', 'R', InputOption::VALUE_NONE, 'Generate FormRequest classes for store and update'],
['singleton', 's', InputOption::VALUE_NONE, 'Generate a singleton resource controller class'],
['creatable', null, InputOption::VALUE_NONE, 'Indicate that a singleton resource should be creatable'],
];
}
}

View File

@@ -2,10 +2,15 @@
namespace Illuminate\Routing\Console;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'make:middleware')]
class MiddlewareMakeCommand extends GeneratorCommand
{
use CreatesMatchingTest;
/**
* The console command name.
*
@@ -13,6 +18,17 @@ class MiddlewareMakeCommand extends GeneratorCommand
*/
protected $name = 'make:middleware';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'make:middleware';
/**
* The console command description.
*
@@ -34,7 +50,20 @@ class MiddlewareMakeCommand extends GeneratorCommand
*/
protected function getStub()
{
return __DIR__.'/stubs/middleware.stub';
return $this->resolveStubPath('/stubs/middleware.stub');
}
/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.$stub;
}
/**

View File

@@ -0,0 +1,64 @@
<?php
namespace {{ namespace }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
class {{ class }} extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace {{ namespace }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
class {{ class }} extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
//
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace {{ namespace }};
use {{ namespacedModel }};
use {{ rootNamespace }}Http\Controllers\Controller;
use {{ namespacedRequests }}
class {{ class }} extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \{{ namespacedStoreRequest }} $request
* @return \Illuminate\Http\Response
*/
public function store({{ storeRequest }} $request)
{
//
}
/**
* Display the specified resource.
*
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function show({{ model }} ${{ modelVariable }})
{
//
}
/**
* Update the specified resource in storage.
*
* @param \{{ namespacedUpdateRequest }} $request
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function update({{ updateRequest }} $request, {{ model }} ${{ modelVariable }})
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function destroy({{ model }} ${{ modelVariable }})
{
//
}
}

View File

@@ -1,12 +1,12 @@
<?php
namespace DummyNamespace;
namespace {{ namespace }};
use DummyFullModelClass;
use Illuminate\Http\Request;
use DummyRootNamespaceHttp\Controllers\Controller;
use {{ namespacedModel }};
use {{ rootNamespace }}Http\Controllers\Controller;
use {{ namespacedRequests }}
class DummyClass extends Controller
class {{ class }} extends Controller
{
/**
* Display a listing of the resource.
@@ -31,10 +31,10 @@ class DummyClass extends Controller
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedStoreRequest }} $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
public function store({{ storeRequest }} $request)
{
//
}
@@ -42,10 +42,10 @@ class DummyClass extends Controller
/**
* Display the specified resource.
*
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function show(DummyModelClass $DummyModelVariable)
public function show({{ model }} ${{ modelVariable }})
{
//
}
@@ -53,10 +53,10 @@ class DummyClass extends Controller
/**
* Show the form for editing the specified resource.
*
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function edit(DummyModelClass $DummyModelVariable)
public function edit({{ model }} ${{ modelVariable }})
{
//
}
@@ -64,11 +64,11 @@ class DummyClass extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedUpdateRequest }} $request
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function update(Request $request, DummyModelClass $DummyModelVariable)
public function update({{ updateRequest }} $request, {{ model }} ${{ modelVariable }})
{
//
}
@@ -76,10 +76,10 @@ class DummyClass extends Controller
/**
* Remove the specified resource from storage.
*
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function destroy(DummyModelClass $DummyModelVariable)
public function destroy({{ model }} ${{ modelVariable }})
{
//
}

View File

@@ -0,0 +1,71 @@
<?php
namespace {{ namespace }};
use {{ namespacedModel }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
use {{ namespacedParentModel }};
class {{ class }} extends Controller
{
/**
* Display a listing of the resource.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function index({{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function store(Request $request, {{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Display the specified resource.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function show({{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function update(Request $request, {{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function destroy({{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace {{ namespace }};
use {{ namespacedModel }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
use {{ namespacedParentModel }};
class {{ class }} extends Controller
{
/**
* Store the newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function store(Request $request, {{ parentModel }} ${{ parentModelVariable }})
{
abort(404);
}
/**
* Display the resource.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function show({{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Update the resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function update(Request $request, {{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Remove the resource from storage.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function destroy({{ parentModel }} ${{ parentModelVariable }})
{
abort(404);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace {{ namespace }};
use {{ namespacedModel }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
use {{ namespacedParentModel }};
class {{ class }} extends Controller
{
/**
* Show the form for creating the new resource.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function create({{ parentModel }} ${{ parentModelVariable }})
{
abort(404);
}
/**
* Store the newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function store(Request $request, {{ parentModel }} ${{ parentModelVariable }})
{
abort(404);
}
/**
* Display the resource.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function show({{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Show the form for editing the resource.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function edit({{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Update the resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function update(Request $request, {{ parentModel }} ${{ parentModelVariable }})
{
//
}
/**
* Remove the resource from storage.
*
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function destroy({{ parentModel }} ${{ parentModelVariable }})
{
abort(404);
}
}

View File

@@ -1,21 +1,21 @@
<?php
namespace DummyNamespace;
namespace {{ namespace }};
use DummyFullModelClass;
use ParentDummyFullModelClass;
use {{ namespacedModel }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
use DummyRootNamespaceHttp\Controllers\Controller;
use {{ namespacedParentModel }};
class DummyClass extends Controller
class {{ class }} extends Controller
{
/**
* Display a listing of the resource.
*
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function index(ParentDummyModelClass $ParentDummyModelVariable)
public function index({{ parentModel }} ${{ parentModelVariable }})
{
//
}
@@ -23,10 +23,10 @@ class DummyClass extends Controller
/**
* Show the form for creating a new resource.
*
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function create(ParentDummyModelClass $ParentDummyModelVariable)
public function create({{ parentModel }} ${{ parentModelVariable }})
{
//
}
@@ -35,10 +35,10 @@ class DummyClass extends Controller
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @return \Illuminate\Http\Response
*/
public function store(Request $request, ParentDummyModelClass $ParentDummyModelVariable)
public function store(Request $request, {{ parentModel }} ${{ parentModelVariable }})
{
//
}
@@ -46,11 +46,11 @@ class DummyClass extends Controller
/**
* Display the specified resource.
*
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function show(ParentDummyModelClass $ParentDummyModelVariable, DummyModelClass $DummyModelVariable)
public function show({{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}
@@ -58,11 +58,11 @@ class DummyClass extends Controller
/**
* Show the form for editing the specified resource.
*
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function edit(ParentDummyModelClass $ParentDummyModelVariable, DummyModelClass $DummyModelVariable)
public function edit({{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}
@@ -71,11 +71,11 @@ class DummyClass extends Controller
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function update(Request $request, ParentDummyModelClass $ParentDummyModelVariable, DummyModelClass $DummyModelVariable)
public function update(Request $request, {{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}
@@ -83,11 +83,11 @@ class DummyClass extends Controller
/**
* Remove the specified resource from storage.
*
* @param \ParentDummyFullModelClass $ParentDummyModelVariable
* @param \DummyFullModelClass $DummyModelVariable
* @param \{{ namespacedParentModel }} ${{ parentModelVariable }}
* @param \{{ namespacedModel }} ${{ modelVariable }}
* @return \Illuminate\Http\Response
*/
public function destroy(ParentDummyModelClass $ParentDummyModelVariable, DummyModelClass $DummyModelVariable)
public function destroy({{ parentModel }} ${{ parentModelVariable }}, {{ model }} ${{ modelVariable }})
{
//
}

View File

@@ -1,11 +1,11 @@
<?php
namespace DummyNamespace;
namespace {{ namespace }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
use DummyRootNamespaceHttp\Controllers\Controller;
class DummyClass extends Controller
class {{ class }} extends Controller
{
//
}

View File

@@ -0,0 +1,51 @@
<?php
namespace {{ namespace }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
class {{ class }} extends Controller
{
/**
* Store the newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
abort(404);
}
/**
* Display the resource.
*
* @return \Illuminate\Http\Response
*/
public function show()
{
//
}
/**
* Update the resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function update(Request $request)
{
//
}
/**
* Remove the resource from storage.
*
* @return \Illuminate\Http\Response
*/
public function destroy()
{
abort(404);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace {{ namespace }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
class {{ class }} extends Controller
{
/**
* Show the form for creating the resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
abort(404);
}
/**
* Store the newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
abort(404);
}
/**
* Display the resource.
*
* @return \Illuminate\Http\Response
*/
public function show()
{
//
}
/**
* Show the form for editing the resource.
*
* @return \Illuminate\Http\Response
*/
public function edit()
{
//
}
/**
* Update the resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function update(Request $request)
{
//
}
/**
* Remove the resource from storage.
*
* @return \Illuminate\Http\Response
*/
public function destroy()
{
abort(404);
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace DummyNamespace;
namespace {{ namespace }};
use {{ rootNamespace }}Http\Controllers\Controller;
use Illuminate\Http\Request;
use DummyRootNamespaceHttp\Controllers\Controller;
class DummyClass extends Controller
class {{ class }} extends Controller
{
/**
* Display a listing of the resource.

View File

@@ -1,19 +1,20 @@
<?php
namespace DummyNamespace;
namespace {{ namespace }};
use Closure;
use Illuminate\Http\Request;
class DummyClass
class {{ class }}
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next)
{
return $next($request);
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Routing\Contracts;
use Illuminate\Routing\Route;
interface CallableDispatcher
{
/**
* Dispatch a request to a given callable.
*
* @param \Illuminate\Routing\Route $route
* @param callable $callable
* @return mixed
*/
public function dispatch(Route $route, $callable);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Illuminate\Routing\Contracts;
use Illuminate\Routing\Route;
interface ControllerDispatcher
{
/**
* Dispatch a request to a given controller and method.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return mixed
*/
public function dispatch(Route $route, $controller, $method);
/**
* Get the middleware for the controller instance.
*
* @param \Illuminate\Routing\Controller $controller
* @param string $method
* @return array
*/
public function getMiddleware($controller, $method);
}

View File

@@ -3,7 +3,6 @@
namespace Illuminate\Routing;
use BadMethodCallException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
abstract class Controller
{
@@ -17,8 +16,8 @@ abstract class Controller
/**
* Register middleware on the controller.
*
* @param array|string|\Closure $middleware
* @param array $options
* @param \Closure|array|string $middleware
* @param array $options
* @return \Illuminate\Routing\ControllerMiddlewareOptions
*/
public function middleware($middleware, array $options = [])
@@ -47,38 +46,27 @@ abstract class Controller
* Execute an action on the controller.
*
* @param string $method
* @param array $parameters
* @param array $parameters
* @return \Symfony\Component\HttpFoundation\Response
*/
public function callAction($method, $parameters)
{
return call_user_func_array([$this, $method], $parameters);
}
/**
* Handle calls to missing methods on the controller.
*
* @param array $parameters
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function missingMethod($parameters = [])
{
throw new NotFoundHttpException('Controller method not found.');
return $this->{$method}(...array_values($parameters));
}
/**
* Handle calls to missing methods on the controller.
*
* @param string $method
* @param array $parameters
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
throw new BadMethodCallException("Method [{$method}] does not exist.");
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
}

View File

@@ -3,8 +3,9 @@
namespace Illuminate\Routing;
use Illuminate\Container\Container;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;
class ControllerDispatcher
class ControllerDispatcher implements ControllerDispatcherContract
{
use RouteDependencyResolverTrait;
@@ -36,9 +37,7 @@ class ControllerDispatcher
*/
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
$parameters = $this->resolveParameters($route, $controller, $method);
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
@@ -47,6 +46,21 @@ class ControllerDispatcher
return $controller->{$method}(...array_values($parameters));
}
/**
* Resolve the parameters for the controller.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return array
*/
protected function resolveParameters(Route $route, $controller, $method)
{
return $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
}
/**
* Get the middleware for the controller instance.
*
@@ -54,7 +68,7 @@ class ControllerDispatcher
* @param string $method
* @return array
*/
public static function getMiddleware($controller, $method)
public function getMiddleware($controller, $method)
{
if (! method_exists($controller, 'getMiddleware')) {
return [];
@@ -72,7 +86,7 @@ class ControllerDispatcher
* @param array $options
* @return bool
*/
protected static function methodExcludedByOptions($method, array $options)
public static function methodExcludedByOptions($method, array $options)
{
return (isset($options['only']) && ! in_array($method, (array) $options['only'])) ||
(! empty($options['except']) && in_array($method, (array) $options['except']));

View File

@@ -0,0 +1,13 @@
<?php
namespace Illuminate\Routing\Controllers;
interface HasMiddleware
{
/**
* Get the middleware that should be assigned to the controller.
*
* @return \Illuminate\Routing\Controllers\Middleware|array
*/
public static function middleware();
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Illuminate\Routing\Controllers;
use Closure;
use Illuminate\Support\Arr;
class Middleware
{
/**
* The middleware that should be assigned.
*
* @var \Closure|string|array
*/
public $middleware;
/**
* The controller methods the middleware should only apply to.
*
* @var array|null
*/
public $only;
/**
* The controller methods the middleware should not apply to.
*
* @var array|null
*/
public $except;
/**
* Create a new controller middleware definition.
*
* @param \Closure|string|array $middleware
* @return void
*/
public function __construct(Closure|string|array $middleware)
{
$this->middleware = $middleware;
}
/**
* Specify the only controller methods the middleware should apply to.
*
* @param array|string $only
* @return $this
*/
public function only(array|string $only)
{
$this->only = Arr::wrap($only);
return $this;
}
/**
* Specify the controller methods the middleware should not apply to.
*
* @param array|string $only
* @return $this
*/
public function except(array|string $except)
{
$this->except = Arr::wrap($except);
return $this;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Support\Arr;
trait CreatesRegularExpressionRouteConstraints
{
/**
* Specify that the given route parameters must be alphabetic.
*
* @param array|string $parameters
* @return $this
*/
public function whereAlpha($parameters)
{
return $this->assignExpressionToParameters($parameters, '[a-zA-Z]+');
}
/**
* Specify that the given route parameters must be alphanumeric.
*
* @param array|string $parameters
* @return $this
*/
public function whereAlphaNumeric($parameters)
{
return $this->assignExpressionToParameters($parameters, '[a-zA-Z0-9]+');
}
/**
* Specify that the given route parameters must be numeric.
*
* @param array|string $parameters
* @return $this
*/
public function whereNumber($parameters)
{
return $this->assignExpressionToParameters($parameters, '[0-9]+');
}
/**
* Specify that the given route parameters must be ULIDs.
*
* @param array|string $parameters
* @return $this
*/
public function whereUlid($parameters)
{
return $this->assignExpressionToParameters($parameters, '[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}');
}
/**
* Specify that the given route parameters must be UUIDs.
*
* @param array|string $parameters
* @return $this
*/
public function whereUuid($parameters)
{
return $this->assignExpressionToParameters($parameters, '[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}');
}
/**
* Specify that the given route parameters must be one of the given values.
*
* @param array|string $parameters
* @param array $values
* @return $this
*/
public function whereIn($parameters, array $values)
{
return $this->assignExpressionToParameters($parameters, implode('|', $values));
}
/**
* Apply the given regular expression to the given parameters.
*
* @param array|string $parameters
* @param string $expression
* @return $this
*/
protected function assignExpressionToParameters($parameters, $expression)
{
return $this->where(collect(Arr::wrap($parameters))
->mapWithKeys(fn ($parameter) => [$parameter => $expression])
->all());
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Routing\Events;
class Routing
{
/**
* The request instance.
*
* @var \Illuminate\Http\Request
*/
public $request;
/**
* Create a new event instance.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct($request)
{
$this->request = $request;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Routing\Exceptions;
use RuntimeException;
class BackedEnumCaseNotFoundException extends RuntimeException
{
/**
* Create a new exception instance.
*
* @param string $backedEnumClass
* @param string $case
* @return void
*/
public function __construct($backedEnumClass, $case)
{
parent::__construct("Case [{$case}] not found on Backed Enum [{$backedEnumClass}].");
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Routing\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;
class InvalidSignatureException extends HttpException
{
/**
* Create a new exception instance.
*
* @return void
*/
public function __construct()
{
parent::__construct(403, 'Invalid signature.');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Illuminate\Routing\Exceptions;
use Illuminate\Http\Response;
use RuntimeException;
use Throwable;
class StreamedResponseException extends RuntimeException
{
/**
* The actual exception thrown during the stream.
*
* @var \Throwable
*/
public $originalException;
/**
* Create a new exception instance.
*
* @param \Throwable $originalException
* @return void
*/
public function __construct(Throwable $originalException)
{
$this->originalException = $originalException;
parent::__construct($originalException->getMessage());
}
/**
* Render the exception.
*
* @return \Illuminate\Http\Response
*/
public function render()
{
return new Response('');
}
/**
* Get the actual exception thrown during the stream.
*
* @return \Throwable
*/
public function getInnerException()
{
return $this->originalException;
}
}

View File

@@ -3,6 +3,8 @@
namespace Illuminate\Routing\Exceptions;
use Exception;
use Illuminate\Routing\Route;
use Illuminate\Support\Str;
class UrlGenerationException extends Exception
{
@@ -10,10 +12,26 @@ class UrlGenerationException extends Exception
* Create a new exception for missing route parameters.
*
* @param \Illuminate\Routing\Route $route
* @param array $parameters
* @return static
*/
public static function forMissingParameters($route)
public static function forMissingParameters(Route $route, array $parameters = [])
{
return new static("Missing required parameters for [Route: {$route->getName()}] [URI: {$route->uri()}].");
$parameterLabel = Str::plural('parameter', count($parameters));
$message = sprintf(
'Missing required %s for [Route: %s] [URI: %s]',
$parameterLabel,
$route->getName(),
$route->uri()
);
if (count($parameters) > 0) {
$message .= sprintf(' [Missing %s: %s]', $parameterLabel, implode(', ', $parameters));
}
$message .= '.';
return new static($message);
}
}

View File

@@ -2,7 +2,12 @@
namespace Illuminate\Routing;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
class ImplicitRouteBinding
{
@@ -12,30 +17,87 @@ class ImplicitRouteBinding
* @param \Illuminate\Container\Container $container
* @param \Illuminate\Routing\Route $route
* @return void
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
* @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException
*/
public static function resolveForRoute($container, $route)
{
$parameters = $route->parameters();
foreach ($route->signatureParameters(Model::class) as $parameter) {
if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
$route = static::resolveBackedEnumsForRoute($route, $parameters);
foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {
if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
continue;
}
$parameterValue = $parameters[$parameterName];
if ($parameterValue instanceof Model) {
if ($parameterValue instanceof UrlRoutable) {
continue;
}
$model = $container->make($parameter->getClass()->name);
$instance = $container->make(Reflector::getParameterClassName($parameter));
$route->setParameter($parameterName, $model->where(
$model->getRouteKeyName(), $parameterValue
)->firstOrFail());
$parent = $route->parentOfParameter($parameterName);
$routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
? 'resolveSoftDeletableRouteBinding'
: 'resolveRouteBinding';
if ($parent instanceof UrlRoutable &&
! $route->preventsScopedBindings() &&
($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
$childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
? 'resolveSoftDeletableChildRouteBinding'
: 'resolveChildRouteBinding';
if (! $model = $parent->{$childRouteBindingMethod}(
$parameterName, $parameterValue, $route->bindingFieldFor($parameterName)
)) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}
} elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}
$route->setParameter($parameterName, $model);
}
}
/**
* Resolve the Backed Enums route bindings for the route.
*
* @param \Illuminate\Routing\Route $route
* @param array $parameters
* @return \Illuminate\Routing\Route
*
* @throws \Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException
*/
protected static function resolveBackedEnumsForRoute($route, $parameters)
{
foreach ($route->signatureParameters(['backedEnum' => true]) as $parameter) {
if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
continue;
}
$parameterValue = $parameters[$parameterName];
$backedEnumClass = (string) $parameter->getType();
$backedEnum = $backedEnumClass::tryFrom((string) $parameterValue);
if (is_null($backedEnum)) {
throw new BackedEnumCaseNotFoundException($backedEnumClass, $parameterValue);
}
$route->setParameter($parameterName, $backedEnum);
}
return $route;
}
/**
* Return the parameter name if it exists in the given parameters.
*
@@ -49,9 +111,7 @@ class ImplicitRouteBinding
return $name;
}
$snakedName = snake_case($name);
if (array_key_exists($snakedName, $parameters)) {
if (array_key_exists($snakedName = Str::snake($name), $parameters)) {
return $snakedName;
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -16,10 +16,12 @@ class HostValidator implements ValidatorInterface
*/
public function matches(Route $route, Request $request)
{
if (is_null($route->getCompiled()->getHostRegex())) {
$hostRegex = $route->getCompiled()->getHostRegex();
if (is_null($hostRegex)) {
return true;
}
return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());
return preg_match($hostRegex, $request->getHost());
}
}

View File

@@ -16,7 +16,7 @@ class UriValidator implements ValidatorInterface
*/
public function matches(Route $route, Request $request)
{
$path = $request->path() == '/' ? '/' : '/'.$request->path();
$path = rtrim($request->getPathInfo(), '/') ?: '/';
return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
}

View File

@@ -4,6 +4,7 @@ namespace Illuminate\Routing\Middleware;
use Closure;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class SubstituteBindings
{
@@ -34,9 +35,17 @@ class SubstituteBindings
*/
public function handle($request, Closure $next)
{
$this->router->substituteBindings($route = $request->route());
try {
$this->router->substituteBindings($route = $request->route());
$this->router->substituteImplicitBindings($route);
$this->router->substituteImplicitBindings($route);
} catch (ModelNotFoundException $exception) {
if ($route->getMissing()) {
return $route->getMissing()($request, $exception);
}
throw $exception;
}
return $next($request);
}

View File

@@ -3,12 +3,19 @@
namespace Illuminate\Routing\Middleware;
use Closure;
use Carbon\Carbon;
use Illuminate\Cache\RateLimiter;
use Illuminate\Cache\RateLimiting\Unlimited;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Support\Arr;
use Illuminate\Support\InteractsWithTime;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
class ThrottleRequests
{
use InteractsWithTime;
/**
* The rate limiter instance.
*
@@ -32,26 +39,121 @@ class ThrottleRequests
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int|string $maxAttempts
* @param float|int $decayMinutes
* @return mixed
* @param string $prefix
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($key, $maxAttempts);
if (is_string($maxAttempts)
&& func_num_args() === 3
&& ! is_null($limiter = $this->limiter->limiter($maxAttempts))) {
return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter);
}
$this->limiter->hit($key, $decayMinutes);
return $this->handleRequest(
$request,
$next,
[
(object) [
'key' => $prefix.$this->resolveRequestSignature($request),
'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts),
'decayMinutes' => $decayMinutes,
'responseCallback' => null,
],
]
);
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $limiterName
* @param \Closure $limiter
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequestUsingNamedLimiter($request, Closure $next, $limiterName, Closure $limiter)
{
$limiterResponse = $limiter($request);
if ($limiterResponse instanceof Response) {
return $limiterResponse;
} elseif ($limiterResponse instanceof Unlimited) {
return $next($request);
}
return $this->handleRequest(
$request,
$next,
collect(Arr::wrap($limiterResponse))->map(function ($limit) use ($limiterName) {
return (object) [
'key' => md5($limiterName.$limit->key),
'maxAttempts' => $limit->maxAttempts,
'decayMinutes' => $limit->decayMinutes,
'responseCallback' => $limit->responseCallback,
];
})->all()
);
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param array $limits
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
$this->limiter->hit($limit->key, $limit->decayMinutes * 60);
}
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
);
}
return $response;
}
/**
* Resolve the number of attempts if the user is authenticated or not.
*
* @param \Illuminate\Http\Request $request
* @param int|string $maxAttempts
* @return int
*/
protected function resolveMaxAttempts($request, $maxAttempts)
{
if (str_contains($maxAttempts, '|')) {
$maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0];
}
if (! is_numeric($maxAttempts) && $request->user()) {
$maxAttempts = $request->user()->{$maxAttempts};
}
return (int) $maxAttempts;
}
/**
@@ -59,30 +161,53 @@ class ThrottleRequests
*
* @param \Illuminate\Http\Request $request
* @return string
*
* @throws \RuntimeException
*/
protected function resolveRequestSignature($request)
{
return $request->fingerprint();
if ($user = $request->user()) {
return sha1($user->getAuthIdentifier());
} elseif ($route = $request->route()) {
return sha1($route->getDomain().'|'.$request->ip());
}
throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
}
/**
* Create a 'too many attempts' response.
* Create a 'too many attempts' exception.
*
* @param \Illuminate\Http\Request $request
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpFoundation\Response
* @param callable|null $responseCallback
* @return \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function buildResponse($key, $maxAttempts)
protected function buildException($request, $key, $maxAttempts, $responseCallback = null)
{
$response = new Response('Too Many Attempts.', 429);
$retryAfter = $this->getTimeUntilNextRetry($key);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$headers = $this->getHeaders(
$maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
return is_callable($responseCallback)
? new HttpResponseException($responseCallback($request, $headers))
: new ThrottleRequestsException('Too Many Attempts.', null, $headers);
}
/**
* Get the number of seconds until the next retry.
*
* @param string $key
* @return int
*/
protected function getTimeUntilNextRetry($key)
{
return $this->limiter->availableIn($key);
}
/**
@@ -96,6 +221,33 @@ class ThrottleRequests
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$response->headers->add(
$this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter, $response)
);
return $response;
}
/**
* Get the limit headers information.
*
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @param \Symfony\Component\HttpFoundation\Response|null $response
* @return array
*/
protected function getHeaders($maxAttempts,
$remainingAttempts,
$retryAfter = null,
?Response $response = null)
{
if ($response &&
! is_null($response->headers->get('X-RateLimit-Remaining')) &&
(int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) {
return [];
}
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
@@ -103,12 +255,10 @@ class ThrottleRequests
if (! is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['X-RateLimit-Reset'] = Carbon::now()->getTimestamp() + $retryAfter;
$headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter);
}
$response->headers->add($headers);
return $response;
return $headers;
}
/**
@@ -121,10 +271,6 @@ class ThrottleRequests
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (is_null($retryAfter)) {
return $this->limiter->retriesLeft($key, $maxAttempts);
}
return 0;
return is_null($retryAfter) ? $this->limiter->retriesLeft($key, $maxAttempts) : 0;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Illuminate\Routing\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Limiters\DurationLimiter;
class ThrottleRequestsWithRedis extends ThrottleRequests
{
/**
* The Redis factory implementation.
*
* @var \Illuminate\Contracts\Redis\Factory
*/
protected $redis;
/**
* The timestamp of the end of the current duration by key.
*
* @var array
*/
public $decaysAt = [];
/**
* The number of remaining slots by key.
*
* @var array
*/
public $remaining = [];
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @param \Illuminate\Contracts\Redis\Factory $redis
* @return void
*/
public function __construct(RateLimiter $limiter, Redis $redis)
{
parent::__construct($limiter);
$this->redis = $redis;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param array $limits
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
}
$response = $next($request);
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
);
}
return $response;
}
/**
* Determine if the given key has been "accessed" too many times.
*
* @param string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
{
$limiter = new DurationLimiter(
$this->redis, $key, $maxAttempts, $decayMinutes * 60
);
return tap(! $limiter->acquire(), function () use ($key, $limiter) {
[$this->decaysAt[$key], $this->remaining[$key]] = [
$limiter->decaysAt, $limiter->remaining,
];
});
}
/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
return is_null($retryAfter) ? $this->remaining[$key] : 0;
}
/**
* Get the number of seconds until the lock is released.
*
* @param string $key
* @return int
*/
protected function getTimeUntilNextRetry($key)
{
return $this->decaysAt[$key] - $this->currentTime();
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Illuminate\Routing\Middleware;
use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
class ValidateSignature
{
/**
* The names of the parameters that should be ignored.
*
* @var array<int, string>
*/
protected $ignore = [
//
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $relative
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Routing\Exceptions\InvalidSignatureException
*/
public function handle($request, Closure $next, $relative = null)
{
$ignore = property_exists($this, 'except') ? $this->except : $this->ignore;
if ($request->hasValidSignatureWhileIgnoring($ignore, $relative !== 'relative')) {
return $next($request);
}
throw new InvalidSignatureException;
}
}

View File

@@ -9,10 +9,10 @@ class MiddlewareNameResolver
/**
* Resolve the middleware name to a class name(s) preserving passed parameters.
*
* @param string $name
* @param \Closure|string $name
* @param array $map
* @param array $middlewareGroups
* @return string|array
* @return \Closure|string|array
*/
public static function resolve($name, $map, $middlewareGroups)
{
@@ -21,26 +21,25 @@ class MiddlewareNameResolver
// convenient on occasions when the developers are experimenting with them.
if ($name instanceof Closure) {
return $name;
} elseif (isset($map[$name]) && $map[$name] instanceof Closure) {
}
if (isset($map[$name]) && $map[$name] instanceof Closure) {
return $map[$name];
}
// If the middleware is the name of a middleware group, we will return the array
// of middlewares that belong to the group. This allows developers to group a
// set of middleware under single keys that can be conveniently referenced.
} elseif (isset($middlewareGroups[$name])) {
return static::parseMiddlewareGroup(
$name, $map, $middlewareGroups
);
if (isset($middlewareGroups[$name])) {
return static::parseMiddlewareGroup($name, $map, $middlewareGroups);
}
// Finally, when the middleware is simply a string mapped to a class name the
// middleware name will get parsed into the full class name and parameters
// which may be run using the Pipeline which accepts this string format.
} else {
list($name, $parameters) = array_pad(explode(':', $name, 2), 2, null);
[$name, $parameters] = array_pad(explode(':', $name, 2), 2, null);
return (isset($map[$name]) ? $map[$name] : $name).
(! is_null($parameters) ? ':'.$parameters : '');
}
return ($map[$name] ?? $name).(! is_null($parameters) ? ':'.$parameters : '');
}
/**
@@ -67,7 +66,7 @@ class MiddlewareNameResolver
continue;
}
list($middleware, $parameters) = array_pad(
[$middleware, $parameters] = array_pad(
explode(':', $middleware, 2), 2, null
);

View File

@@ -0,0 +1,268 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
class PendingResourceRegistration
{
use CreatesRegularExpressionRouteConstraints, Macroable;
/**
* The resource registrar.
*
* @var \Illuminate\Routing\ResourceRegistrar
*/
protected $registrar;
/**
* The resource name.
*
* @var string
*/
protected $name;
/**
* The resource controller.
*
* @var string
*/
protected $controller;
/**
* The resource options.
*
* @var array
*/
protected $options = [];
/**
* The resource's registration status.
*
* @var bool
*/
protected $registered = false;
/**
* Create a new pending resource registration instance.
*
* @param \Illuminate\Routing\ResourceRegistrar $registrar
* @param string $name
* @param string $controller
* @param array $options
* @return void
*/
public function __construct(ResourceRegistrar $registrar, $name, $controller, array $options)
{
$this->name = $name;
$this->options = $options;
$this->registrar = $registrar;
$this->controller = $controller;
}
/**
* Set the methods the controller should apply to.
*
* @param array|string|dynamic $methods
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function only($methods)
{
$this->options['only'] = is_array($methods) ? $methods : func_get_args();
return $this;
}
/**
* Set the methods the controller should exclude.
*
* @param array|string|dynamic $methods
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function except($methods)
{
$this->options['except'] = is_array($methods) ? $methods : func_get_args();
return $this;
}
/**
* Set the route names for controller actions.
*
* @param array|string $names
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function names($names)
{
$this->options['names'] = $names;
return $this;
}
/**
* Set the route name for a controller action.
*
* @param string $method
* @param string $name
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function name($method, $name)
{
$this->options['names'][$method] = $name;
return $this;
}
/**
* Override the route parameter names.
*
* @param array|string $parameters
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function parameters($parameters)
{
$this->options['parameters'] = $parameters;
return $this;
}
/**
* Override a route parameter's name.
*
* @param string $previous
* @param string $new
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function parameter($previous, $new)
{
$this->options['parameters'][$previous] = $new;
return $this;
}
/**
* Add middleware to the resource routes.
*
* @param mixed $middleware
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function middleware($middleware)
{
$middleware = Arr::wrap($middleware);
foreach ($middleware as $key => $value) {
$middleware[$key] = (string) $value;
}
$this->options['middleware'] = $middleware;
return $this;
}
/**
* Specify middleware that should be removed from the resource routes.
*
* @param array|string $middleware
* @return $this|array
*/
public function withoutMiddleware($middleware)
{
$this->options['excluded_middleware'] = array_merge(
(array) ($this->options['excluded_middleware'] ?? []), Arr::wrap($middleware)
);
return $this;
}
/**
* Add "where" constraints to the resource routes.
*
* @param mixed $wheres
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function where($wheres)
{
$this->options['wheres'] = $wheres;
return $this;
}
/**
* Indicate that the resource routes should have "shallow" nesting.
*
* @param bool $shallow
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function shallow($shallow = true)
{
$this->options['shallow'] = $shallow;
return $this;
}
/**
* Define the callable that should be invoked on a missing model exception.
*
* @param callable $callback
* @return $this
*/
public function missing($callback)
{
$this->options['missing'] = $callback;
return $this;
}
/**
* Indicate that the resource routes should be scoped using the given binding fields.
*
* @param array $fields
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function scoped(array $fields = [])
{
$this->options['bindingFields'] = $fields;
return $this;
}
/**
* Define which routes should allow "trashed" models to be retrieved when resolving implicit model bindings.
*
* @param array $methods
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function withTrashed(array $methods = [])
{
$this->options['trashed'] = $methods;
return $this;
}
/**
* Register the resource route.
*
* @return \Illuminate\Routing\RouteCollection
*/
public function register()
{
$this->registered = true;
return $this->registrar->register(
$this->name, $this->controller, $this->options
);
}
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
class PendingSingletonResourceRegistration
{
use CreatesRegularExpressionRouteConstraints, Macroable;
/**
* The resource registrar.
*
* @var \Illuminate\Routing\ResourceRegistrar
*/
protected $registrar;
/**
* The resource name.
*
* @var string
*/
protected $name;
/**
* The resource controller.
*
* @var string
*/
protected $controller;
/**
* The resource options.
*
* @var array
*/
protected $options = [];
/**
* The resource's registration status.
*
* @var bool
*/
protected $registered = false;
/**
* Create a new pending singleton resource registration instance.
*
* @param \Illuminate\Routing\ResourceRegistrar $registrar
* @param string $name
* @param string $controller
* @param array $options
* @return void
*/
public function __construct(ResourceRegistrar $registrar, $name, $controller, array $options)
{
$this->name = $name;
$this->options = $options;
$this->registrar = $registrar;
$this->controller = $controller;
}
/**
* Set the methods the controller should apply to.
*
* @param array|string|dynamic $methods
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function only($methods)
{
$this->options['only'] = is_array($methods) ? $methods : func_get_args();
return $this;
}
/**
* Set the methods the controller should exclude.
*
* @param array|string|dynamic $methods
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function except($methods)
{
$this->options['except'] = is_array($methods) ? $methods : func_get_args();
return $this;
}
/**
* Indicate that the resource should have creation and storage routes.
*
* @param bool $creatable
* @return $this
*/
public function creatable($creatable = true)
{
$this->options['creatable'] = $creatable;
return $this;
}
/**
* Set the route names for controller actions.
*
* @param array|string $names
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function names($names)
{
$this->options['names'] = $names;
return $this;
}
/**
* Set the route name for a controller action.
*
* @param string $method
* @param string $name
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function name($method, $name)
{
$this->options['names'][$method] = $name;
return $this;
}
/**
* Override the route parameter names.
*
* @param array|string $parameters
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function parameters($parameters)
{
$this->options['parameters'] = $parameters;
return $this;
}
/**
* Override a route parameter's name.
*
* @param string $previous
* @param string $new
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function parameter($previous, $new)
{
$this->options['parameters'][$previous] = $new;
return $this;
}
/**
* Add middleware to the resource routes.
*
* @param mixed $middleware
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function middleware($middleware)
{
$middleware = Arr::wrap($middleware);
foreach ($middleware as $key => $value) {
$middleware[$key] = (string) $value;
}
$this->options['middleware'] = $middleware;
return $this;
}
/**
* Specify middleware that should be removed from the resource routes.
*
* @param array|string $middleware
* @return $this|array
*/
public function withoutMiddleware($middleware)
{
$this->options['excluded_middleware'] = array_merge(
(array) ($this->options['excluded_middleware'] ?? []), Arr::wrap($middleware)
);
return $this;
}
/**
* Add "where" constraints to the resource routes.
*
* @param mixed $wheres
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function where($wheres)
{
$this->options['wheres'] = $wheres;
return $this;
}
/**
* Register the singleton resource route.
*
* @return \Illuminate\Routing\RouteCollection
*/
public function register()
{
$this->registered = true;
return $this->registrar->singleton(
$this->name, $this->controller, $this->options
);
}
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
if (! $this->registered) {
$this->register();
}
}
}

View File

@@ -2,13 +2,11 @@
namespace Illuminate\Routing;
use Closure;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline as BasePipeline;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Throwable;
/**
* This extended pipeline catches any exceptions that occur during each slice.
@@ -18,60 +16,31 @@ use Symfony\Component\Debug\Exception\FatalThrowableError;
class Pipeline extends BasePipeline
{
/**
* Get the final piece of the Closure onion.
* Handles the value returned from each pipe before passing it to the next.
*
* @param \Closure $destination
* @return \Closure
* @param mixed $carry
* @return mixed
*/
protected function prepareDestination(Closure $destination)
protected function handleCarry($carry)
{
return function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
}
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
$slice = parent::carry();
$callable = $slice($stack, $pipe);
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
};
return $carry instanceof Responsable
? $carry->toResponse($this->getContainer()->make(Request::class))
: $carry;
}
/**
* Handle the given exception.
*
* @param mixed $passable
* @param \Exception $e
* @param \Throwable $e
* @return mixed
*
* @throws \Exception
* @throws \Throwable
*/
protected function handleException($passable, Exception $e)
protected function handleException($passable, Throwable $e)
{
if (! $this->container->bound(ExceptionHandler::class) || ! $passable instanceof Request) {
if (! $this->container->bound(ExceptionHandler::class) ||
! $passable instanceof Request) {
throw $e;
}
@@ -81,7 +50,7 @@ class Pipeline extends BasePipeline
$response = $handler->render($passable, $e);
if (method_exists($response, 'withException')) {
if (is_object($response) && method_exists($response, 'withException')) {
$response->withException($e);
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class RedirectController extends Controller
{
/**
* Invoke the controller method.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Routing\UrlGenerator $url
* @return \Illuminate\Http\RedirectResponse
*/
public function __invoke(Request $request, UrlGenerator $url)
{
$parameters = collect($request->route()->parameters());
$status = $parameters->get('status');
$destination = $parameters->get('destination');
$parameters->forget('status')->forget('destination');
$route = (new Route('GET', $destination, [
'as' => 'laravel_route_redirect_destination',
]))->bind($request);
$parameters = $parameters->only(
$route->getCompiled()->getPathVariables()
)->toArray();
$url = $url->toRoute($route, $parameters, false);
if (! str_starts_with($destination, '/') && str_starts_with($url, '/')) {
$url = Str::after($url, '/');
}
return new RedirectResponse($url, $status);
}
}

View File

@@ -4,9 +4,12 @@ namespace Illuminate\Routing;
use Illuminate\Http\RedirectResponse;
use Illuminate\Session\Store as SessionStore;
use Illuminate\Support\Traits\Macroable;
class Redirector
{
use Macroable;
/**
* The URL generator instance.
*
@@ -37,6 +40,8 @@ class Redirector
*
* @param int $status
* @return \Illuminate\Http\RedirectResponse
*
* @deprecated Will be removed in a future Laravel version.
*/
public function home($status = 302)
{
@@ -46,7 +51,7 @@ class Redirector
/**
* Create a new redirect response to the previous location.
*
* @param int $status
* @param int $status
* @param array $headers
* @param mixed $fallback
* @return \Illuminate\Http\RedirectResponse
@@ -59,7 +64,7 @@ class Redirector
/**
* Create a new redirect response to the current URI.
*
* @param int $status
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
@@ -72,14 +77,22 @@ class Redirector
* Create a new redirect response, while putting the current URL in the session.
*
* @param string $path
* @param int $status
* @param array $headers
* @param bool $secure
* @param int $status
* @param array $headers
* @param bool|null $secure
* @return \Illuminate\Http\RedirectResponse
*/
public function guest($path, $status = 302, $headers = [], $secure = null)
{
$this->session->put('url.intended', $this->generator->full());
$request = $this->generator->getRequest();
$intended = $request->isMethod('GET') && $request->route() && ! $request->expectsJson()
? $this->generator->full()
: $this->generator->previous();
if ($intended) {
$this->setIntendedUrl($intended);
}
return $this->to($path, $status, $headers, $secure);
}
@@ -87,10 +100,10 @@ class Redirector
/**
* Create a new redirect response to the previously intended location.
*
* @param string $default
* @param int $status
* @param array $headers
* @param bool $secure
* @param mixed $default
* @param int $status
* @param array $headers
* @param bool|null $secure
* @return \Illuminate\Http\RedirectResponse
*/
public function intended($default = '/', $status = 302, $headers = [], $secure = null)
@@ -104,9 +117,9 @@ class Redirector
* Create a new redirect response to the given path.
*
* @param string $path
* @param int $status
* @param array $headers
* @param bool $secure
* @param int $status
* @param array $headers
* @param bool|null $secure
* @return \Illuminate\Http\RedirectResponse
*/
public function to($path, $status = 302, $headers = [], $secure = null)
@@ -118,8 +131,8 @@ class Redirector
* Create a new redirect response to an external URL (no validation).
*
* @param string $path
* @param int $status
* @param array $headers
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
public function away($path, $status = 302, $headers = [])
@@ -131,8 +144,8 @@ class Redirector
* Create a new redirect response to the given HTTPS path.
*
* @param string $path
* @param int $status
* @param array $headers
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
public function secure($path, $status = 302, $headers = [])
@@ -144,9 +157,9 @@ class Redirector
* Create a new redirect response to a named route.
*
* @param string $route
* @param array $parameters
* @param int $status
* @param array $headers
* @param mixed $parameters
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
public function route($route, $parameters = [], $status = 302, $headers = [])
@@ -154,13 +167,43 @@ class Redirector
return $this->to($this->generator->route($route, $parameters), $status, $headers);
}
/**
* Create a new redirect response to a signed named route.
*
* @param string $route
* @param mixed $parameters
* @param \DateTimeInterface|\DateInterval|int|null $expiration
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
public function signedRoute($route, $parameters = [], $expiration = null, $status = 302, $headers = [])
{
return $this->to($this->generator->signedRoute($route, $parameters, $expiration), $status, $headers);
}
/**
* Create a new redirect response to a signed named route.
*
* @param string $route
* @param \DateTimeInterface|\DateInterval|int|null $expiration
* @param mixed $parameters
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
public function temporarySignedRoute($route, $expiration, $parameters = [], $status = 302, $headers = [])
{
return $this->to($this->generator->temporarySignedRoute($route, $expiration, $parameters), $status, $headers);
}
/**
* Create a new redirect response to a controller action.
*
* @param string $action
* @param array $parameters
* @param int $status
* @param array $headers
* @param string|array $action
* @param mixed $parameters
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
public function action($action, $parameters = [], $status = 302, $headers = [])
@@ -172,8 +215,8 @@ class Redirector
* Create a new redirect response.
*
* @param string $path
* @param int $status
* @param array $headers
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
*/
protected function createRedirect($path, $status, $headers)
@@ -207,4 +250,27 @@ class Redirector
{
$this->session = $session;
}
/**
* Get the "intended" URL from the session.
*
* @return string|null
*/
public function getIntendedUrl()
{
return $this->session->get('url.intended');
}
/**
* Set the "intended" URL in the session.
*
* @param string $url
* @return $this
*/
public function setIntendedUrl($url)
{
$this->session->put('url.intended', $url);
return $this;
}
}

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Routing;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class ResourceRegistrar
@@ -16,10 +17,17 @@ class ResourceRegistrar
/**
* The default actions for a resourceful controller.
*
* @var array
* @var string[]
*/
protected $resourceDefaults = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
/**
* The default actions for a singleton resource controller.
*
* @var string[]
*/
protected $singletonResourceDefaults = ['show', 'edit', 'update'];
/**
* The parameters set for this resource instance.
*
@@ -67,8 +75,8 @@ class ResourceRegistrar
*
* @param string $name
* @param string $controller
* @param array $options
* @return void
* @param array $options
* @return \Illuminate\Routing\RouteCollection
*/
public function register($name, $controller, array $options = [])
{
@@ -79,7 +87,7 @@ class ResourceRegistrar
// If the resource name contains a slash, we will assume the developer wishes to
// register these resource routes with a prefix so we will set that up out of
// the box so they don't have to mess with it. Otherwise, we will continue.
if (Str::contains($name, '/')) {
if (str_contains($name, '/')) {
$this->prefixedResource($name, $controller, $options);
return;
@@ -92,9 +100,72 @@ class ResourceRegistrar
$defaults = $this->resourceDefaults;
foreach ($this->getResourceMethods($defaults, $options) as $m) {
$this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
$collection = new RouteCollection;
$resourceMethods = $this->getResourceMethods($defaults, $options);
foreach ($resourceMethods as $m) {
$route = $this->{'addResource'.ucfirst($m)}(
$name, $base, $controller, $options
);
if (isset($options['bindingFields'])) {
$this->setResourceBindingFields($route, $options['bindingFields']);
}
if (isset($options['trashed']) &&
in_array($m, ! empty($options['trashed']) ? $options['trashed'] : array_intersect($resourceMethods, ['show', 'edit', 'update']))) {
$route->withTrashed();
}
$collection->add($route);
}
return $collection;
}
/**
* Route a singleton resource to a controller.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\RouteCollection
*/
public function singleton($name, $controller, array $options = [])
{
if (isset($options['parameters']) && ! isset($this->parameters)) {
$this->parameters = $options['parameters'];
}
// If the resource name contains a slash, we will assume the developer wishes to
// register these singleton routes with a prefix so we will set that up out of
// the box so they don't have to mess with it. Otherwise, we will continue.
if (str_contains($name, '/')) {
$this->prefixedSingleton($name, $controller, $options);
return;
}
$defaults = $this->singletonResourceDefaults;
$collection = new RouteCollection;
$resourceMethods = $this->getResourceMethods($defaults, $options);
foreach ($resourceMethods as $m) {
$route = $this->{'addSingleton'.ucfirst($m)}(
$name, $controller, $options
);
if (isset($options['bindingFields'])) {
$this->setResourceBindingFields($route, $options['bindingFields']);
}
$collection->add($route);
}
return $collection;
}
/**
@@ -102,12 +173,12 @@ class ResourceRegistrar
*
* @param string $name
* @param string $controller
* @param array $options
* @param array $options
* @return void
*/
protected function prefixedResource($name, $controller, array $options)
{
list($name, $prefix) = $this->getResourcePrefix($name);
[$name, $prefix] = $this->getResourcePrefix($name);
// We need to extract the base resource from the resource name. Nested resources
// are supported in the framework, but we need to know what name to use for a
@@ -119,6 +190,28 @@ class ResourceRegistrar
return $this->router->group(compact('prefix'), $callback);
}
/**
* Build a set of prefixed singleton routes.
*
* @param string $name
* @param string $controller
* @param array $options
* @return void
*/
protected function prefixedSingleton($name, $controller, array $options)
{
[$name, $prefix] = $this->getResourcePrefix($name);
// We need to extract the base resource from the resource name. Nested resources
// are supported in the framework, but we need to know what name to use for a
// place-holder on the route parameters, which should be the base resources.
$callback = function ($me) use ($name, $controller, $options) {
$me->singleton($name, $controller, $options);
};
return $this->router->group(compact('prefix'), $callback);
}
/**
* Extract the resource and prefix from a resource name.
*
@@ -146,13 +239,27 @@ class ResourceRegistrar
*/
protected function getResourceMethods($defaults, $options)
{
$methods = $defaults;
if (isset($options['only'])) {
return array_intersect($defaults, (array) $options['only']);
} elseif (isset($options['except'])) {
return array_diff($defaults, (array) $options['except']);
$methods = array_intersect($methods, (array) $options['only']);
}
return $defaults;
if (isset($options['except'])) {
$methods = array_diff($methods, (array) $options['except']);
}
if (isset($options['creatable'])) {
$methods = isset($options['apiSingleton'])
? array_merge(['store', 'destroy'], $methods)
: array_merge(['create', 'store', 'destroy'], $methods);
return $this->getResourceMethods(
$methods, array_values(Arr::except($options, ['creatable']))
);
}
return $methods;
}
/**
@@ -161,13 +268,15 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceIndex($name, $base, $controller, $options)
{
$uri = $this->getResourceUri($name);
unset($options['missing']);
$action = $this->getResourceAction($name, $controller, 'index', $options);
return $this->router->get($uri, $action);
@@ -179,13 +288,15 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceCreate($name, $base, $controller, $options)
{
$uri = $this->getResourceUri($name).'/'.static::$verbs['create'];
unset($options['missing']);
$action = $this->getResourceAction($name, $controller, 'create', $options);
return $this->router->get($uri, $action);
@@ -197,13 +308,15 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceStore($name, $base, $controller, $options)
{
$uri = $this->getResourceUri($name);
unset($options['missing']);
$action = $this->getResourceAction($name, $controller, 'store', $options);
return $this->router->post($uri, $action);
@@ -215,11 +328,13 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceShow($name, $base, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name).'/{'.$base.'}';
$action = $this->getResourceAction($name, $controller, 'show', $options);
@@ -233,11 +348,13 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceEdit($name, $base, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name).'/{'.$base.'}/'.static::$verbs['edit'];
$action = $this->getResourceAction($name, $controller, 'edit', $options);
@@ -251,11 +368,13 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceUpdate($name, $base, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name).'/{'.$base.'}';
$action = $this->getResourceAction($name, $controller, 'update', $options);
@@ -269,11 +388,13 @@ class ResourceRegistrar
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addResourceDestroy($name, $base, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name).'/{'.$base.'}';
$action = $this->getResourceAction($name, $controller, 'destroy', $options);
@@ -281,6 +402,153 @@ class ResourceRegistrar
return $this->router->delete($uri, $action);
}
/**
* Add the create method for a singleton route.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addSingletonCreate($name, $controller, $options)
{
$uri = $this->getResourceUri($name).'/'.static::$verbs['create'];
unset($options['missing']);
$action = $this->getResourceAction($name, $controller, 'create', $options);
return $this->router->get($uri, $action);
}
/**
* Add the store method for a singleton route.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addSingletonStore($name, $controller, $options)
{
$uri = $this->getResourceUri($name);
unset($options['missing']);
$action = $this->getResourceAction($name, $controller, 'store', $options);
return $this->router->post($uri, $action);
}
/**
* Add the show method for a singleton route.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addSingletonShow($name, $controller, $options)
{
$uri = $this->getResourceUri($name);
unset($options['missing']);
$action = $this->getResourceAction($name, $controller, 'show', $options);
return $this->router->get($uri, $action);
}
/**
* Add the edit method for a singleton route.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addSingletonEdit($name, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name).'/'.static::$verbs['edit'];
$action = $this->getResourceAction($name, $controller, 'edit', $options);
return $this->router->get($uri, $action);
}
/**
* Add the update method for a singleton route.
*
* @param string $name
* @param string $base
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addSingletonUpdate($name, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name);
$action = $this->getResourceAction($name, $controller, 'update', $options);
return $this->router->match(['PUT', 'PATCH'], $uri, $action);
}
/**
* Add the destroy method for a singleton route.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\Route
*/
protected function addSingletonDestroy($name, $controller, $options)
{
$name = $this->getShallowName($name, $options);
$uri = $this->getResourceUri($name);
$action = $this->getResourceAction($name, $controller, 'destroy', $options);
return $this->router->delete($uri, $action);
}
/**
* Get the name for a given resource with shallowness applied when applicable.
*
* @param string $name
* @param array $options
* @return string
*/
protected function getShallowName($name, $options)
{
return isset($options['shallow']) && $options['shallow']
? last(explode('.', $name))
: $name;
}
/**
* Set the route's binding fields if the resource is scoped.
*
* @param \Illuminate\Routing\Route $route
* @param array $bindingFields
* @return void
*/
protected function setResourceBindingFields($route, $bindingFields)
{
preg_match_all('/(?<={).*?(?=})/', $route->uri, $matches);
$fields = array_fill_keys($matches[0], null);
$route->setBindingFields(array_replace(
$fields, array_intersect_key($bindingFields, $fields)
));
}
/**
* Get the base resource URI for a given resource.
*
@@ -289,7 +557,7 @@ class ResourceRegistrar
*/
public function getResourceUri($resource)
{
if (! Str::contains($resource, '.')) {
if (! str_contains($resource, '.')) {
return $resource;
}
@@ -306,7 +574,7 @@ class ResourceRegistrar
/**
* Get the URI for a nested resource segment array.
*
* @param array $segments
* @param array $segments
* @return string
*/
protected function getNestedResourceUri(array $segments)
@@ -344,7 +612,7 @@ class ResourceRegistrar
* @param string $resource
* @param string $controller
* @param string $method
* @param array $options
* @param array $options
* @return array
*/
protected function getResourceAction($resource, $controller, $method, $options)
@@ -357,6 +625,18 @@ class ResourceRegistrar
$action['middleware'] = $options['middleware'];
}
if (isset($options['excluded_middleware'])) {
$action['excluded_middleware'] = $options['excluded_middleware'];
}
if (isset($options['wheres'])) {
$action['where'] = $options['wheres'];
}
if (isset($options['missing'])) {
$action['missing'] = $options['missing'];
}
return $action;
}
@@ -365,7 +645,7 @@ class ResourceRegistrar
*
* @param string $resource
* @param string $method
* @param array $options
* @param array $options
* @return string
*/
protected function getResourceRouteName($resource, $method, $options)
@@ -415,7 +695,7 @@ class ResourceRegistrar
/**
* Set the global parameter mapping.
*
* @param array $parameters
* @param array $parameters
* @return void
*/
public static function setParameters(array $parameters = [])

View File

@@ -2,14 +2,16 @@
namespace Illuminate\Routing;
use Illuminate\Support\Str;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Illuminate\Contracts\Routing\ResponseFactory as FactoryContract;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Routing\Exceptions\StreamedResponseException;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Throwable;
class ResponseFactory implements FactoryContract
{
@@ -43,9 +45,9 @@ class ResponseFactory implements FactoryContract
}
/**
* Return a new response from the application.
* Create a new response instance.
*
* @param string $content
* @param mixed $content
* @param int $status
* @param array $headers
* @return \Illuminate\Http\Response
@@ -56,9 +58,21 @@ class ResponseFactory implements FactoryContract
}
/**
* Return a new view response from the application.
* Create a new "no content" response.
*
* @param string $view
* @param int $status
* @param array $headers
* @return \Illuminate\Http\Response
*/
public function noContent($status = 204, array $headers = [])
{
return $this->make('', $status, $headers);
}
/**
* Create a new response for a given view.
*
* @param string|array $view
* @param array $data
* @param int $status
* @param array $headers
@@ -66,11 +80,15 @@ class ResponseFactory implements FactoryContract
*/
public function view($view, $data = [], $status = 200, array $headers = [])
{
if (is_array($view)) {
return $this->make($this->view->first($view, $data), $status, $headers);
}
return $this->make($this->view->make($view, $data), $status, $headers);
}
/**
* Return a new JSON response from the application.
* Create a new JSON response instance.
*
* @param mixed $data
* @param int $status
@@ -84,7 +102,7 @@ class ResponseFactory implements FactoryContract
}
/**
* Return a new JSONP response from the application.
* Create a new JSONP response instance.
*
* @param string $callback
* @param mixed $data
@@ -99,7 +117,7 @@ class ResponseFactory implements FactoryContract
}
/**
* Return a new streamed response from the application.
* Create a new streamed response instance.
*
* @param \Closure $callback
* @param int $status
@@ -111,11 +129,43 @@ class ResponseFactory implements FactoryContract
return new StreamedResponse($callback, $status, $headers);
}
/**
* Create a new streamed response instance as a file download.
*
* @param \Closure $callback
* @param string|null $name
* @param array $headers
* @param string|null $disposition
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function streamDownload($callback, $name = null, array $headers = [], $disposition = 'attachment')
{
$withWrappedException = function () use ($callback) {
try {
$callback();
} catch (Throwable $e) {
throw new StreamedResponseException($e);
}
};
$response = new StreamedResponse($withWrappedException, 200, $headers);
if (! is_null($name)) {
$response->headers->set('Content-Disposition', $response->headers->makeDisposition(
$disposition,
$name,
$this->fallbackName($name)
));
}
return $response;
}
/**
* Create a new file download response.
*
* @param \SplFileInfo|string $file
* @param string $name
* @param string|null $name
* @param array $headers
* @param string|null $disposition
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
@@ -125,12 +175,23 @@ class ResponseFactory implements FactoryContract
$response = new BinaryFileResponse($file, 200, $headers, true, $disposition);
if (! is_null($name)) {
return $response->setContentDisposition($disposition, $name, str_replace('%', '', Str::ascii($name)));
return $response->setContentDisposition($disposition, $name, $this->fallbackName($name));
}
return $response;
}
/**
* Convert the string to ASCII characters that are equivalent to the given name.
*
* @param string $name
* @return string
*/
protected function fallbackName($name)
{
return str_replace('%', '', Str::ascii($name));
}
/**
* Return the raw contents of a binary file.
*
@@ -161,7 +222,7 @@ class ResponseFactory implements FactoryContract
* Create a new redirect response to a named route.
*
* @param string $route
* @param array $parameters
* @param mixed $parameters
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse
@@ -175,7 +236,7 @@ class ResponseFactory implements FactoryContract
* Create a new redirect response to a controller action.
*
* @param string $action
* @param array $parameters
* @param mixed $parameters
* @param int $status
* @param array $headers
* @return \Illuminate\Http\RedirectResponse

View File

@@ -3,21 +3,26 @@
namespace Illuminate\Routing;
use Closure;
use LogicException;
use ReflectionFunction;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Container\Container;
use Illuminate\Routing\Matching\UriValidator;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Contracts\CallableDispatcher;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Matching\HostValidator;
use Illuminate\Routing\Matching\MethodValidator;
use Illuminate\Routing\Matching\SchemeValidator;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Routing\Matching\UriValidator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Laravel\SerializableClosure\SerializableClosure;
use LogicException;
use Symfony\Component\Routing\Route as SymfonyRoute;
class Route
{
use RouteDependencyResolverTrait;
use CreatesRegularExpressionRouteConstraints, Macroable, RouteDependencyResolverTrait;
/**
* The URI pattern the route responds to.
@@ -40,6 +45,13 @@ class Route
*/
public $action;
/**
* Indicates whether the route is a fallback route.
*
* @var bool
*/
public $isFallback = false;
/**
* The controller instance.
*
@@ -64,7 +76,7 @@ class Route
/**
* The array of matched parameters.
*
* @var array
* @var array|null
*/
public $parameters;
@@ -75,6 +87,34 @@ class Route
*/
public $parameterNames;
/**
* The array of the matched parameters' original values.
*
* @var array
*/
protected $originalParameters;
/**
* Indicates "trashed" models can be retrieved when resolving implicit model bindings for this route.
*
* @var bool
*/
protected $withTrashedBindings = false;
/**
* Indicates the maximum number of seconds the route should acquire a session lock for.
*
* @var int|null
*/
protected $lockSeconds;
/**
* Indicates the maximum number of seconds the route should wait while attempting to acquire a session lock.
*
* @var int|null
*/
protected $waitSeconds;
/**
* The computed gathered middleware.
*
@@ -103,6 +143,13 @@ class Route
*/
protected $container;
/**
* The fields that implicit binding should use for a given parameter.
*
* @var array
*/
protected $bindingFields = [];
/**
* The validators used by the routes.
*
@@ -122,15 +169,13 @@ class Route
{
$this->uri = $uri;
$this->methods = (array) $methods;
$this->action = $this->parseAction($action);
$this->action = Arr::except($this->parseAction($action), ['prefix']);
if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
$this->methods[] = 'HEAD';
}
if (isset($this->action['prefix'])) {
$this->prefix($this->action['prefix']);
}
$this->prefix(is_array($action) ? Arr::get($action, 'prefix') : '');
}
/**
@@ -173,7 +218,7 @@ class Route
*/
protected function isControllerAction()
{
return is_string($this->action['uses']);
return is_string($this->action['uses']) && ! $this->isSerializedClosure();
}
/**
@@ -185,9 +230,21 @@ class Route
{
$callable = $this->action['uses'];
return $callable(...array_values($this->resolveMethodDependencies(
$this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
)));
if ($this->isSerializedClosure()) {
$callable = unserialize($this->action['uses'])->getClosure();
}
return $this->container[CallableDispatcher::class]->dispatch($this, $callable);
}
/**
* Determine if the route action is a serialized Closure.
*
* @return bool
*/
protected function isSerializedClosure()
{
return RouteAction::containsSerializedClosure($this->action);
}
/**
@@ -199,7 +256,7 @@ class Route
*/
protected function runController()
{
return (new ControllerDispatcher($this->container))->dispatch(
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
@@ -211,15 +268,25 @@ class Route
*/
public function getController()
{
$class = $this->parseControllerCallback()[0];
if (! $this->controller) {
$this->controller = $this->container->make($class);
$class = $this->getControllerClass();
$this->controller = $this->container->make(ltrim($class, '\\'));
}
return $this->controller;
}
/**
* Get the controller class used for the route.
*
* @return string
*/
public function getControllerClass()
{
return $this->parseControllerCallback()[0];
}
/**
* Get the controller method used for the route.
*
@@ -241,7 +308,18 @@ class Route
}
/**
* Determine if the route matches given request.
* Flush the cached container instance on the route.
*
* @return void
*/
public function flushController()
{
$this->computedMiddleware = null;
$this->controller = null;
}
/**
* Determine if the route matches a given request.
*
* @param \Illuminate\Http\Request $request
* @param bool $includingMethod
@@ -251,7 +329,7 @@ class Route
{
$this->compileRoute();
foreach ($this->getValidators() as $validator) {
foreach (self::getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
@@ -267,12 +345,12 @@ class Route
/**
* Compile the route into a Symfony CompiledRoute instance.
*
* @return void
* @return \Symfony\Component\Routing\CompiledRoute
*/
protected function compileRoute()
{
if (! $this->compiled) {
$this->compiled = (new RouteCompiler($this))->compile();
$this->compiled = $this->toSymfonyRoute()->compile();
}
return $this->compiled;
@@ -291,6 +369,8 @@ class Route
$this->parameters = (new RouteParameterBinder($this))
->parameters($request);
$this->originalParameters = $this->parameters;
return $this;
}
@@ -307,7 +387,7 @@ class Route
/**
* Determine a given parameter exists from the route.
*
* @param string $name
* @param string $name
* @return bool
*/
public function hasParameter($name)
@@ -323,19 +403,31 @@ class Route
* Get a given parameter from the route.
*
* @param string $name
* @param mixed $default
* @return string|object
* @param string|object|null $default
* @return string|object|null
*/
public function parameter($name, $default = null)
{
return Arr::get($this->parameters(), $name, $default);
}
/**
* Get original value of a given parameter from the route.
*
* @param string $name
* @param string|null $default
* @return string|null
*/
public function originalParameter($name, $default = null)
{
return Arr::get($this->originalParameters(), $name, $default);
}
/**
* Set a parameter to the given value.
*
* @param string $name
* @param mixed $value
* @param string|object|null $value
* @return void
*/
public function setParameter($name, $value)
@@ -374,6 +466,22 @@ class Route
throw new LogicException('Route is not bound.');
}
/**
* Get the key / value list of original parameters for the route.
*
* @return array
*
* @throws \LogicException
*/
public function originalParameters()
{
if (isset($this->originalParameters)) {
return $this->originalParameters;
}
throw new LogicException('Route is not bound.');
}
/**
* Get the key / value list of parameters without null values.
*
@@ -381,9 +489,7 @@ class Route
*/
public function parametersWithoutNulls()
{
return array_filter($this->parameters(), function ($p) {
return ! is_null($p);
});
return array_filter($this->parameters(), fn ($p) => ! is_null($p));
}
/**
@@ -407,22 +513,100 @@ class Route
*/
protected function compileParameterNames()
{
preg_match_all('/\{(.*?)\}/', $this->domain().$this->uri, $matches);
preg_match_all('/\{(.*?)\}/', $this->getDomain().$this->uri, $matches);
return array_map(function ($m) {
return trim($m, '?');
}, $matches[1]);
return array_map(fn ($m) => trim($m, '?'), $matches[1]);
}
/**
* Get the parameters that are listed in the route / controller signature.
*
* @param string|null $subClass
* @param array $conditions
* @return array
*/
public function signatureParameters($subClass = null)
public function signatureParameters($conditions = [])
{
return RouteSignatureParameters::fromAction($this->action, $subClass);
if (is_string($conditions)) {
$conditions = ['subClass' => $conditions];
}
return RouteSignatureParameters::fromAction($this->action, $conditions);
}
/**
* Get the binding field for the given parameter.
*
* @param string|int $parameter
* @return string|null
*/
public function bindingFieldFor($parameter)
{
$fields = is_int($parameter) ? array_values($this->bindingFields) : $this->bindingFields;
return $fields[$parameter] ?? null;
}
/**
* Get the binding fields for the route.
*
* @return array
*/
public function bindingFields()
{
return $this->bindingFields ?? [];
}
/**
* Set the binding fields for the route.
*
* @param array $bindingFields
* @return $this
*/
public function setBindingFields(array $bindingFields)
{
$this->bindingFields = $bindingFields;
return $this;
}
/**
* Get the parent parameter of the given parameter.
*
* @param string $parameter
* @return string
*/
public function parentOfParameter($parameter)
{
$key = array_search($parameter, array_keys($this->parameters));
if ($key === 0) {
return;
}
return array_values($this->parameters)[$key - 1];
}
/**
* Allow "trashed" models to be retrieved when resolving implicit model bindings for this route.
*
* @param bool $withTrashed
* @return $this
*/
public function withTrashed($withTrashed = true)
{
$this->withTrashedBindings = $withTrashed;
return $this;
}
/**
* Determines if the route allows "trashed" models to be retrieved when resolving implicit model bindings.
*
* @return bool
*/
public function allowsTrashedBindings()
{
return $this->withTrashedBindings;
}
/**
@@ -439,11 +623,24 @@ class Route
return $this;
}
/**
* Set the default values for the route.
*
* @param array $defaults
* @return $this
*/
public function setDefaults(array $defaults)
{
$this->defaults = $defaults;
return $this;
}
/**
* Set a regular expression requirement on the route.
*
* @param array|string $name
* @param string $expression
* @param string|null $expression
* @return $this
*/
public function where($name, $expression = null)
@@ -473,7 +670,7 @@ class Route
* @param array $wheres
* @return $this
*/
protected function whereArray(array $wheres)
public function setWheres(array $wheres)
{
foreach ($wheres as $name => $expression) {
$this->where($name, $expression);
@@ -482,6 +679,31 @@ class Route
return $this;
}
/**
* Mark this route as a fallback route.
*
* @return $this
*/
public function fallback()
{
$this->isFallback = true;
return $this;
}
/**
* Set the fallback value.
*
* @param bool $isFallback
* @return $this
*/
public function setFallback($isFallback)
{
$this->isFallback = $isFallback;
return $this;
}
/**
* Get the HTTP verbs the route responds to.
*
@@ -522,12 +744,35 @@ class Route
return in_array('https', $this->action, true);
}
/**
* Get or set the domain for the route.
*
* @param string|null $domain
* @return $this|string|null
*/
public function domain($domain = null)
{
if (is_null($domain)) {
return $this->getDomain();
}
$parsed = RouteUri::parse($domain);
$this->action['domain'] = $parsed->uri;
$this->bindingFields = array_merge(
$this->bindingFields, $parsed->bindingFields
);
return $this;
}
/**
* Get the domain defined for the route.
*
* @return string|null
*/
public function domain()
public function getDomain()
{
return isset($this->action['domain'])
? str_replace(['http://', 'https://'], '', $this->action['domain']) : null;
@@ -536,11 +781,11 @@ class Route
/**
* Get the prefix of the route instance.
*
* @return string
* @return string|null
*/
public function getPrefix()
{
return isset($this->action['prefix']) ? $this->action['prefix'] : null;
return $this->action['prefix'] ?? null;
}
/**
@@ -551,11 +796,26 @@ class Route
*/
public function prefix($prefix)
{
$prefix ??= '';
$this->updatePrefixOnAction($prefix);
$uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/');
$this->uri = trim($uri, '/');
return $this->setUri($uri !== '/' ? trim($uri, '/') : $uri);
}
return $this;
/**
* Update the "prefix" attribute on the action array.
*
* @param string $prefix
* @return void
*/
protected function updatePrefixOnAction($prefix)
{
if (! empty($newPrefix = trim(rtrim($prefix, '/').'/'.ltrim($this->action['prefix'] ?? '', '/'), '/'))) {
$this->action['prefix'] = $newPrefix;
}
}
/**
@@ -576,19 +836,34 @@ class Route
*/
public function setUri($uri)
{
$this->uri = $uri;
$this->uri = $this->parseUri($uri);
return $this;
}
/**
* Parse the route URI and normalize / store any implicit binding fields.
*
* @param string $uri
* @return string
*/
protected function parseUri($uri)
{
$this->bindingFields = [];
return tap(RouteUri::parse($uri), function ($uri) {
$this->bindingFields = $uri->bindingFields;
})->uri;
}
/**
* Get the name of the route instance.
*
* @return string
* @return string|null
*/
public function getName()
{
return isset($this->action['as']) ? $this->action['as'] : null;
return $this->action['as'] ?? null;
}
/**
@@ -605,24 +880,38 @@ class Route
}
/**
* Determine whether the route's name matches the given name.
* Determine whether the route's name matches the given patterns.
*
* @param string $name
* @param mixed ...$patterns
* @return bool
*/
public function named($name)
public function named(...$patterns)
{
return $this->getName() === $name;
if (is_null($routeName = $this->getName())) {
return false;
}
foreach ($patterns as $pattern) {
if (Str::is($pattern, $routeName)) {
return true;
}
}
return false;
}
/**
* Set the handler for the route.
*
* @param \Closure|string $action
* @param \Closure|array|string $action
* @return $this
*/
public function uses($action)
{
if (is_array($action)) {
$action = $action[0].'@'.$action[1];
}
$action = is_string($action) ? $this->addGroupNamespaceToStringUses($action) : $action;
return $this->setAction(array_merge($this->action, $this->parseAction([
@@ -641,7 +930,7 @@ class Route
{
$groupStack = last($this->router->getGroupStack());
if (isset($groupStack['namespace']) && strpos($action, '\\') !== 0) {
if (isset($groupStack['namespace']) && ! str_starts_with($action, '\\')) {
return $groupStack['namespace'].'\\'.$action;
}
@@ -655,7 +944,7 @@ class Route
*/
public function getActionName()
{
return isset($this->action['controller']) ? $this->action['controller'] : 'Closure';
return $this->action['controller'] ?? 'Closure';
}
/**
@@ -665,17 +954,18 @@ class Route
*/
public function getActionMethod()
{
return array_last(explode('@', $this->getActionName()));
return Arr::last(explode('@', $this->getActionName()));
}
/**
* Get the action array for the route.
* Get the action array or one of its properties for the route.
*
* @return array
* @param string|null $key
* @return mixed
*/
public function getAction()
public function getAction($key = null)
{
return $this->action;
return Arr::get($this->action, $key);
}
/**
@@ -688,6 +978,38 @@ class Route
{
$this->action = $action;
if (isset($this->action['domain'])) {
$this->domain($this->action['domain']);
}
return $this;
}
/**
* Get the value of the action that should be taken on a missing model exception.
*
* @return \Closure|null
*/
public function getMissing()
{
$missing = $this->action['missing'] ?? null;
return is_string($missing) &&
Str::startsWith($missing, [
'O:47:"Laravel\\SerializableClosure\\SerializableClosure',
]) ? unserialize($missing) : $missing;
}
/**
* Define the callable that should be invoked on a missing model exception.
*
* @param \Closure $missing
* @return $this
*/
public function missing($missing)
{
$this->action['missing'] = $missing;
return $this;
}
@@ -704,34 +1026,52 @@ class Route
$this->computedMiddleware = [];
return $this->computedMiddleware = array_unique(array_merge(
return $this->computedMiddleware = Router::uniqueMiddleware(array_merge(
$this->middleware(), $this->controllerMiddleware()
), SORT_REGULAR);
));
}
/**
* Get or set the middlewares attached to the route.
*
* @param array|string|null $middleware
* @param array|string|null $middleware
* @return $this|array
*/
public function middleware($middleware = null)
{
if (is_null($middleware)) {
return (array) Arr::get($this->action, 'middleware', []);
return (array) ($this->action['middleware'] ?? []);
}
if (is_string($middleware)) {
if (! is_array($middleware)) {
$middleware = func_get_args();
}
foreach ($middleware as $index => $value) {
$middleware[$index] = (string) $value;
}
$this->action['middleware'] = array_merge(
(array) Arr::get($this->action, 'middleware', []), $middleware
(array) ($this->action['middleware'] ?? []), $middleware
);
return $this;
}
/**
* Specify that the "Authorize" / "can" middleware should be applied to the route with the given options.
*
* @param string $ability
* @param array|string $models
* @return $this
*/
public function can($ability, $models = [])
{
return empty($models)
? $this->middleware(['can:'.$ability])
: $this->middleware(['can:'.$ability.','.implode(',', Arr::wrap($models))]);
}
/**
* Get the middleware for the route's controller.
*
@@ -743,9 +1083,168 @@ class Route
return [];
}
return ControllerDispatcher::getMiddleware(
$this->getController(), $this->getControllerMethod()
[$controllerClass, $controllerMethod] = [
$this->getControllerClass(),
$this->getControllerMethod(),
];
if (is_a($controllerClass, HasMiddleware::class, true)) {
return $this->staticallyProvidedControllerMiddleware(
$controllerClass, $controllerMethod
);
}
if (method_exists($controllerClass, 'getMiddleware')) {
return $this->controllerDispatcher()->getMiddleware(
$this->getController(), $controllerMethod
);
}
return [];
}
/**
* Get the statically provided controller middleware for the given class and method.
*
* @param string $class
* @param string $method
* @return array
*/
protected function staticallyProvidedControllerMiddleware(string $class, string $method)
{
return collect($class::middleware())->reject(function ($middleware) use ($method) {
return $this->controllerDispatcher()::methodExcludedByOptions(
$method, ['only' => $middleware->only, 'except' => $middleware->except]
);
})->map->middleware->values()->all();
}
/**
* Specify middleware that should be removed from the given route.
*
* @param array|string $middleware
* @return $this
*/
public function withoutMiddleware($middleware)
{
$this->action['excluded_middleware'] = array_merge(
(array) ($this->action['excluded_middleware'] ?? []), Arr::wrap($middleware)
);
return $this;
}
/**
* Get the middleware should be removed from the route.
*
* @return array
*/
public function excludedMiddleware()
{
return (array) ($this->action['excluded_middleware'] ?? []);
}
/**
* Indicate that the route should enforce scoping of multiple implicit Eloquent bindings.
*
* @return $this
*/
public function scopeBindings()
{
$this->action['scope_bindings'] = true;
return $this;
}
/**
* Indicate that the route should not enforce scoping of multiple implicit Eloquent bindings.
*
* @return $this
*/
public function withoutScopedBindings()
{
$this->action['scope_bindings'] = false;
return $this;
}
/**
* Determine if the route should enforce scoping of multiple implicit Eloquent bindings.
*
* @return bool
*/
public function enforcesScopedBindings()
{
return (bool) ($this->action['scope_bindings'] ?? false);
}
/**
* Determine if the route should prevent scoping of multiple implicit Eloquent bindings.
*
* @return bool
*/
public function preventsScopedBindings()
{
return isset($this->action['scope_bindings']) && $this->action['scope_bindings'] === false;
}
/**
* Specify that the route should not allow concurrent requests from the same session.
*
* @param int|null $lockSeconds
* @param int|null $waitSeconds
* @return $this
*/
public function block($lockSeconds = 10, $waitSeconds = 10)
{
$this->lockSeconds = $lockSeconds;
$this->waitSeconds = $waitSeconds;
return $this;
}
/**
* Specify that the route should allow concurrent requests from the same session.
*
* @return $this
*/
public function withoutBlocking()
{
return $this->block(null, null);
}
/**
* Get the maximum number of seconds the route's session lock should be held for.
*
* @return int|null
*/
public function locksFor()
{
return $this->lockSeconds;
}
/**
* Get the maximum number of seconds to wait while attempting to acquire a session lock.
*
* @return int|null
*/
public function waitsFor()
{
return $this->waitSeconds;
}
/**
* Get the dispatcher for the route's controller.
*
* @return \Illuminate\Routing\Contracts\ControllerDispatcher
*/
public function controllerDispatcher()
{
if ($this->container->bound(ControllerDispatcherContract::class)) {
return $this->container->make(ControllerDispatcherContract::class);
}
return new ControllerDispatcher($this->container);
}
/**
@@ -768,6 +1267,32 @@ class Route
];
}
/**
* Convert the route to a Symfony route.
*
* @return \Symfony\Component\Routing\Route
*/
public function toSymfonyRoute()
{
return new SymfonyRoute(
preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->uri()), $this->getOptionalParameterNames(),
$this->wheres, ['utf8' => true],
$this->getDomain() ?: '', [], $this->methods
);
}
/**
* Get the optional parameter names for the route.
*
* @return array
*/
protected function getOptionalParameterNames()
{
preg_match_all('/\{(\w+?)\?\}/', $this->uri(), $matches);
return isset($matches[1]) ? array_fill_keys($matches[1], null) : [];
}
/**
* Get the compiled version of the route.
*
@@ -814,7 +1339,15 @@ class Route
public function prepareForSerialization()
{
if ($this->action['uses'] instanceof Closure) {
throw new LogicException("Unable to prepare route [{$this->uri}] for serialization. Uses Closure.");
$this->action['uses'] = serialize(
new SerializableClosure($this->action['uses'])
);
}
if (isset($this->action['missing']) && $this->action['missing'] instanceof Closure) {
$this->action['missing'] = serialize(
new SerializableClosure($this->action['missing'])
);
}
$this->compileRoute();

View File

@@ -2,9 +2,10 @@
namespace Illuminate\Routing;
use LogicException;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
use LogicException;
use UnexpectedValueException;
class RouteAction
@@ -28,8 +29,11 @@ class RouteAction
// If the action is already a Closure instance, we will just set that instance
// as the "uses" property, because there is nothing else we need to do when
// it is available. Otherwise we will need to find it in the action list.
if (is_callable($action)) {
return ['uses' => $action];
if (Reflector::isCallable($action, true)) {
return ! is_array($action) ? ['uses' => $action] : [
'uses' => $action[0].'@'.$action[1],
'controller' => $action[0].'@'.$action[1],
];
}
// If no "uses" property has been set, we will dig through the array to find a
@@ -39,7 +43,7 @@ class RouteAction
$action['uses'] = static::findCallable($action);
}
if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
if (! static::containsSerializedClosure($action) && is_string($action['uses']) && ! str_contains($action['uses'], '@')) {
$action['uses'] = static::makeInvokable($action['uses']);
}
@@ -51,6 +55,8 @@ class RouteAction
*
* @param string $uri
* @return array
*
* @throws \LogicException
*/
protected static function missingAction($uri)
{
@@ -68,15 +74,17 @@ class RouteAction
protected static function findCallable(array $action)
{
return Arr::first($action, function ($value, $key) {
return is_callable($value) && is_numeric($key);
return Reflector::isCallable($value) && is_numeric($key);
});
}
/**
* Make an action for an invokable controller.
*
* @param string $action
* @param string $action
* @return string
*
* @throws \UnexpectedValueException
*/
protected static function makeInvokable($action)
{
@@ -86,4 +94,17 @@ class RouteAction
return $action.'@__invoke';
}
/**
* Determine if the given array actions contain a serialized Closure.
*
* @param array $action
* @return bool
*/
public static function containsSerializedClosure(array $action)
{
return is_string($action['uses']) && Str::startsWith($action['uses'], [
'O:47:"Laravel\\SerializableClosure\\SerializableClosure',
]) !== false;
}
}

View File

@@ -3,8 +3,8 @@
namespace Illuminate\Routing;
use Closure;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
class RouteBinding
{
@@ -37,11 +37,11 @@ class RouteBinding
// If the binding has an @ sign, we will assume it's being used to delimit
// the class name from the bind method name. This allows for bindings
// to run multiple bind methods in a single class for convenience.
list($class, $method) = Str::parseCallback($binding, 'bind');
[$class, $method] = Str::parseCallback($binding, 'bind');
$callable = [$container->make($class), $method];
return call_user_func($callable, $value, $route);
return $callable($value, $route);
};
}
@@ -52,6 +52,8 @@ class RouteBinding
* @param string $class
* @param \Closure|null $callback
* @return \Closure
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
*/
public static function forModel($container, $class, $callback = null)
{
@@ -65,7 +67,7 @@ class RouteBinding
// throw a not found exception otherwise we will return the instance.
$instance = $container->make($class);
if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) {
if ($model = $instance->resolveRouteBinding($value)) {
return $model;
}
@@ -73,7 +75,7 @@ class RouteBinding
// what we should do when the model is not found. This just gives these
// developer a little greater flexibility to decide what will happen.
if ($callback instanceof Closure) {
return call_user_func($callback, $value);
return $callback($value);
}
throw (new ModelNotFoundException)->setModel($class);

View File

@@ -2,16 +2,11 @@
namespace Illuminate\Routing;
use Countable;
use ArrayIterator;
use IteratorAggregate;
use Illuminate\Support\Arr;
use Illuminate\Container\Container;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Illuminate\Support\Arr;
class RouteCollection implements Countable, IteratorAggregate
class RouteCollection extends AbstractRouteCollection
{
/**
* An array of the routes keyed by method.
@@ -21,23 +16,23 @@ class RouteCollection implements Countable, IteratorAggregate
protected $routes = [];
/**
* An flattened array of all of the routes.
* A flattened array of all of the routes.
*
* @var array
* @var \Illuminate\Routing\Route[]
*/
protected $allRoutes = [];
/**
* A look-up table of routes by their names.
*
* @var array
* @var \Illuminate\Routing\Route[]
*/
protected $nameList = [];
/**
* A look-up table of routes by controller action.
*
* @var array
* @var \Illuminate\Routing\Route[]
*/
protected $actionList = [];
@@ -64,7 +59,7 @@ class RouteCollection implements Countable, IteratorAggregate
*/
protected function addToCollections($route)
{
$domainAndUri = $route->domain().$route->uri();
$domainAndUri = $route->getDomain().$route->uri();
foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
@@ -84,15 +79,15 @@ class RouteCollection implements Countable, IteratorAggregate
// If the route has a name, we will add it to the name look-up table so that we
// will quickly be able to find any route associate with a name and not have
// to iterate through every route every time we need to perform a look-up.
$action = $route->getAction();
if (isset($action['as'])) {
$this->nameList[$action['as']] = $route;
if ($name = $route->getName()) {
$this->nameList[$name] = $route;
}
// When the route is routing to a controller we will also store the action that
// is used by the route. This will let us reverse route to controllers while
// processing a request and easily generate URLs to the given controllers.
$action = $route->getAction();
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
@@ -152,6 +147,7 @@ class RouteCollection implements Countable, IteratorAggregate
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function match(Request $request)
@@ -163,99 +159,14 @@ class RouteCollection implements Countable, IteratorAggregate
// by the consumer. Otherwise we will check for routes with another verb.
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
// If no route was found we will now check if a matching route is specified by
// another HTTP verb. If it is we will need to throw a MethodNotAllowed and
// inform the user agent of which HTTP verb it should use for this route.
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
/**
* Determine if a route in the array matches the request.
*
* @param array $routes
* @param \Illuminate\http\Request $request
* @param bool $includingMethod
* @return \Illuminate\Routing\Route|null
*/
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
return Arr::first($routes, function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
/**
* Determine if any routes match on another HTTP verb.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function checkForAlternateVerbs($request)
{
$methods = array_diff(Router::$verbs, [$request->getMethod()]);
// Here we will spin through all verbs except for the current request verb and
// check to see if any routes respond to them. If they do, we will return a
// proper error response with the correct headers on the response string.
$others = [];
foreach ($methods as $method) {
if (! is_null($this->matchAgainstRoutes($this->get($method), $request, false))) {
$others[] = $method;
}
}
return $others;
}
/**
* Get a route (if necessary) that responds when other available methods are present.
*
* @param \Illuminate\Http\Request $request
* @param array $methods
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
*/
protected function getRouteForMethods($request, array $methods)
{
if ($request->method() == 'OPTIONS') {
return (new Route('OPTIONS', $request->path(), function () use ($methods) {
return new Response('', 200, ['Allow' => implode(',', $methods)]);
}))->bind($request);
}
$this->methodNotAllowed($methods);
}
/**
* Throw a method not allowed HTTP exception.
*
* @param array $others
* @return void
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
*/
protected function methodNotAllowed(array $others)
{
throw new MethodNotAllowedHttpException($others);
return $this->handleMatchedRoute($request, $route);
}
/**
* Get routes from the collection by method.
*
* @param string|null $method
* @return array
* @return \Illuminate\Routing\Route[]
*/
public function get($method = null)
{
@@ -281,7 +192,7 @@ class RouteCollection implements Countable, IteratorAggregate
*/
public function getByName($name)
{
return isset($this->nameList[$name]) ? $this->nameList[$name] : null;
return $this->nameList[$name] ?? null;
}
/**
@@ -292,13 +203,13 @@ class RouteCollection implements Countable, IteratorAggregate
*/
public function getByAction($action)
{
return isset($this->actionList[$action]) ? $this->actionList[$action] : null;
return $this->actionList[$action] ?? null;
}
/**
* Get all of the routes in the collection.
*
* @return array
* @return \Illuminate\Routing\Route[]
*/
public function getRoutes()
{
@@ -318,7 +229,7 @@ class RouteCollection implements Countable, IteratorAggregate
/**
* Get all of the routes keyed by their name.
*
* @return array
* @return \Illuminate\Routing\Route[]
*/
public function getRoutesByName()
{
@@ -326,22 +237,32 @@ class RouteCollection implements Countable, IteratorAggregate
}
/**
* Get an iterator for the items.
* Convert the collection to a Symfony RouteCollection instance.
*
* @return \ArrayIterator
* @return \Symfony\Component\Routing\RouteCollection
*/
public function getIterator()
public function toSymfonyRouteCollection()
{
return new ArrayIterator($this->getRoutes());
$symfonyRoutes = parent::toSymfonyRouteCollection();
$this->refreshNameLookups();
return $symfonyRoutes;
}
/**
* Count the number of items in the collection.
* Convert the collection to a CompiledRouteCollection instance.
*
* @return int
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Container\Container $container
* @return \Illuminate\Routing\CompiledRouteCollection
*/
public function count()
public function toCompiledRouteCollection(Router $router, Container $container)
{
return count($this->getRoutes());
['compiled' => $compiled, 'attributes' => $attributes] = $this->compile();
return (new CompiledRouteCollection($compiled, $attributes))
->setRouter($router)
->setContainer($container);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Http\Request;
interface RouteCollectionInterface
{
/**
* Add a Route instance to the collection.
*
* @param \Illuminate\Routing\Route $route
* @return \Illuminate\Routing\Route
*/
public function add(Route $route);
/**
* Refresh the name look-up table.
*
* This is done in case any names are fluently defined or if routes are overwritten.
*
* @return void
*/
public function refreshNameLookups();
/**
* Refresh the action look-up table.
*
* This is done in case any actions are overwritten with new controllers.
*
* @return void
*/
public function refreshActionLookups();
/**
* Find the first route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function match(Request $request);
/**
* Get routes from the collection by method.
*
* @param string|null $method
* @return \Illuminate\Routing\Route[]
*/
public function get($method = null);
/**
* Determine if the route collection contains a given named route.
*
* @param string $name
* @return bool
*/
public function hasNamedRoute($name);
/**
* Get a route instance by its name.
*
* @param string $name
* @return \Illuminate\Routing\Route|null
*/
public function getByName($name);
/**
* Get a route instance by its controller action.
*
* @param string $action
* @return \Illuminate\Routing\Route|null
*/
public function getByAction($action);
/**
* Get all of the routes in the collection.
*
* @return \Illuminate\Routing\Route[]
*/
public function getRoutes();
/**
* Get all of the routes keyed by their HTTP verb / method.
*
* @return array
*/
public function getRoutesByMethod();
/**
* Get all of the routes keyed by their name.
*
* @return \Illuminate\Routing\Route[]
*/
public function getRoutesByName();
}

View File

@@ -1,54 +0,0 @@
<?php
namespace Illuminate\Routing;
use Symfony\Component\Routing\Route as SymfonyRoute;
class RouteCompiler
{
/**
* The route instance.
*
* @var \Illuminate\Routing\Route
*/
protected $route;
/**
* Create a new Route compiler instance.
*
* @param \Illuminate\Routing\Route $route
* @return void
*/
public function __construct($route)
{
$this->route = $route;
}
/**
* Compile the route.
*
* @return \Symfony\Component\Routing\CompiledRoute
*/
public function compile()
{
$optionals = $this->getOptionalParameters();
$uri = preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri());
return (
new SymfonyRoute($uri, $optionals, $this->route->wheres, [], $this->route->domain() ?: '')
)->compile();
}
/**
* Get the optional parameters for the route.
*
* @return array
*/
protected function getOptionalParameters()
{
preg_match_all('/\{(\w+?)\?\}/', $this->route->uri(), $matches);
return isset($matches[1]) ? array_fill_keys($matches[1], null) : [];
}
}

View File

@@ -2,10 +2,13 @@
namespace Illuminate\Routing;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use ReflectionClass;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionParameter;
use Illuminate\Support\Arr;
use ReflectionFunctionAbstract;
use stdClass;
trait RouteDependencyResolverTrait
{
@@ -41,12 +44,12 @@ trait RouteDependencyResolverTrait
$values = array_values($parameters);
foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency(
$parameter, $parameters
);
$skippableValue = new stdClass;
if (! is_null($instance)) {
foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency($parameter, $parameters, $skippableValue);
if ($instance !== $skippableValue) {
$instanceCount++;
$this->spliceIntoParameters($parameters, $key, $instance);
@@ -64,18 +67,25 @@ trait RouteDependencyResolverTrait
*
* @param \ReflectionParameter $parameter
* @param array $parameters
* @param object $skippableValue
* @return mixed
*/
protected function transformDependency(ReflectionParameter $parameter, $parameters)
protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue)
{
$class = $parameter->getClass();
$className = Reflector::getParameterClassName($parameter);
// If the parameter has a type-hinted class, we will check to see if it is already in
// the list of parameters. If it is we will just skip it as it is probably a model
// binding and we do not want to mess with those; otherwise, we resolve it here.
if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
return $this->container->make($class->name);
if ($className && ! $this->alreadyInParameters($className, $parameters)) {
$isEnum = method_exists(ReflectionClass::class, 'isEnum') && (new ReflectionClass($className))->isEnum();
return $parameter->isDefaultValueAvailable()
? ($isEnum ? $parameter->getDefaultValue() : null)
: $this->container->make($className);
}
return $skippableValue;
}
/**
@@ -87,9 +97,7 @@ trait RouteDependencyResolverTrait
*/
protected function alreadyInParameters($class, array $parameters)
{
return ! is_null(Arr::first($parameters, function ($value) use ($class) {
return $value instanceof $class;
}));
return ! is_null(Arr::first($parameters, fn ($value) => $value instanceof $class));
}
/**

View File

@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Routing;
class RouteFileRegistrar
{
/**
* The router instance.
*
* @var \Illuminate\Routing\Router
*/
protected $router;
/**
* Create a new route file registrar instance.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function __construct(Router $router)
{
$this->router = $router;
}
/**
* Require the given routes file.
*
* @param string $routes
* @return void
*/
public function register($routes)
{
$router = $this->router;
require $routes;
}
}

View File

@@ -11,17 +11,22 @@ class RouteGroup
*
* @param array $new
* @param array $old
* @param bool $prependExistingPrefix
* @return array
*/
public static function merge($new, $old)
public static function merge($new, $old, $prependExistingPrefix = true)
{
if (isset($new['domain'])) {
unset($old['domain']);
}
if (isset($new['controller'])) {
unset($old['controller']);
}
$new = array_merge(static::formatAs($new, $old), [
'namespace' => static::formatNamespace($new, $old),
'prefix' => static::formatPrefix($new, $old),
'prefix' => static::formatPrefix($new, $old, $prependExistingPrefix),
'where' => static::formatWhere($new, $old),
]);
@@ -40,12 +45,12 @@ class RouteGroup
protected static function formatNamespace($new, $old)
{
if (isset($new['namespace'])) {
return isset($old['namespace'])
return isset($old['namespace']) && ! str_starts_with($new['namespace'], '\\')
? trim($old['namespace'], '\\').'\\'.trim($new['namespace'], '\\')
: trim($new['namespace'], '\\');
}
return isset($old['namespace']) ? $old['namespace'] : null;
return $old['namespace'] ?? null;
}
/**
@@ -53,13 +58,18 @@ class RouteGroup
*
* @param array $new
* @param array $old
* @param bool $prependExistingPrefix
* @return string|null
*/
protected static function formatPrefix($new, $old)
protected static function formatPrefix($new, $old, $prependExistingPrefix = true)
{
$old = Arr::get($old, 'prefix');
$old = $old['prefix'] ?? '';
return isset($new['prefix']) ? trim($old, '/').'/'.trim($new['prefix'], '/') : $old;
if ($prependExistingPrefix) {
return isset($new['prefix']) ? trim($old, '/').'/'.trim($new['prefix'], '/') : $old;
} else {
return isset($new['prefix']) ? trim($new['prefix'], '/').'/'.trim($old, '/') : $old;
}
}
/**
@@ -72,8 +82,8 @@ class RouteGroup
protected static function formatWhere($new, $old)
{
return array_merge(
isset($old['where']) ? $old['where'] : [],
isset($new['where']) ? $new['where'] : []
$old['where'] ?? [],
$new['where'] ?? []
);
}
@@ -87,7 +97,7 @@ class RouteGroup
protected static function formatAs($new, $old)
{
if (isset($old['as'])) {
$new['as'] = $old['as'].Arr::get($new, 'as', '');
$new['as'] = $old['as'].($new['as'] ?? '');
}
return $new;

View File

@@ -32,9 +32,6 @@ class RouteParameterBinder
*/
public function parameters($request)
{
// If the route has a regular expression for the host part of the URI, we will
// compile that and get the parameter matches for this domain. We will then
// merge them into this parameters array so that this array is completed.
$parameters = $this->bindPathParameters($request);
// If the route has a regular expression for the host part of the URI, we will
@@ -106,7 +103,7 @@ class RouteParameterBinder
protected function replaceDefaults(array $parameters)
{
foreach ($parameters as $key => $value) {
$parameters[$key] = isset($value) ? $value : Arr::get($this->route->defaults, $key);
$parameters[$key] = $value ?? Arr::get($this->route->defaults, $key);
}
foreach ($this->route->defaults as $key => $value) {

View File

@@ -2,12 +2,36 @@
namespace Illuminate\Routing;
use Closure;
use BadMethodCallException;
use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use InvalidArgumentException;
/**
* @method \Illuminate\Routing\Route any(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\Route delete(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\Route get(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\Route options(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\Route patch(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\Route post(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\Route put(string $uri, \Closure|array|string|null $action = null)
* @method \Illuminate\Routing\RouteRegistrar as(string $value)
* @method \Illuminate\Routing\RouteRegistrar controller(string $controller)
* @method \Illuminate\Routing\RouteRegistrar domain(string $value)
* @method \Illuminate\Routing\RouteRegistrar middleware(array|string|null $middleware)
* @method \Illuminate\Routing\RouteRegistrar name(string $value)
* @method \Illuminate\Routing\RouteRegistrar namespace(string|null $value)
* @method \Illuminate\Routing\RouteRegistrar prefix(string $prefix)
* @method \Illuminate\Routing\RouteRegistrar scopeBindings()
* @method \Illuminate\Routing\RouteRegistrar where(array $where)
* @method \Illuminate\Routing\RouteRegistrar withoutMiddleware(array|string $middleware)
* @method \Illuminate\Routing\RouteRegistrar withoutScopedBindings()
*/
class RouteRegistrar
{
use CreatesRegularExpressionRouteConstraints;
/**
* The router instance.
*
@@ -25,7 +49,7 @@ class RouteRegistrar
/**
* The methods to dynamically pass through to the router.
*
* @var array
* @var string[]
*/
protected $passthru = [
'get', 'post', 'put', 'patch', 'delete', 'options', 'any',
@@ -34,10 +58,19 @@ class RouteRegistrar
/**
* The attributes that can be set through this class.
*
* @var array
* @var string[]
*/
protected $allowedAttributes = [
'as', 'domain', 'middleware', 'name', 'namespace', 'prefix',
'as',
'controller',
'domain',
'middleware',
'name',
'namespace',
'prefix',
'scopeBindings',
'where',
'withoutMiddleware',
];
/**
@@ -47,6 +80,8 @@ class RouteRegistrar
*/
protected $aliases = [
'name' => 'as',
'scopeBindings' => 'scope_bindings',
'withoutMiddleware' => 'excluded_middleware',
];
/**
@@ -75,7 +110,21 @@ class RouteRegistrar
throw new InvalidArgumentException("Attribute [{$key}] does not exist.");
}
$this->attributes[array_get($this->aliases, $key, $key)] = $value;
if ($key === 'middleware') {
foreach ($value as $index => $middleware) {
$value[$index] = (string) $middleware;
}
}
$attributeKey = Arr::get($this->aliases, $key, $key);
if ($key === 'withoutMiddleware') {
$value = array_merge(
(array) ($this->attributes[$attributeKey] ?? []), Arr::wrap($value)
);
}
$this->attributes[$attributeKey] = $value;
return $this;
}
@@ -86,22 +135,63 @@ class RouteRegistrar
* @param string $name
* @param string $controller
* @param array $options
* @return void
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function resource($name, $controller, array $options = [])
{
$this->router->resource($name, $controller, $this->attributes + $options);
return $this->router->resource($name, $controller, $this->attributes + $options);
}
/**
* Route an API resource to a controller.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\PendingResourceRegistration
*/
public function apiResource($name, $controller, array $options = [])
{
return $this->router->apiResource($name, $controller, $this->attributes + $options);
}
/**
* Route a singleton resource to a controller.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function singleton($name, $controller, array $options = [])
{
return $this->router->singleton($name, $controller, $this->attributes + $options);
}
/**
* Route an API singleton resource to a controller.
*
* @param string $name
* @param string $controller
* @param array $options
* @return \Illuminate\Routing\PendingSingletonResourceRegistration
*/
public function apiSingleton($name, $controller, array $options = [])
{
return $this->router->apiSingleton($name, $controller, $this->attributes + $options);
}
/**
* Create a route group with shared attributes.
*
* @param \Closure|string $callback
* @return void
* @return $this
*/
public function group($callback)
{
$this->router->group($this->attributes, $callback);
return $this;
}
/**
@@ -150,6 +240,18 @@ class RouteRegistrar
$action = ['uses' => $action];
}
if (is_array($action) &&
! Arr::isAssoc($action) &&
Reflector::isCallable($action)) {
if (strncmp($action[0], '\\', 1)) {
$action[0] = '\\'.$action[0];
}
$action = [
'uses' => $action[0].'@'.$action[1],
'controller' => $action[0].'@'.$action[1],
];
}
return array_merge($this->attributes, $action);
}
@@ -159,6 +261,8 @@ class RouteRegistrar
* @param string $method
* @param array $parameters
* @return \Illuminate\Routing\Route|$this
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
@@ -167,9 +271,15 @@ class RouteRegistrar
}
if (in_array($method, $this->allowedAttributes)) {
return $this->attribute($method, $parameters[0]);
if ($method === 'middleware') {
return $this->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
}
return $this->attribute($method, array_key_exists(0, $parameters) ? $parameters[0] : true);
}
throw new BadMethodCallException("Method [{$method}] does not exist.");
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
}

View File

@@ -2,9 +2,10 @@
namespace Illuminate\Routing;
use ReflectionMethod;
use ReflectionFunction;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
use ReflectionFunction;
use ReflectionMethod;
class RouteSignatureParameters
{
@@ -12,18 +13,24 @@ class RouteSignatureParameters
* Extract the route action's signature parameters.
*
* @param array $action
* @param string $subClass
* @param array $conditions
* @return array
*/
public static function fromAction(array $action, $subClass = null)
public static function fromAction(array $action, $conditions = [])
{
$parameters = is_string($action['uses'])
? static::fromClassMethodString($action['uses'])
: (new ReflectionFunction($action['uses']))->getParameters();
$callback = RouteAction::containsSerializedClosure($action)
? unserialize($action['uses'])->getClosure()
: $action['uses'];
return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) {
return $p->getClass() && $p->getClass()->isSubclassOf($subClass);
});
$parameters = is_string($callback)
? static::fromClassMethodString($callback)
: (new ReflectionFunction($callback))->getParameters();
return match (true) {
! empty($conditions['subClass']) => array_filter($parameters, fn ($p) => Reflector::isParameterSubclassOf($p, $conditions['subClass'])),
! empty($conditions['backedEnum']) => array_filter($parameters, fn ($p) => Reflector::isParameterBackedEnumWithStringBackingType($p)),
default => $parameters,
};
}
/**
@@ -34,9 +41,9 @@ class RouteSignatureParameters
*/
protected static function fromClassMethodString($uses)
{
list($class, $method) = Str::parseCallback($uses);
[$class, $method] = Str::parseCallback($uses);
if (! method_exists($class, $method) && is_callable($class, $method)) {
if (! method_exists($class, $method) && Reflector::isCallable($class, $method)) {
return [];
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Illuminate\Routing;
class RouteUri
{
/**
* The route URI.
*
* @var string
*/
public $uri;
/**
* The fields that should be used when resolving bindings.
*
* @var array
*/
public $bindingFields = [];
/**
* Create a new route URI instance.
*
* @param string $uri
* @param array $bindingFields
* @return void
*/
public function __construct(string $uri, array $bindingFields = [])
{
$this->uri = $uri;
$this->bindingFields = $bindingFields;
}
/**
* Parse the given URI.
*
* @param string $uri
* @return static
*/
public static function parse($uri)
{
preg_match_all('/\{([\w\:]+?)\??\}/', $uri, $matches);
$bindingFields = [];
foreach ($matches[0] as $match) {
if (! str_contains($match, ':')) {
continue;
}
$segments = explode(':', trim($match, '{}?'));
$bindingFields[$segments[0]] = $segments[1];
$uri = str_contains($match, '?')
? str_replace($match, '{'.$segments[0].'?}', $uri)
: str_replace($match, '{'.$segments[0].'}', $uri);
}
return new static($uri, $bindingFields);
}
}

View File

@@ -2,16 +2,15 @@
namespace Illuminate\Routing;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Routing\Exceptions\UrlGenerationException;
use Illuminate\Support\Arr;
class RouteUrlGenerator
{
/**
* The URL generator instance.
*
* @param \Illuminate\Routing\UrlGenerator
* @var \Illuminate\Routing\UrlGenerator
*/
protected $url;
@@ -83,11 +82,12 @@ class RouteUrlGenerator
// will need to throw the exception to let the developers know one was not given.
$uri = $this->addQueryString($this->url->format(
$root = $this->replaceRootParameters($route, $domain, $parameters),
$this->replaceRouteParameters($route->uri(), $parameters)
$this->replaceRouteParameters($route->uri(), $parameters),
$route
), $parameters);
if (preg_match('/\{.*?\}/', $uri)) {
throw UrlGenerationException::forMissingParameters($route);
if (preg_match_all('/{(.*?)}/', $uri, $matchedMissingParameters)) {
throw UrlGenerationException::forMissingParameters($route, $matchedMissingParameters[1]);
}
// Once we have ensured that there are no missing parameters in the URI we will encode
@@ -96,7 +96,13 @@ class RouteUrlGenerator
$uri = strtr(rawurlencode($uri), $this->dontEncode);
if (! $absolute) {
return '/'.ltrim(str_replace($root, '', $uri), '/');
$uri = preg_replace('#^(//|[^/?])+#', '', $uri);
if ($base = $this->request->getBaseUrl()) {
$uri = preg_replace('#^'.$base.'#i', '', $uri);
}
return '/'.ltrim($uri, '/');
}
return $uri;
@@ -111,7 +117,7 @@ class RouteUrlGenerator
*/
protected function getRouteDomain($route, &$parameters)
{
return $route->domain() ? $this->formatDomain($route, $parameters) : null;
return $route->getDomain() ? $this->formatDomain($route, $parameters) : null;
}
/**
@@ -124,7 +130,7 @@ class RouteUrlGenerator
protected function formatDomain($route, &$parameters)
{
return $this->addPortToDomain(
$this->getRouteScheme($route).$route->domain()
$this->getRouteScheme($route).$route->getDomain()
);
}
@@ -140,9 +146,9 @@ class RouteUrlGenerator
return 'http://';
} elseif ($route->httpsOnly()) {
return 'https://';
} else {
return $this->url->formatScheme(null);
}
return $this->url->formatScheme();
}
/**
@@ -190,9 +196,12 @@ class RouteUrlGenerator
$path = $this->replaceNamedParameters($path, $parameters);
$path = preg_replace_callback('/\{.*?\}/', function ($match) use (&$parameters) {
return (empty($parameters) && ! Str::endsWith($match[0], '?}'))
// Reset only the numeric keys...
$parameters = array_merge($parameters);
return (! isset($parameters[0]) && ! str_ends_with($match[0], '?}'))
? $match[0]
: array_shift($parameters);
: Arr::pull($parameters, 0);
}, $path);
return trim(preg_replace('/\{.*?\?\}/', '', $path), '/');
@@ -207,14 +216,16 @@ class RouteUrlGenerator
*/
protected function replaceNamedParameters($path, &$parameters)
{
return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) {
if (isset($parameters[$m[1]])) {
return preg_replace_callback('/\{(.*?)(\?)?\}/', function ($m) use (&$parameters) {
if (isset($parameters[$m[1]]) && $parameters[$m[1]] !== '') {
return Arr::pull($parameters, $m[1]);
} elseif (isset($this->defaultParameters[$m[1]])) {
return $this->defaultParameters[$m[1]];
} else {
return $m[0];
} elseif (isset($parameters[$m[1]])) {
Arr::pull($parameters, $m[1]);
}
return $m[0];
}, $path);
}
@@ -250,11 +261,11 @@ class RouteUrlGenerator
// First we will get all of the string parameters that are remaining after we
// have replaced the route wildcards. We'll then build a query string from
// these string parameters then use it as a starting point for the rest.
if (count($parameters) == 0) {
if (count($parameters) === 0) {
return '';
}
$query = http_build_query(
$query = Arr::query(
$keyed = $this->getStringParameters($parameters)
);
@@ -267,7 +278,9 @@ class RouteUrlGenerator
);
}
return '?'.trim($query, '&');
$query = trim($query, '&');
return $query === '' ? '' : "?{$query}";
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,18 @@
namespace Illuminate\Routing;
use Illuminate\Support\ServiceProvider;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response as PsrResponse;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Illuminate\Contracts\View\Factory as ViewFactoryContract;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract;
use Illuminate\Contracts\Routing\UrlGenerator as UrlGeneratorContract;
use Illuminate\Contracts\View\Factory as ViewFactoryContract;
use Illuminate\Routing\Contracts\CallableDispatcher as CallableDispatcherContract;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;
use Illuminate\Support\ServiceProvider;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response as PsrResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
class RoutingServiceProvider extends ServiceProvider
{
@@ -20,16 +25,13 @@ class RoutingServiceProvider extends ServiceProvider
public function register()
{
$this->registerRouter();
$this->registerUrlGenerator();
$this->registerRedirector();
$this->registerPsrRequest();
$this->registerPsrResponse();
$this->registerResponseFactory();
$this->registerCallableDispatcher();
$this->registerControllerDispatcher();
}
/**
@@ -59,14 +61,23 @@ class RoutingServiceProvider extends ServiceProvider
// and all the registered routes will be available to the generator.
$app->instance('routes', $routes);
$url = new UrlGenerator(
return new UrlGenerator(
$routes, $app->rebinding(
'request', $this->requestRebinder()
)
), $app['config']['app.asset_url']
);
});
$this->app->extend('url', function (UrlGeneratorContract $url, $app) {
// Next we will set a few service resolvers on the URL generator so it can
// get the information it needs to function. This just provides some of
// the convenience features to this URL generator like "signed" URLs.
$url->setSessionResolver(function () {
return $this->app['session'];
return $this->app['session'] ?? null;
});
$url->setKeyResolver(function () {
return $this->app->make('config')->get('app.key');
});
// If the route collection is "rebound", for example, when the routes stay
@@ -117,11 +128,20 @@ class RoutingServiceProvider extends ServiceProvider
* Register a binding for the PSR-7 request implementation.
*
* @return void
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function registerPsrRequest()
{
$this->app->bind(ServerRequestInterface::class, function ($app) {
return (new DiactorosFactory)->createRequest($app->make('request'));
if (class_exists(Psr17Factory::class) && class_exists(PsrHttpFactory::class)) {
$psr17Factory = new Psr17Factory;
return (new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory))
->createRequest($app->make('request'));
}
throw new BindingResolutionException('Unable to resolve PSR request. Please install the symfony/psr-http-message-bridge and nyholm/psr7 packages.');
});
}
@@ -129,11 +149,17 @@ class RoutingServiceProvider extends ServiceProvider
* Register a binding for the PSR-7 response implementation.
*
* @return void
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function registerPsrResponse()
{
$this->app->bind(ResponseInterface::class, function ($app) {
return new PsrResponse();
$this->app->bind(ResponseInterface::class, function () {
if (class_exists(PsrResponse::class)) {
return new PsrResponse;
}
throw new BindingResolutionException('Unable to resolve PSR response. Please install the nyholm/psr7 package.');
});
}
@@ -148,4 +174,28 @@ class RoutingServiceProvider extends ServiceProvider
return new ResponseFactory($app[ViewFactoryContract::class], $app['redirect']);
});
}
/**
* Register the callable dispatcher.
*
* @return void
*/
protected function registerCallableDispatcher()
{
$this->app->singleton(CallableDispatcherContract::class, function ($app) {
return new CallableDispatcher($app);
});
}
/**
* Register the controller dispatcher.
*
* @return void
*/
protected function registerControllerDispatcher()
{
$this->app->singleton(ControllerDispatcherContract::class, function ($app) {
return new ControllerDispatcher($app);
});
}
}

View File

@@ -10,7 +10,7 @@ class SortedMiddleware extends Collection
* Create a new Sorted Middleware container.
*
* @param array $priorityMap
* @param array|Collection $middlewares
* @param \Illuminate\Support\Collection|array $middlewares
* @return void
*/
public function __construct(array $priorityMap, $middlewares)
@@ -40,32 +40,67 @@ class SortedMiddleware extends Collection
continue;
}
$stripped = head(explode(':', $middleware));
if (in_array($stripped, $priorityMap)) {
$priorityIndex = array_search($stripped, $priorityMap);
$priorityIndex = $this->priorityMapIndex($priorityMap, $middleware);
if (! is_null($priorityIndex)) {
// This middleware is in the priority map. If we have encountered another middleware
// that was also in the priority map and was at a lower priority than the current
// middleware, we will move this middleware to be above the previous encounter.
if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) {
return $this->sortMiddleware(
$priorityMap, array_values(
$this->moveMiddleware($middlewares, $index, $lastIndex)
)
$priorityMap, array_values($this->moveMiddleware($middlewares, $index, $lastIndex))
);
}
// This middleware is in the priority map; but, this is the first middleware we have
// encountered from the map thus far. We'll save its current index plus its index
// from the priority map so we can compare against them on the next iterations.
} else {
$lastIndex = $index;
$lastPriorityIndex = $priorityIndex;
}
$lastIndex = $index;
$lastPriorityIndex = $priorityIndex;
}
}
return array_values(array_unique($middlewares, SORT_REGULAR));
return Router::uniqueMiddleware($middlewares);
}
/**
* Calculate the priority map index of the middleware.
*
* @param array $priorityMap
* @param string $middleware
* @return int|null
*/
protected function priorityMapIndex($priorityMap, $middleware)
{
foreach ($this->middlewareNames($middleware) as $name) {
$priorityIndex = array_search($name, $priorityMap);
if ($priorityIndex !== false) {
return $priorityIndex;
}
}
}
/**
* Resolve the middleware names to look for in the priority array.
*
* @param string $middleware
* @return \Generator
*/
protected function middlewareNames($middleware)
{
$stripped = head(explode(':', $middleware));
yield $stripped;
$interfaces = @class_implements($stripped);
if ($interfaces !== false) {
foreach ($interfaces as $interface) {
yield $interface;
}
}
}
/**

View File

@@ -2,22 +2,27 @@
namespace Illuminate\Routing;
use BackedEnum;
use Closure;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use InvalidArgumentException;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Routing\UrlGenerator as UrlGeneratorContract;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
class UrlGenerator implements UrlGeneratorContract
{
use Macroable;
use InteractsWithTime, Macroable;
/**
* The route collection.
*
* @var \Illuminate\Routing\RouteCollection
* @var \Illuminate\Routing\RouteCollectionInterface
*/
protected $routes;
@@ -28,6 +33,13 @@ class UrlGenerator implements UrlGeneratorContract
*/
protected $request;
/**
* The asset root URL.
*
* @var string
*/
protected $assetRoot;
/**
* The forced URL root.
*
@@ -36,7 +48,7 @@ class UrlGenerator implements UrlGeneratorContract
protected $forcedRoot;
/**
* The forced schema for URLs.
* The forced scheme for URLs.
*
* @var string
*/
@@ -50,11 +62,11 @@ class UrlGenerator implements UrlGeneratorContract
protected $cachedRoot;
/**
* A cached copy of the URL schema for the current request.
* A cached copy of the URL scheme for the current request.
*
* @var string|null
*/
protected $cachedSchema;
protected $cachedScheme;
/**
* The root namespace being applied to controller actions.
@@ -70,6 +82,13 @@ class UrlGenerator implements UrlGeneratorContract
*/
protected $sessionResolver;
/**
* The encryption key resolver callable.
*
* @var callable
*/
protected $keyResolver;
/**
* The callback to use to format hosts.
*
@@ -87,20 +106,22 @@ class UrlGenerator implements UrlGeneratorContract
/**
* The route URL generator instance.
*
* @var \Illuminate\Routing\RouteUrlGenerator
* @var \Illuminate\Routing\RouteUrlGenerator|null
*/
protected $routeGenerator;
/**
* Create a new URL Generator instance.
*
* @param \Illuminate\Routing\RouteCollection $routes
* @param \Illuminate\Routing\RouteCollectionInterface $routes
* @param \Illuminate\Http\Request $request
* @param string|null $assetRoot
* @return void
*/
public function __construct(RouteCollection $routes, Request $request)
public function __construct(RouteCollectionInterface $routes, Request $request, $assetRoot = null)
{
$this->routes = $routes;
$this->assetRoot = $assetRoot;
$this->setRequest($request);
}
@@ -141,9 +162,22 @@ class UrlGenerator implements UrlGeneratorContract
return $url;
} elseif ($fallback) {
return $this->to($fallback);
} else {
return $this->to('/');
}
return $this->to('/');
}
/**
* Get the previous path info for the request.
*
* @param mixed $fallback
* @return string
*/
public function previousPath($fallback = false)
{
$previousPath = str_replace($this->to('/'), '', rtrim(preg_replace('/\?.*/', '', $this->previous($fallback)), '/'));
return $previousPath === '' ? '/' : $previousPath;
}
/**
@@ -184,7 +218,7 @@ class UrlGenerator implements UrlGeneratorContract
// for passing the array of parameters to this URL as a list of segments.
$root = $this->formatRoot($this->formatScheme($secure));
list($path, $query) = $this->extractQueryString($path);
[$path, $query] = $this->extractQueryString($path);
return $this->format(
$root, '/'.trim($path.'/'.$tail, '/')
@@ -195,7 +229,7 @@ class UrlGenerator implements UrlGeneratorContract
* Generate a secure, absolute URL to the given path.
*
* @param string $path
* @param array $parameters
* @param array $parameters
* @return string
*/
public function secure($path, $parameters = [])
@@ -219,7 +253,7 @@ class UrlGenerator implements UrlGeneratorContract
// Once we get the root URL, we will check to see if it contains an index.php
// file in the paths. If it does, we will remove it since it is not needed
// for asset paths, but only for routes to endpoints in the application.
$root = $this->formatRoot($this->formatScheme($secure));
$root = $this->assetRoot ?: $this->formatRoot($this->formatScheme($secure));
return $this->removeIndex($root).'/'.trim($path, '/');
}
@@ -263,7 +297,7 @@ class UrlGenerator implements UrlGeneratorContract
{
$i = 'index.php';
return Str::contains($root, $i) ? str_replace('/'.$i, '', $root) : $root;
return str_contains($root, $i) ? str_replace('/'.$i, '', $root) : $root;
}
/**
@@ -272,28 +306,157 @@ class UrlGenerator implements UrlGeneratorContract
* @param bool|null $secure
* @return string
*/
public function formatScheme($secure)
public function formatScheme($secure = null)
{
if (! is_null($secure)) {
return $secure ? 'https://' : 'http://';
}
if (is_null($this->cachedSchema)) {
$this->cachedSchema = $this->forceScheme ?: $this->request->getScheme().'://';
if (is_null($this->cachedScheme)) {
$this->cachedScheme = $this->forceScheme ?: $this->request->getScheme().'://';
}
return $this->cachedSchema;
return $this->cachedScheme;
}
/**
* Create a signed route URL for a named route.
*
* @param string $name
* @param mixed $parameters
* @param \DateTimeInterface|\DateInterval|int|null $expiration
* @param bool $absolute
* @return string
*
* @throws \InvalidArgumentException
*/
public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true)
{
$this->ensureSignedRouteParametersAreNotReserved(
$parameters = Arr::wrap($parameters)
);
if ($expiration) {
$parameters = $parameters + ['expires' => $this->availableAt($expiration)];
}
ksort($parameters);
$key = call_user_func($this->keyResolver);
return $this->route($name, $parameters + [
'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key),
], $absolute);
}
/**
* Ensure the given signed route parameters are not reserved.
*
* @param mixed $parameters
* @return void
*/
protected function ensureSignedRouteParametersAreNotReserved($parameters)
{
if (array_key_exists('signature', $parameters)) {
throw new InvalidArgumentException(
'"Signature" is a reserved parameter when generating signed routes. Please rename your route parameter.'
);
}
if (array_key_exists('expires', $parameters)) {
throw new InvalidArgumentException(
'"Expires" is a reserved parameter when generating signed routes. Please rename your route parameter.'
);
}
}
/**
* Create a temporary signed route URL for a named route.
*
* @param string $name
* @param \DateTimeInterface|\DateInterval|int $expiration
* @param array $parameters
* @param bool $absolute
* @return string
*/
public function temporarySignedRoute($name, $expiration, $parameters = [], $absolute = true)
{
return $this->signedRoute($name, $parameters, $expiration, $absolute);
}
/**
* Determine if the given request has a valid signature.
*
* @param \Illuminate\Http\Request $request
* @param bool $absolute
* @param array $ignoreQuery
* @return bool
*/
public function hasValidSignature(Request $request, $absolute = true, array $ignoreQuery = [])
{
return $this->hasCorrectSignature($request, $absolute, $ignoreQuery)
&& $this->signatureHasNotExpired($request);
}
/**
* Determine if the given request has a valid signature for a relative URL.
*
* @param \Illuminate\Http\Request $request
* @param array $ignoreQuery
* @return bool
*/
public function hasValidRelativeSignature(Request $request, array $ignoreQuery = [])
{
return $this->hasValidSignature($request, false, $ignoreQuery);
}
/**
* Determine if the signature from the given request matches the URL.
*
* @param \Illuminate\Http\Request $request
* @param bool $absolute
* @param array $ignoreQuery
* @return bool
*/
public function hasCorrectSignature(Request $request, $absolute = true, array $ignoreQuery = [])
{
$ignoreQuery[] = 'signature';
$url = $absolute ? $request->url() : '/'.$request->path();
$queryString = collect(explode('&', (string) $request->server->get('QUERY_STRING')))
->reject(fn ($parameter) => in_array(Str::before($parameter, '='), $ignoreQuery))
->join('&');
$original = rtrim($url.'?'.$queryString, '?');
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
return hash_equals($signature, (string) $request->query('signature', ''));
}
/**
* Determine if the expires timestamp from the given request is not from the past.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function signatureHasNotExpired(Request $request)
{
$expires = $request->query('expires');
return ! ($expires && Carbon::now()->getTimestamp() > $expires);
}
/**
* Get the URL to a named route.
*
* @param string $name
* @param mixed $parameters
* @param mixed $parameters
* @param bool $absolute
* @return string
*
* @throws \InvalidArgumentException
* @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
*/
public function route($name, $parameters = [], $absolute = true)
{
@@ -301,7 +464,7 @@ class UrlGenerator implements UrlGeneratorContract
return $this->toRoute($route, $parameters, $absolute);
}
throw new InvalidArgumentException("Route [{$name}] not defined.");
throw new RouteNotFoundException("Route [{$name}] not defined.");
}
/**
@@ -309,13 +472,23 @@ class UrlGenerator implements UrlGeneratorContract
*
* @param \Illuminate\Routing\Route $route
* @param mixed $parameters
* @param bool $absolute
* @param bool $absolute
* @return string
*
* @throws \Illuminate\Routing\Exceptions\UrlGenerationException
*/
protected function toRoute($route, $parameters, $absolute)
public function toRoute($route, $parameters, $absolute)
{
$parameters = collect(Arr::wrap($parameters))->map(function ($value, $key) use ($route) {
$value = $value instanceof UrlRoutable && $route->bindingFieldFor($key)
? $value->{$route->bindingFieldFor($key)}
: $value;
return function_exists('enum_exists') && $value instanceof BackedEnum
? $value->value
: $value;
})->all();
return $this->routeUrl()->to(
$route, $this->formatParameters($parameters), $absolute
);
@@ -324,9 +497,9 @@ class UrlGenerator implements UrlGeneratorContract
/**
* Get the URL to a controller action.
*
* @param string $action
* @param mixed $parameters
* @param bool $absolute
* @param string|array $action
* @param mixed $parameters
* @param bool $absolute
* @return string
*
* @throws \InvalidArgumentException
@@ -343,12 +516,16 @@ class UrlGenerator implements UrlGeneratorContract
/**
* Format the given controller action.
*
* @param string $action
* @param string|array $action
* @return string
*/
protected function formatAction($action)
{
if ($this->rootNamespace && ! (strpos($action, '\\') === 0)) {
if (is_array($action)) {
$action = '\\'.implode('@', $action);
}
if ($this->rootNamespace && ! str_starts_with($action, '\\')) {
return $this->rootNamespace.'\\'.$action;
} else {
return trim($action, '\\');
@@ -363,7 +540,7 @@ class UrlGenerator implements UrlGeneratorContract
*/
public function formatParameters($parameters)
{
$parameters = array_wrap($parameters);
$parameters = Arr::wrap($parameters);
foreach ($parameters as $key => $parameter) {
if ($parameter instanceof UrlRoutable) {
@@ -396,7 +573,7 @@ class UrlGenerator implements UrlGeneratorContract
* Get the base URL for the request.
*
* @param string $scheme
* @param string $root
* @param string|null $root
* @return string
*/
public function formatRoot($scheme, $root = null)
@@ -409,7 +586,7 @@ class UrlGenerator implements UrlGeneratorContract
$root = $this->cachedRoot;
}
$start = Str::startsWith($root, 'http://') ? 'http://' : 'https://';
$start = str_starts_with($root, 'http://') ? 'http://' : 'https://';
return preg_replace('~'.$start.'~', $scheme, $root, 1);
}
@@ -419,18 +596,19 @@ class UrlGenerator implements UrlGeneratorContract
*
* @param string $root
* @param string $path
* @param \Illuminate\Routing\Route|null $route
* @return string
*/
public function format($root, $path)
public function format($root, $path, $route = null)
{
$path = '/'.trim($path, '/');
if ($this->formatHostUsing) {
$root = call_user_func($this->formatHostUsing, $root);
$root = call_user_func($this->formatHostUsing, $root, $route);
}
if ($this->formatPathUsing) {
$path = call_user_func($this->formatPathUsing, $path);
$path = call_user_func($this->formatPathUsing, $path, $route);
}
return trim($root.$path, '/');
@@ -444,7 +622,7 @@ class UrlGenerator implements UrlGeneratorContract
*/
public function isValidUrl($path)
{
if (! preg_match('~^(#|//|https?://|mailto:|tel:)~', $path)) {
if (! preg_match('~^(#|//|https?://|(mailto|tel|sms):)~', $path)) {
return filter_var($path, FILTER_VALIDATE_URL) !== false;
}
@@ -476,28 +654,38 @@ class UrlGenerator implements UrlGeneratorContract
$this->routeUrl()->defaults($defaults);
}
/**
* Get the default named parameters used by the URL generator.
*
* @return array
*/
public function getDefaultParameters()
{
return $this->routeUrl()->defaultParameters;
}
/**
* Force the scheme for URLs.
*
* @param string $schema
* @param string|null $scheme
* @return void
*/
public function forceScheme($schema)
public function forceScheme($scheme)
{
$this->cachedSchema = null;
$this->cachedScheme = null;
$this->forceScheme = $schema.'://';
$this->forceScheme = $scheme ? $scheme.'://' : null;
}
/**
* Set the forced root URL.
*
* @param string $root
* @param string|null $root
* @return void
*/
public function forceRootUrl($root)
{
$this->forcedRoot = rtrim($root, '/');
$this->forcedRoot = $root ? rtrim($root, '/') : null;
$this->cachedRoot = null;
}
@@ -561,17 +749,24 @@ class UrlGenerator implements UrlGeneratorContract
$this->request = $request;
$this->cachedRoot = null;
$this->cachedSchema = null;
$this->routeGenerator = null;
$this->cachedScheme = null;
tap(optional($this->routeGenerator)->defaultParameters ?: [], function ($defaults) {
$this->routeGenerator = null;
if (! empty($defaults)) {
$this->defaults($defaults);
}
});
}
/**
* Set the route collection.
*
* @param \Illuminate\Routing\RouteCollection $routes
* @param \Illuminate\Routing\RouteCollectionInterface $routes
* @return $this
*/
public function setRoutes(RouteCollection $routes)
public function setRoutes(RouteCollectionInterface $routes)
{
$this->routes = $routes;
@@ -603,6 +798,40 @@ class UrlGenerator implements UrlGeneratorContract
return $this;
}
/**
* Set the encryption key resolver.
*
* @param callable $keyResolver
* @return $this
*/
public function setKeyResolver(callable $keyResolver)
{
$this->keyResolver = $keyResolver;
return $this;
}
/**
* Clone a new instance of the URL generator with a different encryption key resolver.
*
* @param callable $keyResolver
* @return \Illuminate\Routing\UrlGenerator
*/
public function withKeyResolver(callable $keyResolver)
{
return (clone $this)->setKeyResolver($keyResolver);
}
/**
* Get the root controller namespace.
*
* @return string
*/
public function getRootControllerNamespace()
{
return $this->rootNamespace;
}
/**
* Set the root controller namespace.
*

View File

@@ -0,0 +1,60 @@
<?php
namespace Illuminate\Routing;
use Illuminate\Contracts\Routing\ResponseFactory;
class ViewController extends Controller
{
/**
* The response factory implementation.
*
* @var \Illuminate\Contracts\Routing\ResponseFactory
*/
protected $response;
/**
* Create a new controller instance.
*
* @param \Illuminate\Contracts\Routing\ResponseFactory $response
* @return void
*/
public function __construct(ResponseFactory $response)
{
$this->response = $response;
}
/**
* Invoke the controller method.
*
* @param array $args
* @return \Illuminate\Http\Response
*/
public function __invoke(...$args)
{
$routeParameters = array_filter($args, function ($key) {
return ! in_array($key, ['view', 'data', 'status', 'headers']);
}, ARRAY_FILTER_USE_KEY);
$args['data'] = array_merge($args['data'], $routeParameters);
return $this->response->view(
$args['view'],
$args['data'],
$args['status'],
$args['headers']
);
}
/**
* Execute an action on the controller.
*
* @param string $method
* @param array $parameters
* @return \Symfony\Component\HttpFoundation\Response
*/
public function callAction($method, $parameters)
{
return $this->{$method}(...$parameters);
}
}

View File

@@ -14,17 +14,19 @@
}
],
"require": {
"php": ">=5.6.4",
"illuminate/container": "5.4.*",
"illuminate/contracts": "5.4.*",
"illuminate/http": "5.4.*",
"illuminate/pipeline": "5.4.*",
"illuminate/session": "5.4.*",
"illuminate/support": "5.4.*",
"symfony/debug": "~3.2",
"symfony/http-foundation": "~3.2",
"symfony/http-kernel": "~3.2",
"symfony/routing": "~3.2"
"php": "^8.0.2",
"ext-json": "*",
"illuminate/collections": "^9.0",
"illuminate/container": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/http": "^9.0",
"illuminate/macroable": "^9.0",
"illuminate/pipeline": "^9.0",
"illuminate/session": "^9.0",
"illuminate/support": "^9.0",
"symfony/http-foundation": "^6.0",
"symfony/http-kernel": "^6.0",
"symfony/routing": "^6.0"
},
"autoload": {
"psr-4": {
@@ -33,12 +35,13 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.4-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"illuminate/console": "Required to use the make commands (5.4.*).",
"symfony/psr-http-message-bridge": "Required to psr7 bridging features (0.2.*)."
"illuminate/console": "Required to use the make commands (^9.0).",
"nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)."
},
"config": {
"sort-packages": true