vendor/friendsofsymfony/rest-bundle/Routing/Loader/Reader/RestActionReader.php line 14

Open in your IDE?
  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. @trigger_error(sprintf('The %s\RestActionReader class is deprecated since FOSRestBundle 2.8.'__NAMESPACE__), E_USER_DEPRECATED);
  12. use Doctrine\Common\Annotations\Reader;
  13. use FOS\RestBundle\Controller\Annotations\Route as RouteAnnotation;
  14. use FOS\RestBundle\Inflector\InflectorInterface;
  15. use FOS\RestBundle\Request\ParamFetcherInterface;
  16. use FOS\RestBundle\Request\ParamReaderInterface;
  17. use FOS\RestBundle\Routing\RestRouteCollection;
  18. use Psr\Http\Message\MessageInterface;
  19. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  22. use Symfony\Component\Routing\Route;
  23. use Symfony\Component\Security\Core\User\UserInterface;
  24. use Symfony\Component\Validator\ConstraintViolationListInterface;
  25. /**
  26.  * REST controller actions reader.
  27.  *
  28.  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  29.  *
  30.  * @deprecated since 2.8
  31.  */
  32. class RestActionReader
  33. {
  34.     const COLLECTION_ROUTE_PREFIX 'c';
  35.     private $annotationReader;
  36.     private $paramReader;
  37.     private $inflector;
  38.     private $formats;
  39.     private $includeFormat;
  40.     private $routePrefix;
  41.     private $namePrefix;
  42.     private $versions;
  43.     private $pluralize;
  44.     private $parents = [];
  45.     private $availableHTTPMethods = [
  46.         'get',
  47.         'post',
  48.         'put',
  49.         'patch',
  50.         'delete',
  51.         'link',
  52.         'unlink',
  53.         'head',
  54.         'options',
  55.         'mkcol',
  56.         'propfind',
  57.         'proppatch',
  58.         'move',
  59.         'copy',
  60.         'lock',
  61.         'unlock',
  62.     ];
  63.     private $availableConventionalActions = ['new''edit''remove'];
  64.     private $hasMethodPrefix;
  65.     /**
  66.      * ignore several type hinted arguments.
  67.      */
  68.     private $ignoredClasses = [
  69.         ConstraintViolationListInterface::class,
  70.         MessageInterface::class,
  71.         ParamConverter::class,
  72.         ParamFetcherInterface::class,
  73.         Request::class,
  74.         SessionInterface::class,
  75.         UserInterface::class,
  76.     ];
  77.     public function __construct(Reader $annotationReaderParamReaderInterface $paramReaderInflectorInterface $inflectorbool $includeFormat, array $formats = [], bool $hasMethodPrefix true)
  78.     {
  79.         $this->annotationReader $annotationReader;
  80.         $this->paramReader $paramReader;
  81.         $this->inflector $inflector;
  82.         $this->includeFormat $includeFormat;
  83.         $this->formats $formats;
  84.         $this->hasMethodPrefix $hasMethodPrefix;
  85.     }
  86.     /**
  87.      * @param string|null $prefix
  88.      */
  89.     public function setRoutePrefix($prefix null)
  90.     {
  91.         $this->routePrefix $prefix;
  92.     }
  93.     /**
  94.      * @return string
  95.      */
  96.     public function getRoutePrefix()
  97.     {
  98.         return $this->routePrefix;
  99.     }
  100.     /**
  101.      * @param string|null $prefix
  102.      */
  103.     public function setNamePrefix($prefix null)
  104.     {
  105.         $this->namePrefix $prefix;
  106.     }
  107.     /**
  108.      * @return string
  109.      */
  110.     public function getNamePrefix()
  111.     {
  112.         return $this->namePrefix;
  113.     }
  114.     /**
  115.      * @param string[]|string|null $versions
  116.      */
  117.     public function setVersions($versions null)
  118.     {
  119.         $this->versions = (array) $versions;
  120.     }
  121.     /**
  122.      * @return string[]|null
  123.      */
  124.     public function getVersions()
  125.     {
  126.         return $this->versions;
  127.     }
  128.     /**
  129.      * @param bool|null $pluralize
  130.      */
  131.     public function setPluralize($pluralize)
  132.     {
  133.         $this->pluralize $pluralize;
  134.     }
  135.     /**
  136.      * @return bool|null
  137.      */
  138.     public function getPluralize()
  139.     {
  140.         return $this->pluralize;
  141.     }
  142.     /**
  143.      * @param string[] $parents Array of parent resources names
  144.      */
  145.     public function setParents(array $parents)
  146.     {
  147.         $this->parents $parents;
  148.     }
  149.     /**
  150.      * @return string[]
  151.      */
  152.     public function getParents()
  153.     {
  154.         return $this->parents;
  155.     }
  156.     /**
  157.      * Set ignored classes.
  158.      *
  159.      * These classes will be ignored for route construction when
  160.      * type hinted as method argument.
  161.      *
  162.      * @param string[] $ignoredClasses
  163.      */
  164.     public function setIgnoredClasses(array $ignoredClasses): void
  165.     {
  166.         $this->ignoredClasses $ignoredClasses;
  167.     }
  168.     /**
  169.      * Get ignored classes.
  170.      *
  171.      * @return string[]
  172.      */
  173.     public function getIgnoredClasses(): array
  174.     {
  175.         return $this->ignoredClasses;
  176.     }
  177.     /**
  178.      * @param string[] $resource
  179.      *
  180.      * @throws \InvalidArgumentException
  181.      *
  182.      * @return Route
  183.      */
  184.     public function read(RestRouteCollection $collection, \ReflectionMethod $method$resource)
  185.     {
  186.         // check that every route parent has non-empty singular name
  187.         foreach ($this->parents as $parent) {
  188.             if (empty($parent) || '/' === substr($parent, -1)) {
  189.                 throw new \InvalidArgumentException('Every parent controller must have `get{SINGULAR}Action(\$id)` method where {SINGULAR} is a singular form of associated object');
  190.             }
  191.         }
  192.         // if method is not readable - skip
  193.         if (!$this->isMethodReadable($method)) {
  194.             return;
  195.         }
  196.         // if we can't get http-method and resources from method name - skip
  197.         $httpMethodAndResources $this->getHttpMethodAndResourcesFromMethod($method$resource);
  198.         if (!$httpMethodAndResources) {
  199.             return;
  200.         }
  201.         [$httpMethod$resources$isCollection$isInflectable] = $httpMethodAndResources;
  202.         $arguments $this->getMethodArguments($method);
  203.         // if we have only 1 resource & 1 argument passed, then it's object call, so
  204.         // we can set collection singular name
  205.         if (=== count($resources) && === count($arguments) - count($this->parents)) {
  206.             $collection->setSingularName($resources[0]);
  207.         }
  208.         // if we have parents passed - merge them with own resource names
  209.         if (count($this->parents)) {
  210.             $resources array_merge($this->parents$resources);
  211.         }
  212.         if (empty($resources)) {
  213.             $resources[] = null;
  214.         }
  215.         $routeName $httpMethod.$this->generateRouteName($resources);
  216.         $urlParts $this->generateUrlParts($resources$arguments$httpMethod);
  217.         // if passed method is not valid HTTP method then it's either
  218.         // a hypertext driver, a custom object (PUT) or collection (GET)
  219.         // method
  220.         if (!in_array($httpMethod$this->availableHTTPMethods)) {
  221.             $urlParts[] = $httpMethod;
  222.             $httpMethod $this->getCustomHttpMethod($httpMethod$resources$arguments);
  223.         }
  224.         // generated parameters
  225.         $routeName strtolower($routeName);
  226.         $path implode('/'$urlParts);
  227.         $defaults = ['_controller' => $method->getName()];
  228.         $requirements = [];
  229.         $options = [];
  230.         $host '';
  231.         $versionCondition $this->getVersionCondition();
  232.         $versionRequirement $this->getVersionRequirement();
  233.         $annotations $this->readRouteAnnotation($method);
  234.         if (!empty($annotations)) {
  235.             foreach ($annotations as $annotation) {
  236.                 $path implode('/'$urlParts);
  237.                 $defaults = ['_controller' => $method->getName()];
  238.                 $requirements = [];
  239.                 $options = [];
  240.                 $methods explode('|'$httpMethod);
  241.                 $annoRequirements $annotation->getRequirements();
  242.                 $annoMethods $annotation->getMethods();
  243.                 if (!empty($annoMethods)) {
  244.                     $methods $annoMethods;
  245.                 }
  246.                 $path null !== $annotation->getPath() ? $this->routePrefix.$annotation->getPath() : $path;
  247.                 $requirements array_merge($requirements$annoRequirements);
  248.                 $options array_merge($options$annotation->getOptions());
  249.                 $defaults array_merge($defaults$annotation->getDefaults());
  250.                 $host $annotation->getHost();
  251.                 $schemes $annotation->getSchemes();
  252.                 if ($this->hasVersionPlaceholder($path)) {
  253.                     $combinedCondition $annotation->getCondition();
  254.                     $requirements array_merge($versionRequirement$requirements);
  255.                 } else {
  256.                     $combinedCondition $this->combineConditions($versionCondition$annotation->getCondition());
  257.                 }
  258.                 $this->includeFormatIfNeeded($path$requirements);
  259.                 // add route to collection
  260.                 $route = new Route(
  261.                     $path,
  262.                     $defaults,
  263.                     $requirements,
  264.                     $options,
  265.                     $host,
  266.                     $schemes,
  267.                     $methods,
  268.                     $combinedCondition
  269.                 );
  270.                 $this->addRoute($collection$routeName$route$isCollection$isInflectable$annotation);
  271.             }
  272.         } else {
  273.             if ($this->hasVersionPlaceholder($path)) {
  274.                 $versionCondition null;
  275.                 $requirements $versionRequirement;
  276.             }
  277.             $this->includeFormatIfNeeded($path$requirements);
  278.             $methods explode('|'strtoupper($httpMethod));
  279.             // add route to collection
  280.             $route = new Route(
  281.                 $path,
  282.                 $defaults,
  283.                 $requirements,
  284.                 $options,
  285.                 $host,
  286.                 [],
  287.                 $methods,
  288.                 $versionCondition
  289.             );
  290.             $this->addRoute($collection$routeName$route$isCollection$isInflectable);
  291.         }
  292.     }
  293.     private function getVersionCondition(): ?string
  294.     {
  295.         if (empty($this->versions)) {
  296.             return null;
  297.         }
  298.         return sprintf("request.attributes.get('version') in ['%s']"implode("', '"$this->versions));
  299.     }
  300.     private function combineConditions(?string $conditionOne, ?string $conditionTwo): ?string
  301.     {
  302.         if (null === $conditionOne) {
  303.             return $conditionTwo;
  304.         }
  305.         if (null === $conditionTwo) {
  306.             return $conditionOne;
  307.         }
  308.         return sprintf('(%s) and (%s)'$conditionOne$conditionTwo);
  309.     }
  310.     private function getVersionRequirement(): array
  311.     {
  312.         if (empty($this->versions)) {
  313.             return [];
  314.         }
  315.         return ['version' => implode('|'$this->versions)];
  316.     }
  317.     private function hasVersionPlaceholder(string $path): bool
  318.     {
  319.         return false !== strpos($path'{version}');
  320.     }
  321.     private function includeFormatIfNeeded(string &$path, array &$requirements)
  322.     {
  323.         if (true === $this->includeFormat) {
  324.             $path .= '.{_format}';
  325.             if (!isset($requirements['_format']) && !empty($this->formats)) {
  326.                 $requirements['_format'] = implode('|'array_keys($this->formats));
  327.             }
  328.         }
  329.     }
  330.     private function isMethodReadable(\ReflectionMethod $method): bool
  331.     {
  332.         // if method starts with _ - skip
  333.         if ('_' === substr($method->getName(), 01)) {
  334.             return false;
  335.         }
  336.         $hasNoRouteMethod = (bool) $this->readMethodAnnotation($method'NoRoute');
  337.         $hasNoRouteClass = (bool) $this->readClassAnnotation($method->getDeclaringClass(), 'NoRoute');
  338.         $hasNoRoute $hasNoRouteMethod || $hasNoRouteClass;
  339.         // since NoRoute extends Route we need to exclude all the method NoRoute annotations
  340.         $hasRoute = (bool) $this->readMethodAnnotation($method'Route') && !$hasNoRouteMethod;
  341.         // if method has NoRoute annotation and does not have Route annotation - skip
  342.         if ($hasNoRoute && !$hasRoute) {
  343.             return false;
  344.         }
  345.         return true;
  346.     }
  347.     /**
  348.      * @param string[] $resource
  349.      *
  350.      * @return bool|array
  351.      */
  352.     private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method, array $resource)
  353.     {
  354.         // if method doesn't match regex - skip
  355.         if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/'$method->getName(), $matches)) {
  356.             return false;
  357.         }
  358.         $httpMethod strtolower($matches[1]);
  359.         $resources preg_split(
  360.             '/([A-Z][^A-Z]*)/',
  361.             $matches[2],
  362.             -1,
  363.             PREG_SPLIT_NO_EMPTY PREG_SPLIT_DELIM_CAPTURE
  364.         );
  365.         $isCollection false;
  366.         $isInflectable true;
  367.         if (=== strpos($httpMethodself::COLLECTION_ROUTE_PREFIX)
  368.             && in_array(substr($httpMethod1), $this->availableHTTPMethods)
  369.         ) {
  370.             $isCollection true;
  371.             $httpMethod substr($httpMethod1);
  372.         } elseif ('options' === $httpMethod) {
  373.             $isCollection true;
  374.         }
  375.         if ($isCollection && !empty($resource)) {
  376.             $resourcePluralized $this->generateResourceName(end($resource));
  377.             $isInflectable = ($resourcePluralized != $resource[count($resource) - 1]);
  378.             $resource[count($resource) - 1] = $resourcePluralized;
  379.         }
  380.         $resources array_merge($resource$resources);
  381.         return [$httpMethod$resources$isCollection$isInflectable];
  382.     }
  383.     /**
  384.      * @return \ReflectionParameter[]
  385.      */
  386.     private function getMethodArguments(\ReflectionMethod $method): array
  387.     {
  388.         // ignore all query params
  389.         $params $this->paramReader->getParamsFromMethod($method);
  390.         // check if a parameter is coming from the request body
  391.         $ignoreParameters = [];
  392.         if (class_exists(ParamConverter::class)) {
  393.             $ignoreParameters array_map(function ($annotation) {
  394.                 return
  395.                     $annotation instanceof ParamConverter &&
  396.                     'fos_rest.request_body' === $annotation->getConverter()
  397.                         ? $annotation->getName() : null;
  398.             }, $this->annotationReader->getMethodAnnotations($method));
  399.         }
  400.         $arguments = [];
  401.         foreach ($method->getParameters() as $argument) {
  402.             if (isset($params[$argument->getName()])) {
  403.                 continue;
  404.             }
  405.             $argumentClass $argument->getType();
  406.             if ($argumentClass && !$argumentClass->isBuiltIn()) {
  407.                 $className method_exists($argumentClass'getName') ? $argumentClass->getName() : (string) $argumentClass;
  408.                 foreach ($this->getIgnoredClasses() as $class) {
  409.                     if ($className === $class || is_subclass_of($className$class)) {
  410.                         continue 2;
  411.                     }
  412.                 }
  413.             }
  414.             if (in_array($argument->getName(), $ignoreParameterstrue)) {
  415.                 continue;
  416.             }
  417.             $arguments[] = $argument;
  418.         }
  419.         return $arguments;
  420.     }
  421.     /**
  422.      * @param string|bool $resource
  423.      */
  424.     private function generateResourceName($resource): string
  425.     {
  426.         if (false === $this->pluralize) {
  427.             return $resource;
  428.         }
  429.         return $this->inflector->pluralize($resource);
  430.     }
  431.     /**
  432.      * @param string[] $resources
  433.      */
  434.     private function generateRouteName(array $resources): string
  435.     {
  436.         $routeName '';
  437.         foreach ($resources as $resource) {
  438.             if (null !== $resource) {
  439.                 $routeName .= '_'.basename($resource);
  440.             }
  441.         }
  442.         return $routeName;
  443.     }
  444.     /**
  445.      * @param string[]               $resources
  446.      * @param \ReflectionParameter[] $arguments
  447.      */
  448.     private function generateUrlParts(array $resources, array $argumentsstring $httpMethod): array
  449.     {
  450.         $urlParts = [];
  451.         foreach ($resources as $i => $resource) {
  452.             // if we already added all parent routes paths to URL & we have
  453.             // prefix - add it
  454.             if (!empty($this->routePrefix) && $i === count($this->parents)) {
  455.                 $urlParts[] = $this->routePrefix;
  456.             }
  457.             // if we have argument for current resource, then it's object.
  458.             // otherwise - it's collection
  459.             if (isset($arguments[$i])) {
  460.                 if (null !== $resource) {
  461.                     $urlParts[] =
  462.                         strtolower($this->generateResourceName($resource))
  463.                         .'/{'.$arguments[$i]->getName().'}';
  464.                 } else {
  465.                     $urlParts[] = '{'.$arguments[$i]->getName().'}';
  466.                 }
  467.             } elseif (null !== $resource) {
  468.                 if ((=== count($arguments) && !in_array($httpMethod$this->availableHTTPMethods))
  469.                     || 'new' === $httpMethod
  470.                     || 'post' === $httpMethod
  471.                 ) {
  472.                     $urlParts[] = $this->generateResourceName(strtolower($resource));
  473.                 } else {
  474.                     $urlParts[] = strtolower($resource);
  475.                 }
  476.             }
  477.         }
  478.         return $urlParts;
  479.     }
  480.     /**
  481.      * @param string[]               $resources
  482.      * @param \ReflectionParameter[] $arguments
  483.      */
  484.     private function getCustomHttpMethod(string $httpMethod, array $resources, array $arguments): string
  485.     {
  486.         if (in_array($httpMethod$this->availableConventionalActions)) {
  487.             // allow hypertext as the engine of application state
  488.             // through conventional GET actions
  489.             return 'get';
  490.         }
  491.         if (count($arguments) < count($resources)) {
  492.             // resource collection
  493.             return 'get';
  494.         }
  495.         // custom object
  496.         return 'patch';
  497.     }
  498.     /**
  499.      * @return RouteAnnotation[]
  500.      */
  501.     private function readRouteAnnotation(\ReflectionMethod $reflectionMethod): array
  502.     {
  503.         $annotations = [];
  504.         if ($newAnnotations $this->readMethodAnnotations($reflectionMethod'Route')) {
  505.             $annotations array_merge($annotations$newAnnotations);
  506.         }
  507.         return $annotations;
  508.     }
  509.     private function readClassAnnotation(\ReflectionClass $reflectionClassstring $annotationName): ?RouteAnnotation
  510.     {
  511.         $annotationClass "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  512.         if ($annotation $this->annotationReader->getClassAnnotation($reflectionClass$annotationClass)) {
  513.             return $annotation;
  514.         }
  515.         return null;
  516.     }
  517.     private function readMethodAnnotation(\ReflectionMethod $reflectionMethodstring $annotationName): ?RouteAnnotation
  518.     {
  519.         $annotationClass "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  520.         if ($annotation $this->annotationReader->getMethodAnnotation($reflectionMethod$annotationClass)) {
  521.             return $annotation;
  522.         }
  523.         return null;
  524.     }
  525.     /**
  526.      * @return RouteAnnotation[]
  527.      */
  528.     private function readMethodAnnotations(\ReflectionMethod $reflectionMethodstring $annotationName): array
  529.     {
  530.         $annotations = [];
  531.         $annotationClass "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  532.         if ($annotations_new $this->annotationReader->getMethodAnnotations($reflectionMethod)) {
  533.             foreach ($annotations_new as $annotation) {
  534.                 if ($annotation instanceof $annotationClass) {
  535.                     $annotations[] = $annotation;
  536.                 }
  537.             }
  538.         }
  539.         return $annotations;
  540.     }
  541.     private function addRoute(RestRouteCollection $collectionstring $routeNameRoute $routebool $isCollectionbool $isInflectableRouteAnnotation $annotation null)
  542.     {
  543.         if ($annotation && null !== $annotation->getName()) {
  544.             $options $annotation->getOptions();
  545.             if (false === $this->hasMethodPrefix || (isset($options['method_prefix']) && false === $options['method_prefix'])) {
  546.                 $routeName $annotation->getName();
  547.             } else {
  548.                 $routeName .= $annotation->getName();
  549.             }
  550.         }
  551.         $fullRouteName $this->namePrefix.$routeName;
  552.         if ($isCollection && !$isInflectable) {
  553.             $collection->add($this->namePrefix.self::COLLECTION_ROUTE_PREFIX.$routeName$route);
  554.             if (!$collection->get($fullRouteName)) {
  555.                 $collection->add($fullRouteName, clone $route);
  556.             }
  557.         } else {
  558.             $collection->add($fullRouteName$route);
  559.         }
  560.     }
  561. }