| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- <?php
- declare(strict_types=1);
- /**
- * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
- * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- *
- * Licensed under The MIT License
- * For full copyright and license information, please see the LICENSE.txt
- * Redistributions of files must retain the above copyright notice.
- *
- * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- * @link https://cakephp.org CakePHP(tm) Project
- * @since 3.3.0
- * @license https://opensource.org/licenses/mit-license.php MIT License
- */
- namespace Cake\Controller;
- use Cake\Controller\Exception\InvalidParameterException;
- use Cake\Core\App;
- use Cake\Core\ContainerInterface;
- use Cake\Http\ControllerFactoryInterface;
- use Cake\Http\Exception\MissingControllerException;
- use Cake\Http\MiddlewareQueue;
- use Cake\Http\Runner;
- use Cake\Http\ServerRequest;
- use Closure;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Http\Message\ServerRequestInterface;
- use Psr\Http\Server\RequestHandlerInterface;
- use ReflectionClass;
- use ReflectionFunction;
- use ReflectionNamedType;
- /**
- * Factory method for building controllers for request.
- *
- * @implements \Cake\Http\ControllerFactoryInterface<\Cake\Controller\Controller>
- */
- class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInterface
- {
- /**
- * @var \Cake\Core\ContainerInterface
- */
- protected ContainerInterface $container;
- /**
- * @var \Cake\Controller\Controller
- */
- protected Controller $controller;
- /**
- * Constructor
- *
- * @param \Cake\Core\ContainerInterface $container The container to build controllers with.
- */
- public function __construct(ContainerInterface $container)
- {
- $this->container = $container;
- }
- /**
- * Create a controller for a given request.
- *
- * @param \Psr\Http\Message\ServerRequestInterface $request The request to build a controller for.
- * @return \Cake\Controller\Controller
- * @throws \Cake\Http\Exception\MissingControllerException
- */
- public function create(ServerRequestInterface $request): Controller
- {
- $className = $this->getControllerClass($request);
- if ($className === null) {
- throw $this->missingController($request);
- }
- $reflection = new ReflectionClass($className);
- if ($reflection->isAbstract()) {
- throw $this->missingController($request);
- }
- // Get the controller from the container if defined.
- // The request is in the container by default.
- if ($this->container->has($className)) {
- $controller = $this->container->get($className);
- } else {
- $controller = $reflection->newInstance($request);
- }
- return $controller;
- }
- /**
- * Invoke a controller's action and wrapping methods.
- *
- * @param \Cake\Controller\Controller $controller The controller to invoke.
- * @return \Psr\Http\Message\ResponseInterface The response
- * @throws \Cake\Controller\Exception\MissingActionException If controller action is not found.
- * @throws \UnexpectedValueException If return value of action method is not null or ResponseInterface instance.
- */
- public function invoke(mixed $controller): ResponseInterface
- {
- $this->controller = $controller;
- $middlewares = $controller->getMiddleware();
- if ($middlewares) {
- $middlewareQueue = new MiddlewareQueue($middlewares);
- $runner = new Runner();
- return $runner->run($middlewareQueue, $controller->getRequest(), $this);
- }
- return $this->handle($controller->getRequest());
- }
- /**
- * Invoke the action.
- *
- * @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
- * @return \Psr\Http\Message\ResponseInterface
- */
- public function handle(ServerRequestInterface $request): ResponseInterface
- {
- $controller = $this->controller;
- /** @psalm-suppress ArgumentTypeCoercion */
- $controller->setRequest($request);
- $result = $controller->startupProcess();
- if ($result instanceof ResponseInterface) {
- return $result;
- }
- $action = $controller->getAction();
- $args = $this->getActionArgs(
- $action,
- array_values((array)$controller->getRequest()->getParam('pass'))
- );
- $controller->invokeAction($action, $args);
- $result = $controller->shutdownProcess();
- if ($result instanceof ResponseInterface) {
- return $result;
- }
- return $controller->getResponse();
- }
- /**
- * Get the arguments for the controller action invocation.
- *
- * @param \Closure $action Controller action.
- * @param array $passedParams Params passed by the router.
- * @return array
- */
- protected function getActionArgs(Closure $action, array $passedParams): array
- {
- $resolved = [];
- $function = new ReflectionFunction($action);
- foreach ($function->getParameters() as $parameter) {
- $type = $parameter->getType();
- if ($type && !$type instanceof ReflectionNamedType) {
- // Only single types are supported
- throw new InvalidParameterException([
- 'template' => 'unsupported_type',
- 'parameter' => $parameter->getName(),
- 'controller' => $this->controller->getName(),
- 'action' => $this->controller->getRequest()->getParam('action'),
- 'prefix' => $this->controller->getRequest()->getParam('prefix'),
- 'plugin' => $this->controller->getRequest()->getParam('plugin'),
- ]);
- }
- // Check for dependency injection for classes
- if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
- $typeName = $type->getName();
- if ($this->container->has($typeName)) {
- $resolved[] = $this->container->get($typeName);
- continue;
- }
- // Use passedParams as a source of typed dependencies.
- // The accepted types for passedParams was never defined and userland code relies on that.
- if ($passedParams && is_object($passedParams[0]) && $passedParams[0] instanceof $typeName) {
- $resolved[] = array_shift($passedParams);
- continue;
- }
- // Add default value if provided
- // Do not allow positional arguments for classes
- if ($parameter->isDefaultValueAvailable()) {
- $resolved[] = $parameter->getDefaultValue();
- continue;
- }
- throw new InvalidParameterException([
- 'template' => 'missing_dependency',
- 'parameter' => $parameter->getName(),
- 'type' => $typeName,
- 'controller' => $this->controller->getName(),
- 'action' => $this->controller->getRequest()->getParam('action'),
- 'prefix' => $this->controller->getRequest()->getParam('prefix'),
- 'plugin' => $this->controller->getRequest()->getParam('plugin'),
- ]);
- }
- // Use any passed params as positional arguments
- if ($passedParams) {
- $argument = array_shift($passedParams);
- if (is_string($argument) && $type instanceof ReflectionNamedType) {
- $typedArgument = $this->coerceStringToType($argument, $type);
- if ($typedArgument === null) {
- throw new InvalidParameterException([
- 'template' => 'failed_coercion',
- 'passed' => $argument,
- 'type' => $type->getName(),
- 'parameter' => $parameter->getName(),
- 'controller' => $this->controller->getName(),
- 'action' => $this->controller->getRequest()->getParam('action'),
- 'prefix' => $this->controller->getRequest()->getParam('prefix'),
- 'plugin' => $this->controller->getRequest()->getParam('plugin'),
- ]);
- }
- $argument = $typedArgument;
- }
- $resolved[] = $argument;
- continue;
- }
- // Add default value if provided
- if ($parameter->isDefaultValueAvailable()) {
- $resolved[] = $parameter->getDefaultValue();
- continue;
- }
- // Variadic parameter can have 0 arguments
- if ($parameter->isVariadic()) {
- continue;
- }
- throw new InvalidParameterException([
- 'template' => 'missing_parameter',
- 'parameter' => $parameter->getName(),
- 'controller' => $this->controller->getName(),
- 'action' => $this->controller->getRequest()->getParam('action'),
- 'prefix' => $this->controller->getRequest()->getParam('prefix'),
- 'plugin' => $this->controller->getRequest()->getParam('plugin'),
- ]);
- }
- return array_merge($resolved, $passedParams);
- }
- /**
- * Coerces string argument to primitive type.
- *
- * @param string $argument Argument to coerce
- * @param \ReflectionNamedType $type Parameter type
- * @return array|string|float|int|bool|null
- */
- protected function coerceStringToType(string $argument, ReflectionNamedType $type): array|string|float|int|bool|null
- {
- switch ($type->getName()) {
- case 'string':
- return $argument;
- case 'float':
- return is_numeric($argument) ? (float)$argument : null;
- case 'int':
- return ctype_digit($argument) ? (int)$argument : null;
- case 'bool':
- return $argument === '0' ? false : ($argument === '1' ? true : null);
- case 'array':
- return $argument === '' ? [] : explode(',', $argument);
- }
- return null;
- }
- /**
- * Determine the controller class name based on current request and controller param
- *
- * @param \Cake\Http\ServerRequest $request The request to build a controller for.
- * @return string|null
- * @psalm-return class-string<\Cake\Controller\Controller>|null
- */
- public function getControllerClass(ServerRequest $request): ?string
- {
- $pluginPath = '';
- $namespace = 'Controller';
- $controller = $request->getParam('controller', '');
- if ($request->getParam('plugin')) {
- $pluginPath = $request->getParam('plugin') . '.';
- }
- if ($request->getParam('prefix')) {
- $prefix = $request->getParam('prefix');
- $namespace .= '/' . $prefix;
- }
- $firstChar = substr($controller, 0, 1);
- // Disallow plugin short forms, / and \\ from
- // controller names as they allow direct references to
- // be created.
- if (
- str_contains($controller, '\\') ||
- str_contains($controller, '/') ||
- str_contains($controller, '.') ||
- $firstChar === strtolower($firstChar)
- ) {
- throw $this->missingController($request);
- }
- /** @var class-string<\Cake\Controller\Controller>|null */
- return App::className($pluginPath . $controller, $namespace, 'Controller');
- }
- /**
- * Throws an exception when a controller is missing.
- *
- * @param \Cake\Http\ServerRequest $request The request.
- * @return \Cake\Http\Exception\MissingControllerException
- */
- protected function missingController(ServerRequest $request): MissingControllerException
- {
- return new MissingControllerException([
- 'class' => $request->getParam('controller'),
- 'plugin' => $request->getParam('plugin'),
- 'prefix' => $request->getParam('prefix'),
- '_ext' => $request->getParam('_ext'),
- ]);
- }
- }
|