Added URL registry stuff.

Tests coming later, I have a headache :D
This commit is contained in:
flash 2024-10-03 23:27:58 +00:00
parent 93f0f27561
commit 05455d2be8
7 changed files with 326 additions and 2 deletions

View file

@ -1 +1 @@
0.2410.32039
0.2410.32326

View file

@ -1,7 +1,7 @@
<?php
// Router.php
// Created: 2024-03-28
// Updated: 2024-10-02
// Updated: 2024-10-03
namespace Index\Http\Routing;
@ -9,6 +9,9 @@ use InvalidArgumentException;
use RuntimeException;
use Index\Http\HttpRequest;
/**
* Provides an interface for defining HTTP routers.
*/
interface Router {
/**
* Creates a scoped version of this router.

View file

@ -0,0 +1,123 @@
<?php
// ArrayUrlRegistry.php
// Created: 2024-10-03
// Updated: 2024-10-03
namespace Index\Urls;
use InvalidArgumentException;
use Stringable;
/**
* Provides an array backed URL registry implementation.
*/
class ArrayUrlRegistry implements UrlRegistry {
/** @var array<string, array{path: string, query: array<string, string>, 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<string, mixed> $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);
}
}

View file

@ -0,0 +1,52 @@
<?php
// ScopedUrlRegistry.php
// Created: 2024-10-03
// Updated: 2024-10-03
namespace Index\Urls;
use InvalidArgumentException;
/**
* Provides a scoped URL registry implementation.
*/
class ScopedUrlRegistry implements UrlRegistry {
/**
* @param UrlRegistry $registry Base URL registry, preferably not a scoped one to avoid overhead.
* @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 /.
*/
public function __construct(
private UrlRegistry $registry,
private string $namePrefix,
private string $pathPrefix
) {
if($pathPrefix !== '' && !str_starts_with($pathPrefix, '/'))
throw new InvalidArgumentException('$pathPrefix must start with a / if it is non-empty');
}
public function register(string $name, string $path, array $query = [], string $fragment = ''): void {
$this->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
);
}
}

89
src/Urls/UrlFormat.php Normal file
View file

@ -0,0 +1,89 @@
<?php
// UrlFormat.php
// Created: 2024-10-03
// Updated: 2024-10-03
namespace Index\Urls;
use Attribute;
use ReflectionAttribute;
use ReflectionObject;
/**
* An attribute for denoting URL formatting for a route handler.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class UrlFormat {
/**
* @param string $name Name of the URL format.
* @param string $path Path component template of the URL.
* @param array<string, string> $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<string, string>
*/
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(),
);
}
}
}
}

47
src/Urls/UrlRegistry.php Normal file
View file

@ -0,0 +1,47 @@
<?php
// UrlRegistry.php
// Created: 2024-10-03
// Updated: 2024-10-03
namespace Index\Urls;
use InvalidArgumentException;
/**
* Provides an interface for defining a URL registry.
*/
interface UrlRegistry {
/**
* Registers a URL format.
*
* TODO: explain formatting system
*
* @param string $name Name of the format.
* @param string $path Path portion of the URL.
* @param array<string, string> $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<string, mixed> $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;
}

10
src/Urls/UrlSource.php Normal file
View file

@ -0,0 +1,10 @@
<?php
// UrlSource.php
// Created: 2024-10-03
// Updated: 2024-10-03
namespace Index\Urls;
interface UrlSource {
function registerUrls(UrlRegistry $registry): void;
}