/** * Loads a Routes collection by parsing Controller method names. * * @param string $controller Some identifier for the controller * @param string $type The resource type * * @return RouteCollection A RouteCollection instance */ public function load($controller, $type = null) { if (class_exists($controller)) { // full class name $class = $controller; $controllerPrefix = $class . '::'; } elseif (false !== strpos($controller, ':')) { // bundle:controller notation try { $notation = $this->parser->parse($controller . ':method'); list($class, $method) = explode('::', $notation); $controllerPrefix = $class . '::'; } catch (\Exception $e) { throw new \InvalidArgumentException(sprintf('Can\'t locate "%s" controller.', $controller)); } } elseif ($this->container->has($controller)) { // service_id $controllerPrefix = $controller . ':'; // FIXME: this is ugly, but I do not see any good alternative $this->container->enterScope('request'); $this->container->set('request', new Request()); $class = get_class($this->container->get($controller)); $this->container->leaveScope('request'); } if (empty($class)) { throw new \InvalidArgumentException(sprintf('Class could not be determined for Controller identified by "%s".', $controller)); } // Check that every passed parent has non-empty singular name foreach ($this->parents as $parent) { if (empty($parent) || '/' === substr($parent, -1)) { throw new \InvalidArgumentException('All parent controllers must have ::getSINGULAR_NAME() action'); } } $class = new \ReflectionClass($class); $collection = new RestRouteCollection(); $collection->addResource(new FileResource($class->getFileName())); $prefixAnnotationClass = 'FOS\\RestBundle\\Controller\\Annotations\\Prefix'; $prefix = $this->reader->getClassAnnotation($class, $prefixAnnotationClass); if ($prefix) { $this->prefix = $prefix->value; } $namePrefixAnnotationClass = 'FOS\\RestBundle\\Controller\\Annotations\\NamePrefix'; $namePrefix = $this->reader->getClassAnnotation($class, $namePrefixAnnotationClass); if ($namePrefix) { $this->namePrefix = $namePrefix->value; } // Trim "/" at the start if (null !== $this->prefix && isset($this->prefix[0]) && '/' === $this->prefix[0]) { $this->prefix = substr($this->prefix, 1); } $routeAnnotationClass = 'FOS\\RestBundle\\Controller\\Annotations\\Route'; $patternStartRoute = $this->reader->getClassAnnotation($class, $routeAnnotationClass); $patternStart = null; if ($patternStartRoute) { $patternStart = trim($patternStartRoute->getPattern(), "/"); } $routes = array(); foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { $matches = array(); // If method name starts with underscore - skip if ('_' === substr($method->getName(), 0, 1)) { continue; } // If method has @NoRoute annotation - skip $noAnnotationClass = 'FOS\\RestBundle\\Controller\\Annotations\\NoRoute'; if (null !== $this->reader->getMethodAnnotation($method, $noAnnotationClass)) { continue; } if (preg_match('/([a-z][_a-z0-9]+)(.*)Action/', $method->getName(), $matches)) { $httpMethod = $matches[1]; $resources = preg_split('/([A-Z][^A-Z]*)/', $matches[2], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $arguments = $method->getParameters(); // Ignore arguments that are or extend from Symfony\Component\HttpFoundation\Request foreach ($arguments as $key => $argument) { $class = $argument->getClass(); if ($class && ($class->getName() === 'Symfony\\Component\\HttpFoundation\\Request' || is_subclass_of($class->getName(), 'Symfony\\Component\\HttpFoundation\\Request'))) { unset($arguments[$key]); } } // If we have 1 resource passed & 1 argument, then it's object call, so // we can set collection singular name if (1 === count($resources) && 1 === count($arguments) - count($this->parents)) { $collection->setSingularName($resources[0]); } // If we have parents passed - merge them with own resource names if (count($this->parents)) { $resources = array_merge($this->parents, $resources); } $urlParts = array(); $routeName = $httpMethod; if (empty($resources)) { $resources[] = null; } foreach ($resources as $i => $resource) { if (null !== $resource) { $routeName .= '_' . basename($resource); } // If we already added all parent routes paths to URL & we have prefix - add it if (!empty($this->prefix) && $i === count($this->parents)) { $urlParts[] = $this->prefix; } // If we have argument for current resource, then it's object. Otherwise - it's collection if (isset($arguments[$i])) { if ($patternStart) { $urlParts[] = strtolower($patternStart) . '/{' . $arguments[$i]->getName() . '}'; } elseif (null !== $resource) { $urlParts[] = strtolower(Pluralization::pluralize($resource)) . '/{' . $arguments[$i]->getName() . '}'; } else { $urlParts[] = '{' . $arguments[$i]->getName() . '}'; } } else { if ($patternStart) { $urlParts[] = $patternStart; } elseif (null !== $resource) { $urlParts[] = strtolower($resource); } } } // If passed method is not valid HTTP method // then it's either a hypertext driver, // a custom object (PUT) or collection (GET) method if (!in_array($httpMethod, $this->availableHTTPMethods)) { $urlParts[] = $httpMethod; // allow hypertext as the engine of application state // through conventional GET actions if (in_array($httpMethod, $this->availableConventionalActions)) { $httpMethod = 'get'; } else { //custom object $httpMethod = 'post'; // resource collection if (count($arguments) < count($resources)) { $httpMethod = 'get'; } } } $pattern = implode('/', $urlParts); $defaults = array('_controller' => $controllerPrefix . $method->getName(), '_format' => $this->defaultFormat); $requirements = array('_method' => strtoupper($httpMethod)); $options = array(); // Read annotations foreach ($this->annotationClasses as $annotationClass) { $routeAnnnotation = $this->reader->getMethodAnnotation($method, $annotationClass); if (null !== $routeAnnnotation) { $annoRequirements = $routeAnnnotation->getRequirements(); if (!isset($annoRequirements['_method']) || null === $annoRequirements['_method']) { $annoRequirements['_method'] = $requirements['_method']; } $pattern = $routeAnnnotation->getPattern() ?: $pattern; $requirements = array_merge($requirements, $annoRequirements); $options = array_merge($options, $routeAnnnotation->getOptions()); $defaults = array_merge($defaults, $routeAnnnotation->getDefaults()); break; } } //Adding in the optional _format param for serialization $pattern .= ".{_format}"; // Create route with gathered parameters $route = new Route($pattern, $defaults, $requirements, $options); $routeName = $this->namePrefix . strtolower($routeName); // Move custom actions at the beginning, default at the end if (!preg_match('/^(' . implode('|', $this->availableHTTPMethods) . ')/', $routeName)) { array_unshift($routes, array('name' => $routeName, 'route' => $route)); } else { $routes[] = array('name' => $routeName, 'route' => $route); } } } foreach ($routes as $routeInfo) { $collection->add($routeInfo['name'], $routeInfo['route']); } $this->prefix = null; $this->namePrefix = null; return $collection; }
/** * Test that pluralization singularize words correctly. * * @dataProvider getWords * * @param string $singular * @param string $plural */ public function testSingularize($singular, $plural) { $this->assertEquals($singular, Pluralization::singularize($plural)); }
/** * Generates URL parts for route from resources list. * * @param array $resources * @param array $arguments * * @return array */ private function generateUrlParts(array $resources, array $arguments) { $urlParts = array(); foreach ($resources as $i => $resource) { // if we already added all parent routes paths to URL & we have // prefix - add it if (!empty($this->routePrefix) && $i === count($this->parents)) { $urlParts[] = $this->routePrefix; } // if we have argument for current resource, then it's object. // otherwise - it's collection if (isset($arguments[$i])) { if (null !== $resource) { $urlParts[] = strtolower(Pluralization::pluralize($resource)) . '/{' . $arguments[$i]->getName() . '}'; } else { $urlParts[] = '{' . $arguments[$i]->getName() . '}'; } } elseif (null !== $resource) { $urlParts[] = strtolower($resource); } } return $urlParts; }