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
|
||||
// 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.
|
||||
|
|
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