defaults = $defaults; $this->parts = $this->parseRouteDefinition($route); $this->regex = $this->buildRegex($this->parts, $constraints); } /** * factory(): defined by RouteInterface interface. * * @see \Laminas\Router\RouteInterface::factory() * * @param array|Traversable $options * @return Hostname * @throws Exception\InvalidArgumentException */ public static function factory($options = []) { if ($options instanceof Traversable) { $options = ArrayUtils::iteratorToArray($options); } elseif (! is_array($options)) { throw new Exception\InvalidArgumentException(sprintf( '%s expects an array or Traversable set of options', __METHOD__ )); } if (! isset($options['route'])) { throw new Exception\InvalidArgumentException('Missing "route" in options array'); } if (! isset($options['constraints'])) { $options['constraints'] = []; } if (! isset($options['defaults'])) { $options['defaults'] = []; } return new static($options['route'], $options['constraints'], $options['defaults']); } /** * Parse a route definition. * * @param string $def * @return array * @throws Exception\RuntimeException */ protected function parseRouteDefinition($def) { $currentPos = 0; $length = strlen($def); $parts = []; $levelParts = [&$parts]; $level = 0; while ($currentPos < $length) { if (! preg_match('(\G(?P[a-z0-9-.]*)(?P[:{\[\]]|$))', $def, $matches, 0, $currentPos)) { throw new Exception\RuntimeException('Matched hostname literal contains a disallowed character'); } $currentPos += strlen($matches[0]); if (! empty($matches['literal'])) { $levelParts[$level][] = ['literal', $matches['literal']]; } if ($matches['token'] === ':') { if ( ! preg_match( '(\G(?P[^:.{\[\]]+)(?:{(?P[^}]+)})?:?)', $def, $matches, 0, $currentPos ) ) { throw new Exception\RuntimeException('Found empty parameter name'); } $levelParts[$level][] = [ 'parameter', $matches['name'], $matches['delimiters'] ?? null, ]; $currentPos += strlen($matches[0]); } elseif ($matches['token'] === '[') { $levelParts[$level][] = ['optional', []]; $levelParts[$level + 1] = &$levelParts[$level][count($levelParts[$level]) - 1][1]; $level++; } elseif ($matches['token'] === ']') { unset($levelParts[$level]); $level--; if ($level < 0) { throw new Exception\RuntimeException('Found closing bracket without matching opening bracket'); } } else { break; } } if ($level > 0) { throw new Exception\RuntimeException('Found unbalanced brackets'); } return $parts; } /** * Build the matching regex from parsed parts. * * @param array $parts * @param array $constraints * @param int $groupIndex * @return string * @throws Exception\RuntimeException */ protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1) { $regex = ''; foreach ($parts as $part) { switch ($part[0]) { case 'literal': $regex .= preg_quote($part[1]); break; case 'parameter': $groupName = '?P'; if (isset($constraints[$part[1]])) { $regex .= '(' . $groupName . $constraints[$part[1]] . ')'; } elseif ($part[2] === null) { $regex .= '(' . $groupName . '[^.]+)'; } else { $regex .= '(' . $groupName . '[^' . $part[2] . ']+)'; } $this->paramMap['param' . $groupIndex++] = $part[1]; break; case 'optional': $regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')?'; break; } } return $regex; } /** * Build host. * * @param array $parts * @param array $mergedParams * @param bool $isOptional * @return string * @throws Exception\RuntimeException * @throws Exception\InvalidArgumentException */ protected function buildHost(array $parts, array $mergedParams, $isOptional) { $host = ''; $skip = true; $skippable = false; foreach ($parts as $part) { switch ($part[0]) { case 'literal': $host .= $part[1]; break; case 'parameter': $skippable = true; if (! isset($mergedParams[$part[1]])) { if (! $isOptional) { throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); } return ''; } elseif ( ! $isOptional || ! isset($this->defaults[$part[1]]) || $this->defaults[$part[1]] !== $mergedParams[$part[1]] ) { $skip = false; } $host .= $mergedParams[$part[1]]; $this->assembledParams[] = $part[1]; break; case 'optional': $skippable = true; $optionalPart = $this->buildHost($part[1], $mergedParams, true); if ($optionalPart !== '') { $host .= $optionalPart; $skip = false; } break; } } if ($isOptional && $skippable && $skip) { return ''; } return $host; } /** * match(): defined by RouteInterface interface. * * @see \Laminas\Router\RouteInterface::match() * * @return RouteMatch|null */ public function match(Request $request) { if (! method_exists($request, 'getUri')) { return null; } /** @var UriInterface $uri */ $uri = $request->getUri(); $host = $uri->getHost() ?? ''; $result = preg_match('(^' . $this->regex . '$)', $host, $matches); if (! $result) { return null; } $params = []; foreach ($this->paramMap as $index => $name) { if (isset($matches[$index]) && $matches[$index] !== '') { $params[$name] = $matches[$index]; } } return new RouteMatch(array_merge($this->defaults, $params)); } /** * assemble(): Defined by RouteInterface interface. * * @see \Laminas\Router\RouteInterface::assemble() * * @param array $params * @param array $options * @return mixed */ public function assemble(array $params = [], array $options = []) { $this->assembledParams = []; if (isset($options['uri'])) { $host = $this->buildHost( $this->parts, array_merge($this->defaults, $params), false ); $options['uri']->setHost($host); } // A hostname does not contribute to the path, thus nothing is returned. return ''; } /** * getAssembledParams(): defined by RouteInterface interface. * * @see RouteInterface::getAssembledParams * * @return array */ public function getAssembledParams() { return $this->assembledParams; } }