ControllerFactory.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Controller;
  17. use Cake\Controller\Exception\MissingActionException;
  18. use Cake\Core\App;
  19. use Cake\Core\ContainerInterface;
  20. use Cake\Http\ControllerFactoryInterface;
  21. use Cake\Http\Exception\MissingControllerException;
  22. use Cake\Http\MiddlewareQueue;
  23. use Cake\Http\Runner;
  24. use Cake\Http\ServerRequest;
  25. use Cake\Utility\Inflector;
  26. use Closure;
  27. use Psr\Http\Message\ResponseInterface;
  28. use Psr\Http\Message\ServerRequestInterface;
  29. use Psr\Http\Server\RequestHandlerInterface;
  30. use ReflectionClass;
  31. use ReflectionFunction;
  32. use ReflectionNamedType;
  33. /**
  34. * Factory method for building controllers for request.
  35. *
  36. * @implements \Cake\Http\ControllerFactoryInterface<\Cake\Controller\Controller>
  37. */
  38. class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInterface
  39. {
  40. /**
  41. * @var \Cake\Core\ContainerInterface
  42. */
  43. protected $container;
  44. /**
  45. * @var \Cake\Controller\Controller
  46. */
  47. protected $controller;
  48. /**
  49. * Constructor
  50. *
  51. * @param \Cake\Core\ContainerInterface $container The container to build controllers with.
  52. */
  53. public function __construct(ContainerInterface $container)
  54. {
  55. $this->container = $container;
  56. }
  57. /**
  58. * Create a controller for a given request.
  59. *
  60. * @param \Psr\Http\Message\ServerRequestInterface $request The request to build a controller for.
  61. * @return \Cake\Controller\Controller
  62. * @throws \Cake\Http\Exception\MissingControllerException
  63. */
  64. public function create(ServerRequestInterface $request): Controller
  65. {
  66. $className = $this->getControllerClass($request);
  67. if ($className === null) {
  68. throw $this->missingController($request);
  69. }
  70. $reflection = new ReflectionClass($className);
  71. if ($reflection->isAbstract()) {
  72. throw $this->missingController($request);
  73. }
  74. // If the controller has a container definition
  75. // add the request as a service.
  76. if ($this->container->has($className)) {
  77. $this->container->add(ServerRequest::class, $request);
  78. $controller = $this->container->get($className);
  79. } else {
  80. $controller = $reflection->newInstance($request);
  81. }
  82. return $controller;
  83. }
  84. /**
  85. * Invoke a controller's action and wrapping methods.
  86. *
  87. * @param \Cake\Controller\Controller $controller The controller to invoke.
  88. * @return \Psr\Http\Message\ResponseInterface The response
  89. * @throws \Cake\Controller\Exception\MissingActionException If controller action is not found.
  90. * @throws \UnexpectedValueException If return value of action method is not null or ResponseInterface instance.
  91. */
  92. public function invoke($controller): ResponseInterface
  93. {
  94. $this->controller = $controller;
  95. $middlewares = $controller->getMiddleware();
  96. if ($middlewares) {
  97. $middlewareQueue = new MiddlewareQueue($middlewares);
  98. $runner = new Runner();
  99. return $runner->run($middlewareQueue, $controller->getRequest(), $this);
  100. }
  101. return $this->handle($controller->getRequest());
  102. }
  103. /**
  104. * Invoke the action.
  105. *
  106. * @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
  107. * @return \Psr\Http\Message\ResponseInterface
  108. */
  109. public function handle(ServerRequestInterface $request): ResponseInterface
  110. {
  111. $controller = $this->controller;
  112. /** @psalm-suppress ArgumentTypeCoercion */
  113. $controller->setRequest($request);
  114. $result = $controller->startupProcess();
  115. if ($result instanceof ResponseInterface) {
  116. return $result;
  117. }
  118. $action = $controller->getAction();
  119. $args = $this->getActionArgs(
  120. $action,
  121. array_values((array)$controller->getRequest()->getParam('pass'))
  122. );
  123. $controller->invokeAction($action, $args);
  124. $result = $controller->shutdownProcess();
  125. if ($result instanceof ResponseInterface) {
  126. return $result;
  127. }
  128. return $controller->getResponse();
  129. }
  130. /**
  131. * Get the arguments for the controller action invocation.
  132. *
  133. * @param \Closure $action Controller action.
  134. * @param array $passedParams Params passed by the router.
  135. * @return array
  136. */
  137. protected function getActionArgs(Closure $action, array $passedParams): array
  138. {
  139. $resolved = [];
  140. $function = new ReflectionFunction($action);
  141. foreach ($function->getParameters() as $parameter) {
  142. $type = $parameter->getType();
  143. if ($type && !$type instanceof ReflectionNamedType) {
  144. // Only single types are supported
  145. throw new MissingActionException(sprintf(
  146. 'Action %s::%s() has an unsupported type for parameter `%s`.',
  147. $this->controller->getName(),
  148. $function->getName(),
  149. $parameter->getName()
  150. ));
  151. }
  152. // Check for dependency injection for classes
  153. if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
  154. if ($this->container->has($type->getName())) {
  155. $resolved[] = $this->container->get($type->getName());
  156. continue;
  157. }
  158. // Add default value if provided
  159. // Do not allow positional arguments for classes
  160. if ($parameter->isDefaultValueAvailable()) {
  161. $resolved[] = $parameter->getDefaultValue();
  162. continue;
  163. }
  164. throw new MissingActionException(sprintf(
  165. 'Action %s::%s() cannot inject parameter `%s` from service container.',
  166. $this->controller->getName(),
  167. $function->getName(),
  168. $parameter->getName()
  169. ));
  170. }
  171. // Use any passed params as positional arguments
  172. if ($passedParams) {
  173. $argument = array_shift($passedParams);
  174. if ($type instanceof ReflectionNamedType) {
  175. $typedArgument = $this->coerceStringToType($argument, $type);
  176. if ($typedArgument === null) {
  177. throw new MissingActionException(sprintf(
  178. 'Action %s::%s() cannot coerce "%s" to `%s` for parameter `%s`.',
  179. $this->controller->getName(),
  180. $function->getName(),
  181. $argument,
  182. $type->getName(),
  183. $parameter->getName()
  184. ));
  185. }
  186. $argument = $typedArgument;
  187. }
  188. $resolved[] = $argument;
  189. continue;
  190. }
  191. // Add default value if provided
  192. if ($parameter->isDefaultValueAvailable()) {
  193. $resolved[] = $parameter->getDefaultValue();
  194. continue;
  195. }
  196. // Variadic parameter can have 0 arguments
  197. if ($parameter->isVariadic()) {
  198. continue;
  199. }
  200. throw new MissingActionException(sprintf(
  201. 'Action %s::%s() expected passed parameter for `%s`.',
  202. $this->controller->getName(),
  203. $function->getName(),
  204. $parameter->getName()
  205. ));
  206. }
  207. return array_merge($resolved, $passedParams);
  208. }
  209. /**
  210. * Coerces string argument to primitive type.
  211. *
  212. * @param string $argument Argument to coerce
  213. * @param \ReflectionNamedType $type Parameter type
  214. * @return string|float|int|bool|null
  215. */
  216. protected function coerceStringToType(string $argument, ReflectionNamedType $type)
  217. {
  218. switch ($type->getName()) {
  219. case 'string':
  220. return $argument;
  221. case 'float':
  222. return is_numeric($argument) ? (float)$argument : null;
  223. case 'int':
  224. return filter_var($argument, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
  225. case 'bool':
  226. return $argument === '0' ? false : ($argument === '1' ? true : null);
  227. }
  228. return null;
  229. }
  230. /**
  231. * Determine the controller class name based on current request and controller param
  232. *
  233. * @param \Cake\Http\ServerRequest $request The request to build a controller for.
  234. * @return string|null
  235. * @psalm-return class-string<\Cake\Controller\Controller>|null
  236. */
  237. public function getControllerClass(ServerRequest $request): ?string
  238. {
  239. $pluginPath = '';
  240. $namespace = 'Controller';
  241. $controller = $request->getParam('controller', '');
  242. if ($request->getParam('plugin')) {
  243. $pluginPath = $request->getParam('plugin') . '.';
  244. }
  245. if ($request->getParam('prefix')) {
  246. $prefix = $request->getParam('prefix');
  247. $firstChar = substr($prefix, 0, 1);
  248. if ($firstChar !== strtoupper($firstChar)) {
  249. deprecationWarning(
  250. "The `{$prefix}` prefix did not start with an upper case character. " .
  251. 'Routing prefixes should be defined as CamelCase values. ' .
  252. 'Prefix inflection will be removed in 5.0'
  253. );
  254. if (strpos($prefix, '/') === false) {
  255. $namespace .= '/' . Inflector::camelize($prefix);
  256. } else {
  257. $prefixes = array_map(
  258. function ($val) {
  259. return Inflector::camelize($val);
  260. },
  261. explode('/', $prefix)
  262. );
  263. $namespace .= '/' . implode('/', $prefixes);
  264. }
  265. } else {
  266. $namespace .= '/' . $prefix;
  267. }
  268. }
  269. $firstChar = substr($controller, 0, 1);
  270. // Disallow plugin short forms, / and \\ from
  271. // controller names as they allow direct references to
  272. // be created.
  273. if (
  274. strpos($controller, '\\') !== false ||
  275. strpos($controller, '/') !== false ||
  276. strpos($controller, '.') !== false ||
  277. $firstChar === strtolower($firstChar)
  278. ) {
  279. throw $this->missingController($request);
  280. }
  281. /** @var class-string<\Cake\Controller\Controller>|null */
  282. return App::className($pluginPath . $controller, $namespace, 'Controller');
  283. }
  284. /**
  285. * Throws an exception when a controller is missing.
  286. *
  287. * @param \Cake\Http\ServerRequest $request The request.
  288. * @return \Cake\Http\Exception\MissingControllerException
  289. */
  290. protected function missingController(ServerRequest $request)
  291. {
  292. return new MissingControllerException([
  293. 'class' => $request->getParam('controller'),
  294. 'plugin' => $request->getParam('plugin'),
  295. 'prefix' => $request->getParam('prefix'),
  296. '_ext' => $request->getParam('_ext'),
  297. ]);
  298. }
  299. }