PageRenderTime 45ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/mautic/vendor/friendsofsymfony/rest-bundle/FOS/RestBundle/Routing/Loader/Reader/RestActionReader.php

https://gitlab.com/randydanniswara/website
PHP | 470 lines | 249 code | 61 blank | 160 comment | 45 complexity | a9c9518a32b61dd167753fb1d10075e6 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the FOSRestBundle package.
  4. *
  5. * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace FOS\RestBundle\Routing\Loader\Reader;
  11. use Doctrine\Common\Annotations\Reader;
  12. use Symfony\Component\Routing\Route;
  13. use FOS\RestBundle\Util\Inflector\InflectorInterface;
  14. use FOS\RestBundle\Routing\RestRouteCollection;
  15. use FOS\RestBundle\Request\ParamReader;
  16. /**
  17. * REST controller actions reader.
  18. *
  19. * @author Konstantin Kudryashov <ever.zet@gmail.com>
  20. */
  21. class RestActionReader
  22. {
  23. private $annotationReader;
  24. private $paramReader;
  25. private $inflector;
  26. private $formats;
  27. private $includeFormat;
  28. private $routePrefix;
  29. private $namePrefix;
  30. private $parents = array();
  31. private $availableHTTPMethods = array('get', 'post', 'put', 'patch', 'delete', 'link', 'unlink', 'head', 'options');
  32. private $availableConventionalActions = array('new', 'edit', 'remove');
  33. /**
  34. * Initializes controller reader.
  35. *
  36. * @param Reader $annotationReader annotation reader
  37. * @param ParamReader $paramReader query param reader
  38. * @param InflectorInterface $inflector
  39. * @param boolean $includeFormat
  40. * @param array $formats
  41. */
  42. public function __construct(Reader $annotationReader, ParamReader $paramReader, InflectorInterface $inflector, $includeFormat, array $formats = array())
  43. {
  44. $this->annotationReader = $annotationReader;
  45. $this->paramReader = $paramReader;
  46. $this->inflector = $inflector;
  47. $this->includeFormat = $includeFormat;
  48. $this->formats = $formats;
  49. }
  50. /**
  51. * Set routes prefix.
  52. *
  53. * @param string $prefix Routes prefix
  54. */
  55. public function setRoutePrefix($prefix = null)
  56. {
  57. $this->routePrefix = $prefix;
  58. }
  59. /**
  60. * Returns route prefix.
  61. *
  62. * @return string
  63. */
  64. public function getRoutePrefix()
  65. {
  66. return $this->routePrefix;
  67. }
  68. /**
  69. * Set route names prefix.
  70. *
  71. * @param string $prefix Route names prefix
  72. */
  73. public function setNamePrefix($prefix = null)
  74. {
  75. $this->namePrefix = $prefix;
  76. }
  77. /**
  78. * Returns name prefix.
  79. *
  80. * @return string
  81. */
  82. public function getNamePrefix()
  83. {
  84. return $this->namePrefix;
  85. }
  86. /**
  87. * Set parent routes.
  88. *
  89. * @param array $parents Array of parent resources names
  90. */
  91. public function setParents(array $parents)
  92. {
  93. $this->parents = $parents;
  94. }
  95. /**
  96. * Returns parents.
  97. *
  98. * @return array
  99. */
  100. public function getParents()
  101. {
  102. return $this->parents;
  103. }
  104. /**
  105. * Reads action route.
  106. *
  107. * @param RestRouteCollection $collection route collection to read into
  108. * @param \ReflectionMethod $method method reflection
  109. * @param array $resource
  110. *
  111. * @return Route
  112. */
  113. public function read(RestRouteCollection $collection, \ReflectionMethod $method, $resource)
  114. {
  115. // check that every route parent has non-empty singular name
  116. foreach ($this->parents as $parent) {
  117. if (empty($parent) || '/' === substr($parent, -1)) {
  118. throw new \InvalidArgumentException(
  119. "Every parent controller must have `get{SINGULAR}Action(\$id)` method\n".
  120. "where {SINGULAR} is a singular form of associated object"
  121. );
  122. }
  123. }
  124. // if method is not readable - skip
  125. if (!$this->isMethodReadable($method)) {
  126. return;
  127. }
  128. // if we can't get http-method and resources from method name - skip
  129. $httpMethodAndResources = $this->getHttpMethodAndResourcesFromMethod($method, $resource);
  130. if (!$httpMethodAndResources) {
  131. return;
  132. }
  133. list($httpMethod, $resources) = $httpMethodAndResources;
  134. $arguments = $this->getMethodArguments($method);
  135. // if we have only 1 resource & 1 argument passed, then it's object call, so
  136. // we can set collection singular name
  137. if (1 === count($resources) && 1 === count($arguments) - count($this->parents)) {
  138. $collection->setSingularName($resources[0]);
  139. }
  140. // if we have parents passed - merge them with own resource names
  141. if (count($this->parents)) {
  142. $resources = array_merge($this->parents, $resources);
  143. }
  144. if (empty($resources)) {
  145. $resources[] = null;
  146. }
  147. $routeName = $httpMethod.$this->generateRouteName($resources);
  148. $urlParts = $this->generateUrlParts($resources, $arguments, $httpMethod);
  149. // if passed method is not valid HTTP method then it's either
  150. // a hypertext driver, a custom object (PUT) or collection (GET)
  151. // method
  152. if (!in_array($httpMethod, $this->availableHTTPMethods)) {
  153. $urlParts[] = $httpMethod;
  154. $httpMethod = $this->getCustomHttpMethod($httpMethod, $resources, $arguments);
  155. }
  156. // generated parameters
  157. $routeName = $this->namePrefix.strtolower($routeName);
  158. $pattern = implode('/', $urlParts);
  159. $defaults = array('_controller' => $method->getName());
  160. $requirements = array('_method' => strtoupper($httpMethod));
  161. $options = array();
  162. $host = '';
  163. $schemes = array();
  164. $annotation = $this->readRouteAnnotation($method);
  165. if ($annotation) {
  166. $annoRequirements = $annotation->getRequirements();
  167. if (!isset($annoRequirements['_method'])) {
  168. $annoRequirements['_method'] = $requirements['_method'];
  169. }
  170. $pattern = $annotation->getPattern() !== null ? $this->routePrefix . $annotation->getPattern() : $pattern;
  171. $requirements = array_merge($requirements, $annoRequirements);
  172. $options = array_merge($options, $annotation->getOptions());
  173. $defaults = array_merge($defaults, $annotation->getDefaults());
  174. //TODO remove checks after Symfony requirement is bumped to 2.2
  175. if (method_exists($annotation, 'getHost')) {
  176. $host = $annotation->getHost();
  177. }
  178. if (method_exists($annotation, 'getSchemes')) {
  179. $schemes = $annotation->getSchemes();
  180. }
  181. }
  182. if ($this->includeFormat === true) {
  183. $pattern .= '.{_format}';
  184. if (!isset($requirements['_format']) && !empty($this->formats)) {
  185. $requirements['_format'] = implode('|', array_keys($this->formats));
  186. }
  187. }
  188. // add route to collection
  189. $collection->add($routeName, new Route(
  190. $pattern, $defaults, $requirements, $options, $host, $schemes
  191. ));
  192. }
  193. /**
  194. * Checks whether provided method is readable.
  195. *
  196. * @param \ReflectionMethod $method
  197. *
  198. * @return Boolean
  199. */
  200. private function isMethodReadable(\ReflectionMethod $method)
  201. {
  202. // if method starts with _ - skip
  203. if ('_' === substr($method->getName(), 0, 1)) {
  204. return false;
  205. }
  206. $hasNoRouteMethod = (bool) $this->readMethodAnnotation($method, 'NoRoute');
  207. $hasNoRouteClass = (bool) $this->readClassAnnotation($method->getDeclaringClass(), 'NoRoute');
  208. $hasNoRoute = $hasNoRoute = $hasNoRouteMethod || $hasNoRouteClass;
  209. // since NoRoute extends Route we need to exclude all the method NoRoute annotations
  210. $hasRoute = (bool) $this->readMethodAnnotation($method, 'Route') && !$hasNoRouteMethod;
  211. // if method has NoRoute annotation and does not have Route annotation - skip
  212. if ($hasNoRoute && !$hasRoute) {
  213. return false;
  214. }
  215. return true;
  216. }
  217. /**
  218. * Returns HTTP method and resources list from method signature.
  219. *
  220. * @param \ReflectionMethod $method
  221. * @param array $resource
  222. *
  223. * @return Boolean|array
  224. */
  225. private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method, $resource)
  226. {
  227. // if method doesn't match regex - skip
  228. if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/', $method->getName(), $matches)) {
  229. return false;
  230. }
  231. $httpMethod = strtolower($matches[1]);
  232. $resources = preg_split(
  233. '/([A-Z][^A-Z]*)/', $matches[2], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
  234. );
  235. if (0 === strpos($httpMethod, 'c')
  236. && in_array(substr($httpMethod, 1), $this->availableHTTPMethods)
  237. ) {
  238. $httpMethod = substr($httpMethod, 1);
  239. if (!empty($resource)) {
  240. $resource[count($resource)-1] = $this->inflector->pluralize(end($resource));
  241. }
  242. }
  243. $resources = array_merge($resource, $resources);
  244. return array($httpMethod, $resources);
  245. }
  246. /**
  247. * Returns readable arguments from method.
  248. *
  249. * @param \ReflectionMethod $method
  250. *
  251. * @return array
  252. */
  253. private function getMethodArguments(\ReflectionMethod $method)
  254. {
  255. // ignore all query params
  256. $params = $this->paramReader->getParamsFromMethod($method);
  257. // ignore type hinted arguments that are or extend from:
  258. // * Symfony\Component\HttpFoundation\Request
  259. // * FOS\RestBundle\Request\QueryFetcher
  260. // * Symfony\Component\Validator\ConstraintViolationList
  261. $ignoreClasses = array(
  262. 'Symfony\Component\HttpFoundation\Request',
  263. 'FOS\RestBundle\Request\ParamFetcherInterface',
  264. 'Symfony\Component\Validator\ConstraintViolationListInterface',
  265. );
  266. $arguments = array();
  267. foreach ($method->getParameters() as $argument) {
  268. if (isset($params[$argument->getName()])) {
  269. continue;
  270. }
  271. $argumentClass = $argument->getClass();
  272. if ($argumentClass) {
  273. foreach ($ignoreClasses as $class) {
  274. if ($argumentClass->getName() === $class || $argumentClass->isSubclassOf($class)) {
  275. continue 2;
  276. }
  277. }
  278. }
  279. $arguments[] = $argument;
  280. }
  281. return $arguments;
  282. }
  283. /**
  284. * Generates route name from resources list.
  285. *
  286. * @param array $resources
  287. *
  288. * @return string
  289. */
  290. private function generateRouteName(array $resources)
  291. {
  292. $routeName = '';
  293. foreach ($resources as $resource) {
  294. if (null !== $resource) {
  295. $routeName .= '_' . basename($resource);
  296. }
  297. }
  298. return $routeName;
  299. }
  300. /**
  301. * Generates URL parts for route from resources list.
  302. *
  303. * @param array $resources
  304. * @param array $arguments
  305. * @param string $httpMethod
  306. *
  307. * @return array
  308. */
  309. private function generateUrlParts(array $resources, array $arguments, $httpMethod)
  310. {
  311. $urlParts = array();
  312. foreach ($resources as $i => $resource) {
  313. // if we already added all parent routes paths to URL & we have
  314. // prefix - add it
  315. if (!empty($this->routePrefix) && $i === count($this->parents)) {
  316. $urlParts[] = $this->routePrefix;
  317. }
  318. // if we have argument for current resource, then it's object.
  319. // otherwise - it's collection
  320. if (isset($arguments[$i])) {
  321. if (null !== $resource) {
  322. $urlParts[] =
  323. strtolower($this->inflector->pluralize($resource))
  324. .'/{'.$arguments[$i]->getName().'}';
  325. } else {
  326. $urlParts[] = '{'.$arguments[$i]->getName().'}';
  327. }
  328. } elseif (null !== $resource) {
  329. if ((0 === count($arguments) && !in_array($httpMethod, $this->availableHTTPMethods))
  330. || 'new' === $httpMethod
  331. || 'post' === $httpMethod
  332. ) {
  333. $urlParts[] = $this->inflector->pluralize(strtolower($resource));
  334. } else {
  335. $urlParts[] = strtolower($resource);
  336. }
  337. }
  338. }
  339. return $urlParts;
  340. }
  341. /**
  342. * Returns custom HTTP method for provided list of resources, arguments, method.
  343. *
  344. * @param string $httpMethod current HTTP method
  345. * @param array $resources resources list
  346. * @param array $arguments list of method arguments
  347. *
  348. * @return string
  349. */
  350. private function getCustomHttpMethod($httpMethod, array $resources, array $arguments)
  351. {
  352. if (in_array($httpMethod, $this->availableConventionalActions)) {
  353. // allow hypertext as the engine of application state
  354. // through conventional GET actions
  355. return 'get';
  356. }
  357. if (count($arguments) < count($resources)) {
  358. // resource collection
  359. return 'get';
  360. }
  361. //custom object
  362. return 'patch';
  363. }
  364. /**
  365. * Returns first route annotation for method.
  366. *
  367. * @param \ReflectionMethod $reflection
  368. *
  369. * @return Annotation|null
  370. */
  371. private function readRouteAnnotation(\ReflectionMethod $reflection)
  372. {
  373. foreach (array('Route','Get','Post','Put','Patch','Delete','Head') as $annotationName) {
  374. if ($annotation = $this->readMethodAnnotation($reflection, $annotationName)) {
  375. return $annotation;
  376. }
  377. }
  378. }
  379. /**
  380. * Reads class annotations.
  381. *
  382. * @param ReflectionClass $reflection controller class
  383. * @param string $annotationName annotation name
  384. *
  385. * @return Annotation|null
  386. */
  387. private function readClassAnnotation(\ReflectionClass $reflection, $annotationName)
  388. {
  389. $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  390. if ($annotation = $this->annotationReader->getClassAnnotation($reflection, $annotationClass)) {
  391. return $annotation;
  392. }
  393. }
  394. /**
  395. * Reads method annotations.
  396. *
  397. * @param ReflectionMethod $reflection controller action
  398. * @param string $annotationName annotation name
  399. *
  400. * @return Annotation|null
  401. */
  402. private function readMethodAnnotation(\ReflectionMethod $reflection, $annotationName)
  403. {
  404. $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  405. if ($annotation = $this->annotationReader->getMethodAnnotation($reflection, $annotationClass)) {
  406. return $annotation;
  407. }
  408. }
  409. }