diff --git a/VERSION b/VERSION index c5fd99e..58214c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2410.32039 +0.2410.32326 diff --git a/src/Http/Routing/Router.php b/src/Http/Routing/Router.php index c05cce9..8574171 100644 --- a/src/Http/Routing/Router.php +++ b/src/Http/Routing/Router.php @@ -1,7 +1,7 @@ , fragment: string}> */ + private array $urls = []; + + public function register(string $name, string $path, array $query = [], string $fragment = ''): void { + if(array_key_exists($name, $this->urls)) + throw new InvalidArgumentException('A URL format with $name has already been registered.'); + if($path === '') + throw new InvalidArgumentException('$path may not be empty.'); + if($path[0] !== '/') + throw new InvalidArgumentException('$path must begin with /.'); + + $this->urls[$name] = [ + 'path' => $path, + 'query' => $query, + 'fragment' => $fragment, + ]; + } + + public function format(string $name, array $vars = [], bool $spacesAsPlus = false): string { + if(!array_key_exists($name, $this->urls)) + return ''; + + $format = $this->urls[$name]; + $string = self::replaceVariables($format['path'], $vars); + + if(!empty($format['query'])) { + $query = []; + foreach($format['query'] as $name => $value) { + $value = self::replaceVariables($value, $vars); + if($value !== '' && !($name === 'page' && (int)$value <= 1)) + $query[$name] = $value; + } + + if(!empty($query)) + $string .= '?' . http_build_query($query, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986); + } + + if(!empty($format['fragment'])) { + $fragment = self::replaceVariables($format['fragment'], $vars); + if($fragment !== '') + $string .= '#' . $fragment; + } + + return $string; + } + + /** + * Formats a component template. + * + * @param string $str Template string. + * @param array $vars Replacement variables. + * @return string Formatted string. + */ + private static function replaceVariables(string $str, array $vars): string { + // i: have no idea what i'm doing + // i: still don't i literally copy pasted this from Misuzu lmao + $out = ''; + $varName = ''; + $inVarName = false; + + $length = strlen($str); + for($i = 0; $i < $length; ++$i) { + $char = $str[$i]; + + if($inVarName) { + if($char === '>') { + $inVarName = false; + if(array_key_exists($varName, $vars)) { + $varValue = $vars[$varName]; + if(is_array($varValue)) + $varValue = empty($varValue) ? '' : implode(',', $varValue); + elseif(is_int($varValue)) + $varValue = ($varName === 'page' ? $varValue < 2 : $varValue === 0) ? '' : (string)$varValue; + elseif(is_scalar($varValue) || $varValue instanceof Stringable) + $varValue = (string)$varValue; + else + $varValue = ''; + } else + $varValue = ''; + + $out .= $varValue; + $varName = ''; + continue; + } + + if($char === '<') { + $out .= '<' . $varName; + $varName = ''; + continue; + } + + $varName .= $char; + } else + $inVarName = $char === '<'; + + if(!$inVarName) + $out .= $char; + } + + if($varName !== '') + $out .= $varName; + + return $out; + } + + public function scopeTo(string $namePrefix = '', string $pathPrefix = ''): UrlRegistry { + return new ScopedUrlRegistry($this, $namePrefix, $pathPrefix); + } +} diff --git a/src/Urls/ScopedUrlRegistry.php b/src/Urls/ScopedUrlRegistry.php new file mode 100644 index 0000000..5a903fd --- /dev/null +++ b/src/Urls/ScopedUrlRegistry.php @@ -0,0 +1,52 @@ +registry->register( + $this->namePrefix . $name, + $this->pathPrefix . $path, + $query, + $fragment + ); + } + + public function format(string $name, array $vars = [], bool $spacesAsPlus = false): string { + return $this->registry->format( + $this->namePrefix . $name, + $vars, + $spacesAsPlus + ); + } + + public function scopeTo(string $namePrefix = '', string $pathPrefix = ''): UrlRegistry { + return $this->registry->scopeTo( + $this->namePrefix . $namePrefix, + $this->pathPrefix . $pathPrefix + ); + } +} diff --git a/src/Urls/UrlFormat.php b/src/Urls/UrlFormat.php new file mode 100644 index 0000000..9d0120e --- /dev/null +++ b/src/Urls/UrlFormat.php @@ -0,0 +1,89 @@ + $query Query string component template for the URL. + * @param string $fragment Fragment component template for the URL. + */ + public function __construct( + private string $name, + private string $path, + private array $query = [], + private string $fragment = '' + ) {} + + /** + * Returns the name for this URL format. + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * Returns the path component template for this URL format. + * + * @return string + */ + public function getPath(): string { + return $this->path; + } + + /** + * Returns the query component template for this URL format. + * + * @return array + */ + public function getQuery(): array { + return $this->query; + } + + /** + * Returns the fragment component template for this URL format. + * + * @return string + */ + public function getFragment(): string { + return $this->fragment; + } + + /** + * Reads attributes from methods in a UrlSource instance and registers them to a given UrlRegistry instance. + * + * @param UrlRegistry $registry + * @param UrlSource $source + */ + public static function register(UrlRegistry $registry, UrlSource $source): void { + $objectInfo = new ReflectionObject($source); + $methodInfos = $objectInfo->getMethods(); + + foreach($methodInfos as $methodInfo) { + $attrInfos = $methodInfo->getAttributes(UrlFormat::class, ReflectionAttribute::IS_INSTANCEOF); + foreach($attrInfos as $attrInfo) { + $formatInfo = $attrInfo->newInstance(); + $registry->register( + $formatInfo->getName(), + $formatInfo->getPath(), + $formatInfo->getQuery(), + $formatInfo->getFragment(), + ); + } + } + } +} diff --git a/src/Urls/UrlRegistry.php b/src/Urls/UrlRegistry.php new file mode 100644 index 0000000..7fdd2f8 --- /dev/null +++ b/src/Urls/UrlRegistry.php @@ -0,0 +1,47 @@ + $query Query string variables of the URL. + * @param string $fragment Fragment portion of the URL. + * @throws InvalidArgumentException If $name has already been registered. + * @throws InvalidArgumentException If $path if empty or does not start with a /. + */ + function register(string $name, string $path, array $query = [], string $fragment = ''): void; + + /** + * Format a URL. + * + * @param string $name Name of the URL to format. + * @param array $vars Values to replace the variables in the path, query and fragment with. + * @param bool $spacesAsPlus Whether to represent spaces as a + instead of %20. + * @return string Formatted URL. + */ + function format(string $name, array $vars = [], bool $spacesAsPlus = false): string; + + /** + * Creates a scoped URL registry. + * + * @param string $namePrefix String to prefix names in the scope with. + * @param string $pathPrefix String to prefix paths in the scope with, must start with / if not empty. + * @throws InvalidArgumentException If $pathPrefix is non-empty but doesn't start with a /. + * @return UrlRegistry A scoped URL registry. + */ + function scopeTo(string $namePrefix = '', string $pathPrefix = ''): UrlRegistry; +} diff --git a/src/Urls/UrlSource.php b/src/Urls/UrlSource.php new file mode 100644 index 0000000..fc3b44d --- /dev/null +++ b/src/Urls/UrlSource.php @@ -0,0 +1,10 @@ +