Added URL registry stuff.
Tests coming later, I have a headache :D
This commit is contained in:
parent
93f0f27561
commit
05455d2be8
7 changed files with 326 additions and 2 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
0.2410.32039
|
0.2410.32326
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// Router.php
|
// Router.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-10-02
|
// Updated: 2024-10-03
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\Http\HttpRequest;
|
use Index\Http\HttpRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an interface for defining HTTP routers.
|
||||||
|
*/
|
||||||
interface Router {
|
interface Router {
|
||||||
/**
|
/**
|
||||||
* Creates a scoped version of this router.
|
* Creates a scoped version of this router.
|
||||||
|
|
123
src/Urls/ArrayUrlRegistry.php
Normal file
123
src/Urls/ArrayUrlRegistry.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
52
src/Urls/ScopedUrlRegistry.php
Normal file
52
src/Urls/ScopedUrlRegistry.php
Normal 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
89
src/Urls/UrlFormat.php
Normal 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
47
src/Urls/UrlRegistry.php
Normal 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
10
src/Urls/UrlSource.php
Normal 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;
|
||||||
|
}
|
Loading…
Reference in a new issue