├── src ├── Exception │ ├── ExceptionInterface.php │ ├── RuntimeException.php │ └── InvalidArgumentException.php ├── PriorityList.php ├── Http │ ├── RouteInterface.php │ ├── HttpRouterFactory.php │ ├── RouteMatch.php │ ├── Placeholder.php │ ├── Method.php │ ├── Scheme.php │ ├── Literal.php │ ├── Regex.php │ ├── TranslatorAwareTreeRouteStack.php │ ├── Wildcard.php │ ├── Chain.php │ ├── Part.php │ ├── Hostname.php │ ├── Segment.php │ └── TreeRouteStack.php ├── Module.php ├── RouteInterface.php ├── RouteStackInterface.php ├── RouterConfigTrait.php ├── RoutePluginManagerFactory.php ├── RouterFactory.php ├── ConfigProvider.php ├── RouteMatch.php ├── RouteInvokableFactory.php ├── RoutePluginManager.php └── SimpleRouteStack.php ├── README.md ├── LICENSE.md ├── composer.json └── CHANGELOG.md /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | $provider->getDependencyConfig(), 25 | 'route_manager' => $provider->getRouteManagerConfig(), 26 | 'router' => ['routes' => []], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-router 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-router](https://github.com/laminas/laminas-router). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-router.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-router) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-router/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-router?branch=master) 9 | 10 | zend-router provides flexible HTTP routing. 11 | 12 | Routing currently works against the [zend-http](https://github.com/zendframework/zend-http) 13 | request and responses, and provides capabilities around: 14 | 15 | - Literal path matches 16 | - Path segment matches (at path boundaries, and optionally validated using regex) 17 | - Regular expression path matches 18 | - HTTP request scheme 19 | - HTTP request method 20 | - Hostname 21 | 22 | Additionally, it supports combinations of different route types in tree 23 | structures, allowing for fast, b-tree lookups. 24 | 25 | - File issues at https://github.com/zendframework/zend-router/issues 26 | - Documentation is at https://docs.zendframework.com/zend-router 27 | -------------------------------------------------------------------------------- /src/RouteInterface.php: -------------------------------------------------------------------------------- 1 | get('RoutePluginManager'); 31 | $config['route_plugins'] = $routePluginManager; 32 | } 33 | 34 | // Obtain an instance 35 | $factory = sprintf('%s::factory', $class); 36 | return call_user_func($factory, $config); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RoutePluginManagerFactory.php: -------------------------------------------------------------------------------- 1 | get('HttpRouter'); 29 | } 30 | 31 | /** 32 | * Create and return RouteStackInterface instance 33 | * 34 | * For use with zend-servicemanager v2; proxies to __invoke(). 35 | * 36 | * @param ServiceLocatorInterface $container 37 | * @param null|string $normalizedName 38 | * @param null|string $requestedName 39 | * @return RouteStackInterface 40 | */ 41 | public function createService(ServiceLocatorInterface $container, $normalizedName = null, $requestedName = null) 42 | { 43 | $requestedName = $requestedName ?: 'Router'; 44 | return $this($container, $requestedName); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/HttpRouterFactory.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 35 | 36 | // Defaults 37 | $class = TreeRouteStack::class; 38 | $config = isset($config['router']) ? $config['router'] : []; 39 | 40 | return $this->createRouter($class, $config, $container); 41 | } 42 | 43 | /** 44 | * Create and return RouteStackInterface instance 45 | * 46 | * For use with zend-servicemanager v2; proxies to __invoke(). 47 | * 48 | * @param ServiceLocatorInterface $container 49 | * @return RouteStackInterface 50 | */ 51 | public function createService(ServiceLocatorInterface $container) 52 | { 53 | return $this($container, RouteStackInterface::class); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencyConfig(), 29 | 'route_manager' => $this->getRouteManagerConfig(), 30 | ]; 31 | } 32 | 33 | /** 34 | * Provide default container dependency configuration. 35 | * 36 | * @return array 37 | */ 38 | public function getDependencyConfig() 39 | { 40 | return [ 41 | 'aliases' => [ 42 | 'HttpRouter' => Http\TreeRouteStack::class, 43 | 'router' => RouteStackInterface::class, 44 | 'Router' => RouteStackInterface::class, 45 | 'RoutePluginManager' => RoutePluginManager::class, 46 | ], 47 | 'factories' => [ 48 | Http\TreeRouteStack::class => Http\HttpRouterFactory::class, 49 | RoutePluginManager::class => RoutePluginManagerFactory::class, 50 | RouteStackInterface::class => RouterFactory::class, 51 | ], 52 | ]; 53 | } 54 | 55 | /** 56 | * Provide default route plugin manager configuration. 57 | * 58 | * @return array 59 | */ 60 | public function getRouteManagerConfig() 61 | { 62 | return []; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Http/RouteMatch.php: -------------------------------------------------------------------------------- 1 | length = $length; 35 | } 36 | 37 | /** 38 | * setMatchedRouteName(): defined by BaseRouteMatch. 39 | * 40 | * @see BaseRouteMatch::setMatchedRouteName() 41 | * @param string $name 42 | * @return RouteMatch 43 | */ 44 | public function setMatchedRouteName($name) 45 | { 46 | if ($this->matchedRouteName === null) { 47 | $this->matchedRouteName = $name; 48 | } else { 49 | $this->matchedRouteName = $name . '/' . $this->matchedRouteName; 50 | } 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Merge parameters from another match. 57 | * 58 | * @param RouteMatch $match 59 | * @return RouteMatch 60 | */ 61 | public function merge(RouteMatch $match) 62 | { 63 | $this->params = array_merge($this->params, $match->getParams()); 64 | $this->length += $match->getLength(); 65 | 66 | $this->matchedRouteName = $match->getMatchedRouteName(); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get the matched path length. 73 | * 74 | * @return int 75 | */ 76 | public function getLength() 77 | { 78 | return $this->length; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/RouteMatch.php: -------------------------------------------------------------------------------- 1 | params = $params; 37 | } 38 | 39 | /** 40 | * Set name of matched route. 41 | * 42 | * @param string $name 43 | * @return RouteMatch 44 | */ 45 | public function setMatchedRouteName($name) 46 | { 47 | $this->matchedRouteName = $name; 48 | return $this; 49 | } 50 | 51 | /** 52 | * Get name of matched route. 53 | * 54 | * @return string 55 | */ 56 | public function getMatchedRouteName() 57 | { 58 | return $this->matchedRouteName; 59 | } 60 | 61 | /** 62 | * Set a parameter. 63 | * 64 | * @param string $name 65 | * @param mixed $value 66 | * @return RouteMatch 67 | */ 68 | public function setParam($name, $value) 69 | { 70 | $this->params[$name] = $value; 71 | return $this; 72 | } 73 | 74 | /** 75 | * Get all parameters. 76 | * 77 | * @return array 78 | */ 79 | public function getParams() 80 | { 81 | return $this->params; 82 | } 83 | 84 | /** 85 | * Get a specific parameter. 86 | * 87 | * @param string $name 88 | * @param mixed $default 89 | * @return mixed 90 | */ 91 | public function getParam($name, $default = null) 92 | { 93 | if (array_key_exists($name, $this->params)) { 94 | return $this->params[$name]; 95 | } 96 | 97 | return $default; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-router", 3 | "description": "Flexible routing system for HTTP and console applications", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zend", 8 | "zendframework", 9 | "mvc", 10 | "routing" 11 | ], 12 | "support": { 13 | "docs": "https://docs.zendframework.com/zend-router/", 14 | "issues": "https://github.com/zendframework/zend-router/issues", 15 | "source": "https://github.com/zendframework/zend-router", 16 | "rss": "https://github.com/zendframework/zend-router/releases.atom", 17 | "slack": "https://zendframework-slack.herokuapp.com", 18 | "forum": "https://discourse.zendframework.com/c/questions/components" 19 | }, 20 | "require": { 21 | "php": "^5.6 || ^7.0", 22 | "container-interop/container-interop": "^1.2", 23 | "zendframework/zend-http": "^2.8.1", 24 | "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", 25 | "zendframework/zend-stdlib": "^3.2.1" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^5.7.22 || ^6.4.1", 29 | "zendframework/zend-coding-standard": "~1.0.0", 30 | "zendframework/zend-i18n": "^2.7.4" 31 | }, 32 | "conflict": { 33 | "zendframework/zend-mvc": "<3.0.0" 34 | }, 35 | "suggest": { 36 | "zendframework/zend-i18n": "^2.7.4, if defining translatable HTTP path segments" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Zend\\Router\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "ZendTest\\Router\\": "test/" 46 | } 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "3.3.x-dev", 54 | "dev-develop": "4.0.x-dev" 55 | }, 56 | "zf": { 57 | "component": "Zend\\Router", 58 | "config-provider": "Zend\\Router\\ConfigProvider" 59 | } 60 | }, 61 | "scripts": { 62 | "check": [ 63 | "@cs-check", 64 | "@test" 65 | ], 66 | "cs-check": "phpcs", 67 | "cs-fix": "phpcbf", 68 | "test": "phpunit --colors=always", 69 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Http/Placeholder.php: -------------------------------------------------------------------------------- 1 | defaults = $defaults; 25 | } 26 | 27 | /** 28 | * factory(): defined by RouteInterface interface. 29 | * 30 | * @see \Zend\Router\RouteInterface::factory() 31 | * @param array|Traversable $options 32 | * @return Placeholder 33 | * @throws Exception\InvalidArgumentException 34 | */ 35 | public static function factory($options = []) 36 | { 37 | if ($options instanceof Traversable) { 38 | $options = ArrayUtils::iteratorToArray($options); 39 | } 40 | 41 | if (! is_array($options)) { 42 | throw new Exception\InvalidArgumentException(sprintf( 43 | '%s expects an array or Traversable set of options', 44 | __METHOD__ 45 | )); 46 | } 47 | 48 | if (! isset($options['defaults'])) { 49 | $options['defaults'] = []; 50 | } 51 | 52 | if (! is_array($options['defaults'])) { 53 | throw new Exception\InvalidArgumentException('options[defaults] expected to be an array if set'); 54 | } 55 | 56 | return new static($options['defaults']); 57 | } 58 | 59 | /** 60 | * match(): defined by RouteInterface interface. 61 | * 62 | * @see \Zend\Router\RouteInterface::match() 63 | * @param Request $request 64 | * @param integer|null $pathOffset 65 | * @return RouteMatch|null 66 | */ 67 | public function match(Request $request, $pathOffset = null) 68 | { 69 | return new RouteMatch($this->defaults); 70 | } 71 | 72 | /** 73 | * assemble(): Defined by RouteInterface interface. 74 | * 75 | * @see \Zend\Router\RouteInterface::assemble() 76 | * @param array $params 77 | * @param array $options 78 | * @return mixed 79 | */ 80 | public function assemble(array $params = [], array $options = []) 81 | { 82 | return ''; 83 | } 84 | 85 | /** 86 | * getAssembledParams(): defined by RouteInterface interface. 87 | * 88 | * @see RouteInterface::getAssembledParams 89 | * @return array 90 | */ 91 | public function getAssembledParams() 92 | { 93 | return []; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Http/Method.php: -------------------------------------------------------------------------------- 1 | verb = $verb; 43 | $this->defaults = $defaults; 44 | } 45 | 46 | /** 47 | * factory(): defined by RouteInterface interface. 48 | * 49 | * @see \Zend\Router\RouteInterface::factory() 50 | * @param array|Traversable $options 51 | * @return Method 52 | * @throws Exception\InvalidArgumentException 53 | */ 54 | public static function factory($options = []) 55 | { 56 | if ($options instanceof Traversable) { 57 | $options = ArrayUtils::iteratorToArray($options); 58 | } elseif (! is_array($options)) { 59 | throw new Exception\InvalidArgumentException(sprintf( 60 | '%s expects an array or Traversable set of options', 61 | __METHOD__ 62 | )); 63 | } 64 | 65 | if (! isset($options['verb'])) { 66 | throw new Exception\InvalidArgumentException('Missing "verb" in options array'); 67 | } 68 | 69 | if (! isset($options['defaults'])) { 70 | $options['defaults'] = []; 71 | } 72 | 73 | return new static($options['verb'], $options['defaults']); 74 | } 75 | 76 | /** 77 | * match(): defined by RouteInterface interface. 78 | * 79 | * @see \Zend\Router\RouteInterface::match() 80 | * @param Request $request 81 | * @return RouteMatch|null 82 | */ 83 | public function match(Request $request) 84 | { 85 | if (! method_exists($request, 'getMethod')) { 86 | return; 87 | } 88 | 89 | $requestVerb = strtoupper($request->getMethod()); 90 | $matchVerbs = explode(',', strtoupper($this->verb)); 91 | $matchVerbs = array_map('trim', $matchVerbs); 92 | 93 | if (in_array($requestVerb, $matchVerbs)) { 94 | return new RouteMatch($this->defaults); 95 | } 96 | 97 | return; 98 | } 99 | 100 | /** 101 | * assemble(): Defined by RouteInterface interface. 102 | * 103 | * @see \Zend\Router\RouteInterface::assemble() 104 | * @param array $params 105 | * @param array $options 106 | * @return mixed 107 | */ 108 | public function assemble(array $params = [], array $options = []) 109 | { 110 | // The request method does not contribute to the path, thus nothing is returned. 111 | return ''; 112 | } 113 | 114 | /** 115 | * getAssembledParams(): defined by RouteInterface interface. 116 | * 117 | * @see RouteInterface::getAssembledParams 118 | * @return array 119 | */ 120 | public function getAssembledParams() 121 | { 122 | return []; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Http/Scheme.php: -------------------------------------------------------------------------------- 1 | scheme = $scheme; 43 | $this->defaults = $defaults; 44 | } 45 | 46 | /** 47 | * factory(): defined by RouteInterface interface. 48 | * 49 | * @see \Zend\Router\RouteInterface::factory() 50 | * @param array|Traversable $options 51 | * @return Scheme 52 | * @throws Exception\InvalidArgumentException 53 | */ 54 | public static function factory($options = []) 55 | { 56 | if ($options instanceof Traversable) { 57 | $options = ArrayUtils::iteratorToArray($options); 58 | } elseif (! is_array($options)) { 59 | throw new Exception\InvalidArgumentException(sprintf( 60 | '%s expects an array or Traversable set of options', 61 | __METHOD__ 62 | )); 63 | } 64 | 65 | if (! isset($options['scheme'])) { 66 | throw new Exception\InvalidArgumentException('Missing "scheme" in options array'); 67 | } 68 | 69 | if (! isset($options['defaults'])) { 70 | $options['defaults'] = []; 71 | } 72 | 73 | return new static($options['scheme'], $options['defaults']); 74 | } 75 | 76 | /** 77 | * match(): defined by RouteInterface interface. 78 | * 79 | * @see \Zend\Router\RouteInterface::match() 80 | * @param Request $request 81 | * @return RouteMatch|null 82 | */ 83 | public function match(Request $request) 84 | { 85 | if (! method_exists($request, 'getUri')) { 86 | return; 87 | } 88 | 89 | $uri = $request->getUri(); 90 | $scheme = $uri->getScheme(); 91 | 92 | if ($scheme !== $this->scheme) { 93 | return; 94 | } 95 | 96 | return new RouteMatch($this->defaults); 97 | } 98 | 99 | /** 100 | * assemble(): Defined by RouteInterface interface. 101 | * 102 | * @see \Zend\Router\RouteInterface::assemble() 103 | * @param array $params 104 | * @param array $options 105 | * @return mixed 106 | */ 107 | public function assemble(array $params = [], array $options = []) 108 | { 109 | if (isset($options['uri'])) { 110 | $options['uri']->setScheme($this->scheme); 111 | } 112 | 113 | // A scheme does not contribute to the path, thus nothing is returned. 114 | return ''; 115 | } 116 | 117 | /** 118 | * getAssembledParams(): defined by RouteInterface interface. 119 | * 120 | * @see RouteInterface::getAssembledParams 121 | * @return array 122 | */ 123 | public function getAssembledParams() 124 | { 125 | return []; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Http/Literal.php: -------------------------------------------------------------------------------- 1 | route = $route; 43 | $this->defaults = $defaults; 44 | } 45 | 46 | /** 47 | * factory(): defined by RouteInterface interface. 48 | * 49 | * @see \Zend\Router\RouteInterface::factory() 50 | * @param array|Traversable $options 51 | * @return Literal 52 | * @throws Exception\InvalidArgumentException 53 | */ 54 | public static function factory($options = []) 55 | { 56 | if ($options instanceof Traversable) { 57 | $options = ArrayUtils::iteratorToArray($options); 58 | } elseif (! is_array($options)) { 59 | throw new Exception\InvalidArgumentException(sprintf( 60 | '%s expects an array or Traversable set of options', 61 | __METHOD__ 62 | )); 63 | } 64 | 65 | if (! isset($options['route'])) { 66 | throw new Exception\InvalidArgumentException('Missing "route" in options array'); 67 | } 68 | 69 | if (! isset($options['defaults'])) { 70 | $options['defaults'] = []; 71 | } 72 | 73 | return new static($options['route'], $options['defaults']); 74 | } 75 | 76 | /** 77 | * match(): defined by RouteInterface interface. 78 | * 79 | * @see \Zend\Router\RouteInterface::match() 80 | * @param Request $request 81 | * @param integer|null $pathOffset 82 | * @return RouteMatch|null 83 | */ 84 | public function match(Request $request, $pathOffset = null) 85 | { 86 | if (! method_exists($request, 'getUri')) { 87 | return; 88 | } 89 | 90 | $uri = $request->getUri(); 91 | $path = $uri->getPath(); 92 | 93 | if ($pathOffset !== null) { 94 | if ($pathOffset >= 0 && strlen($path) >= $pathOffset && ! empty($this->route)) { 95 | if (strpos($path, $this->route, $pathOffset) === $pathOffset) { 96 | return new RouteMatch($this->defaults, strlen($this->route)); 97 | } 98 | } 99 | 100 | return; 101 | } 102 | 103 | if ($path === $this->route) { 104 | return new RouteMatch($this->defaults, strlen($this->route)); 105 | } 106 | 107 | return; 108 | } 109 | 110 | /** 111 | * assemble(): Defined by RouteInterface interface. 112 | * 113 | * @see \Zend\Router\RouteInterface::assemble() 114 | * @param array $params 115 | * @param array $options 116 | * @return mixed 117 | */ 118 | public function assemble(array $params = [], array $options = []) 119 | { 120 | return $this->route; 121 | } 122 | 123 | /** 124 | * getAssembledParams(): defined by RouteInterface interface. 125 | * 126 | * @see RouteInterface::getAssembledParams 127 | * @return array 128 | */ 129 | public function getAssembledParams() 130 | { 131 | return []; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/RouteInvokableFactory.php: -------------------------------------------------------------------------------- 1 | canCreate($container, $routeName); 68 | } 69 | 70 | /** 71 | * Create and return a RouteInterface instance. 72 | * 73 | * If the specified $routeName class does not exist or does not implement 74 | * RouteInterface, this method will raise an exception. 75 | * 76 | * Otherwise, it uses the class' `factory()` method with the provided 77 | * $options to produce an instance. 78 | * 79 | * @param ContainerInterface $container 80 | * @param string $routeName 81 | * @param null|array $options 82 | * @return RouteInterface 83 | */ 84 | public function __invoke(ContainerInterface $container, $routeName, array $options = null) 85 | { 86 | $options = $options ?: []; 87 | 88 | if (! class_exists($routeName)) { 89 | throw new ServiceNotCreatedException(sprintf( 90 | '%s: failed retrieving invokable class "%s"; class does not exist', 91 | __CLASS__, 92 | $routeName 93 | )); 94 | } 95 | 96 | if (! is_subclass_of($routeName, RouteInterface::class)) { 97 | throw new ServiceNotCreatedException(sprintf( 98 | '%s: failed retrieving invokable class "%s"; class does not implement %s', 99 | __CLASS__, 100 | $routeName, 101 | RouteInterface::class 102 | )); 103 | } 104 | 105 | return $routeName::factory($options); 106 | } 107 | 108 | /** 109 | * Create a route instance with the given name. (v2) 110 | * 111 | * Proxies to __invoke(). 112 | * 113 | * @param ServiceLocatorInterface $container 114 | * @param string $normalizedName 115 | * @param string $routeName 116 | * @return RouteInterface 117 | */ 118 | public function createServiceWithName(ServiceLocatorInterface $container, $normalizedName, $routeName) 119 | { 120 | return $this($container, $routeName, $this->creationOptions); 121 | } 122 | 123 | /** 124 | * Create and return RouteInterface instance 125 | * 126 | * For use with zend-servicemanager v2; proxies to __invoke(). 127 | * 128 | * @param ServiceLocatorInterface $container 129 | * @return RouteInterface 130 | */ 131 | public function createService(ServiceLocatorInterface $container, $normalizedName = null, $routeName = null) 132 | { 133 | $routeName = $routeName ?: RouteInterface::class; 134 | return $this($container, $routeName, $this->creationOptions); 135 | } 136 | 137 | /** 138 | * Set options to use when creating a service (v2) 139 | * 140 | * @param array $creationOptions 141 | */ 142 | public function setCreationOptions(array $creationOptions) 143 | { 144 | $this->creationOptions = $creationOptions; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Http/Regex.php: -------------------------------------------------------------------------------- 1 | regex = $regex; 60 | $this->spec = $spec; 61 | $this->defaults = $defaults; 62 | } 63 | 64 | /** 65 | * factory(): defined by RouteInterface interface. 66 | * 67 | * @see \Zend\Router\RouteInterface::factory() 68 | * @param array|Traversable $options 69 | * @return Regex 70 | * @throws \Zend\Router\Exception\InvalidArgumentException 71 | */ 72 | public static function factory($options = []) 73 | { 74 | if ($options instanceof Traversable) { 75 | $options = ArrayUtils::iteratorToArray($options); 76 | } elseif (! is_array($options)) { 77 | throw new Exception\InvalidArgumentException(sprintf( 78 | '%s expects an array or Traversable set of options', 79 | __METHOD__ 80 | )); 81 | } 82 | 83 | if (! isset($options['regex'])) { 84 | throw new Exception\InvalidArgumentException('Missing "regex" in options array'); 85 | } 86 | 87 | if (! isset($options['spec'])) { 88 | throw new Exception\InvalidArgumentException('Missing "spec" in options array'); 89 | } 90 | 91 | if (! isset($options['defaults'])) { 92 | $options['defaults'] = []; 93 | } 94 | 95 | return new static($options['regex'], $options['spec'], $options['defaults']); 96 | } 97 | 98 | /** 99 | * match(): defined by RouteInterface interface. 100 | * 101 | * @param Request $request 102 | * @param int $pathOffset 103 | * @return RouteMatch|null 104 | */ 105 | public function match(Request $request, $pathOffset = null) 106 | { 107 | if (! method_exists($request, 'getUri')) { 108 | return; 109 | } 110 | 111 | $uri = $request->getUri(); 112 | $path = $uri->getPath(); 113 | 114 | if ($pathOffset !== null) { 115 | $result = preg_match('(\G' . $this->regex . ')', $path, $matches, null, $pathOffset); 116 | } else { 117 | $result = preg_match('(^' . $this->regex . '$)', $path, $matches); 118 | } 119 | 120 | if (! $result) { 121 | return; 122 | } 123 | 124 | $matchedLength = strlen($matches[0]); 125 | 126 | foreach ($matches as $key => $value) { 127 | if (is_numeric($key) || is_int($key) || $value === '') { 128 | unset($matches[$key]); 129 | } else { 130 | $matches[$key] = rawurldecode($value); 131 | } 132 | } 133 | 134 | return new RouteMatch(array_merge($this->defaults, $matches), $matchedLength); 135 | } 136 | 137 | /** 138 | * assemble(): Defined by RouteInterface interface. 139 | * 140 | * @see \Zend\Router\RouteInterface::assemble() 141 | * @param array $params 142 | * @param array $options 143 | * @return mixed 144 | */ 145 | public function assemble(array $params = [], array $options = []) 146 | { 147 | $url = $this->spec; 148 | $mergedParams = array_merge($this->defaults, $params); 149 | $this->assembledParams = []; 150 | 151 | foreach ($mergedParams as $key => $value) { 152 | $spec = '%' . $key . '%'; 153 | 154 | if (strpos($url, $spec) !== false) { 155 | $url = str_replace($spec, rawurlencode($value), $url); 156 | 157 | $this->assembledParams[] = $key; 158 | } 159 | } 160 | 161 | return $url; 162 | } 163 | 164 | /** 165 | * getAssembledParams(): defined by RouteInterface interface. 166 | * 167 | * @see RouteInterface::getAssembledParams 168 | * @return array 169 | */ 170 | public function getAssembledParams() 171 | { 172 | return $this->assembledParams; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Http/TranslatorAwareTreeRouteStack.php: -------------------------------------------------------------------------------- 1 | hasTranslator() && $this->isTranslatorEnabled() && ! isset($options['translator'])) { 53 | $options['translator'] = $this->getTranslator(); 54 | } 55 | 56 | if (! isset($options['text_domain'])) { 57 | $options['text_domain'] = $this->getTranslatorTextDomain(); 58 | } 59 | 60 | return parent::match($request, $pathOffset, $options); 61 | } 62 | 63 | /** 64 | * assemble(): defined by \Zend\Router\RouteInterface interface. 65 | * 66 | * @see \Zend\Router\RouteInterface::assemble() 67 | * @param array $params 68 | * @param array $options 69 | * @return mixed 70 | * @throws Exception\InvalidArgumentException 71 | * @throws Exception\RuntimeException 72 | */ 73 | public function assemble(array $params = [], array $options = []) 74 | { 75 | if ($this->hasTranslator() && $this->isTranslatorEnabled() && ! isset($options['translator'])) { 76 | $options['translator'] = $this->getTranslator(); 77 | } 78 | 79 | if (! isset($options['text_domain'])) { 80 | $options['text_domain'] = $this->getTranslatorTextDomain(); 81 | } 82 | 83 | return parent::assemble($params, $options); 84 | } 85 | 86 | /** 87 | * setTranslator(): defined by TranslatorAwareInterface. 88 | * 89 | * @see TranslatorAwareInterface::setTranslator() 90 | * @param Translator $translator 91 | * @param string $textDomain 92 | * @return TreeRouteStack 93 | */ 94 | public function setTranslator(Translator $translator = null, $textDomain = null) 95 | { 96 | $this->translator = $translator; 97 | 98 | if ($textDomain !== null) { 99 | $this->setTranslatorTextDomain($textDomain); 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * getTranslator(): defined by TranslatorAwareInterface. 107 | * 108 | * @see TranslatorAwareInterface::getTranslator() 109 | * @return Translator 110 | */ 111 | public function getTranslator() 112 | { 113 | return $this->translator; 114 | } 115 | 116 | /** 117 | * hasTranslator(): defined by TranslatorAwareInterface. 118 | * 119 | * @see TranslatorAwareInterface::hasTranslator() 120 | * @return bool 121 | */ 122 | public function hasTranslator() 123 | { 124 | return $this->translator !== null; 125 | } 126 | 127 | /** 128 | * setTranslatorEnabled(): defined by TranslatorAwareInterface. 129 | * 130 | * @see TranslatorAwareInterface::setTranslatorEnabled() 131 | * @param bool $enabled 132 | * @return TreeRouteStack 133 | */ 134 | public function setTranslatorEnabled($enabled = true) 135 | { 136 | $this->translatorEnabled = $enabled; 137 | return $this; 138 | } 139 | 140 | /** 141 | * isTranslatorEnabled(): defined by TranslatorAwareInterface. 142 | * 143 | * @see TranslatorAwareInterface::isTranslatorEnabled() 144 | * @return bool 145 | */ 146 | public function isTranslatorEnabled() 147 | { 148 | return $this->translatorEnabled; 149 | } 150 | 151 | /** 152 | * setTranslatorTextDomain(): defined by TranslatorAwareInterface. 153 | * 154 | * @see TranslatorAwareInterface::setTranslatorTextDomain() 155 | * @param string $textDomain 156 | * @return self 157 | */ 158 | public function setTranslatorTextDomain($textDomain = 'default') 159 | { 160 | $this->translatorTextDomain = $textDomain; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * getTranslatorTextDomain(): defined by TranslatorAwareInterface. 167 | * 168 | * @see TranslatorAwareInterface::getTranslatorTextDomain() 169 | * @return string 170 | */ 171 | public function getTranslatorTextDomain() 172 | { 173 | return $this->translatorTextDomain; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 3.3.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 3.3.0 - 2019-02-26 28 | 29 | ### Added 30 | 31 | - [#53](https://github.com/zendframework/zend-router/pull/53) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - [#53](https://github.com/zendframework/zend-router/pull/53) removes support for zend-stdlib v2 releases. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 3.2.1 - 2019-01-09 50 | 51 | ### Added 52 | 53 | - Nothing. 54 | 55 | ### Changed 56 | 57 | - Nothing. 58 | 59 | ### Deprecated 60 | 61 | - Nothing. 62 | 63 | ### Removed 64 | 65 | - Nothing. 66 | 67 | ### Fixed 68 | 69 | - [#50](https://github.com/zendframework/zend-router/pull/54) Corrected PHPDoc 70 | for `RouterInterface#factory()` return type 71 | 72 | ## 3.2.0 - 2018-08-01 73 | 74 | ### Added 75 | 76 | - [#50](https://github.com/zendframework/zend-router/pull/50) adds `Zend\Router\Http\Placeholder`, which can be used within reusable 77 | modules to indicate a route with child routes where the root route may be 78 | overridden. By default, the `Placeholder` route always matches, passing on 79 | further matching to the defined child routes. 80 | 81 | ### Changed 82 | 83 | - [#38](https://github.com/zendframework/zend-router/pull/38) bumps the minimum supported zend-http version to 2.8.1. 84 | 85 | ### Deprecated 86 | 87 | - Nothing. 88 | 89 | ### Removed 90 | 91 | - Nothing. 92 | 93 | ### Fixed 94 | 95 | - Nothing. 96 | 97 | ## 3.1.0 - 2018-06-18 98 | 99 | ### Added 100 | 101 | - Nothing. 102 | 103 | ### Changed 104 | 105 | - Nothing. 106 | 107 | ### Deprecated 108 | 109 | - Nothing. 110 | 111 | ### Removed 112 | 113 | - [#34](https://github.com/zendframework/zend-router/pull/34) dropped php 5.5 support 114 | 115 | ### Fixed 116 | 117 | - [#47](https://github.com/zendframework/zend-router/pull/47) fixes how the `Wildcard` URL assembly works. Previously, it would 118 | attempt to `rawurlencode()` all values provided to the method as merged with any default values. 119 | It now properly skips any non-scalar values when assembling the URL path. This fixes an issue 120 | discovered when providing an array of middleware as a `middleware` default route parameter. 121 | 122 | ## 3.0.2 - 2016-05-31 123 | 124 | ### Added 125 | 126 | - Nothing. 127 | 128 | ### Deprecated 129 | 130 | - Nothing. 131 | 132 | ### Removed 133 | 134 | - Nothing. 135 | 136 | ### Fixed 137 | 138 | - [#5](https://github.com/zendframework/zend-router/pull/5) marks zend-mvc 139 | versions less than 3.0.0 as conflicts. 140 | 141 | ## 3.0.1 - 2016-04-18 142 | 143 | ### Added 144 | 145 | - [#3](https://github.com/zendframework/zend-router/pull/3) adds a 146 | `config-provider` entry in `composer.json`, pointing to 147 | `Zend\Router\ConfigProvider`. 148 | 149 | ### Deprecated 150 | 151 | - Nothing. 152 | 153 | ### Removed 154 | 155 | - Nothing. 156 | 157 | ### Fixed 158 | 159 | - [#3](https://github.com/zendframework/zend-router/pull/3) fixes the 160 | `component` entry in `composer.json` to properly read `Zend\Router`. 161 | 162 | ## 3.0.0 - 2016-03-21 163 | 164 | First release as standalone package in its own namespace. This is the first 165 | version that will be used with zend-mvc v3; see its [migration document](https://docs.zendframework.com/zend-router/migration/v2-to-v3/) 166 | for details on how to update existing routing to this version. 167 | 168 | In particular, the `Zend\Mvc\Router` namespace was renamed to `Zend\Router`. 169 | 170 | ### Added 171 | 172 | - [#2](https://github.com/zendframework/zend-router/pull/2) adds 173 | `ConfigProvider`, which is an invokable class that returns dependency 174 | configuration for the component; in particular, this will be useful for 175 | zend-expressive-zendrouter. 176 | - [#2](https://github.com/zendframework/zend-router/pull/2) adds the `Module` 177 | class, for use with zend-mvc + zend-modulemanager. It provides dependency 178 | configuration for the component when used in that context. 179 | - [#2](https://github.com/zendframework/zend-router/pull/2) adds 180 | zend-component-installer configuration for the above `ConfigProvider` and 181 | `Module`, to allow auto-registration with the application. 182 | - [#2](https://github.com/zendframework/zend-router/pull/2) adds the following 183 | factories: 184 | - `Zend\Router\RouteInvokableFactory`, which provides a custom "invokable" 185 | factory for routes that uses the route class' `factory()` method for 186 | instantiation. 187 | - `Zend\Router\RoutePluginManagerFactory`, for creating a `RoutePluginManager` 188 | instance. 189 | - `Zend\Router\Http\HttpRouterFactory`, for returning a `TreeRouteStack` 190 | instance. 191 | - `Zend\Router\RouterFactory`, which essentially proxies to 192 | `Zend\Router\Http\HttpRouterFactory`. 193 | 194 | 195 | ### Deprecated 196 | 197 | - Nothing. 198 | 199 | ### Removed 200 | 201 | - [#2](https://github.com/zendframework/zend-router/pull/2) removes all 202 | console-related routing. These will be part of a new component, 203 | zend-mvc-console. 204 | - [#2](https://github.com/zendframework/zend-router/pull/2) removes the `Query` 205 | route, as it had been deprecated starting with version 2.3. 206 | 207 | ### Fixed 208 | 209 | - Nothing. 210 | -------------------------------------------------------------------------------- /src/RoutePluginManager.php: -------------------------------------------------------------------------------- 1 | addAbstractFactory(RouteInvokableFactory::class); 59 | parent::__construct($configOrContainerInstance, $v3config); 60 | } 61 | 62 | /** 63 | * Validate a route plugin. (v2) 64 | * 65 | * @param object $plugin 66 | * @throws InvalidServiceException 67 | */ 68 | public function validate($plugin) 69 | { 70 | if (! $plugin instanceof $this->instanceOf) { 71 | throw new InvalidServiceException(sprintf( 72 | 'Plugin of type %s is invalid; must implement %s', 73 | (is_object($plugin) ? get_class($plugin) : gettype($plugin)), 74 | RouteInterface::class 75 | )); 76 | } 77 | } 78 | 79 | /** 80 | * Validate a route plugin. (v2) 81 | * 82 | * @param object $plugin 83 | * @throws Exception\RuntimeException 84 | */ 85 | public function validatePlugin($plugin) 86 | { 87 | try { 88 | $this->validate($plugin); 89 | } catch (InvalidServiceException $e) { 90 | throw new Exception\RuntimeException( 91 | $e->getMessage(), 92 | $e->getCode(), 93 | $e 94 | ); 95 | } 96 | } 97 | 98 | /** 99 | * Pre-process configuration. (v3) 100 | * 101 | * Checks for invokables, and, if found, maps them to the 102 | * component-specific RouteInvokableFactory; removes the invokables entry 103 | * before passing to the parent. 104 | * 105 | * @param array $config 106 | * @return void 107 | */ 108 | public function configure(array $config) 109 | { 110 | if (isset($config['invokables']) && ! empty($config['invokables'])) { 111 | $aliases = $this->createAliasesForInvokables($config['invokables']); 112 | $factories = $this->createFactoriesForInvokables($config['invokables']); 113 | 114 | if (! empty($aliases)) { 115 | $config['aliases'] = isset($config['aliases']) 116 | ? array_merge($config['aliases'], $aliases) 117 | : $aliases; 118 | } 119 | 120 | $config['factories'] = isset($config['factories']) 121 | ? array_merge($config['factories'], $factories) 122 | : $factories; 123 | 124 | unset($config['invokables']); 125 | } 126 | 127 | parent::configure($config); 128 | } 129 | 130 | /** 131 | * Create aliases for invokable classes. 132 | * 133 | * If an invokable service name does not match the class it maps to, this 134 | * creates an alias to the class (which will later be mapped as an 135 | * invokable factory). 136 | * 137 | * @param array $invokables 138 | * @return array 139 | */ 140 | protected function createAliasesForInvokables(array $invokables) 141 | { 142 | $aliases = []; 143 | foreach ($invokables as $name => $class) { 144 | if ($name === $class) { 145 | continue; 146 | } 147 | $aliases[$name] = $class; 148 | } 149 | return $aliases; 150 | } 151 | 152 | /** 153 | * Create invokable factories for invokable classes. 154 | * 155 | * If an invokable service name does not match the class it maps to, this 156 | * creates an invokable factory entry for the class name; otherwise, it 157 | * creates an invokable factory for the entry name. 158 | * 159 | * @param array $invokables 160 | * @return array 161 | */ 162 | protected function createFactoriesForInvokables(array $invokables) 163 | { 164 | $factories = []; 165 | foreach ($invokables as $name => $class) { 166 | if ($name === $class) { 167 | $factories[$name] = RouteInvokableFactory::class; 168 | continue; 169 | } 170 | 171 | $factories[$class] = RouteInvokableFactory::class; 172 | } 173 | return $factories; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Http/Wildcard.php: -------------------------------------------------------------------------------- 1 | keyValueDelimiter = $keyValueDelimiter; 62 | $this->paramDelimiter = $paramDelimiter; 63 | $this->defaults = $defaults; 64 | } 65 | 66 | /** 67 | * factory(): defined by RouteInterface interface. 68 | * 69 | * @see \Zend\Router\RouteInterface::factory() 70 | * @param array|Traversable $options 71 | * @return Wildcard 72 | * @throws Exception\InvalidArgumentException 73 | */ 74 | public static function factory($options = []) 75 | { 76 | if ($options instanceof Traversable) { 77 | $options = ArrayUtils::iteratorToArray($options); 78 | } elseif (! is_array($options)) { 79 | throw new Exception\InvalidArgumentException(sprintf( 80 | '%s expects an array or Traversable set of options', 81 | __METHOD__ 82 | )); 83 | } 84 | 85 | if (! isset($options['key_value_delimiter'])) { 86 | $options['key_value_delimiter'] = '/'; 87 | } 88 | 89 | if (! isset($options['param_delimiter'])) { 90 | $options['param_delimiter'] = '/'; 91 | } 92 | 93 | if (! isset($options['defaults'])) { 94 | $options['defaults'] = []; 95 | } 96 | 97 | return new static($options['key_value_delimiter'], $options['param_delimiter'], $options['defaults']); 98 | } 99 | 100 | /** 101 | * match(): defined by RouteInterface interface. 102 | * 103 | * @see \Zend\Router\RouteInterface::match() 104 | * @param Request $request 105 | * @param integer|null $pathOffset 106 | * @return RouteMatch|null 107 | */ 108 | public function match(Request $request, $pathOffset = null) 109 | { 110 | if (! method_exists($request, 'getUri')) { 111 | return; 112 | } 113 | 114 | $uri = $request->getUri(); 115 | $path = $uri->getPath() ?: ''; 116 | 117 | if ($path === '/') { 118 | $path = ''; 119 | } 120 | 121 | if ($pathOffset !== null) { 122 | $path = substr($path, $pathOffset) ?: ''; 123 | } 124 | 125 | $matches = []; 126 | $params = explode($this->paramDelimiter, $path); 127 | 128 | if (count($params) > 1 && ($params[0] !== '' || end($params) === '')) { 129 | return; 130 | } 131 | 132 | if ($this->keyValueDelimiter === $this->paramDelimiter) { 133 | $count = count($params); 134 | 135 | for ($i = 1; $i < $count; $i += 2) { 136 | if (isset($params[$i + 1])) { 137 | $matches[rawurldecode($params[$i])] = rawurldecode($params[$i + 1]); 138 | } 139 | } 140 | } else { 141 | array_shift($params); 142 | 143 | foreach ($params as $param) { 144 | $param = explode($this->keyValueDelimiter, $param, 2); 145 | 146 | if (isset($param[1])) { 147 | $matches[rawurldecode($param[0])] = rawurldecode($param[1]); 148 | } 149 | } 150 | } 151 | 152 | return new RouteMatch(array_merge($this->defaults, $matches), strlen($path)); 153 | } 154 | 155 | /** 156 | * assemble(): Defined by RouteInterface interface. 157 | * 158 | * @see \Zend\Router\RouteInterface::assemble() 159 | * @param array $params 160 | * @param array $options 161 | * @return mixed 162 | */ 163 | public function assemble(array $params = [], array $options = []) 164 | { 165 | $elements = []; 166 | $mergedParams = array_merge($this->defaults, $params); 167 | $this->assembledParams = []; 168 | 169 | if ($mergedParams) { 170 | foreach ($mergedParams as $key => $value) { 171 | if (! is_scalar($value)) { 172 | continue; 173 | } 174 | 175 | $elements[] = rawurlencode($key) . $this->keyValueDelimiter . rawurlencode($value); 176 | $this->assembledParams[] = $key; 177 | } 178 | 179 | return $this->paramDelimiter . implode($this->paramDelimiter, $elements); 180 | } 181 | 182 | return ''; 183 | } 184 | 185 | /** 186 | * getAssembledParams(): defined by RouteInterface interface. 187 | * 188 | * @see RouteInterface::getAssembledParams 189 | * @return array 190 | */ 191 | public function getAssembledParams() 192 | { 193 | return $this->assembledParams; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Http/Chain.php: -------------------------------------------------------------------------------- 1 | chainRoutes = array_reverse($routes); 47 | $this->routePluginManager = $routePlugins; 48 | $this->routes = new PriorityList(); 49 | $this->prototypes = $prototypes; 50 | } 51 | 52 | /** 53 | * factory(): defined by RouteInterface interface. 54 | * 55 | * @see \Zend\Router\RouteInterface::factory() 56 | * @param mixed $options 57 | * @throws Exception\InvalidArgumentException 58 | * @return Part 59 | */ 60 | public static function factory($options = []) 61 | { 62 | if ($options instanceof Traversable) { 63 | $options = ArrayUtils::iteratorToArray($options); 64 | } elseif (! is_array($options)) { 65 | throw new Exception\InvalidArgumentException(sprintf( 66 | '%s expects an array or Traversable set of options', 67 | __METHOD__ 68 | )); 69 | } 70 | 71 | if (! isset($options['routes'])) { 72 | throw new Exception\InvalidArgumentException('Missing "routes" in options array'); 73 | } 74 | 75 | if (! isset($options['prototypes'])) { 76 | $options['prototypes'] = null; 77 | } 78 | 79 | if ($options['routes'] instanceof Traversable) { 80 | $options['routes'] = ArrayUtils::iteratorToArray($options['child_routes']); 81 | } 82 | 83 | if (! isset($options['route_plugins'])) { 84 | throw new Exception\InvalidArgumentException('Missing "route_plugins" in options array'); 85 | } 86 | 87 | return new static( 88 | $options['routes'], 89 | $options['route_plugins'], 90 | $options['prototypes'] 91 | ); 92 | } 93 | 94 | /** 95 | * match(): defined by RouteInterface interface. 96 | * 97 | * @see \Zend\Router\RouteInterface::match() 98 | * @param Request $request 99 | * @param int|null $pathOffset 100 | * @param array $options 101 | * @return RouteMatch|null 102 | */ 103 | public function match(Request $request, $pathOffset = null, array $options = []) 104 | { 105 | if (! method_exists($request, 'getUri')) { 106 | return; 107 | } 108 | 109 | if ($pathOffset === null) { 110 | $mustTerminate = true; 111 | $pathOffset = 0; 112 | } else { 113 | $mustTerminate = false; 114 | } 115 | 116 | if ($this->chainRoutes !== null) { 117 | $this->addRoutes($this->chainRoutes); 118 | $this->chainRoutes = null; 119 | } 120 | 121 | $match = new RouteMatch([]); 122 | $uri = $request->getUri(); 123 | $pathLength = strlen($uri->getPath()); 124 | 125 | foreach ($this->routes as $route) { 126 | $subMatch = $route->match($request, $pathOffset, $options); 127 | 128 | if ($subMatch === null) { 129 | return; 130 | } 131 | 132 | $match->merge($subMatch); 133 | $pathOffset += $subMatch->getLength(); 134 | } 135 | 136 | if ($mustTerminate && $pathOffset !== $pathLength) { 137 | return; 138 | } 139 | 140 | return $match; 141 | } 142 | 143 | /** 144 | * assemble(): Defined by RouteInterface interface. 145 | * 146 | * @see \Zend\Router\RouteInterface::assemble() 147 | * @param array $params 148 | * @param array $options 149 | * @return mixed 150 | */ 151 | public function assemble(array $params = [], array $options = []) 152 | { 153 | if ($this->chainRoutes !== null) { 154 | $this->addRoutes($this->chainRoutes); 155 | $this->chainRoutes = null; 156 | } 157 | 158 | $this->assembledParams = []; 159 | 160 | $routes = ArrayUtils::iteratorToArray($this->routes); 161 | 162 | end($routes); 163 | $lastRouteKey = key($routes); 164 | $path = ''; 165 | 166 | foreach ($routes as $key => $route) { 167 | $chainOptions = $options; 168 | $hasChild = isset($options['has_child']) ? $options['has_child'] : false; 169 | 170 | $chainOptions['has_child'] = ($hasChild || $key !== $lastRouteKey); 171 | 172 | $path .= $route->assemble($params, $chainOptions); 173 | $params = array_diff_key($params, array_flip($route->getAssembledParams())); 174 | 175 | $this->assembledParams += $route->getAssembledParams(); 176 | } 177 | 178 | return $path; 179 | } 180 | 181 | /** 182 | * getAssembledParams(): defined by RouteInterface interface. 183 | * 184 | * @see RouteInterface::getAssembledParams 185 | * @return array 186 | */ 187 | public function getAssembledParams() 188 | { 189 | return $this->assembledParams; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Http/Part.php: -------------------------------------------------------------------------------- 1 | routePluginManager = $routePlugins; 62 | 63 | if (! $route instanceof RouteInterface) { 64 | $route = $this->routeFromArray($route); 65 | } 66 | 67 | if ($route instanceof self) { 68 | throw new Exception\InvalidArgumentException('Base route may not be a part route'); 69 | } 70 | 71 | $this->route = $route; 72 | $this->mayTerminate = $mayTerminate; 73 | $this->childRoutes = $childRoutes; 74 | $this->prototypes = $prototypes; 75 | $this->routes = new PriorityList(); 76 | } 77 | 78 | /** 79 | * factory(): defined by RouteInterface interface. 80 | * 81 | * @see \Zend\Router\RouteInterface::factory() 82 | * @param mixed $options 83 | * @return Part 84 | * @throws Exception\InvalidArgumentException 85 | */ 86 | public static function factory($options = []) 87 | { 88 | if ($options instanceof Traversable) { 89 | $options = ArrayUtils::iteratorToArray($options); 90 | } elseif (! is_array($options)) { 91 | throw new Exception\InvalidArgumentException(sprintf( 92 | '%s expects an array or Traversable set of options', 93 | __METHOD__ 94 | )); 95 | } 96 | 97 | if (! isset($options['route'])) { 98 | throw new Exception\InvalidArgumentException('Missing "route" in options array'); 99 | } 100 | 101 | if (! isset($options['route_plugins'])) { 102 | throw new Exception\InvalidArgumentException('Missing "route_plugins" in options array'); 103 | } 104 | 105 | if (! isset($options['prototypes'])) { 106 | $options['prototypes'] = null; 107 | } 108 | 109 | if (! isset($options['may_terminate'])) { 110 | $options['may_terminate'] = false; 111 | } 112 | 113 | if (! isset($options['child_routes']) || ! $options['child_routes']) { 114 | $options['child_routes'] = null; 115 | } 116 | 117 | if ($options['child_routes'] instanceof Traversable) { 118 | $options['child_routes'] = ArrayUtils::iteratorToArray($options['child_routes']); 119 | } 120 | 121 | return new static( 122 | $options['route'], 123 | $options['may_terminate'], 124 | $options['route_plugins'], 125 | $options['child_routes'], 126 | $options['prototypes'] 127 | ); 128 | } 129 | 130 | /** 131 | * match(): defined by RouteInterface interface. 132 | * 133 | * @see \Zend\Router\RouteInterface::match() 134 | * @param Request $request 135 | * @param integer|null $pathOffset 136 | * @param array $options 137 | * @return RouteMatch|null 138 | */ 139 | public function match(Request $request, $pathOffset = null, array $options = []) 140 | { 141 | if ($pathOffset === null) { 142 | $pathOffset = 0; 143 | } 144 | 145 | $match = $this->route->match($request, $pathOffset, $options); 146 | 147 | if ($match !== null && method_exists($request, 'getUri')) { 148 | if ($this->childRoutes !== null) { 149 | $this->addRoutes($this->childRoutes); 150 | $this->childRoutes = null; 151 | } 152 | 153 | $nextOffset = $pathOffset + $match->getLength(); 154 | 155 | $uri = $request->getUri(); 156 | $pathLength = strlen($uri->getPath()); 157 | 158 | if ($this->mayTerminate && $nextOffset === $pathLength) { 159 | return $match; 160 | } 161 | 162 | if (isset($options['translator']) 163 | && ! isset($options['locale']) 164 | && null !== ($locale = $match->getParam('locale', null)) 165 | ) { 166 | $options['locale'] = $locale; 167 | } 168 | 169 | foreach ($this->routes as $name => $route) { 170 | if (($subMatch = $route->match($request, $nextOffset, $options)) instanceof RouteMatch) { 171 | if ($match->getLength() + $subMatch->getLength() + $pathOffset === $pathLength) { 172 | return $match->merge($subMatch)->setMatchedRouteName($name); 173 | } 174 | } 175 | } 176 | } 177 | 178 | return; 179 | } 180 | 181 | /** 182 | * assemble(): Defined by RouteInterface interface. 183 | * 184 | * @see \Zend\Router\RouteInterface::assemble() 185 | * @param array $params 186 | * @param array $options 187 | * @return mixed 188 | * @throws Exception\RuntimeException 189 | */ 190 | public function assemble(array $params = [], array $options = []) 191 | { 192 | if ($this->childRoutes !== null) { 193 | $this->addRoutes($this->childRoutes); 194 | $this->childRoutes = null; 195 | } 196 | 197 | $options['has_child'] = (isset($options['name'])); 198 | 199 | if (isset($options['translator']) && ! isset($options['locale']) && isset($params['locale'])) { 200 | $options['locale'] = $params['locale']; 201 | } 202 | 203 | $path = $this->route->assemble($params, $options); 204 | $params = array_diff_key($params, array_flip($this->route->getAssembledParams())); 205 | 206 | if (! isset($options['name'])) { 207 | if (! $this->mayTerminate) { 208 | throw new Exception\RuntimeException('Part route may not terminate'); 209 | } else { 210 | return $path; 211 | } 212 | } 213 | 214 | unset($options['has_child']); 215 | $options['only_return_path'] = true; 216 | $path .= parent::assemble($params, $options); 217 | 218 | return $path; 219 | } 220 | 221 | /** 222 | * getAssembledParams(): defined by RouteInterface interface. 223 | * 224 | * @see RouteInterface::getAssembledParams 225 | * @return array 226 | */ 227 | public function getAssembledParams() 228 | { 229 | // Part routes may not occur as base route of other part routes, so we 230 | // don't have to return anything here. 231 | return []; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/SimpleRouteStack.php: -------------------------------------------------------------------------------- 1 | routes = new PriorityList(); 49 | 50 | if (null === $routePluginManager) { 51 | $routePluginManager = new RoutePluginManager(new ServiceManager()); 52 | } 53 | 54 | $this->routePluginManager = $routePluginManager; 55 | 56 | $this->init(); 57 | } 58 | 59 | /** 60 | * factory(): defined by RouteInterface interface. 61 | * 62 | * @see \Zend\Router\RouteInterface::factory() 63 | * @param array|Traversable $options 64 | * @return SimpleRouteStack 65 | * @throws Exception\InvalidArgumentException 66 | */ 67 | public static function factory($options = []) 68 | { 69 | if ($options instanceof Traversable) { 70 | $options = ArrayUtils::iteratorToArray($options); 71 | } elseif (! is_array($options)) { 72 | throw new Exception\InvalidArgumentException(sprintf( 73 | '%s expects an array or Traversable set of options', 74 | __METHOD__ 75 | )); 76 | } 77 | 78 | $routePluginManager = null; 79 | if (isset($options['route_plugins'])) { 80 | $routePluginManager = $options['route_plugins']; 81 | } 82 | 83 | $instance = new static($routePluginManager); 84 | 85 | if (isset($options['routes'])) { 86 | $instance->addRoutes($options['routes']); 87 | } 88 | 89 | if (isset($options['default_params'])) { 90 | $instance->setDefaultParams($options['default_params']); 91 | } 92 | 93 | return $instance; 94 | } 95 | 96 | /** 97 | * Init method for extending classes. 98 | * 99 | * @return void 100 | */ 101 | protected function init() 102 | { 103 | } 104 | 105 | /** 106 | * Set the route plugin manager. 107 | * 108 | * @param RoutePluginManager $routePlugins 109 | * @return SimpleRouteStack 110 | */ 111 | public function setRoutePluginManager(RoutePluginManager $routePlugins) 112 | { 113 | $this->routePluginManager = $routePlugins; 114 | return $this; 115 | } 116 | 117 | /** 118 | * Get the route plugin manager. 119 | * 120 | * @return RoutePluginManager 121 | */ 122 | public function getRoutePluginManager() 123 | { 124 | return $this->routePluginManager; 125 | } 126 | 127 | /** 128 | * addRoutes(): defined by RouteStackInterface interface. 129 | * 130 | * @see RouteStackInterface::addRoutes() 131 | * @param array|Traversable $routes 132 | * @return SimpleRouteStack 133 | * @throws Exception\InvalidArgumentException 134 | */ 135 | public function addRoutes($routes) 136 | { 137 | if (! is_array($routes) && ! $routes instanceof Traversable) { 138 | throw new Exception\InvalidArgumentException('addRoutes expects an array or Traversable set of routes'); 139 | } 140 | 141 | foreach ($routes as $name => $route) { 142 | $this->addRoute($name, $route); 143 | } 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * addRoute(): defined by RouteStackInterface interface. 150 | * 151 | * @see RouteStackInterface::addRoute() 152 | * @param string $name 153 | * @param mixed $route 154 | * @param int $priority 155 | * @return SimpleRouteStack 156 | */ 157 | public function addRoute($name, $route, $priority = null) 158 | { 159 | if (! $route instanceof RouteInterface) { 160 | $route = $this->routeFromArray($route); 161 | } 162 | 163 | if ($priority === null && isset($route->priority)) { 164 | $priority = $route->priority; 165 | } 166 | 167 | $this->routes->insert($name, $route, $priority); 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * removeRoute(): defined by RouteStackInterface interface. 174 | * 175 | * @see RouteStackInterface::removeRoute() 176 | * @param string $name 177 | * @return SimpleRouteStack 178 | */ 179 | public function removeRoute($name) 180 | { 181 | $this->routes->remove($name); 182 | return $this; 183 | } 184 | 185 | /** 186 | * setRoutes(): defined by RouteStackInterface interface. 187 | * 188 | * @param array|Traversable $routes 189 | * @return SimpleRouteStack 190 | */ 191 | public function setRoutes($routes) 192 | { 193 | $this->routes->clear(); 194 | $this->addRoutes($routes); 195 | return $this; 196 | } 197 | 198 | /** 199 | * Get the added routes 200 | * 201 | * @return Traversable list of all routes 202 | */ 203 | public function getRoutes() 204 | { 205 | return $this->routes; 206 | } 207 | 208 | /** 209 | * Check if a route with a specific name exists 210 | * 211 | * @param string $name 212 | * @return bool true if route exists 213 | */ 214 | public function hasRoute($name) 215 | { 216 | return $this->routes->get($name) !== null; 217 | } 218 | 219 | /** 220 | * Get a route by name 221 | * 222 | * @param string $name 223 | * @return RouteInterface the route 224 | */ 225 | public function getRoute($name) 226 | { 227 | return $this->routes->get($name); 228 | } 229 | 230 | /** 231 | * Set a default parameters. 232 | * 233 | * @param array $params 234 | * @return SimpleRouteStack 235 | */ 236 | public function setDefaultParams(array $params) 237 | { 238 | $this->defaultParams = $params; 239 | return $this; 240 | } 241 | 242 | /** 243 | * Set a default parameter. 244 | * 245 | * @param string $name 246 | * @param mixed $value 247 | * @return SimpleRouteStack 248 | */ 249 | public function setDefaultParam($name, $value) 250 | { 251 | $this->defaultParams[$name] = $value; 252 | return $this; 253 | } 254 | 255 | /** 256 | * Create a route from array specifications. 257 | * 258 | * @param array|Traversable $specs 259 | * @return RouteInterface 260 | * @throws Exception\InvalidArgumentException 261 | */ 262 | protected function routeFromArray($specs) 263 | { 264 | if ($specs instanceof Traversable) { 265 | $specs = ArrayUtils::iteratorToArray($specs); 266 | } 267 | 268 | if (! is_array($specs)) { 269 | throw new Exception\InvalidArgumentException('Route definition must be an array or Traversable object'); 270 | } 271 | 272 | if (! isset($specs['type'])) { 273 | throw new Exception\InvalidArgumentException('Missing "type" option'); 274 | } 275 | 276 | if (! isset($specs['options'])) { 277 | $specs['options'] = []; 278 | } 279 | 280 | $route = $this->getRoutePluginManager()->get($specs['type'], $specs['options']); 281 | 282 | if (isset($specs['priority'])) { 283 | $route->priority = $specs['priority']; 284 | } 285 | 286 | return $route; 287 | } 288 | 289 | /** 290 | * match(): defined by RouteInterface interface. 291 | * 292 | * @see \Zend\Router\RouteInterface::match() 293 | * @param Request $request 294 | * @return RouteMatch|null 295 | */ 296 | public function match(Request $request) 297 | { 298 | foreach ($this->routes as $name => $route) { 299 | if (($match = $route->match($request)) instanceof RouteMatch) { 300 | $match->setMatchedRouteName($name); 301 | 302 | foreach ($this->defaultParams as $paramName => $value) { 303 | if ($match->getParam($paramName) === null) { 304 | $match->setParam($paramName, $value); 305 | } 306 | } 307 | 308 | return $match; 309 | } 310 | } 311 | 312 | return; 313 | } 314 | 315 | /** 316 | * assemble(): defined by RouteInterface interface. 317 | * 318 | * @see \Zend\Router\RouteInterface::assemble() 319 | * @param array $params 320 | * @param array $options 321 | * @return mixed 322 | * @throws Exception\InvalidArgumentException 323 | * @throws Exception\RuntimeException 324 | */ 325 | public function assemble(array $params = [], array $options = []) 326 | { 327 | if (! isset($options['name'])) { 328 | throw new Exception\InvalidArgumentException('Missing "name" option'); 329 | } 330 | 331 | $route = $this->routes->get($options['name']); 332 | 333 | if (! $route) { 334 | throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $options['name'])); 335 | } 336 | 337 | unset($options['name']); 338 | 339 | return $route->assemble(array_merge($this->defaultParams, $params), $options); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/Http/Hostname.php: -------------------------------------------------------------------------------- 1 | defaults = $defaults; 65 | $this->parts = $this->parseRouteDefinition($route); 66 | $this->regex = $this->buildRegex($this->parts, $constraints); 67 | } 68 | 69 | /** 70 | * factory(): defined by RouteInterface interface. 71 | * 72 | * @see \Zend\Router\RouteInterface::factory() 73 | * @param array|Traversable $options 74 | * @return Hostname 75 | * @throws Exception\InvalidArgumentException 76 | */ 77 | public static function factory($options = []) 78 | { 79 | if ($options instanceof Traversable) { 80 | $options = ArrayUtils::iteratorToArray($options); 81 | } elseif (! is_array($options)) { 82 | throw new Exception\InvalidArgumentException(sprintf( 83 | '%s expects an array or Traversable set of options', 84 | __METHOD__ 85 | )); 86 | } 87 | 88 | if (! isset($options['route'])) { 89 | throw new Exception\InvalidArgumentException('Missing "route" in options array'); 90 | } 91 | 92 | if (! isset($options['constraints'])) { 93 | $options['constraints'] = []; 94 | } 95 | 96 | if (! isset($options['defaults'])) { 97 | $options['defaults'] = []; 98 | } 99 | 100 | return new static($options['route'], $options['constraints'], $options['defaults']); 101 | } 102 | 103 | /** 104 | * Parse a route definition. 105 | * 106 | * @param string $def 107 | * @return array 108 | * @throws Exception\RuntimeException 109 | */ 110 | protected function parseRouteDefinition($def) 111 | { 112 | $currentPos = 0; 113 | $length = strlen($def); 114 | $parts = []; 115 | $levelParts = [&$parts]; 116 | $level = 0; 117 | 118 | while ($currentPos < $length) { 119 | if (! preg_match('(\G(?P[a-z0-9-.]*)(?P[:{\[\]]|$))', $def, $matches, 0, $currentPos)) { 120 | throw new Exception\RuntimeException('Matched hostname literal contains a disallowed character'); 121 | } 122 | 123 | $currentPos += strlen($matches[0]); 124 | 125 | if (! empty($matches['literal'])) { 126 | $levelParts[$level][] = ['literal', $matches['literal']]; 127 | } 128 | 129 | if ($matches['token'] === ':') { 130 | if (! preg_match( 131 | '(\G(?P[^:.{\[\]]+)(?:{(?P[^}]+)})?:?)', 132 | $def, 133 | $matches, 134 | 0, 135 | $currentPos 136 | )) { 137 | throw new Exception\RuntimeException('Found empty parameter name'); 138 | } 139 | 140 | $levelParts[$level][] = [ 141 | 'parameter', 142 | $matches['name'], 143 | isset($matches['delimiters']) ? $matches['delimiters'] : null 144 | ]; 145 | 146 | $currentPos += strlen($matches[0]); 147 | } elseif ($matches['token'] === '[') { 148 | $levelParts[$level][] = ['optional', []]; 149 | $levelParts[$level + 1] = &$levelParts[$level][count($levelParts[$level]) - 1][1]; 150 | 151 | $level++; 152 | } elseif ($matches['token'] === ']') { 153 | unset($levelParts[$level]); 154 | $level--; 155 | 156 | if ($level < 0) { 157 | throw new Exception\RuntimeException('Found closing bracket without matching opening bracket'); 158 | } 159 | } else { 160 | break; 161 | } 162 | } 163 | 164 | if ($level > 0) { 165 | throw new Exception\RuntimeException('Found unbalanced brackets'); 166 | } 167 | 168 | return $parts; 169 | } 170 | 171 | /** 172 | * Build the matching regex from parsed parts. 173 | * 174 | * @param array $parts 175 | * @param array $constraints 176 | * @param int $groupIndex 177 | * @return string 178 | * @throws Exception\RuntimeException 179 | */ 180 | protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1) 181 | { 182 | $regex = ''; 183 | 184 | foreach ($parts as $part) { 185 | switch ($part[0]) { 186 | case 'literal': 187 | $regex .= preg_quote($part[1]); 188 | break; 189 | 190 | case 'parameter': 191 | $groupName = '?P'; 192 | 193 | if (isset($constraints[$part[1]])) { 194 | $regex .= '(' . $groupName . $constraints[$part[1]] . ')'; 195 | } elseif ($part[2] === null) { 196 | $regex .= '(' . $groupName . '[^.]+)'; 197 | } else { 198 | $regex .= '(' . $groupName . '[^' . $part[2] . ']+)'; 199 | } 200 | 201 | $this->paramMap['param' . $groupIndex++] = $part[1]; 202 | break; 203 | 204 | case 'optional': 205 | $regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')?'; 206 | break; 207 | } 208 | } 209 | 210 | return $regex; 211 | } 212 | 213 | /** 214 | * Build host. 215 | * 216 | * @param array $parts 217 | * @param array $mergedParams 218 | * @param bool $isOptional 219 | * @return string 220 | * @throws Exception\RuntimeException 221 | * @throws Exception\InvalidArgumentException 222 | */ 223 | protected function buildHost(array $parts, array $mergedParams, $isOptional) 224 | { 225 | $host = ''; 226 | $skip = true; 227 | $skippable = false; 228 | 229 | foreach ($parts as $part) { 230 | switch ($part[0]) { 231 | case 'literal': 232 | $host .= $part[1]; 233 | break; 234 | 235 | case 'parameter': 236 | $skippable = true; 237 | 238 | if (! isset($mergedParams[$part[1]])) { 239 | if (! $isOptional) { 240 | throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); 241 | } 242 | 243 | return ''; 244 | } elseif (! $isOptional 245 | || ! isset($this->defaults[$part[1]]) 246 | || $this->defaults[$part[1]] !== $mergedParams[$part[1]] 247 | ) { 248 | $skip = false; 249 | } 250 | 251 | $host .= $mergedParams[$part[1]]; 252 | 253 | $this->assembledParams[] = $part[1]; 254 | break; 255 | 256 | case 'optional': 257 | $skippable = true; 258 | $optionalPart = $this->buildHost($part[1], $mergedParams, true); 259 | 260 | if ($optionalPart !== '') { 261 | $host .= $optionalPart; 262 | $skip = false; 263 | } 264 | break; 265 | } 266 | } 267 | 268 | if ($isOptional && $skippable && $skip) { 269 | return ''; 270 | } 271 | 272 | return $host; 273 | } 274 | 275 | /** 276 | * match(): defined by RouteInterface interface. 277 | * 278 | * @see \Zend\Router\RouteInterface::match() 279 | * @param Request $request 280 | * @return RouteMatch|null 281 | */ 282 | public function match(Request $request) 283 | { 284 | if (! method_exists($request, 'getUri')) { 285 | return; 286 | } 287 | 288 | $uri = $request->getUri(); 289 | $host = $uri->getHost(); 290 | 291 | $result = preg_match('(^' . $this->regex . '$)', $host, $matches); 292 | 293 | if (! $result) { 294 | return; 295 | } 296 | 297 | $params = []; 298 | 299 | foreach ($this->paramMap as $index => $name) { 300 | if (isset($matches[$index]) && $matches[$index] !== '') { 301 | $params[$name] = $matches[$index]; 302 | } 303 | } 304 | 305 | return new RouteMatch(array_merge($this->defaults, $params)); 306 | } 307 | 308 | /** 309 | * assemble(): Defined by RouteInterface interface. 310 | * 311 | * @see \Zend\Router\RouteInterface::assemble() 312 | * @param array $params 313 | * @param array $options 314 | * @return mixed 315 | */ 316 | public function assemble(array $params = [], array $options = []) 317 | { 318 | $this->assembledParams = []; 319 | 320 | if (isset($options['uri'])) { 321 | $host = $this->buildHost( 322 | $this->parts, 323 | array_merge($this->defaults, $params), 324 | false 325 | ); 326 | 327 | $options['uri']->setHost($host); 328 | } 329 | 330 | // A hostname does not contribute to the path, thus nothing is returned. 331 | return ''; 332 | } 333 | 334 | /** 335 | * getAssembledParams(): defined by RouteInterface interface. 336 | * 337 | * @see RouteInterface::getAssembledParams 338 | * @return array 339 | */ 340 | public function getAssembledParams() 341 | { 342 | return $this->assembledParams; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/Http/Segment.php: -------------------------------------------------------------------------------- 1 | "!", // sub-delims 42 | '%24' => "$", // sub-delims 43 | '%26' => "&", // sub-delims 44 | '%27' => "'", // sub-delims 45 | '%28' => "(", // sub-delims 46 | '%29' => ")", // sub-delims 47 | '%2A' => "*", // sub-delims 48 | '%2B' => "+", // sub-delims 49 | '%2C' => ",", // sub-delims 50 | // '%2D' => "-", // unreserved - not touched by rawurlencode 51 | // '%2E' => ".", // unreserved - not touched by rawurlencode 52 | '%3A' => ":", // pchar 53 | '%3B' => ";", // sub-delims 54 | '%3D' => "=", // sub-delims 55 | '%40' => "@", // pchar 56 | // '%5F' => "_", // unreserved - not touched by rawurlencode 57 | // '%7E' => "~", // unreserved - not touched by rawurlencode 58 | ]; 59 | 60 | /** 61 | * Parts of the route. 62 | * 63 | * @var array 64 | */ 65 | protected $parts; 66 | 67 | /** 68 | * Regex used for matching the route. 69 | * 70 | * @var string 71 | */ 72 | protected $regex; 73 | 74 | /** 75 | * Map from regex groups to parameter names. 76 | * 77 | * @var array 78 | */ 79 | protected $paramMap = []; 80 | 81 | /** 82 | * Default values. 83 | * 84 | * @var array 85 | */ 86 | protected $defaults; 87 | 88 | /** 89 | * List of assembled parameters. 90 | * 91 | * @var array 92 | */ 93 | protected $assembledParams = []; 94 | 95 | /** 96 | * Translation keys used in the regex. 97 | * 98 | * @var array 99 | */ 100 | protected $translationKeys = []; 101 | 102 | /** 103 | * Create a new regex route. 104 | * 105 | * @param string $route 106 | * @param array $constraints 107 | * @param array $defaults 108 | */ 109 | public function __construct($route, array $constraints = [], array $defaults = []) 110 | { 111 | $this->defaults = $defaults; 112 | $this->parts = $this->parseRouteDefinition($route); 113 | $this->regex = $this->buildRegex($this->parts, $constraints); 114 | } 115 | 116 | /** 117 | * factory(): defined by RouteInterface interface. 118 | * 119 | * @see \Zend\Router\RouteInterface::factory() 120 | * @param array|Traversable $options 121 | * @return Segment 122 | * @throws Exception\InvalidArgumentException 123 | */ 124 | public static function factory($options = []) 125 | { 126 | if ($options instanceof Traversable) { 127 | $options = ArrayUtils::iteratorToArray($options); 128 | } elseif (! is_array($options)) { 129 | throw new Exception\InvalidArgumentException(sprintf( 130 | '%s expects an array or Traversable set of options', 131 | __METHOD__ 132 | )); 133 | } 134 | 135 | if (! isset($options['route'])) { 136 | throw new Exception\InvalidArgumentException('Missing "route" in options array'); 137 | } 138 | 139 | if (! isset($options['constraints'])) { 140 | $options['constraints'] = []; 141 | } 142 | 143 | if (! isset($options['defaults'])) { 144 | $options['defaults'] = []; 145 | } 146 | 147 | return new static($options['route'], $options['constraints'], $options['defaults']); 148 | } 149 | 150 | /** 151 | * Parse a route definition. 152 | * 153 | * @param string $def 154 | * @return array 155 | * @throws Exception\RuntimeException 156 | */ 157 | protected function parseRouteDefinition($def) 158 | { 159 | $currentPos = 0; 160 | $length = strlen($def); 161 | $parts = []; 162 | $levelParts = [&$parts]; 163 | $level = 0; 164 | 165 | while ($currentPos < $length) { 166 | preg_match('(\G(?P[^:{\[\]]*)(?P[:{\[\]]|$))', $def, $matches, 0, $currentPos); 167 | 168 | $currentPos += strlen($matches[0]); 169 | 170 | if (! empty($matches['literal'])) { 171 | $levelParts[$level][] = ['literal', $matches['literal']]; 172 | } 173 | 174 | if ($matches['token'] === ':') { 175 | if (! preg_match( 176 | '(\G(?P[^:/{\[\]]+)(?:{(?P[^}]+)})?:?)', 177 | $def, 178 | $matches, 179 | 0, 180 | $currentPos 181 | )) { 182 | throw new Exception\RuntimeException('Found empty parameter name'); 183 | } 184 | 185 | $levelParts[$level][] = [ 186 | 'parameter', 187 | $matches['name'], 188 | isset($matches['delimiters']) ? $matches['delimiters'] : null 189 | ]; 190 | 191 | $currentPos += strlen($matches[0]); 192 | } elseif ($matches['token'] === '{') { 193 | if (! preg_match('(\G(?P[^}]+)\})', $def, $matches, 0, $currentPos)) { 194 | throw new Exception\RuntimeException('Translated literal missing closing bracket'); 195 | } 196 | 197 | $currentPos += strlen($matches[0]); 198 | 199 | $levelParts[$level][] = ['translated-literal', $matches['literal']]; 200 | } elseif ($matches['token'] === '[') { 201 | $levelParts[$level][] = ['optional', []]; 202 | $levelParts[$level + 1] = &$levelParts[$level][count($levelParts[$level]) - 1][1]; 203 | 204 | $level++; 205 | } elseif ($matches['token'] === ']') { 206 | unset($levelParts[$level]); 207 | $level--; 208 | 209 | if ($level < 0) { 210 | throw new Exception\RuntimeException('Found closing bracket without matching opening bracket'); 211 | } 212 | } else { 213 | break; 214 | } 215 | } 216 | 217 | if ($level > 0) { 218 | throw new Exception\RuntimeException('Found unbalanced brackets'); 219 | } 220 | 221 | return $parts; 222 | } 223 | 224 | /** 225 | * Build the matching regex from parsed parts. 226 | * 227 | * @param array $parts 228 | * @param array $constraints 229 | * @param int $groupIndex 230 | * @return string 231 | */ 232 | protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1) 233 | { 234 | $regex = ''; 235 | 236 | foreach ($parts as $part) { 237 | switch ($part[0]) { 238 | case 'literal': 239 | $regex .= preg_quote($part[1]); 240 | break; 241 | 242 | case 'parameter': 243 | $groupName = '?P'; 244 | 245 | if (isset($constraints[$part[1]])) { 246 | $regex .= '(' . $groupName . $constraints[$part[1]] . ')'; 247 | } elseif ($part[2] === null) { 248 | $regex .= '(' . $groupName . '[^/]+)'; 249 | } else { 250 | $regex .= '(' . $groupName . '[^' . $part[2] . ']+)'; 251 | } 252 | 253 | $this->paramMap['param' . $groupIndex++] = $part[1]; 254 | break; 255 | 256 | case 'optional': 257 | $regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')?'; 258 | break; 259 | 260 | case 'translated-literal': 261 | $regex .= '#' . $part[1] . '#'; 262 | $this->translationKeys[] = $part[1]; 263 | break; 264 | } 265 | } 266 | 267 | return $regex; 268 | } 269 | 270 | /** 271 | * Build a path. 272 | * 273 | * @param array $parts 274 | * @param array $mergedParams 275 | * @param bool $isOptional 276 | * @param bool $hasChild 277 | * @param array $options 278 | * @return string 279 | * @throws Exception\InvalidArgumentException 280 | * @throws Exception\RuntimeException 281 | */ 282 | protected function buildPath(array $parts, array $mergedParams, $isOptional, $hasChild, array $options) 283 | { 284 | if ($this->translationKeys) { 285 | if (! isset($options['translator']) || ! $options['translator'] instanceof Translator) { 286 | throw new Exception\RuntimeException('No translator provided'); 287 | } 288 | 289 | $translator = $options['translator']; 290 | $textDomain = (isset($options['text_domain']) ? $options['text_domain'] : 'default'); 291 | $locale = (isset($options['locale']) ? $options['locale'] : null); 292 | } 293 | 294 | $path = ''; 295 | $skip = true; 296 | $skippable = false; 297 | 298 | foreach ($parts as $part) { 299 | switch ($part[0]) { 300 | case 'literal': 301 | $path .= $part[1]; 302 | break; 303 | 304 | case 'parameter': 305 | $skippable = true; 306 | 307 | if (! isset($mergedParams[$part[1]])) { 308 | if (! $isOptional || $hasChild) { 309 | throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); 310 | } 311 | 312 | return ''; 313 | } elseif (! $isOptional 314 | || $hasChild 315 | || ! isset($this->defaults[$part[1]]) 316 | || $this->defaults[$part[1]] !== $mergedParams[$part[1]] 317 | ) { 318 | $skip = false; 319 | } 320 | 321 | $path .= $this->encode($mergedParams[$part[1]]); 322 | 323 | $this->assembledParams[] = $part[1]; 324 | break; 325 | 326 | case 'optional': 327 | $skippable = true; 328 | $optionalPart = $this->buildPath($part[1], $mergedParams, true, $hasChild, $options); 329 | 330 | if ($optionalPart !== '') { 331 | $path .= $optionalPart; 332 | $skip = false; 333 | } 334 | break; 335 | 336 | case 'translated-literal': 337 | $path .= $translator->translate($part[1], $textDomain, $locale); 338 | break; 339 | } 340 | } 341 | 342 | if ($isOptional && $skippable && $skip) { 343 | return ''; 344 | } 345 | 346 | return $path; 347 | } 348 | 349 | /** 350 | * match(): defined by RouteInterface interface. 351 | * 352 | * @see \Zend\Router\RouteInterface::match() 353 | * @param Request $request 354 | * @param string|null $pathOffset 355 | * @param array $options 356 | * @return RouteMatch|null 357 | * @throws Exception\RuntimeException 358 | */ 359 | public function match(Request $request, $pathOffset = null, array $options = []) 360 | { 361 | if (! method_exists($request, 'getUri')) { 362 | return; 363 | } 364 | 365 | $uri = $request->getUri(); 366 | $path = $uri->getPath(); 367 | 368 | $regex = $this->regex; 369 | 370 | if ($this->translationKeys) { 371 | if (! isset($options['translator']) || ! $options['translator'] instanceof Translator) { 372 | throw new Exception\RuntimeException('No translator provided'); 373 | } 374 | 375 | $translator = $options['translator']; 376 | $textDomain = (isset($options['text_domain']) ? $options['text_domain'] : 'default'); 377 | $locale = (isset($options['locale']) ? $options['locale'] : null); 378 | 379 | foreach ($this->translationKeys as $key) { 380 | $regex = str_replace('#' . $key . '#', $translator->translate($key, $textDomain, $locale), $regex); 381 | } 382 | } 383 | 384 | if ($pathOffset !== null) { 385 | $result = preg_match('(\G' . $regex . ')', $path, $matches, null, $pathOffset); 386 | } else { 387 | $result = preg_match('(^' . $regex . '$)', $path, $matches); 388 | } 389 | 390 | if (! $result) { 391 | return; 392 | } 393 | 394 | $matchedLength = strlen($matches[0]); 395 | $params = []; 396 | 397 | foreach ($this->paramMap as $index => $name) { 398 | if (isset($matches[$index]) && $matches[$index] !== '') { 399 | $params[$name] = $this->decode($matches[$index]); 400 | } 401 | } 402 | 403 | return new RouteMatch(array_merge($this->defaults, $params), $matchedLength); 404 | } 405 | 406 | /** 407 | * assemble(): Defined by RouteInterface interface. 408 | * 409 | * @see \Zend\Router\RouteInterface::assemble() 410 | * @param array $params 411 | * @param array $options 412 | * @return mixed 413 | */ 414 | public function assemble(array $params = [], array $options = []) 415 | { 416 | $this->assembledParams = []; 417 | 418 | return $this->buildPath( 419 | $this->parts, 420 | array_merge($this->defaults, $params), 421 | false, 422 | (isset($options['has_child']) ? $options['has_child'] : false), 423 | $options 424 | ); 425 | } 426 | 427 | /** 428 | * getAssembledParams(): defined by RouteInterface interface. 429 | * 430 | * @see RouteInterface::getAssembledParams 431 | * @return array 432 | */ 433 | public function getAssembledParams() 434 | { 435 | return $this->assembledParams; 436 | } 437 | 438 | /** 439 | * Encode a path segment. 440 | * 441 | * @param string $value 442 | * @return string 443 | */ 444 | protected function encode($value) 445 | { 446 | $key = (string) $value; 447 | if (! isset(static::$cacheEncode[$key])) { 448 | static::$cacheEncode[$key] = rawurlencode($value); 449 | static::$cacheEncode[$key] = strtr(static::$cacheEncode[$key], static::$urlencodeCorrectionMap); 450 | } 451 | return static::$cacheEncode[$key]; 452 | } 453 | 454 | /** 455 | * Decode a path segment. 456 | * 457 | * @param string $value 458 | * @return string 459 | */ 460 | protected function decode($value) 461 | { 462 | return rawurldecode($value); 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/Http/TreeRouteStack.php: -------------------------------------------------------------------------------- 1 | addPrototypes($options['prototypes']); 74 | } 75 | 76 | return $instance; 77 | } 78 | 79 | /** 80 | * init(): defined by SimpleRouteStack. 81 | * 82 | * @see SimpleRouteStack::init() 83 | */ 84 | protected function init() 85 | { 86 | $this->prototypes = new ArrayObject; 87 | 88 | (new Config([ 89 | 'aliases' => [ 90 | 'chain' => Chain::class, 91 | 'Chain' => Chain::class, 92 | 'hostname' => Hostname::class, 93 | 'Hostname' => Hostname::class, 94 | 'hostName' => Hostname::class, 95 | 'HostName' => Hostname::class, 96 | 'literal' => Literal::class, 97 | 'Literal' => Literal::class, 98 | 'method' => Method::class, 99 | 'Method' => Method::class, 100 | 'part' => Part::class, 101 | 'Part' => Part::class, 102 | 'regex' => Regex::class, 103 | 'Regex' => Regex::class, 104 | 'scheme' => Scheme::class, 105 | 'Scheme' => Scheme::class, 106 | 'segment' => Segment::class, 107 | 'Segment' => Segment::class, 108 | 'wildcard' => Wildcard::class, 109 | 'Wildcard' => Wildcard::class, 110 | 'wildCard' => Wildcard::class, 111 | 'WildCard' => Wildcard::class, 112 | ], 113 | 'factories' => [ 114 | Chain::class => RouteInvokableFactory::class, 115 | Hostname::class => RouteInvokableFactory::class, 116 | Literal::class => RouteInvokableFactory::class, 117 | Method::class => RouteInvokableFactory::class, 118 | Part::class => RouteInvokableFactory::class, 119 | Regex::class => RouteInvokableFactory::class, 120 | Scheme::class => RouteInvokableFactory::class, 121 | Segment::class => RouteInvokableFactory::class, 122 | Wildcard::class => RouteInvokableFactory::class, 123 | 124 | // v2 normalized names 125 | 126 | 'zendmvcrouterhttpchain' => RouteInvokableFactory::class, 127 | 'zendmvcrouterhttphostname' => RouteInvokableFactory::class, 128 | 'zendmvcrouterhttpliteral' => RouteInvokableFactory::class, 129 | 'zendmvcrouterhttpmethod' => RouteInvokableFactory::class, 130 | 'zendmvcrouterhttppart' => RouteInvokableFactory::class, 131 | 'zendmvcrouterhttpregex' => RouteInvokableFactory::class, 132 | 'zendmvcrouterhttpscheme' => RouteInvokableFactory::class, 133 | 'zendmvcrouterhttpsegment' => RouteInvokableFactory::class, 134 | 'zendmvcrouterhttpwildcard' => RouteInvokableFactory::class, 135 | ], 136 | ]))->configureServiceManager($this->routePluginManager); 137 | } 138 | 139 | /** 140 | * addRoute(): defined by RouteStackInterface interface. 141 | * 142 | * @see RouteStackInterface::addRoute() 143 | * @param string $name 144 | * @param mixed $route 145 | * @param int $priority 146 | * @return TreeRouteStack 147 | */ 148 | public function addRoute($name, $route, $priority = null) 149 | { 150 | if (! $route instanceof RouteInterface) { 151 | $route = $this->routeFromArray($route); 152 | } 153 | 154 | return parent::addRoute($name, $route, $priority); 155 | } 156 | 157 | /** 158 | * routeFromArray(): defined by SimpleRouteStack. 159 | * 160 | * @see SimpleRouteStack::routeFromArray() 161 | * @param string|array|Traversable $specs 162 | * @return RouteInterface 163 | * @throws Exception\InvalidArgumentException When route definition is not an array nor traversable 164 | * @throws Exception\InvalidArgumentException When chain routes are not an array nor traversable 165 | * @throws Exception\RuntimeException When a generated routes does not implement the HTTP route interface 166 | */ 167 | protected function routeFromArray($specs) 168 | { 169 | if (is_string($specs)) { 170 | if (null === ($route = $this->getPrototype($specs))) { 171 | throw new Exception\RuntimeException(sprintf('Could not find prototype with name %s', $specs)); 172 | } 173 | 174 | return $route; 175 | } elseif ($specs instanceof Traversable) { 176 | $specs = ArrayUtils::iteratorToArray($specs); 177 | } elseif (! is_array($specs)) { 178 | throw new Exception\InvalidArgumentException('Route definition must be an array or Traversable object'); 179 | } 180 | 181 | if (isset($specs['chain_routes'])) { 182 | if (! is_array($specs['chain_routes'])) { 183 | throw new Exception\InvalidArgumentException('Chain routes must be an array or Traversable object'); 184 | } 185 | 186 | $chainRoutes = array_merge([$specs], $specs['chain_routes']); 187 | unset($chainRoutes[0]['chain_routes']); 188 | 189 | if (isset($specs['child_routes'])) { 190 | unset($chainRoutes[0]['child_routes']); 191 | } 192 | 193 | $options = [ 194 | 'routes' => $chainRoutes, 195 | 'route_plugins' => $this->routePluginManager, 196 | 'prototypes' => $this->prototypes, 197 | ]; 198 | 199 | $route = $this->routePluginManager->get('chain', $options); 200 | } else { 201 | $route = parent::routeFromArray($specs); 202 | } 203 | 204 | if (! $route instanceof RouteInterface) { 205 | throw new Exception\RuntimeException('Given route does not implement HTTP route interface'); 206 | } 207 | 208 | if (isset($specs['child_routes'])) { 209 | $options = [ 210 | 'route' => $route, 211 | 'may_terminate' => (isset($specs['may_terminate']) && $specs['may_terminate']), 212 | 'child_routes' => $specs['child_routes'], 213 | 'route_plugins' => $this->routePluginManager, 214 | 'prototypes' => $this->prototypes, 215 | ]; 216 | 217 | $priority = (isset($route->priority) ? $route->priority : null); 218 | 219 | $route = $this->routePluginManager->get('part', $options); 220 | $route->priority = $priority; 221 | } 222 | 223 | return $route; 224 | } 225 | 226 | /** 227 | * Add multiple prototypes at once. 228 | * 229 | * @param Traversable $routes 230 | * @return TreeRouteStack 231 | * @throws Exception\InvalidArgumentException 232 | */ 233 | public function addPrototypes($routes) 234 | { 235 | if (! is_array($routes) && ! $routes instanceof Traversable) { 236 | throw new Exception\InvalidArgumentException('addPrototypes expects an array or Traversable set of routes'); 237 | } 238 | 239 | foreach ($routes as $name => $route) { 240 | $this->addPrototype($name, $route); 241 | } 242 | 243 | return $this; 244 | } 245 | 246 | /** 247 | * Add a prototype. 248 | * 249 | * @param string $name 250 | * @param mixed $route 251 | * @return TreeRouteStack 252 | */ 253 | public function addPrototype($name, $route) 254 | { 255 | if (! $route instanceof RouteInterface) { 256 | $route = $this->routeFromArray($route); 257 | } 258 | 259 | $this->prototypes[$name] = $route; 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * Get a prototype. 266 | * 267 | * @param string $name 268 | * @return RouteInterface|null 269 | */ 270 | public function getPrototype($name) 271 | { 272 | if (isset($this->prototypes[$name])) { 273 | return $this->prototypes[$name]; 274 | } 275 | 276 | return; 277 | } 278 | 279 | /** 280 | * match(): defined by \Zend\Router\RouteInterface 281 | * 282 | * @see \Zend\Router\RouteInterface::match() 283 | * @param Request $request 284 | * @param integer|null $pathOffset 285 | * @param array $options 286 | * @return RouteMatch|null 287 | */ 288 | public function match(Request $request, $pathOffset = null, array $options = []) 289 | { 290 | if (! method_exists($request, 'getUri')) { 291 | return; 292 | } 293 | 294 | if ($this->baseUrl === null && method_exists($request, 'getBaseUrl')) { 295 | $this->setBaseUrl($request->getBaseUrl()); 296 | } 297 | 298 | $uri = $request->getUri(); 299 | $baseUrlLength = strlen($this->baseUrl) ?: null; 300 | 301 | if ($pathOffset !== null) { 302 | $baseUrlLength += $pathOffset; 303 | } 304 | 305 | if ($this->requestUri === null) { 306 | $this->setRequestUri($uri); 307 | } 308 | 309 | if ($baseUrlLength !== null) { 310 | $pathLength = strlen($uri->getPath()) - $baseUrlLength; 311 | } else { 312 | $pathLength = null; 313 | } 314 | 315 | foreach ($this->routes as $name => $route) { 316 | if (($match = $route->match($request, $baseUrlLength, $options)) instanceof RouteMatch 317 | && ($pathLength === null || $match->getLength() === $pathLength) 318 | ) { 319 | $match->setMatchedRouteName($name); 320 | 321 | foreach ($this->defaultParams as $paramName => $value) { 322 | if ($match->getParam($paramName) === null) { 323 | $match->setParam($paramName, $value); 324 | } 325 | } 326 | 327 | return $match; 328 | } 329 | } 330 | 331 | return; 332 | } 333 | 334 | /** 335 | * assemble(): defined by \Zend\Router\RouteInterface interface. 336 | * 337 | * @see \Zend\Router\RouteInterface::assemble() 338 | * @param array $params 339 | * @param array $options 340 | * @return mixed 341 | * @throws Exception\InvalidArgumentException 342 | * @throws Exception\RuntimeException 343 | */ 344 | public function assemble(array $params = [], array $options = []) 345 | { 346 | if (! isset($options['name'])) { 347 | throw new Exception\InvalidArgumentException('Missing "name" option'); 348 | } 349 | 350 | $names = explode('/', $options['name'], 2); 351 | $route = $this->routes->get($names[0]); 352 | 353 | if (! $route) { 354 | throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $names[0])); 355 | } 356 | 357 | if (isset($names[1])) { 358 | if (! $route instanceof TreeRouteStack) { 359 | throw new Exception\RuntimeException(sprintf( 360 | 'Route with name "%s" does not have child routes', 361 | $names[0] 362 | )); 363 | } 364 | $options['name'] = $names[1]; 365 | } else { 366 | unset($options['name']); 367 | } 368 | 369 | if (isset($options['only_return_path']) && $options['only_return_path']) { 370 | return $this->baseUrl . $route->assemble(array_merge($this->defaultParams, $params), $options); 371 | } 372 | 373 | if (! isset($options['uri'])) { 374 | $uri = new HttpUri(); 375 | 376 | if (isset($options['force_canonical']) && $options['force_canonical']) { 377 | if ($this->requestUri === null) { 378 | throw new Exception\RuntimeException('Request URI has not been set'); 379 | } 380 | 381 | $uri->setScheme($this->requestUri->getScheme()) 382 | ->setHost($this->requestUri->getHost()) 383 | ->setPort($this->requestUri->getPort()); 384 | } 385 | 386 | $options['uri'] = $uri; 387 | } else { 388 | $uri = $options['uri']; 389 | } 390 | 391 | $path = $this->baseUrl . $route->assemble(array_merge($this->defaultParams, $params), $options); 392 | 393 | if (isset($options['query'])) { 394 | $uri->setQuery($options['query']); 395 | } 396 | 397 | if (isset($options['fragment'])) { 398 | $uri->setFragment($options['fragment']); 399 | } 400 | 401 | if ((isset($options['force_canonical']) 402 | && $options['force_canonical']) 403 | || $uri->getHost() !== null 404 | || $uri->getScheme() !== null 405 | ) { 406 | if (($uri->getHost() === null || $uri->getScheme() === null) && $this->requestUri === null) { 407 | throw new Exception\RuntimeException('Request URI has not been set'); 408 | } 409 | 410 | if ($uri->getHost() === null) { 411 | $uri->setHost($this->requestUri->getHost()); 412 | } 413 | 414 | if ($uri->getScheme() === null) { 415 | $uri->setScheme($this->requestUri->getScheme()); 416 | } 417 | 418 | $uri->setPath($path); 419 | 420 | if (! isset($options['normalize_path']) || $options['normalize_path']) { 421 | $uri->normalize(); 422 | } 423 | 424 | return $uri->toString(); 425 | } elseif (! $uri->isAbsolute() && $uri->isValidRelative()) { 426 | $uri->setPath($path); 427 | 428 | if (! isset($options['normalize_path']) || $options['normalize_path']) { 429 | $uri->normalize(); 430 | } 431 | 432 | return $uri->toString(); 433 | } 434 | 435 | return $path; 436 | } 437 | 438 | /** 439 | * Set the base URL. 440 | * 441 | * @param string $baseUrl 442 | * @return self 443 | */ 444 | public function setBaseUrl($baseUrl) 445 | { 446 | $this->baseUrl = rtrim($baseUrl, '/'); 447 | return $this; 448 | } 449 | 450 | /** 451 | * Get the base URL. 452 | * 453 | * @return string 454 | */ 455 | public function getBaseUrl() 456 | { 457 | return $this->baseUrl; 458 | } 459 | 460 | /** 461 | * Set the request URI. 462 | * 463 | * @param HttpUri $uri 464 | * @return TreeRouteStack 465 | */ 466 | public function setRequestUri(HttpUri $uri) 467 | { 468 | $this->requestUri = $uri; 469 | return $this; 470 | } 471 | 472 | /** 473 | * Get the request URI. 474 | * 475 | * @return HttpUri 476 | */ 477 | public function getRequestUri() 478 | { 479 | return $this->requestUri; 480 | } 481 | } 482 | --------------------------------------------------------------------------------