Compare commits
No commits in common. "trunk" and "router-v3" have entirely different histories.
35 changed files with 171 additions and 672 deletions
README.mdVERSIONcomposer.lock
src
Db/NullDb
Dependencies.phpHttp
Content
HttpParameters.phpHttpParametersCommon.phpHttpRequest.phpHttpResponseBuilder.phpHttpUri.phpRouting
Performance
Snowflake
Templating/Extension
XArray.phpXDateTime.phptests
|
@ -7,7 +7,7 @@ It provides a number of components that I would otherwise copy between projects
|
|||
|
||||
## Requirements and Dependencies
|
||||
|
||||
Index currently targets **PHP 8.4** with the `mbstring` extension installed.
|
||||
Index currently targets **PHP 8.3** with the `mbstring` extension installed.
|
||||
|
||||
### `Index\Cache\Memcached`
|
||||
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2504.51944
|
||||
0.2503.192123
|
||||
|
|
34
composer.lock
generated
34
composer.lock
generated
|
@ -976,16 +976,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.11",
|
||||
"version": "2.1.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30"
|
||||
"reference": "f9adff3b87c03b12cc7e46a30a524648e497758f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30",
|
||||
"reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f",
|
||||
"reference": "f9adff3b87c03b12cc7e46a30a524648e497758f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1030,20 +1030,20 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-24T13:45:00+00:00"
|
||||
"time": "2025-03-09T09:30:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "12.1.2",
|
||||
"version": "12.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "05c33d01a856f9f62488d144bafddc3d7b7a4ebb"
|
||||
"reference": "d331a5ced3d9a2b917baa9841b2211e72f9e780d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/05c33d01a856f9f62488d144bafddc3d7b7a4ebb",
|
||||
"reference": "05c33d01a856f9f62488d144bafddc3d7b7a4ebb",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d331a5ced3d9a2b917baa9841b2211e72f9e780d",
|
||||
"reference": "d331a5ced3d9a2b917baa9841b2211e72f9e780d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1099,7 +1099,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.1.2"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1107,7 +1107,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-03T14:34:39+00:00"
|
||||
"time": "2025-03-17T13:56:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
|
@ -1356,16 +1356,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.0.10",
|
||||
"version": "12.0.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "6075843014de23bcd6992842d69ca99d25d6a433"
|
||||
"reference": "7835bb4276780e0bbb385ce0a777b839e03096db"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6075843014de23bcd6992842d69ca99d25d6a433",
|
||||
"reference": "6075843014de23bcd6992842d69ca99d25d6a433",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7835bb4276780e0bbb385ce0a777b839e03096db",
|
||||
"reference": "7835bb4276780e0bbb385ce0a777b839e03096db",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1433,7 +1433,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.0.10"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.0.9"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1449,7 +1449,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-23T16:03:59+00:00"
|
||||
"time": "2025-03-19T13:47:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// NullDbStatement.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2025-04-05
|
||||
// Updated: 2025-01-18
|
||||
|
||||
namespace Index\Db\NullDb;
|
||||
|
||||
|
@ -11,7 +11,7 @@ use Index\Db\{DbType,DbResult,DbResultIterator,DbStatement};
|
|||
* Represents a dummy database statement.
|
||||
*/
|
||||
class NullDbStatement implements DbStatement {
|
||||
public private(set) int $paramCount = 0;
|
||||
public private(set) int $parameterCount = 0;
|
||||
|
||||
public function addParameter(int $ordinal, mixed $value, DbType $type = DbType::Auto): void {}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Dependencies.php
|
||||
// Created: 2025-01-18
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -36,52 +36,52 @@ class Dependencies {
|
|||
* @return array<string|int, mixed>
|
||||
*/
|
||||
private function resolveArgs(ReflectionFunctionAbstract $constructor, array $args): array {
|
||||
$namedArgs = [];
|
||||
$seqArgs = [];
|
||||
foreach($args as $name => $value) {
|
||||
if(is_int($name) || ctype_digit($name))
|
||||
$seqArgs[] = $value;
|
||||
else
|
||||
$namedArgs[$name] = $value;
|
||||
}
|
||||
|
||||
$args = [];
|
||||
$realArgs = [];
|
||||
|
||||
$params = $constructor->getParameters();
|
||||
foreach($params as $paramInfo) {
|
||||
$paramName = $paramInfo->getName();
|
||||
if(array_key_exists($paramName, $args)) {
|
||||
$realArgs[] = $args[$paramName];
|
||||
unset($args[$paramName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// named values always take precedence, including explicit type erroring
|
||||
if(array_key_exists($paramName, $namedArgs)) {
|
||||
$value = $namedArgs[$paramName];
|
||||
} else {
|
||||
$value = null;
|
||||
try {
|
||||
$typeInfo = $paramInfo->getType();
|
||||
$allowsNull = $typeInfo === null || $typeInfo->allowsNull();
|
||||
$value = $paramInfo->isDefaultValueAvailable() ? $paramInfo->getDefaultValue() : null;
|
||||
|
||||
// maybe support intersection and union someday
|
||||
if($typeInfo instanceof ReflectionNamedType && !$typeInfo->isBuiltin()) {
|
||||
$className = $typeInfo->getName();
|
||||
if(class_exists($className) || interface_exists($className))
|
||||
$value = $this->resolve($className);
|
||||
if(!class_exists($className) && !interface_exists($className))
|
||||
throw new RuntimeException(sprintf('class in type does not exist: %s', $className));
|
||||
|
||||
$value = $this->resolve($className);
|
||||
if($value === null && !$allowsNull)
|
||||
throw new RuntimeException(sprintf('was not able to resolve %s', $className));
|
||||
}
|
||||
|
||||
if($value === null) {
|
||||
if(count($seqArgs) > 0) {
|
||||
$value = array_shift($seqArgs);
|
||||
if(!$typeInfo instanceof ReflectionNamedType || !(!$typeInfo->isBuiltin() || $typeInfo->getName() === gettype($value)))
|
||||
$value = null;
|
||||
}
|
||||
if($value === null && !$allowsNull)
|
||||
throw new RuntimeException('required parameter has unsupported type definition');
|
||||
|
||||
if($value === null && !$allowsNull && $paramInfo->isDefaultValueAvailable())
|
||||
$value = $paramInfo->getDefaultValue();
|
||||
$realArgs[] = $value;
|
||||
continue;
|
||||
} catch(RuntimeException $ex) {
|
||||
if(count($args) > 0) {
|
||||
$realArgs[] = array_shift($args);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!$paramInfo->isOptional())
|
||||
throw $ex;
|
||||
|
||||
$realArgs[] = null;
|
||||
}
|
||||
|
||||
$args[$paramName] = $value;
|
||||
}
|
||||
|
||||
return $args;
|
||||
return $realArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -213,7 +213,7 @@ class Dependencies {
|
|||
if(!array_key_exists($class, $this->objects))
|
||||
return null;
|
||||
|
||||
$object = XArray::first($this->objects[$class], $predicate); // @phpstan-ignore argument.type
|
||||
$object = XArray::first($this->objects[$class], $predicate); // @phpstan-ignore-line: IDC
|
||||
if($object === null || !is_a($object, $class))
|
||||
return null;
|
||||
|
||||
|
@ -254,8 +254,8 @@ class Dependencies {
|
|||
return [];
|
||||
|
||||
if($predicate === null)
|
||||
return $this->objects[$class]; // @phpstan-ignore return.type
|
||||
return $this->objects[$class]; // @phpstan-ignore-line: trust me
|
||||
|
||||
return XArray::where($this->objects[$class], $predicate); // @phpstan-ignore argument.type, return.type
|
||||
return XArray::where($this->objects[$class], $predicate); // @phpstan-ignore-line: this is fine
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MultipartFormContent.php
|
||||
// Created: 2025-03-12
|
||||
// Updated: 2025-03-25
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
|
@ -88,7 +88,7 @@ class MultipartFormContent implements FormContent, Iterator {
|
|||
/**
|
||||
* Parses multipart form data in a stream.
|
||||
*
|
||||
* @param StreamInterface $stream Stream to parse.
|
||||
* @param StreamInterface $stream Stream to parse, if seekable gets rewound.
|
||||
* @param string $boundary Boundary string.
|
||||
* @throws RuntimeException if the form could not be parsed correctly
|
||||
* @return MultipartFormContent
|
||||
|
@ -174,10 +174,10 @@ class MultipartFormContent implements FormContent, Iterator {
|
|||
continue;
|
||||
|
||||
$value = trim($infoParts[1]);
|
||||
if(str_starts_with($value, '"') && str_ends_with($value, '"'))
|
||||
$value = substr($value, 1, -1);
|
||||
if(!str_starts_with($value, '"') || !str_ends_with($value, '"'))
|
||||
continue;
|
||||
|
||||
$info[$name] = urldecode($value);
|
||||
$info[$name] = urldecode(substr($value, 1, -1));
|
||||
}
|
||||
|
||||
return $info;
|
||||
|
@ -221,6 +221,9 @@ class MultipartFormContent implements FormContent, Iterator {
|
|||
$params[$part->name] = [$part];
|
||||
}
|
||||
|
||||
if($stream->isSeekable())
|
||||
$stream->rewind();
|
||||
|
||||
return new MultipartFormContent($stream, $params, $files);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// UrlEncodedFormContent.php
|
||||
// Created: 2025-03-12
|
||||
// Updated: 2025-03-25
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
|
@ -58,10 +58,14 @@ class UrlEncodedFormContent implements FormContent {
|
|||
/**
|
||||
* Parses URL encoded form params in a stream.
|
||||
*
|
||||
* @param StreamInterface $stream Stream to parse.
|
||||
* @param StreamInterface $stream Stream to parse, if seekable gets rewound.
|
||||
* @return UrlEncodedFormContent
|
||||
*/
|
||||
public static function parseStream(StreamInterface $stream): UrlEncodedFormContent {
|
||||
return new UrlEncodedFormContent($stream, HttpUri::parseQueryString((string)$stream));
|
||||
$params = HttpUri::parseQueryString((string)$stream);
|
||||
if($stream->isSeekable())
|
||||
$stream->rewind();
|
||||
|
||||
return new UrlEncodedFormContent($stream, $params);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpParameters.php
|
||||
// Created: 2025-03-15
|
||||
// Updated: 2025-03-20
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -84,12 +84,4 @@ interface HttpParameters {
|
|||
array|int $options = 0,
|
||||
mixed $default = null,
|
||||
): mixed;
|
||||
|
||||
/**
|
||||
* Retrieves parameters as a query string.
|
||||
*
|
||||
* @param ?bool $spacesAsPlus null to use the Index url encoder, other wise whether to represent spaces as a + instead of %20 with the legacy encoder.
|
||||
* @return string Query string representation of all parameters.
|
||||
*/
|
||||
public function getParamString(?bool $spacesAsPlus = null): string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpParametersCommon.php
|
||||
// Created: 2025-03-15
|
||||
// Updated: 2025-03-20
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -99,26 +99,4 @@ trait HttpParametersCommon {
|
|||
? filter_var($this->params[$name][$index], $filter, $options)
|
||||
: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves parameters as a query string.
|
||||
*
|
||||
* @param ?bool $spacesAsPlus null to use the Index url encoder, other wise whether to represent spaces as a + instead of %20 with the legacy encoder.
|
||||
* @return string Query string representation of all parameters.
|
||||
*/
|
||||
public function getParamString(?bool $spacesAsPlus = null): string {
|
||||
if($spacesAsPlus !== null) {
|
||||
$params = [];
|
||||
foreach($this->params as $name => $values)
|
||||
$params[$name] = match(count($values)) {
|
||||
0 => '',
|
||||
1 => $values[0],
|
||||
default => $values,
|
||||
};
|
||||
|
||||
return http_build_query($params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
return HttpUri::buildQueryString($this->params);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequest.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -267,6 +267,18 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface, HttpPar
|
|||
return $this->with(params: $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all HTTP request query fields as a query string.
|
||||
*
|
||||
* @param ?bool $spacesAsPlus null to use the Index url encoder, other wise whether to represent spaces as a + instead of %20 with the legacy encoder.
|
||||
* @return string Query string representation of query fields.
|
||||
*/
|
||||
public function getParamString(?bool $spacesAsPlus = null): string {
|
||||
return $spacesAsPlus === null
|
||||
? HttpUri::buildQueryString($this->params)
|
||||
: http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, UploadedFileInterface[]> An array tree of UploadedFileInterface instances; an empty array MUST be returned if no data is present.
|
||||
*/
|
||||
|
@ -360,15 +372,16 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface, HttpPar
|
|||
|
||||
return new HttpRequest(
|
||||
$request->getProtocolVersion(),
|
||||
$request->getHeaders(), // @phpstan-ignore argument.type
|
||||
// @phpstan-ignore-next-line: interface erroneously defines as string[][] instead of array<string, string[]>
|
||||
$request->getHeaders(),
|
||||
$request->getBody(),
|
||||
[],
|
||||
$request->getMethod(),
|
||||
HttpUri::castUri($request->getUri()),
|
||||
$queryParams,
|
||||
$cookieParams,
|
||||
$request->getParsedBody(), // @phpstan-ignore argument.type
|
||||
$request->getUploadedFiles(), // @phpstan-ignore argument.type
|
||||
$request->getParsedBody(), // @phpstan-ignore-line: dont care
|
||||
$request->getUploadedFiles(), // @phpstan-ignore-line: dont care
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpResponseBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-04-03
|
||||
// Updated: 2025-03-12
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -191,7 +191,19 @@ final class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
* @param Timings $timings Timings to supply to the devtools.
|
||||
*/
|
||||
public function setServerTiming(Timings $timings): void {
|
||||
$this->setHeader('Server-Timing', (string)$timings);
|
||||
$laps = $timings->laps;
|
||||
$timings = [];
|
||||
|
||||
foreach($laps as $lap) {
|
||||
$timing = $lap->name;
|
||||
if(!empty($lap->comment))
|
||||
$timing .= ';desc="' . strtr($lap->comment, ['"' => '\\"']) . '"';
|
||||
|
||||
$timing .= ';dur=' . round($lap->durationTime, 5);
|
||||
$timings[] = $timing;
|
||||
}
|
||||
|
||||
$this->setHeader('Server-Timing', implode(', ', $timings));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpUri.php
|
||||
// Created: 2025-02-28
|
||||
// Updated: 2025-03-20
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// PatternFilter.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-23
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing\Filters;
|
||||
|
||||
|
@ -18,14 +18,14 @@ class PatternFilter extends FilterAttribute {
|
|||
/**
|
||||
* @param string $pattern URI path pattern.
|
||||
* @param bool $raw If true, $pattern is taken as is, if false, $pattern is prefixed with #^ and suffixed with #u or $#uD
|
||||
* @param bool $prefix Only has effect if $raw is false. If true is #u appended to $pattern, otherwise /?$#uD.
|
||||
* @param bool $prefix Only has effect if $raw is false. If true is #u appended to $pattern, otherwise $#uD.
|
||||
*/
|
||||
public function __construct(
|
||||
string $pattern,
|
||||
bool $raw = false,
|
||||
bool $prefix = true,
|
||||
) {
|
||||
$this->pattern = $raw ? $pattern : sprintf('#^%s%s', $pattern, $prefix ? '#u' : '/?$#uD');
|
||||
$this->pattern = $raw ? $pattern : sprintf('#^%s%s', $pattern, $prefix ? '#u' : '$#uD');
|
||||
}
|
||||
|
||||
public function createInstance(Closure $handler): FilterInfo {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Router.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-25
|
||||
// Updated: 2025-03-19
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -268,15 +268,10 @@ class Router implements RequestHandlerInterface {
|
|||
$match = $routeInfo->matcher->match($context->request->uri);
|
||||
if($match !== false) {
|
||||
$routeInfo->readAccessControl();
|
||||
if(!array_key_exists($routeInfo->method, $methods)
|
||||
|| ( // using less matched variables to mean the match was more accurate
|
||||
$methods[$routeInfo->method]->matches === null ? 0 : count($methods[$routeInfo->method]->matches))
|
||||
> ($match === true ? 0 : count($match))
|
||||
)
|
||||
$methods[$routeInfo->method] = (object)[
|
||||
'info' => $routeInfo,
|
||||
'matches' => is_array($match) ? $match : null,
|
||||
];
|
||||
$methods[$routeInfo->method] = (object)[
|
||||
'info' => $routeInfo,
|
||||
'matches' => is_array($match) ? $match : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,7 +400,7 @@ class Router implements RequestHandlerInterface {
|
|||
|
||||
foreach($response->getHeaders() as $name => $lines)
|
||||
foreach($lines as $line)
|
||||
header(sprintf('%s: %s', $name, $line), false);
|
||||
header(sprintf('%s: %s', $name, $line));
|
||||
|
||||
$stream = $response->getBody();
|
||||
if($stream->isReadable())
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// RouterProcessors.php
|
||||
// Created: 2025-03-15
|
||||
// Updated: 2025-03-25
|
||||
// Updated: 2025-03-15
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -23,20 +23,18 @@ class RouterProcessors implements RouteHandler {
|
|||
if(!$contentType->equals('application/x-www-form-urlencoded'))
|
||||
return false;
|
||||
|
||||
$stream = $request->getBody();
|
||||
if($stream->isSeekable())
|
||||
$stream->rewind();
|
||||
|
||||
$context->deps->register(UrlEncodedFormContent::parseStream($stream));
|
||||
$context->deps->register(UrlEncodedFormContent::parseStream($request->getBody()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return void|int */
|
||||
#[Preprocessor('input:urlencoded')]
|
||||
public function preInputUrlencoded(HandlerContext $context, RequestInterface $request, bool $required = true) {
|
||||
if(!$this->handlePreInputUrlencoded($context, $request) && $required)
|
||||
return 400;
|
||||
public function preInputUrlencoded(HandlerContext $context, RequestInterface $request, bool $required = true): void {
|
||||
if(!$this->handlePreInputUrlencoded($context, $request) && $required) {
|
||||
$context->response->statusCode = 400;
|
||||
$context->response->reasonPhrase = '';
|
||||
$context->halt();
|
||||
}
|
||||
}
|
||||
|
||||
private function handlePreInputMultipart(HandlerContext $context, RequestInterface $request): bool {
|
||||
|
@ -48,18 +46,13 @@ class RouterProcessors implements RouteHandler {
|
|||
if(empty($boundary))
|
||||
return false;
|
||||
|
||||
$stream = $request->getBody();
|
||||
if($stream->isSeekable())
|
||||
$stream->rewind();
|
||||
|
||||
$context->deps->register(MultipartFormContent::parseStream($stream, $boundary));
|
||||
$context->deps->register(MultipartFormContent::parseStream($request->getBody(), $boundary));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return void|int */
|
||||
#[Preprocessor('input:multipart')]
|
||||
public function preInputMultipart(HandlerContext $context, RequestInterface $request, bool $required = true) {
|
||||
public function preInputMultipart(HandlerContext $context, RequestInterface $request, bool $required = true): void {
|
||||
try {
|
||||
$result = $this->handlePreInputMultipart($context, $request);
|
||||
} catch(RuntimeException $ex) {
|
||||
|
@ -67,8 +60,11 @@ class RouterProcessors implements RouteHandler {
|
|||
$required = true;
|
||||
}
|
||||
|
||||
if(!$result && $required)
|
||||
return 400;
|
||||
if(!$result && $required) {
|
||||
$context->response->statusCode = 400;
|
||||
$context->response->reasonPhrase = '';
|
||||
$context->halt();
|
||||
}
|
||||
}
|
||||
|
||||
#[Postprocessor('output:stream')]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// PatternRoute.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-23
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing\Routes;
|
||||
|
||||
|
@ -19,15 +19,13 @@ class PatternRoute extends RouteAttribute {
|
|||
* @param string $method HTTP Method.
|
||||
* @param string $pattern URI path pattern.
|
||||
* @param bool $raw If true, $pattern is taken as is, if false, $pattern is prefixed with #^ and suffixed with $#uD
|
||||
* @param bool $slash Only has effect if $raw is false. If true /? is appended to $pattern to allow for trailing slashes.
|
||||
*/
|
||||
public function __construct(
|
||||
private string $method,
|
||||
string $pattern,
|
||||
bool $raw = false,
|
||||
bool $slash = true,
|
||||
bool $raw = false
|
||||
) {
|
||||
$this->pattern = $raw ? $pattern : sprintf('#^%s%s$#uD', $pattern, $slash ? '/?' : '');
|
||||
$this->pattern = $raw ? $pattern : sprintf('#^%s$#uD', $pattern);
|
||||
}
|
||||
|
||||
public function createInstance(Closure $handler): RouteInfo {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ExactPathUriMatcher.php
|
||||
// Created: 2025-03-07
|
||||
// Updated: 2025-03-23
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing\UriMatchers;
|
||||
|
||||
|
@ -19,7 +19,6 @@ class ExactPathUriMatcher implements UriMatcher {
|
|||
) {}
|
||||
|
||||
public function match(UriInterface $uri): array|bool {
|
||||
$path = $uri->getPath();
|
||||
return $path === $this->path || $path === ($this->path . '/');
|
||||
return $uri->getPath() === $this->path;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Stopwatch.php
|
||||
// Created: 2021-04-26
|
||||
// Updated: 2025-04-03
|
||||
// Updated: 2025-02-27
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
|
@ -9,15 +9,11 @@ namespace Index\Performance;
|
|||
* Provides a means to measure elapsed time at a high resolution.
|
||||
*/
|
||||
class Stopwatch {
|
||||
private int $start = -1;
|
||||
private int $elapsed = 0;
|
||||
private int $frequency;
|
||||
|
||||
/**
|
||||
* @param int $start Starting value, must be the output of hrtime(true) if specified.
|
||||
*/
|
||||
public function __construct(
|
||||
private int $start = -1
|
||||
) {
|
||||
public function __construct() {
|
||||
$this->frequency = PerformanceCounter::frequency();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<?php
|
||||
// TimingPoint.php
|
||||
// Created: 2022-02-16
|
||||
// Updated: 2025-04-03
|
||||
// Updated: 2025-02-27
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Represents a timing point.
|
||||
*/
|
||||
class TimingPoint implements Stringable {
|
||||
class TimingPoint {
|
||||
/**
|
||||
* @param int $timePoint Starting tick count.
|
||||
* @param int $duration Duration tick count.
|
||||
|
@ -32,14 +30,4 @@ class TimingPoint implements Stringable {
|
|||
public int|float $durationTime {
|
||||
get => $this->duration / PerformanceCounter::frequency();
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
$string = $this->name;
|
||||
if(!empty($this->comment))
|
||||
$string .= sprintf(';desc="%s"', rawurlencode($this->comment));
|
||||
|
||||
$string .= sprintf(';dur=%.5f', $this->durationTime);
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
<?php
|
||||
// Timings.php
|
||||
// Created: 2022-02-16
|
||||
// Updated: 2025-04-03
|
||||
// Updated: 2025-02-27
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Represents a Stopwatch with timing points.
|
||||
*/
|
||||
class Timings implements Stringable {
|
||||
class Timings {
|
||||
public private(set) Stopwatch $stopwatch;
|
||||
private int $lastLapTicks;
|
||||
|
||||
|
@ -75,8 +74,4 @@ class Timings implements Stringable {
|
|||
$comment
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return implode(', ', $this->laps);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
// BinarySnowflake.php
|
||||
// Created: 2025-03-26
|
||||
// Updated: 2025-03-26
|
||||
|
||||
namespace Index\Snowflake;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Implements a snowflake generator wrapper that takes user provided data.
|
||||
*/
|
||||
class BinarySnowflake {
|
||||
/**
|
||||
* Reference to the underlying generator.
|
||||
*/
|
||||
public private(set) SnowflakeGenerator $generator;
|
||||
|
||||
/**
|
||||
* @param bool $littleEndian Whether to convert the given data to a little endian integer or big endian.
|
||||
* @param ?SnowflakeGenerator $generator Snowflake generator instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public private(set) bool $littleEndian = false,
|
||||
?SnowflakeGenerator $generator = null,
|
||||
) {
|
||||
$this->generator = $generator ?? new SnowflakeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next snowflake.
|
||||
*
|
||||
* @param string $data Data to shove in.
|
||||
* @param DateTimeInterface|string|int<0, max>|null $at Timestamp to base at.
|
||||
* @return int
|
||||
*/
|
||||
public function next(string $data, DateTimeInterface|string|int|null $at = null): int {
|
||||
$length = strlen($data);
|
||||
if($length <= 1) {
|
||||
$format = 'C';
|
||||
$data = str_pad($data, 1, "\0", STR_PAD_LEFT);
|
||||
} elseif($length === 2) {
|
||||
$format = $this->littleEndian ? 'v' : 'n';
|
||||
} elseif($length <= 4) {
|
||||
$format = $this->littleEndian ? 'V' : 'N';
|
||||
$data = str_pad($data, 4, "\0", STR_PAD_LEFT);
|
||||
} else {
|
||||
$format = $this->littleEndian ? 'P' : 'J';
|
||||
if($length < 8)
|
||||
$data = str_pad($data, 8, "\0", STR_PAD_LEFT);
|
||||
elseif($length > 8)
|
||||
$data = substr($data, 0, 8);
|
||||
}
|
||||
|
||||
$data = unpack(sprintf('%sdata', $format), $data);
|
||||
$data = is_array($data) && isset($data['data']) && is_int($data['data']) && $data['data'] >= 0 ? $data['data'] : 0;
|
||||
|
||||
return $this->generator->next($data, $at);
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
// IncrementalSnowflake.php
|
||||
// Created: 2025-03-26
|
||||
// Updated: 2025-03-26
|
||||
|
||||
namespace Index\Snowflake;
|
||||
|
||||
use DateTimeInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Implements a snowflake generator wrapper that just counts up.
|
||||
*/
|
||||
class IncrementalSnowflake {
|
||||
/**
|
||||
* Reference to the underlying generator.
|
||||
*/
|
||||
public private(set) SnowflakeGenerator $generator;
|
||||
|
||||
/**
|
||||
* @param int<0, max> $counter Incremental counter value.
|
||||
* @param ?SnowflakeGenerator $generator Snowflake generator instance.
|
||||
* @throws InvalidArgumentException if $counter is less than 0.
|
||||
*/
|
||||
public function __construct(
|
||||
public private(set) int $counter = 0,
|
||||
?SnowflakeGenerator $generator = null,
|
||||
) {
|
||||
if($counter < 0)
|
||||
throw new InvalidArgumentException('$counter must be a positive integer');
|
||||
|
||||
$this->generator = $generator ?? new SnowflakeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the counter, with overflow protection.
|
||||
*
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function increment(): int {
|
||||
return $this->counter = ($this->counter >= PHP_INT_MAX ? 0 : ($this->counter + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next snowflake.
|
||||
*
|
||||
* @param DateTimeInterface|string|int<0, max>|null $at Timestamp to base at.
|
||||
* @return int
|
||||
*/
|
||||
public function next(DateTimeInterface|string|int|null $at = null): int {
|
||||
return $this->generator->next($this->increment(), $at);
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
// RandomSnowflake.php
|
||||
// Created: 2025-03-26
|
||||
// Updated: 2025-03-26
|
||||
|
||||
namespace Index\Snowflake;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Random\{Engine,Randomizer};
|
||||
|
||||
/**
|
||||
* Implements a snowflake generator wrapper that uses a randomizer engine.
|
||||
*/
|
||||
class RandomSnowflake {
|
||||
/**
|
||||
* Underlying randomness source.
|
||||
*/
|
||||
public private(set) Randomizer $randomizer;
|
||||
|
||||
/**
|
||||
* Reference to the underlying generator.
|
||||
*/
|
||||
public private(set) SnowflakeGenerator $generator;
|
||||
|
||||
/**
|
||||
* @param ?Engine $engine Randomness engine to use.
|
||||
* @param ?SnowflakeGenerator $generator Snowflake generator instance.
|
||||
*/
|
||||
public function __construct(
|
||||
?Engine $engine = null,
|
||||
?SnowflakeGenerator $generator = null,
|
||||
) {
|
||||
$this->randomizer = new Randomizer($engine);
|
||||
$this->generator = $generator ?? new SnowflakeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next snowflake.
|
||||
*
|
||||
* @param DateTimeInterface|string|int<0, max>|null $at Timestamp to base at.
|
||||
* @return int
|
||||
*/
|
||||
public function next(DateTimeInterface|string|int|null $at = null): int {
|
||||
return $this->generator->next(abs($this->randomizer->nextInt()), $at);
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
<?php
|
||||
// SnowflakeGenerator.php
|
||||
// Created: 2025-03-26
|
||||
// Updated: 2025-03-26
|
||||
|
||||
namespace Index\Snowflake;
|
||||
|
||||
use DateTimeInterface;
|
||||
use InvalidArgumentException;
|
||||
use Index\XDateTime;
|
||||
|
||||
/**
|
||||
* Base snowflake generator.
|
||||
*/
|
||||
final class SnowflakeGenerator {
|
||||
/**
|
||||
* Masks the output for a signed 64-bit integer.
|
||||
*/
|
||||
public const int MASK = 0x7FFFFFFFFFFFFFFF;
|
||||
|
||||
/**
|
||||
* Default epoch value, 1st of January 2013 at 0:00.
|
||||
*/
|
||||
public const int EPOCH = 1356998400000;
|
||||
|
||||
/**
|
||||
* Amount by which to shift the timestamp left and clear space for sequence data.
|
||||
*/
|
||||
public const int SHIFT = 16;
|
||||
|
||||
/**
|
||||
* Cached mask for timestamps.
|
||||
*/
|
||||
public private(set) int $tsMask;
|
||||
|
||||
/**
|
||||
* Cached mask for sequence data.
|
||||
*/
|
||||
public private(set) int $sqMask;
|
||||
|
||||
/**
|
||||
* @param int<0, max> $epoch Timestamp epoch.
|
||||
* @param int<0, 63> $shift Amount to shift the timestamp left by.
|
||||
* @throws InvalidArgumentException if $epoch or $shift is not within the given boundaries.
|
||||
*/
|
||||
public function __construct(
|
||||
public private(set) int $epoch = self::EPOCH,
|
||||
public private(set) int $shift = self::SHIFT,
|
||||
) {
|
||||
if($epoch < 0 || $epoch > self::MASK)
|
||||
throw new InvalidArgumentException('$epoch is not a valid positive integer');
|
||||
if($shift < 1 || $shift > 63)
|
||||
throw new InvalidArgumentException('$shift must be between 0 and 63 inclusive');
|
||||
|
||||
$this->tsMask = ~(~0 << (63 - $shift));
|
||||
$this->sqMask = ~(~0 << $shift);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current timestamp with the epoch subtracted.
|
||||
*
|
||||
* @param DateTimeInterface|string|int<0, max>|null $at Timestamp to base at.
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function nowMs(DateTimeInterface|string|int|null $at = null): int {
|
||||
return max(0, XDateTime::toUnixMilliseconds($at ?? XDateTime::now(), true) - $this->epoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a snowflake.
|
||||
*
|
||||
* @param int<0, max> $sequence Sequence data, will be masked to fit within $shift in the constructor.
|
||||
* @param DateTimeInterface|string|int<0, max>|null $at Timestamp to base at.
|
||||
* @throws InvalidArgumentException if $sequence is not a positive integer.
|
||||
* @return int
|
||||
*/
|
||||
public function next(int $sequence = 0, DateTimeInterface|string|int|null $at = null): int {
|
||||
if($sequence < 0)
|
||||
throw new InvalidArgumentException('$sequence must be a positive integer');
|
||||
|
||||
return (($this->nowMs($at) & $this->tsMask) << $this->shift) | ($sequence & $this->sqMask);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
// TplExtensionCommon.php
|
||||
// Created: 2025-04-01
|
||||
// Updated: 2025-04-01
|
||||
|
||||
namespace Index\Templating\Extension;
|
||||
|
||||
use ReflectionClass;
|
||||
use Twig\Node\Expression\Binary\AbstractBinary;
|
||||
use Twig\Node\Expression\Unary\AbstractUnary;
|
||||
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||
use Twig\TokenParser\TokenParserInterface;
|
||||
use Twig\{ExpressionParser,OperatorPrecedenceChange,TwigFilter,TwigFunction,TwigTest};
|
||||
|
||||
/**
|
||||
* Provides common implementations of the Twig extension methods so you can yoink them without having to extend AbstractExtension.
|
||||
*/
|
||||
trait TplExtensionCommon {
|
||||
/** @return TokenParserInterface[] */
|
||||
public function getTokenParsers() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return NodeVisitorInterface[] */
|
||||
public function getNodeVisitors() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return TwigFilter[] */
|
||||
public function getFilters() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return TwigTest[] */
|
||||
public function getTests() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return TwigFunction[] */
|
||||
public function getFunctions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractUnary>}>,
|
||||
* array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class?: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
|
||||
* }
|
||||
*/
|
||||
public function getOperators() {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
public function getLastModified(): int {
|
||||
$path = (new ReflectionClass($this))->getFileName();
|
||||
if($path === false || !is_file($path))
|
||||
return 0;
|
||||
|
||||
$time = filemtime($path);
|
||||
if($time === false)
|
||||
return 0;
|
||||
|
||||
return $time;
|
||||
}
|
||||
}
|
|
@ -1,28 +1,24 @@
|
|||
<?php
|
||||
// TplIndexExtension.php
|
||||
// Created: 2024-08-04
|
||||
// Updated: 2025-04-01
|
||||
// Updated: 2024-10-04
|
||||
|
||||
namespace Index\Templating\Extension;
|
||||
|
||||
use Index\{ByteFormat,Index};
|
||||
use Twig\{Environment,TwigFilter,TwigFunction};
|
||||
use Twig\Extension\LastModifiedExtensionInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
|
||||
/**
|
||||
* Provides version functions and additional functionality implemented in Index.
|
||||
*/
|
||||
class TplIndexExtension implements LastModifiedExtensionInterface {
|
||||
use TplExtensionCommon;
|
||||
|
||||
#[\Override]
|
||||
class TplIndexExtension extends AbstractExtension {
|
||||
public function getFilters() {
|
||||
return [
|
||||
new TwigFilter('format_filesize', ByteFormat::format(...)),
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getFunctions() {
|
||||
return [
|
||||
new TwigFunction('ndx_version', Index::version(...)),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// XArray.php
|
||||
// Created: 2022-02-02
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2025-01-21
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -10,7 +10,6 @@ use InvalidArgumentException;
|
|||
use Countable;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Provides various helper methods for collections.
|
||||
|
@ -481,7 +480,7 @@ final class XArray {
|
|||
if($iterable instanceof Iterator)
|
||||
return $iterable;
|
||||
if($iterable instanceof IteratorAggregate)
|
||||
return self::extractIterator($iterable->getIterator()); // @phpstan-ignore return.type
|
||||
return $iterable->getIterator(); // @phpstan-ignore-line
|
||||
if(is_array($iterable))
|
||||
return new ArrayIterator($iterable);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// XDateTime.php
|
||||
// Created: 2024-07-31
|
||||
// Updated: 2025-03-26
|
||||
// Updated: 2025-01-18
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -24,30 +24,6 @@ final class XDateTime {
|
|||
return new DateTimeImmutable('now');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current date and time in seconds since the 0:00 on January 1st, 1970.
|
||||
*
|
||||
* @param DateTimeInterface|string|int $dt
|
||||
* @return int
|
||||
*/
|
||||
public static function toUnixSeconds(DateTimeInterface|string|int|null $dt): int {
|
||||
return (int)self::format($dt, 'U');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current date and time in milliseconds since the 0:00 on January 1st, 1970.
|
||||
*
|
||||
* @param DateTimeInterface|string|int $dt
|
||||
* @param bool $intDtAsMilliseconds Whether to treat $dt as milliseconds instead of seconds if it is an integer.
|
||||
* @return int
|
||||
*/
|
||||
public static function toUnixMilliseconds(DateTimeInterface|string|int|null $dt, bool $intDtAsMilliseconds = false): int {
|
||||
if($intDtAsMilliseconds && is_int($dt))
|
||||
$dt = new DateTimeImmutable(sprintf('@%.3F', (float)$dt / 1000));
|
||||
|
||||
return (int)self::format($dt, 'Uv');
|
||||
}
|
||||
|
||||
private static function toCompareFormat(DateTimeInterface|string|int $dt): string {
|
||||
if($dt instanceof DateTimeInterface)
|
||||
return $dt->format(self::COMPARE_FORMAT);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// BencodeSerialisableTest.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2025-01-18
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -53,9 +53,9 @@ final class BencodeSerialisableTest extends TestCase {
|
|||
#[BencodeProperty(omitIfNull: false)]
|
||||
public mixed $nullValPresent = null;
|
||||
|
||||
/** @var array<?scalar> */
|
||||
#[BencodeProperty]
|
||||
public array $scalarVals = [null, 0, 1234, 12.34, 'str', true, false];
|
||||
/** @var scalar[] */
|
||||
public array $scalarVals = [null, 0, 1234, 12.34, 'str', true, false]; // @phpstan-ignore-line: idgi??
|
||||
|
||||
#[BencodeProperty('stringVal_method')]
|
||||
public function getStringVal(): string {
|
||||
|
@ -112,8 +112,8 @@ final class BencodeSerialisableTest extends TestCase {
|
|||
return null;
|
||||
}
|
||||
|
||||
/** @return array<?scalar> */
|
||||
#[BencodeProperty]
|
||||
#[BencodeProperty] // @phpstan-ignore-line: idgi??
|
||||
/** @return scalar[] */
|
||||
public function getScalarVals(): array {
|
||||
return [null, 0, 1234, 12.34, 'str', true, false];
|
||||
}
|
||||
|
@ -156,11 +156,11 @@ final class BencodeSerialisableTest extends TestCase {
|
|||
use BencodeSerializableCommon;
|
||||
|
||||
#[BencodeProperty('test1')]
|
||||
#[BencodeProperty('test2')] // @phpstan-ignore attribute.nonRepeatable
|
||||
#[BencodeProperty('test2')] // @phpstan-ignore-line: this is meant to test the dupe exception
|
||||
public string $stringVal = 'string value';
|
||||
|
||||
#[BencodeProperty('test3')]
|
||||
#[BencodeProperty('test4')] // @phpstan-ignore attribute.nonRepeatable
|
||||
#[BencodeProperty('test4')] // @phpstan-ignore-line: this is meant to test the dupe exception
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// BencodeTest.php
|
||||
// Created: 2023-07-21
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2024-12-02
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -35,15 +35,15 @@ final class BencodeTest extends TestCase {
|
|||
|
||||
$this->assertIsObject($decoded);
|
||||
$this->assertObjectHasProperty('announce', $decoded);
|
||||
$this->assertEquals('https://tracker.flashii.net/announce.php/meow', $decoded->announce); // @phpstan-ignore property.notFound
|
||||
$this->assertEquals('https://tracker.flashii.net/announce.php/meow', $decoded->announce); // @phpstan-ignore-line: checked above
|
||||
$this->assertObjectHasProperty('creation date', $decoded);
|
||||
$this->assertEquals(1689973664, $decoded->{'creation date'}); // @phpstan-ignore property.notFound
|
||||
$this->assertEquals(1689973664, $decoded->{'creation date'}); // @phpstan-ignore-line: checked above
|
||||
$this->assertObjectHasProperty('comment', $decoded);
|
||||
$this->assertEquals('this is the comments field', $decoded->comment); // @phpstan-ignore property.notFound
|
||||
$this->assertEquals('this is the comments field', $decoded->comment); // @phpstan-ignore-line: checked above
|
||||
$this->assertObjectHasProperty('info', $decoded);
|
||||
$this->assertIsObject($decoded->info); // @phpstan-ignore property.notFound
|
||||
$this->assertIsObject($decoded->info); // @phpstan-ignore-line: checked above
|
||||
$this->assertObjectHasProperty('private', $decoded->info);
|
||||
$this->assertEquals(1, $decoded->info->private); // @phpstan-ignore property.notFound
|
||||
$this->assertEquals(1, $decoded->info->private); // @phpstan-ignore-line: checked above
|
||||
}
|
||||
|
||||
public function testEncode(): void {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbConfigTest.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2025-01-18
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -17,8 +17,8 @@ use Index\Db\{DbBackends,DbConnection};
|
|||
#[CoversClass(GetValueInfoCommon::class)]
|
||||
#[CoversClass(GetValuesCommon::class)]
|
||||
final class DbConfigTest extends TestCase {
|
||||
private DbConnection $dbConn; // @phpstan-ignore property.uninitialized
|
||||
private DbConfig $config; // @phpstan-ignore property.uninitialized
|
||||
private DbConnection $dbConn; // @phpstan-ignore-line: defined by PHPunit in setUp()
|
||||
private DbConfig $config; // @phpstan-ignore-line: defined by PHPunit in setUp()
|
||||
|
||||
private const VALUES = [
|
||||
'private.allow_password_reset' => 'b:1;',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DependenciesTest.php
|
||||
// Created: 2025-01-21
|
||||
// Updated: 2025-03-21
|
||||
// Updated: 2025-03-07
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -60,7 +60,8 @@ namespace {
|
|||
}
|
||||
|
||||
public function testRequiredDependency(): void {
|
||||
$this->expectException(TypeError::class);
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage(sprintf('was not able to resolve %s', DependenciesTest\DependencyTestInterface::class));
|
||||
|
||||
$deps = new Dependencies;
|
||||
$deps->register(DependenciesTest\RequiredParamTestClass::class);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// JsonSerializableTest.php
|
||||
// Created: 2024-09-29
|
||||
// Updated: 2025-04-02
|
||||
// Updated: 2025-01-18
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -51,9 +51,9 @@ final class JsonSerializableTest extends TestCase {
|
|||
#[JsonProperty(omitIfNull: false)]
|
||||
public mixed $nullValPresent = null;
|
||||
|
||||
/** @var array<?scalar> */
|
||||
#[JsonProperty]
|
||||
public array $scalarVals = [null, 0, 1234, 12.34, 'str', true, false];
|
||||
/** @var scalar[] */
|
||||
public array $scalarVals = [null, 0, 1234, 12.34, 'str', true, false]; // @phpstan-ignore-line: idgi??
|
||||
|
||||
#[JsonProperty('stringVal_method')]
|
||||
public function getStringVal(): string {
|
||||
|
@ -110,8 +110,8 @@ final class JsonSerializableTest extends TestCase {
|
|||
return null;
|
||||
}
|
||||
|
||||
/** @return array<?scalar> */
|
||||
#[JsonProperty]
|
||||
#[JsonProperty] // @phpstan-ignore-line: idgi??
|
||||
/** @return scalar[] */
|
||||
public function getScalarVals(): array {
|
||||
return [null, 0, 1234, 12.34, 'str', true, false];
|
||||
}
|
||||
|
@ -154,11 +154,11 @@ final class JsonSerializableTest extends TestCase {
|
|||
use JsonSerializableCommon;
|
||||
|
||||
#[JsonProperty('test1')]
|
||||
#[JsonProperty('test2')] // @phpstan-ignore attribute.nonRepeatable
|
||||
#[JsonProperty('test2')] // @phpstan-ignore-line: this is meant to test the dupe exception
|
||||
public string $stringVal = 'string value';
|
||||
|
||||
#[JsonProperty('test3')]
|
||||
#[JsonProperty('test4')] // @phpstan-ignore attribute.nonRepeatable
|
||||
#[JsonProperty('test4')] // @phpstan-ignore-line: this is meant to test the dupe exception
|
||||
public function getIntVal(): int {
|
||||
return 1234;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// RouterTest.php
|
||||
// Created: 2022-01-20
|
||||
// Updated: 2025-03-23
|
||||
// Updated: 2025-03-19
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -112,39 +112,6 @@ final class RouterTest extends TestCase {
|
|||
return sprintf('still the profile of %s', $beans);
|
||||
}
|
||||
|
||||
#[ExactRoute('GET', '/announce')]
|
||||
#[ExactRoute('GET', '/announce.php')]
|
||||
#[PatternRoute('GET', '/announce/([A-Za-z0-9]+)')]
|
||||
#[PatternRoute('GET', '/announce.php/([A-Za-z0-9]+)')]
|
||||
public function getMultipleTestWithDefault(string $key = 'empty'): string {
|
||||
return $key;
|
||||
}
|
||||
|
||||
#[PatternRoute('GET', '/uploads/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
|
||||
public function getEepromEdgeCase(
|
||||
string $uploadId,
|
||||
string $variant = '',
|
||||
string $extension = ''
|
||||
): string {
|
||||
return sprintf('%s//%s//%s', $uploadId, $variant, $extension);
|
||||
}
|
||||
|
||||
#[ExactRoute('GET', '/messages/stats')]
|
||||
public function getStats(): string {
|
||||
return 'hit exact';
|
||||
}
|
||||
|
||||
#[PatternRoute('GET', '/messages/([A-Za-z0-9]+)')]
|
||||
public function getView(): string {
|
||||
return 'hit pattern';
|
||||
}
|
||||
|
||||
#[ExactRoute('GET', '/assets/avatar')]
|
||||
#[PatternRoute('GET', '/assets/avatar/([0-9]+)(?:\.[a-z]+)?')]
|
||||
public function getTrailingSlash(): string {
|
||||
return 'hit';
|
||||
}
|
||||
|
||||
public function hasNoAttr(): string {
|
||||
return 'not a route';
|
||||
}
|
||||
|
@ -159,18 +126,6 @@ final class RouterTest extends TestCase {
|
|||
$this->assertSame('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/meow'))->getBody());
|
||||
$this->assertSame('profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile/Cool134'))->getBody());
|
||||
$this->assertSame('still the profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile-but-raw/Cool134'))->getBody());
|
||||
$this->assertSame('empty', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce'))->getBody());
|
||||
$this->assertSame('empty', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce.php'))->getBody());
|
||||
$this->assertSame('Z643QANLgGNkF4D4h4qvFpyeXjx4TcDE', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce/Z643QANLgGNkF4D4h4qvFpyeXjx4TcDE'))->getBody());
|
||||
$this->assertSame('1aKq8VaGyHohNUUR7RzU1W57Z3hQ6m0YMazAkr2IoiSPsvQJ6QoQutywwiOBlNka', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce.php/1aKq8VaGyHohNUUR7RzU1W57Z3hQ6m0YMazAkr2IoiSPsvQJ6QoQutywwiOBlNka'))->getBody());
|
||||
$this->assertSame('1RJNSRYmxrvXUr////', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr'))->getBody());
|
||||
$this->assertSame('1RJNSRYmxrvXUr//thumb//', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr-thumb'))->getBody());
|
||||
$this->assertSame('1RJNSRYmxrvXUr//thumb//jpg', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr-thumb.jpg'))->getBody());
|
||||
$this->assertSame('1RJNSRYmxrvXUr////jpg', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr.jpg'))->getBody());
|
||||
$this->assertSame('hit exact', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/stats'))->getBody());
|
||||
$this->assertSame('hit pattern', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/soaps'))->getBody());
|
||||
$this->assertSame('hit', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/assets/avatar/'))->getBody());
|
||||
$this->assertSame('hit', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/assets/avatar/123/'))->getBody());
|
||||
}
|
||||
|
||||
public function testEEPROMSituation(): void {
|
||||
|
@ -299,19 +254,6 @@ final class RouterTest extends TestCase {
|
|||
public function testOutputBencode(): array {
|
||||
return ['benben' => 12345];
|
||||
}
|
||||
|
||||
#[After('output:bencode')]
|
||||
#[ExactRoute('GET', '/output/bencode/object')]
|
||||
public function testOutputBencodeObject(): object {
|
||||
return new class implements \Index\Bencode\BencodeSerializable {
|
||||
use \Index\Bencode\BencodeSerializableCommon;
|
||||
|
||||
public function __construct(
|
||||
#[\Index\Bencode\BencodeProperty('failure reason')]
|
||||
public private(set) string $reason = 'Invalid info hash.'
|
||||
) {}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/stream'));
|
||||
|
@ -346,10 +288,6 @@ final class RouterTest extends TestCase {
|
|||
$this->assertSame('application/x-bittorrent', $response->getHeaderLine('Content-Type'));
|
||||
$this->assertSame('d6:benbeni12345ee', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/bencode/object'));
|
||||
$this->assertSame('application/x-bittorrent', $response->getHeaderLine('Content-Type'));
|
||||
$this->assertSame('d14:failure reason18:Invalid info hash.e', (string)$response->getBody());
|
||||
|
||||
$response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/optional-form'));
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('none', (string)$response->getBody());
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
// SnowflakeTest.php
|
||||
// Created: 2025-03-26
|
||||
// Updated: 2025-03-26
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
|
||||
use Index\XDateTime;
|
||||
use Index\Snowflake\{BinarySnowflake,IncrementalSnowflake,RandomSnowflake,SnowflakeGenerator};
|
||||
use Random\Engine\Mt19937;
|
||||
|
||||
#[CoversClass(IncrementalSnowflake::class)]
|
||||
#[CoversClass(SnowflakeGenerator::class)]
|
||||
#[UsesClass(XDateTime::class)]
|
||||
final class SnowflakeTest extends TestCase {
|
||||
public function testSnowflakeGenerator(): void {
|
||||
// default epoch and shift
|
||||
$generator = new SnowflakeGenerator;
|
||||
$this->assertSame(SnowflakeGenerator::EPOCH, $generator->epoch);
|
||||
$this->assertSame(SnowflakeGenerator::SHIFT, $generator->shift);
|
||||
$this->assertSame(0x7FFFFFFFFFFF, $generator->tsMask);
|
||||
$this->assertSame(0xFFFF, $generator->sqMask);
|
||||
$snowflake = $generator->next(0x123456, 1742946997548);
|
||||
$this->assertSame(0x59DC543D2C3456, $snowflake);
|
||||
|
||||
// copy date exactly
|
||||
$generator = new SnowflakeGenerator(epoch: 0);
|
||||
$this->assertSame(0, $generator->epoch);
|
||||
$this->assertSame(SnowflakeGenerator::SHIFT, $generator->shift);
|
||||
$this->assertSame(0x7FFFFFFFFFFF, $generator->tsMask);
|
||||
$this->assertSame(0xFFFF, $generator->sqMask);
|
||||
$snowflake = $generator->next(0x654321, 1742946997548);
|
||||
$this->assertSame(0x195CFBC952C4321, $snowflake);
|
||||
|
||||
// quirky shift
|
||||
$generator = new SnowflakeGenerator(shift: 14);
|
||||
$this->assertSame(SnowflakeGenerator::EPOCH, $generator->epoch);
|
||||
$this->assertSame(14, $generator->shift);
|
||||
$this->assertSame(0x1FFFFFFFFFFFF, $generator->tsMask);
|
||||
$this->assertSame(0x3FFF, $generator->sqMask);
|
||||
$snowflake = $generator->next(0xFDE9, 1742947403098);
|
||||
$this->assertSame(0x1677169B56BDE9, $snowflake);
|
||||
|
||||
// incremental
|
||||
$incremental = new IncrementalSnowflake(0x10001, $generator);
|
||||
$this->assertSame(0x10002, $incremental->increment());
|
||||
$this->assertSame(0x16771F5C1C4003, $incremental->next(1742949697649));
|
||||
|
||||
// big endian binary
|
||||
$big = new BinarySnowflake(generator: new SnowflakeGenerator(shift: 20));
|
||||
$this->assertSame(0x59DC920D23000DE, $big->next("\xDE", 1742951048483));
|
||||
$this->assertSame(0x59DC920D230DEAD, $big->next("\xDE\xAD", 1742951048483));
|
||||
$this->assertSame(0x59DC920D23EADBE, $big->next("\xDE\xAD\xBE", 1742951048483));
|
||||
$this->assertSame(0x59DC920D23FBEAD, $big->next("\xDE\xAF\xBE\xAD", 1742951048483));
|
||||
|
||||
// little endian binary
|
||||
$little = new BinarySnowflake(littleEndian: true, generator: new SnowflakeGenerator(shift: 20));
|
||||
$this->assertSame(0x59DC920D23000DE, $little->next("\xDE", 1742951048483));
|
||||
$this->assertSame(0x59DC920D230ADDE, $little->next("\xDE\xAD", 1742951048483));
|
||||
$this->assertSame(0x59DC920D23DDE00, $little->next("\xDE\xAD\xBE", 1742951048483));
|
||||
$this->assertSame(0x59DC920D23EAFDE, $little->next("\xDE\xAF\xBE\xAD", 1742951048483));
|
||||
|
||||
// random
|
||||
$random = new RandomSnowflake(new Mt19937(2525));
|
||||
$this->assertSame(0x59DCAD88D22D3E, $random->next(1742952849618));
|
||||
$this->assertSame(0x59DCAD88D2CFDC, $random->next(1742952849618));
|
||||
$this->assertSame(0x59DCAD88D27EA2, $random->next(1742952849618));
|
||||
$this->assertSame(0x59DCAD88D26E0A, $random->next(1742952849618));
|
||||
$this->assertSame(0x59DCAD88D24C90, $random->next(1742952849618));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue