Raised PHPStan analysis level and fixed issues.
This commit is contained in:
parent
5be38a7090
commit
6e3e86f416
63 changed files with 600 additions and 280 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2408.20020
|
||||
0.2408.32027
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
parameters:
|
||||
level: 5 # Raise this eventually
|
||||
level: 9
|
||||
paths:
|
||||
- src
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ArrayIterator.php
|
||||
// Created: 2022-02-03
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -9,12 +9,14 @@ use Iterator;
|
|||
|
||||
/**
|
||||
* Provides an Iterator implementation for normal arrays.
|
||||
*
|
||||
* @implements Iterator<mixed, mixed>
|
||||
*/
|
||||
class ArrayIterator implements Iterator {
|
||||
private bool $wasValid = true;
|
||||
|
||||
/**
|
||||
* @param array $array Array to iterate upon.
|
||||
* @param array<mixed, mixed> $array Array to iterate upon.
|
||||
*/
|
||||
public function __construct(
|
||||
private array $array
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// CSRFP.php
|
||||
// Created: 2021-06-11
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -74,7 +74,10 @@ class CSRFP {
|
|||
return false;
|
||||
|
||||
$unpacked = unpack('Vts', $token);
|
||||
$uTime = (int)$unpacked['ts'];
|
||||
if($unpacked === false)
|
||||
return false;
|
||||
|
||||
$uTime = (int)($unpacked['ts'] ?? 0);
|
||||
if($uTime < 0)
|
||||
return false;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ArrayCacheBackend.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\ArrayCache;
|
||||
|
||||
|
@ -34,7 +34,7 @@ class ArrayCacheBackend implements ICacheBackend {
|
|||
*
|
||||
* ArrayCache has no parameters that can be controlled using the DSN.
|
||||
*
|
||||
* @param string|array $dsn DSN with provider information.
|
||||
* @param string|array<string, string|int> $dsn DSN with provider information.
|
||||
* @return ArrayCacheProviderInfo Dummy provider info instance.
|
||||
*/
|
||||
public function parseDsn(string|array $dsn): ICacheProviderInfo {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ArrayCacheProvider.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\ArrayCache;
|
||||
|
||||
|
@ -12,6 +12,7 @@ use Index\Cache\ICacheProvider;
|
|||
* Represents a dummy cache provider.
|
||||
*/
|
||||
class ArrayCacheProvider implements ICacheProvider {
|
||||
/** @var array<string, array{ttl: int, value: string}> */
|
||||
private array $items = [];
|
||||
|
||||
public function get(string $key): mixed {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// CacheTools.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache;
|
||||
|
||||
|
@ -34,6 +34,7 @@ final class CacheTools {
|
|||
'keydb' => Valkey\ValkeyBackend::class,
|
||||
];
|
||||
|
||||
/** @return array<string, int|string> */
|
||||
private static function parseDsnUri(string $dsn): array {
|
||||
$uri = parse_url($dsn);
|
||||
if($uri === false)
|
||||
|
@ -41,6 +42,7 @@ final class CacheTools {
|
|||
return $uri;
|
||||
}
|
||||
|
||||
/** @param array<string, int|string> $uri */
|
||||
private static function resolveBackend(array $uri): ICacheBackend {
|
||||
static $backends = [];
|
||||
|
||||
|
@ -54,7 +56,7 @@ final class CacheTools {
|
|||
if(array_key_exists($scheme, self::CACHE_PROTOS))
|
||||
$name = self::CACHE_PROTOS[$scheme];
|
||||
else
|
||||
$name = str_replace('-', '\\', $scheme);
|
||||
$name = str_replace('-', '\\', (string)$scheme);
|
||||
|
||||
if(class_exists($name) && is_subclass_of($name, ICacheBackend::class)) {
|
||||
$backend = new $name;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ICacheBackend.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache;
|
||||
|
||||
|
@ -29,7 +29,7 @@ interface ICacheBackend {
|
|||
/**
|
||||
* Constructs a cache info instance from a dsn.
|
||||
*
|
||||
* @param string|array $dsn DSN with provider information.
|
||||
* @param string|array<string, string|int> $dsn DSN with provider information.
|
||||
* @return ICacheProviderInfo Provider info based on the dsn.
|
||||
*/
|
||||
function parseDsn(string|array $dsn): ICacheProviderInfo;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MemcachedBackend.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\Memcached;
|
||||
|
||||
|
@ -71,7 +71,7 @@ class MemcachedBackend implements ICacheBackend {
|
|||
*
|
||||
* Why is the legacy memcache extension supported? cuz I felt like it.
|
||||
*
|
||||
* @param string|array $dsn DSN with provider information.
|
||||
* @param string|array<string, string|int> $dsn DSN with provider information.
|
||||
* @return MemcachedProviderInfo Memcached provider info instance.
|
||||
*/
|
||||
public function parseDsn(string|array $dsn): ICacheProviderInfo {
|
||||
|
@ -92,13 +92,13 @@ class MemcachedBackend implements ICacheBackend {
|
|||
if(isset($dsn['port']))
|
||||
$host .= ':' . $dsn['port'];
|
||||
|
||||
$endPoints[] = [EndPoint::parse($host), 0];
|
||||
$endPoints[] = [EndPoint::parse((string)$host), 0];
|
||||
}
|
||||
|
||||
$endPoint = $isPool ? null : EndPoint::parse($host);
|
||||
$prefixKey = str_replace('/', ':', ltrim($dsn['path'] ?? '', '/'));
|
||||
$endPoint = $isPool ? null : EndPoint::parse((string)$host);
|
||||
$prefixKey = str_replace('/', ':', ltrim((string)($dsn['path'] ?? ''), '/'));
|
||||
|
||||
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query);
|
||||
parse_str(str_replace('+', '%2B', (string)($dsn['query'] ?? '')), $query);
|
||||
|
||||
if(isset($query['server'])) {
|
||||
if(is_string($query['server']))
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
<?php
|
||||
// MemcachedProviderInfo.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\Memcached;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\Cache\ICacheProviderInfo;
|
||||
use Index\Net\EndPoint;
|
||||
|
||||
/**
|
||||
* Represents Memcached provider info.
|
||||
*/
|
||||
class MemcachedProviderInfo implements ICacheProviderInfo {
|
||||
/**
|
||||
* @param array<int, array{EndPoint, int}> $endPoints Memcached end points.
|
||||
* @param string $prefixKey Prefix to apply to keys.
|
||||
* @param string $persistName Persistent connection name.
|
||||
* @param bool $useBinaryProto Use the binary protocol.
|
||||
* @param bool $enableCompression Use compression.
|
||||
* @param bool $tcpNoDelay Disable Nagle's algorithm.
|
||||
*/
|
||||
public function __construct(
|
||||
private array $endPoints,
|
||||
private string $prefixKey,
|
||||
|
@ -24,7 +33,7 @@ class MemcachedProviderInfo implements ICacheProviderInfo {
|
|||
/**
|
||||
* Gets server endpoints.
|
||||
*
|
||||
* @return array
|
||||
* @return array<int, array{EndPoint, int}>
|
||||
*/
|
||||
public function getEndPoints(): array {
|
||||
return $this->endPoints;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MemcachedProviderLegacy.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\Memcached;
|
||||
|
||||
|
@ -42,7 +42,7 @@ class MemcachedProviderLegacy extends MemcachedProvider {
|
|||
|
||||
public function get(string $key): mixed {
|
||||
$value = $this->memcache->get($this->prefix . $key);
|
||||
return $value === false ? null : unserialize($value);
|
||||
return is_string($value) ? unserialize($value) : null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, int $ttl = 0): void {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MemcachedProviderModern.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\Memcached;
|
||||
|
||||
|
@ -17,7 +17,7 @@ class MemcachedProviderModern extends MemcachedProvider {
|
|||
private Memcached $memcached;
|
||||
|
||||
public function __construct(MemcachedProviderInfo $providerInfo) {
|
||||
$this->memcached = new Memcached($providerInfo->getPersistentName());
|
||||
$this->memcached = $providerInfo->isPersistent() ? new Memcached($providerInfo->getPersistentName() ?? '') : new Memcached;
|
||||
$this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, $providerInfo->shouldUseBinaryProtocol());
|
||||
$this->memcached->setOption(Memcached::OPT_COMPRESSION, $providerInfo->shouldEnableCompression());
|
||||
$this->memcached->setOption(Memcached::OPT_TCP_NODELAY, $providerInfo->shouldTcpNoDelay());
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ValkeyBackend.php
|
||||
// Created: 2024-04-10
|
||||
// Updated: 2024-04-10
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Cache\Valkey;
|
||||
|
||||
|
@ -52,7 +52,7 @@ class ValkeyBackend implements ICacheBackend {
|
|||
* - `db=<number>`: Allows you to select a different database.
|
||||
* - `persist`: Uses a persistent connection.
|
||||
*
|
||||
* @param string|array $dsn DSN with provider information.
|
||||
* @param string|array<string, int|string> $dsn DSN with provider information.
|
||||
* @return ValkeyProviderInfo Valkey provider info instance.
|
||||
*/
|
||||
public function parseDsn(string|array $dsn): ICacheProviderInfo {
|
||||
|
@ -84,11 +84,11 @@ class ValkeyBackend implements ICacheBackend {
|
|||
if(isset($dsn['port']))
|
||||
$host .= ':' . $dsn['port'];
|
||||
|
||||
$endPoint = EndPoint::parse($host);
|
||||
$endPoint = EndPoint::parse((string)$host);
|
||||
}
|
||||
|
||||
$prefix = str_replace('/', ':', ltrim($dsn['path'] ?? '', '/'));
|
||||
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query);
|
||||
$prefix = str_replace('/', ':', ltrim((string)($dsn['path'] ?? ''), '/'));
|
||||
parse_str(str_replace('+', '%2B', (string)($dsn['query'] ?? '')), $query);
|
||||
|
||||
$unixPath = isset($query['socket']) && is_string($query['socket']) ? $query['socket'] : '';
|
||||
$dbNumber = isset($query['db']) && is_string($query['db']) && ctype_digit($query['db']) ? (int)$query['db'] : 0;
|
||||
|
@ -100,6 +100,6 @@ class ValkeyBackend implements ICacheBackend {
|
|||
$endPoint = new UnixEndPoint($unixPath);
|
||||
}
|
||||
|
||||
return new ValkeyProviderInfo($endPoint, $prefix, $persist, $username, $password, $dbNumber);
|
||||
return new ValkeyProviderInfo($endPoint, $prefix, $persist, (string)$username, (string)$password, $dbNumber);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Colour.php
|
||||
// Created: 2023-01-02
|
||||
// Updated: 2023-11-09
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Colour;
|
||||
|
||||
|
@ -194,9 +194,9 @@ abstract class Colour implements Stringable {
|
|||
}
|
||||
|
||||
if($length === 6)
|
||||
return ColourRGB::fromRawRGB(hexdec($value));
|
||||
return ColourRGB::fromRawRGB((int)hexdec($value));
|
||||
if($length === 8)
|
||||
return ColourRGB::fromRawRGBA(hexdec($value));
|
||||
return ColourRGB::fromRawRGBA((int)hexdec($value));
|
||||
}
|
||||
|
||||
return self::$none;
|
||||
|
@ -223,10 +223,12 @@ abstract class Colour implements Stringable {
|
|||
if($parts !== 3 && $parts !== 4)
|
||||
return self::$none;
|
||||
|
||||
$value[0] = (int)trim($value[0]);
|
||||
$value[1] = (int)trim($value[1]);
|
||||
$value[2] = (int)trim($value[2]);
|
||||
$value[3] = (float)trim($value[3] ?? '1');
|
||||
$value = [
|
||||
(int)trim($value[0]),
|
||||
(int)trim($value[1]),
|
||||
(int)trim($value[2]),
|
||||
(float)trim($value[3] ?? '1'),
|
||||
];
|
||||
}
|
||||
|
||||
return new ColourRGB(...$value);
|
||||
|
@ -256,41 +258,46 @@ abstract class Colour implements Stringable {
|
|||
for($i = 0; $i < $parts; ++$i)
|
||||
$value[$i] = trim($value[$i]);
|
||||
|
||||
if(str_ends_with($value[1], '%'))
|
||||
$value[1] = substr($value[1], 0, -1);
|
||||
$hue = $value[0];
|
||||
$saturation = $value[1];
|
||||
$lightness = $value[2];
|
||||
$alpha = $value[3] ?? null;
|
||||
|
||||
if(str_ends_with($value[2], '%'))
|
||||
$value[2] = substr($value[2], 0, -1);
|
||||
if(str_ends_with($saturation, '%'))
|
||||
$saturation = substr($saturation, 0, -1);
|
||||
|
||||
$value[1] = (float)$value[1];
|
||||
$value[2] = (float)$value[2];
|
||||
if(str_ends_with($lightness, '%'))
|
||||
$lightness = substr($lightness, 0, -1);
|
||||
|
||||
if($value[1] < 0 || $value[1] > 100 || $value[2] < 0 || $value[2] > 100)
|
||||
$saturation = (float)$saturation;
|
||||
$lightness = (float)$lightness;
|
||||
|
||||
if($saturation < 0 || $saturation > 100 || $lightness < 0 || $lightness > 100)
|
||||
return self::$none;
|
||||
|
||||
$value[1] /= 100.0;
|
||||
$value[2] /= 100.0;
|
||||
$saturation /= 100.0;
|
||||
$lightness /= 100.0;
|
||||
|
||||
if(ctype_digit($value[0])) {
|
||||
$value[0] = (float)$value[0];
|
||||
if(ctype_digit($hue)) {
|
||||
$hue = (float)$hue;
|
||||
} else {
|
||||
if(str_ends_with($value[0], 'deg')) {
|
||||
$value[0] = (float)substr($value[0], 0, -3);
|
||||
} elseif(str_ends_with($value[0], 'grad')) {
|
||||
$value[0] = 0.9 * (float)substr($value[0], 0, -4);
|
||||
} elseif(str_ends_with($value[0], 'rad')) {
|
||||
$value[0] = round(rad2deg((float)substr($value[0], 0, -3)));
|
||||
} elseif(str_ends_with($value[0], 'turn')) {
|
||||
$value[0] = 360.0 * ((float)substr($value[0], 0, -4));
|
||||
if(str_ends_with($hue, 'deg')) {
|
||||
$hue = (float)substr($hue, 0, -3);
|
||||
} elseif(str_ends_with($hue, 'grad')) {
|
||||
$hue = 0.9 * (float)substr($hue, 0, -4);
|
||||
} elseif(str_ends_with($hue, 'rad')) {
|
||||
$hue = round(rad2deg((float)substr($hue, 0, -3)));
|
||||
} elseif(str_ends_with($hue, 'turn')) {
|
||||
$hue = 360.0 * ((float)substr($hue, 0, -4));
|
||||
} else {
|
||||
return self::$none;
|
||||
}
|
||||
}
|
||||
|
||||
$value[3] = (float)trim($value[3] ?? '1');
|
||||
$alpha = (float)trim($alpha ?? '1');
|
||||
}
|
||||
|
||||
return new ColourHSL(...$value);
|
||||
return new ColourHSL($hue, $saturation, $lightness, $alpha);
|
||||
}
|
||||
|
||||
return self::$none;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbResultIterator.php
|
||||
// Created: 2024-02-06
|
||||
// Updated: 2024-02-06
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
|
@ -10,6 +10,8 @@ use Iterator;
|
|||
|
||||
/**
|
||||
* Implements an iterator and constructor wrapper for IDbResult.
|
||||
*
|
||||
* @implements Iterator<int, object>
|
||||
*/
|
||||
class DbResultIterator implements Iterator {
|
||||
private bool $wasValid;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbResultTrait.php
|
||||
// Created: 2023-11-09
|
||||
// Updated: 2024-02-06
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
|
@ -10,27 +10,30 @@ namespace Index\Data;
|
|||
*/
|
||||
trait DbResultTrait {
|
||||
public function getString(int|string $index): string {
|
||||
return (string)$this->getValue($index);
|
||||
$value = $this->getValue($index);
|
||||
return is_scalar($value) ? (string)$value : '';
|
||||
}
|
||||
|
||||
public function getStringOrNull(int|string $index): ?string {
|
||||
return $this->isNull($index) ? null : (string)$this->getValue($index);
|
||||
return $this->isNull($index) ? null : $this->getString($index);
|
||||
}
|
||||
|
||||
public function getInteger(int|string $index): int {
|
||||
return (int)$this->getValue($index);
|
||||
$value = $this->getValue($index);
|
||||
return is_scalar($value) ? (int)$value : 0;
|
||||
}
|
||||
|
||||
public function getIntegerOrNull(int|string $index): ?int {
|
||||
return $this->isNull($index) ? null : (int)$this->getValue($index);
|
||||
return $this->isNull($index) ? null : $this->getInteger($index);
|
||||
}
|
||||
|
||||
public function getFloat(int|string $index): float {
|
||||
return (float)$this->getValue($index);
|
||||
$value = $this->getValue($index);
|
||||
return is_scalar($value) ? (float)$value : 0.0;
|
||||
}
|
||||
|
||||
public function getFloatOrNull(int|string $index): ?float {
|
||||
return $this->isNull($index) ? null : (float)$this->getValue($index);
|
||||
return $this->isNull($index) ? null : $this->getFloat($index);
|
||||
}
|
||||
|
||||
public function getBoolean(int|string $index): bool {
|
||||
|
@ -38,7 +41,7 @@ trait DbResultTrait {
|
|||
}
|
||||
|
||||
public function getBooleanOrNull(int|string $index): ?bool {
|
||||
return $this->isNull($index) ? null : ($this->getInteger($index) !== 0);
|
||||
return $this->isNull($index) ? null : $this->getBoolean($index);
|
||||
}
|
||||
|
||||
public function getIterator(callable $construct): DbResultIterator {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbStatementCache.php
|
||||
// Created: 2023-07-21
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
|
@ -9,6 +9,7 @@ namespace Index\Data;
|
|||
* Container to avoid having many prepared instances of the same query.
|
||||
*/
|
||||
class DbStatementCache {
|
||||
/** @var array<string, IDbStatement> */
|
||||
private array $stmts = [];
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbTools.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
|
@ -33,6 +33,7 @@ final class DbTools {
|
|||
'sqlite3' => SQLite\SQLiteBackend::class,
|
||||
];
|
||||
|
||||
/** @return array<string, string|int> */
|
||||
private static function parseDsnUri(string $dsn): array {
|
||||
$uri = parse_url($dsn);
|
||||
if($uri === false)
|
||||
|
@ -40,6 +41,7 @@ final class DbTools {
|
|||
return $uri;
|
||||
}
|
||||
|
||||
/** @param array<string, string|int> $uri */
|
||||
private static function resolveBackend(array $uri): IDbBackend {
|
||||
static $backends = [];
|
||||
|
||||
|
@ -53,7 +55,7 @@ final class DbTools {
|
|||
if(array_key_exists($scheme, self::DB_PROTOS))
|
||||
$name = self::DB_PROTOS[$scheme];
|
||||
else
|
||||
$name = str_replace('-', '\\', $scheme);
|
||||
$name = str_replace('-', '\\', (string)$scheme);
|
||||
|
||||
if(class_exists($name) && is_subclass_of($name, IDbBackend::class)) {
|
||||
$backend = new $name;
|
||||
|
@ -161,7 +163,7 @@ final class DbTools {
|
|||
/**
|
||||
* Constructs a partial query for prepared statements of lists.
|
||||
*
|
||||
* @param Countable|array|int $count Amount of times to repeat the string in the $repeat parameter.
|
||||
* @param Countable|mixed[]|int $count Amount of times to repeat the string in the $repeat parameter.
|
||||
* @param string $repeat String to repeat.
|
||||
* @param string $glue Glue character.
|
||||
* @return string Glued string ready for being thrown into your prepared statement.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// IDbBackend.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2022-02-28
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
|
@ -28,7 +28,7 @@ interface IDbBackend {
|
|||
/**
|
||||
* Constructs a connection info instance from a dsn.
|
||||
*
|
||||
* @param string|array $dsn DSN with connection information.
|
||||
* @param string|array<string, int|string> $dsn DSN with connection information.
|
||||
* @return IDbConnectionInfo Connection info based on the dsn.
|
||||
*/
|
||||
function parseDsn(string|array $dsn): IDbConnectionInfo;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBBackend.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -77,7 +77,7 @@ class MariaDBBackend implements IDbBackend {
|
|||
* Previously supported query parameters:
|
||||
* - `enc_verify=<anything>`: Enabled verification of server certificate. Replaced with `enc_no_verify` as it now defaults to on.
|
||||
*
|
||||
* @param string|array $dsn DSN with connection information.
|
||||
* @param string|array<string, int|string> $dsn DSN with connection information.
|
||||
* @return MariaDBConnectionInfo MariaDB connection info.
|
||||
*/
|
||||
public function parseDsn(string|array $dsn): IDbConnectionInfo {
|
||||
|
@ -98,21 +98,37 @@ class MariaDBBackend implements IDbBackend {
|
|||
if(!$needsUnix && isset($dsn['port']))
|
||||
$host .= ':' . $dsn['port'];
|
||||
|
||||
$user = $dsn['user'] ?? '';
|
||||
$pass = $dsn['pass'] ?? '';
|
||||
$endPoint = $needsUnix ? null : EndPoint::parse($host);
|
||||
$dbName = str_replace('/', '_', trim($dsn['path'], '/')); // cute for table prefixes i think
|
||||
$user = (string)($dsn['user'] ?? '');
|
||||
$pass = (string)($dsn['pass'] ?? '');
|
||||
$endPoint = $needsUnix ? null : EndPoint::parse((string)$host);
|
||||
$dbName = str_replace('/', '_', trim((string)$dsn['path'], '/')); // cute for table prefixes i think
|
||||
|
||||
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query);
|
||||
parse_str(str_replace('+', '%2B', (string)($dsn['query'] ?? '')), $query);
|
||||
|
||||
$unixPath = $query['socket'] ?? null;
|
||||
if(!is_string($unixPath)) $unixPath = null;
|
||||
|
||||
$charSet = $query['charset'] ?? null;
|
||||
if(!is_string($charSet)) $charSet = null;
|
||||
|
||||
$initCommand = $query['init'] ?? null;
|
||||
if(!is_string($initCommand)) $initCommand = null;
|
||||
|
||||
$keyPath = $query['enc_key'] ?? null;
|
||||
if(!is_string($keyPath)) $keyPath = null;
|
||||
|
||||
$certPath = $query['enc_cert'] ?? null;
|
||||
if(!is_string($certPath)) $certPath = null;
|
||||
|
||||
$certAuthPath = $query['enc_authority'] ?? null;
|
||||
if(!is_string($certAuthPath)) $certAuthPath = null;
|
||||
|
||||
$trustedCertsPath = $query['enc_trusted_certs'] ?? null;
|
||||
if(!is_string($trustedCertsPath)) $trustedCertsPath = null;
|
||||
|
||||
$cipherAlgos = $query['enc_ciphers'] ?? null;
|
||||
if(!is_string($cipherAlgos)) $cipherAlgos = null;
|
||||
|
||||
$verifyCert = !isset($query['enc_no_verify']);
|
||||
$useCompression = isset($query['compress']);
|
||||
|
||||
|
@ -122,6 +138,9 @@ class MariaDBBackend implements IDbBackend {
|
|||
$endPoint = new UnixEndPoint($unixPath);
|
||||
}
|
||||
|
||||
if($endPoint === null)
|
||||
throw new InvalidArgumentException('No end point could be determined from the provided DSN.');
|
||||
|
||||
return new MariaDBConnectionInfo(
|
||||
$endPoint, $user, $pass, $dbName,
|
||||
$charSet, $initCommand, $keyPath, $certPath,
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<?php
|
||||
// MariaDBCharacterSetInfo.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Contains information about the character set.
|
||||
*
|
||||
|
@ -16,11 +14,11 @@ class MariaDBCharacterSetInfo {
|
|||
/**
|
||||
* Creates a new character set info instance.
|
||||
*
|
||||
* @param stdClass $charSet Anonymous object containing the information.
|
||||
* @param object $charSet Anonymous object containing the information.
|
||||
* @return MariaDBCharacterSetInfo Character set information class.
|
||||
*/
|
||||
public function __construct(
|
||||
private stdClass $charSet
|
||||
private object $charSet
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +27,7 @@ class MariaDBCharacterSetInfo {
|
|||
* @return string Character set name.
|
||||
*/
|
||||
public function getCharacterSet(): string {
|
||||
return $this->charSet->charset;
|
||||
return $this->charSet->charset ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,7 +36,7 @@ class MariaDBCharacterSetInfo {
|
|||
* @return string Default collation name.
|
||||
*/
|
||||
public function getDefaultCollation(): string {
|
||||
return $this->charSet->collation;
|
||||
return $this->charSet->collation ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,7 +46,7 @@ class MariaDBCharacterSetInfo {
|
|||
* @return string Source directory.
|
||||
*/
|
||||
public function getDirectory(): string {
|
||||
return $this->charSet->dir;
|
||||
return $this->charSet->dir ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,7 +55,7 @@ class MariaDBCharacterSetInfo {
|
|||
* @return int Minimum character width in bytes.
|
||||
*/
|
||||
public function getMinimumWidth(): int {
|
||||
return $this->charSet->min_length;
|
||||
return $this->charSet->min_length ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +64,7 @@ class MariaDBCharacterSetInfo {
|
|||
* @return int Maximum character width in bytes.
|
||||
*/
|
||||
public function getMaximumWidth(): int {
|
||||
return $this->charSet->max_length;
|
||||
return $this->charSet->max_length ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,7 +73,7 @@ class MariaDBCharacterSetInfo {
|
|||
* @return int Character set identifier.
|
||||
*/
|
||||
public function getId(): int {
|
||||
return $this->charSet->number;
|
||||
return $this->charSet->number ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,6 +84,6 @@ class MariaDBCharacterSetInfo {
|
|||
* @return int Character set status.
|
||||
*/
|
||||
public function getState(): int {
|
||||
return $this->charSet->state;
|
||||
return $this->charSet->state ?? 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<?php
|
||||
// MariaDBConnection.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
use mysqli;
|
||||
use mysqli_sql_exception;
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Index\Data\{IDbConnection,IDbTransactions};
|
||||
|
@ -87,14 +88,18 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
// nothing suggests otherwise too.
|
||||
// The output of mysqli_init is just an object anyway so we can safely use it instead
|
||||
// but continue to use it as an object.
|
||||
$this->connection = mysqli_init();
|
||||
$connection = mysqli_init();
|
||||
if($connection === false)
|
||||
throw new RuntimeException('mysqli_init() returned false!');
|
||||
|
||||
$this->connection = $connection;
|
||||
$this->connection->options(MYSQLI_OPT_LOCAL_INFILE, 0);
|
||||
|
||||
if($connectionInfo->hasCharacterSet())
|
||||
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet());
|
||||
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet() ?? '');
|
||||
|
||||
if($connectionInfo->hasInitCommand())
|
||||
$this->connection->options(MYSQLI_INIT_COMMAND, $connectionInfo->getInitCommand());
|
||||
$this->connection->options(MYSQLI_INIT_COMMAND, $connectionInfo->getInitCommand() ?? '');
|
||||
|
||||
$flags = $connectionInfo->shouldUseCompression() ? MYSQLI_CLIENT_COMPRESS : 0;
|
||||
|
||||
|
@ -242,7 +247,7 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
/**
|
||||
* Gets a list of errors from the last command.
|
||||
*
|
||||
* @return array List of last errors.
|
||||
* @return MariaDBWarning[] List of last errors.
|
||||
*/
|
||||
public function getLastErrors(): array {
|
||||
return MariaDBWarning::fromLastErrors($this->connection->error_list);
|
||||
|
@ -262,7 +267,7 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
*
|
||||
* The result of SHOW WARNINGS;
|
||||
*
|
||||
* @return array List of warnings.
|
||||
* @return MariaDBWarning[] List of warnings.
|
||||
*/
|
||||
public function getWarnings(): array {
|
||||
return MariaDBWarning::fromGetWarnings($this->connection->get_warnings());
|
||||
|
@ -370,8 +375,14 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
} catch(mysqli_sql_exception $ex) {
|
||||
throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex);
|
||||
}
|
||||
|
||||
if($result === false)
|
||||
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode());
|
||||
|
||||
// the mysql library is very adorable
|
||||
if($result === true)
|
||||
throw new RuntimeException('Query succeeded but you should have use the execute method for that query.');
|
||||
|
||||
// Yes, this always uses Native, for some reason the stupid limitation in libmysql only applies to preparing
|
||||
return new MariaDBResultNative($result);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBResult.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -17,6 +17,7 @@ use Index\IO\{Stream,TempFileStream};
|
|||
abstract class MariaDBResult implements IDbResult {
|
||||
use DbResultTrait;
|
||||
|
||||
/** @var array<int|string, mixed> */
|
||||
protected array $currentRow = [];
|
||||
|
||||
/**
|
||||
|
@ -70,7 +71,12 @@ abstract class MariaDBResult implements IDbResult {
|
|||
public function getStream(int|string $index): ?Stream {
|
||||
if($this->isNull($index))
|
||||
return null;
|
||||
return new TempFileStream($this->getValue($index));
|
||||
|
||||
$value = $this->getValue($index);
|
||||
if(!is_scalar($value))
|
||||
return null;
|
||||
|
||||
return new TempFileStream((string)$value);
|
||||
}
|
||||
|
||||
abstract function close(): void;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBResultLib.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -14,7 +14,10 @@ use RuntimeException;
|
|||
* @internal
|
||||
*/
|
||||
class MariaDBResultLib extends MariaDBResult {
|
||||
/** @var mixed[] */
|
||||
private array $fields = [];
|
||||
|
||||
/** @var mixed[] */
|
||||
private array $data = [];
|
||||
|
||||
public function __construct(mysqli_stmt $statement) {
|
||||
|
@ -24,6 +27,9 @@ class MariaDBResultLib extends MariaDBResult {
|
|||
throw new RuntimeException($statement->error, $statement->errno);
|
||||
|
||||
$metadata = $statement->result_metadata();
|
||||
if($metadata === false)
|
||||
throw new RuntimeException('result_metadata output was false');
|
||||
|
||||
while($field = $metadata->fetch_field())
|
||||
$this->fields[] = &$this->data[$field->name];
|
||||
|
||||
|
@ -31,6 +37,9 @@ class MariaDBResultLib extends MariaDBResult {
|
|||
}
|
||||
|
||||
public function next(): bool {
|
||||
if(!($this->result instanceof mysqli_stmt))
|
||||
return false;
|
||||
|
||||
$result = $this->result->fetch();
|
||||
if($result === false)
|
||||
throw new RuntimeException($this->result->error, $this->result->errno);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBResultNative.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -27,9 +27,13 @@ class MariaDBResultNative extends MariaDBResult {
|
|||
}
|
||||
|
||||
public function next(): bool {
|
||||
if(!($this->result instanceof mysqli_result))
|
||||
return false;
|
||||
|
||||
$result = $this->result->fetch_array(MYSQLI_BOTH);
|
||||
if($result === null)
|
||||
return false;
|
||||
|
||||
$this->currentRow = $result;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBStatement.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -15,6 +15,7 @@ use Index\IO\Stream;
|
|||
* Represents a MariaDB/MySQL prepared statement.
|
||||
*/
|
||||
class MariaDBStatement implements IDbStatement {
|
||||
/** @var array<int, MariaDBParameter> */
|
||||
private array $params = [];
|
||||
|
||||
/**
|
||||
|
@ -28,6 +29,8 @@ class MariaDBStatement implements IDbStatement {
|
|||
) {}
|
||||
|
||||
private static bool $constructed = false;
|
||||
|
||||
/** @var class-string<MariaDBResult> */
|
||||
private static string $resultImplementation;
|
||||
|
||||
/**
|
||||
|
@ -84,7 +87,7 @@ class MariaDBStatement implements IDbStatement {
|
|||
/**
|
||||
* Gets a list of errors from the last command.
|
||||
*
|
||||
* @return array List of last errors.
|
||||
* @return MariaDBWarning[] List of last errors.
|
||||
*/
|
||||
public function getLastErrors(): array {
|
||||
return MariaDBWarning::fromLastErrors($this->statement->error_list);
|
||||
|
@ -95,7 +98,7 @@ class MariaDBStatement implements IDbStatement {
|
|||
*
|
||||
* The result of SHOW WARNINGS;
|
||||
*
|
||||
* @return array List of warnings.
|
||||
* @return MariaDBWarning[] List of warnings.
|
||||
*/
|
||||
public function getWarnings(): array {
|
||||
return MariaDBWarning::fromGetWarnings($this->statement->get_warnings());
|
||||
|
@ -126,7 +129,7 @@ class MariaDBStatement implements IDbStatement {
|
|||
} elseif(is_resource($value)) {
|
||||
while($data = fread($value, 8192))
|
||||
$this->statement->send_long_data($key, $data);
|
||||
} else
|
||||
} elseif(is_scalar($value))
|
||||
$this->statement->send_long_data($key, (string)$value);
|
||||
|
||||
$value = null;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBWarning.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -61,8 +61,8 @@ class MariaDBWarning implements Stringable {
|
|||
/**
|
||||
* Creates an array of warning objects from the output of $last_errors.
|
||||
*
|
||||
* @param array $errors Array of warning arrays.
|
||||
* @return array Array of warnings objects.
|
||||
* @param array<int, array{errno: int, sqlstate: string, error: string}> $errors Array of warning arrays.
|
||||
* @return MariaDBWarning[] Array of warnings objects.
|
||||
*/
|
||||
public static function fromLastErrors(array $errors): array {
|
||||
return XArray::select($errors, fn($error) => new MariaDBWarning($error['error'], $error['sqlstate'], $error['errno']));
|
||||
|
@ -72,7 +72,7 @@ class MariaDBWarning implements Stringable {
|
|||
* Creates an array of warning objects from a mysqli_warning instance.
|
||||
*
|
||||
* @param mysqli_warning|false $warnings Warning object.
|
||||
* @return array Array of warning objects.
|
||||
* @return MariaDBWarning[] Array of warning objects.
|
||||
*/
|
||||
public static function fromGetWarnings(mysqli_warning|false $warnings): array {
|
||||
$array = [];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbMigrationManager.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
|
@ -200,7 +200,7 @@ EOF;
|
|||
* Runs all migrations present in a migration repository. This process is irreversible!
|
||||
*
|
||||
* @param IDbMigrationRepo $migrations Migrations repository to fetch migrations from.
|
||||
* @return array Names of migrations that have been completed.
|
||||
* @return string[] Names of migrations that have been completed.
|
||||
*/
|
||||
public function processMigrations(IDbMigrationRepo $migrations): array {
|
||||
$migrations = $migrations->getMigrations();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// FsDbMigrationInfo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
|
@ -41,6 +41,9 @@ class FsDbMigrationInfo implements IDbMigrationInfo {
|
|||
|
||||
public function migrate(IDbConnection $conn): void {
|
||||
require_once $this->path;
|
||||
(new $this->className)->migrate($conn);
|
||||
|
||||
$migration = new $this->className;
|
||||
if($migration instanceof IDbMigration)
|
||||
$migration->migrate($conn);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<?php
|
||||
// FsDbMigrationRepo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FsDbMigrationRepo implements IDbMigrationRepo {
|
||||
/**
|
||||
* @param string $path Filesystem path to the directory containing the migration files.
|
||||
|
@ -16,7 +18,11 @@ class FsDbMigrationRepo implements IDbMigrationRepo {
|
|||
public function getMigrations(): array {
|
||||
if(!is_dir($this->path))
|
||||
return [];
|
||||
|
||||
$files = glob(realpath($this->path) . '/*.php');
|
||||
if($files === false)
|
||||
throw new RuntimeException('Failed to glob migration files.');
|
||||
|
||||
$migrations = [];
|
||||
|
||||
foreach($files as $file)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// NullDbBackend.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\NullDb;
|
||||
|
||||
|
@ -30,7 +30,7 @@ class NullDbBackend implements IDbBackend {
|
|||
*
|
||||
* NullDb has no parameters that can be controlled using the DSN.
|
||||
*
|
||||
* @param string|array $dsn DSN with connection information.
|
||||
* @param string|array<string, int|string> $dsn DSN with connection information.
|
||||
* @return NullDbConnectionInfo Dummy connection info instance.
|
||||
*/
|
||||
public function parseDsn(string|array $dsn): IDbConnectionInfo {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// SQLiteBackend.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\SQLite;
|
||||
|
||||
|
@ -51,7 +51,7 @@ class SQLiteBackend implements IDbBackend {
|
|||
* - `readOnly` to open a database in read-only mode.
|
||||
* - `openOnly` to prevent a new file from being created if the specified path does not exist.
|
||||
*
|
||||
* @param string|array $dsn DSN with connection information.
|
||||
* @param string|array<string, int|string> $dsn DSN with connection information.
|
||||
* @return SQLiteConnectionInfo SQLite connection info.
|
||||
*/
|
||||
public function parseDsn(string|array $dsn): IDbConnectionInfo {
|
||||
|
@ -63,14 +63,16 @@ class SQLiteBackend implements IDbBackend {
|
|||
|
||||
$encKey = '';
|
||||
|
||||
$path = $dsn['path'] ?? '';
|
||||
$path = (string)($dsn['path'] ?? '');
|
||||
|
||||
if($path === 'memory:')
|
||||
$path = ':memory:';
|
||||
|
||||
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query);
|
||||
parse_str(str_replace('+', '%2B', (string)($dsn['query'] ?? '')), $query);
|
||||
|
||||
$encKey = $query['key'] ?? '';
|
||||
if(!is_string($encKey)) $encKey = '';
|
||||
|
||||
$readOnly = isset($query['readOnly']);
|
||||
$create = !isset($query['openOnly']);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// SQLiteConnection.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\SQLite;
|
||||
|
||||
|
@ -477,7 +477,8 @@ class SQLiteConnection implements IDbConnection, IDbTransactions {
|
|||
$handle = $this->connection->openBlob($table, $column, $rowId);
|
||||
if(!is_resource($handle))
|
||||
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
|
||||
return new GenericStream($this->connection->openBlob($table, $column, $rowId));
|
||||
|
||||
return new GenericStream($handle);
|
||||
}
|
||||
|
||||
public function getLastInsertId(): int|string {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// SQLiteResult.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Data\SQLite;
|
||||
|
||||
|
@ -16,6 +16,7 @@ use Index\IO\{Stream,TempFileStream};
|
|||
class SQLiteResult implements IDbResult {
|
||||
use DbResultTrait;
|
||||
|
||||
/** @var array<int|string, mixed> */
|
||||
private array $currentRow = [];
|
||||
|
||||
/**
|
||||
|
@ -60,7 +61,12 @@ class SQLiteResult implements IDbResult {
|
|||
public function getStream(int|string $index): ?Stream {
|
||||
if($this->isNull($index))
|
||||
return null;
|
||||
return new TempFileStream($this->getValue($index));
|
||||
|
||||
$value = $this->getValue($index);
|
||||
if(!is_scalar($value))
|
||||
return null;
|
||||
|
||||
return new TempFileStream((string)$value);
|
||||
}
|
||||
|
||||
public function close(): void {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// FormContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
|
@ -14,7 +14,7 @@ use Index\Http\HttpUploadedFile;
|
|||
class FormContent implements IHttpContent {
|
||||
/**
|
||||
* @param array<string, mixed> $postFields Form fields.
|
||||
* @param array<string, HttpUploadedFile> $uploadedFiles Uploaded files.
|
||||
* @param array<string, HttpUploadedFile|array<string, mixed>> $uploadedFiles Uploaded files.
|
||||
*/
|
||||
public function __construct(
|
||||
private array $postFields,
|
||||
|
@ -26,7 +26,7 @@ class FormContent implements IHttpContent {
|
|||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @param array<string, mixed>|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the form field, null if not present.
|
||||
*/
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
|
@ -84,6 +84,9 @@ class FormContent implements IHttpContent {
|
|||
public function getUploadedFile(string $name): HttpUploadedFile {
|
||||
if(!isset($this->uploadedFiles[$name]))
|
||||
throw new RuntimeException('No file with name $name present.');
|
||||
if(is_array($this->uploadedFiles[$name]))
|
||||
throw new RuntimeException('Array based accessors are unsupported currently.');
|
||||
|
||||
return $this->uploadedFiles[$name];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// JsonContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
|
@ -38,7 +38,11 @@ class JsonContent implements IHttpContent, JsonSerializable {
|
|||
* @return string JSON encoded string.
|
||||
*/
|
||||
public function encode(): string {
|
||||
return json_encode($this->content);
|
||||
$encoded = json_encode($this->content);
|
||||
if($encoded === false)
|
||||
return '';
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
|
@ -48,10 +52,10 @@ class JsonContent implements IHttpContent, JsonSerializable {
|
|||
/**
|
||||
* Creates an instance from encoded content.
|
||||
*
|
||||
* @param mixed $encoded JSON encoded content.
|
||||
* @param string $encoded JSON encoded content.
|
||||
* @return JsonContent Instance representing the provided content.
|
||||
*/
|
||||
public static function fromEncoded(mixed $encoded): JsonContent {
|
||||
public static function fromEncoded(string $encoded): JsonContent {
|
||||
return new JsonContent(json_decode($encoded));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// StringContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
|
@ -48,7 +48,11 @@ class StringContent implements IHttpContent {
|
|||
* @return StringContent Instance representing the provided path.
|
||||
*/
|
||||
public static function fromFile(string $path): StringContent {
|
||||
return new StringContent(file_get_contents($path));
|
||||
$string = file_get_contents($path);
|
||||
if($string === false)
|
||||
$string = '';
|
||||
|
||||
return new StringContent($string);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// StreamContentHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\ContentHandling;
|
||||
|
||||
|
@ -17,6 +17,7 @@ class StreamContentHandler implements IContentHandler {
|
|||
return $content instanceof Stream;
|
||||
}
|
||||
|
||||
/** @param Stream $content */
|
||||
public function handle(HttpResponseBuilder $response, mixed $content): void {
|
||||
if(!$response->hasContentType())
|
||||
$response->setTypeStream();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HtmlErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\ErrorHandling;
|
||||
|
||||
|
@ -28,8 +28,13 @@ class HtmlErrorHandler implements IErrorHandler {
|
|||
|
||||
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
|
||||
$response->setTypeHTML();
|
||||
|
||||
$charSet = mb_preferred_mime_name(mb_internal_encoding());
|
||||
if($charSet === false)
|
||||
$charSet = 'UTF-8';
|
||||
|
||||
$response->setContent(strtr(self::TEMPLATE, [
|
||||
':charset' => strtolower(mb_preferred_mime_name(mb_internal_encoding())),
|
||||
':charset' => strtolower($charSet),
|
||||
':code' => sprintf('%03d', $code),
|
||||
':message' => $message,
|
||||
]));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpHeader.php
|
||||
// Created: 2022-02-14
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -11,6 +11,7 @@ use Stringable;
|
|||
* Represents a generic HTTP header.
|
||||
*/
|
||||
class HttpHeader implements Stringable {
|
||||
/** @var string[] */
|
||||
private array $lines;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpHeadersBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -11,6 +11,7 @@ use InvalidArgumentException;
|
|||
* Represents a HTTP message header builder.
|
||||
*/
|
||||
class HttpHeadersBuilder {
|
||||
/** @var array<string, string[]> */
|
||||
private array $headers = [];
|
||||
|
||||
/**
|
||||
|
@ -18,9 +19,9 @@ class HttpHeadersBuilder {
|
|||
* If a header with the same name is already present, it will be appended with a new line.
|
||||
*
|
||||
* @param string $name Name of the header to add.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
* @param string $value Value to apply for this header.
|
||||
*/
|
||||
public function addHeader(string $name, mixed $value): void {
|
||||
public function addHeader(string $name, string $value): void {
|
||||
$nameLower = strtolower($name);
|
||||
if(!isset($this->headers[$nameLower]))
|
||||
$this->headers[$nameLower] = [$name];
|
||||
|
@ -32,9 +33,9 @@ class HttpHeadersBuilder {
|
|||
* If a header with the same name is already present, it will be overwritten.
|
||||
*
|
||||
* @param string $name Name of the header to set.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
* @param string $value Value to apply for this header.
|
||||
*/
|
||||
public function setHeader(string $name, mixed $value): void {
|
||||
public function setHeader(string $name, string $value): void {
|
||||
$this->headers[strtolower($name)] = [$name, $value];
|
||||
}
|
||||
|
||||
|
@ -66,7 +67,7 @@ class HttpHeadersBuilder {
|
|||
$headers = [];
|
||||
|
||||
foreach($this->headers as $index => $lines)
|
||||
$headers[] = new HttpHeader(array_shift($lines), ...$lines);
|
||||
$headers[] = new HttpHeader(array_shift($lines) ?? '', ...$lines);
|
||||
|
||||
return new HttpHeaders($headers);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpMessageBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -61,9 +61,9 @@ class HttpMessageBuilder {
|
|||
* If a header with the same name is already present, it will be appended with a new line.
|
||||
*
|
||||
* @param string $name Name of the header to add.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
* @param string $value Value to apply for this header.
|
||||
*/
|
||||
public function addHeader(string $name, mixed $value): void {
|
||||
public function addHeader(string $name, string $value): void {
|
||||
$this->headers->addHeader($name, $value);
|
||||
}
|
||||
|
||||
|
@ -72,9 +72,9 @@ class HttpMessageBuilder {
|
|||
* If a header with the same name is already present, it will be overwritten.
|
||||
*
|
||||
* @param string $name Name of the header to set.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
* @param string $value Value to apply for this header.
|
||||
*/
|
||||
public function setHeader(string $name, mixed $value): void {
|
||||
public function setHeader(string $name, string $value): void {
|
||||
$this->headers->setHeader($name, $value);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequest.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -77,7 +77,7 @@ class HttpRequest extends HttpMessage {
|
|||
*
|
||||
* @param string $name Name of the request query field.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @param mixed[]|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the query field, null if not present.
|
||||
*/
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
|
@ -110,7 +110,7 @@ class HttpRequest extends HttpMessage {
|
|||
*
|
||||
* @param string $name Name of the request cookie.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @param array<string, mixed>|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the cookie, null if not present.
|
||||
*/
|
||||
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
|
@ -187,6 +187,7 @@ class HttpRequest extends HttpMessage {
|
|||
return $build->toRequest();
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private static function getRawRequestHeaders(): array {
|
||||
if(function_exists('getallheaders'))
|
||||
return getallheaders();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequestBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -11,7 +11,11 @@ namespace Index\Http;
|
|||
class HttpRequestBuilder extends HttpMessageBuilder {
|
||||
private string $method = 'GET';
|
||||
private string $path = '/';
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $params = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
private array $cookies = [];
|
||||
|
||||
/**
|
||||
|
@ -109,9 +113,9 @@ class HttpRequestBuilder extends HttpMessageBuilder {
|
|||
* Sets a HTTP request cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param mixed $value Value of the cookie.
|
||||
* @param string $value Value of the cookie.
|
||||
*/
|
||||
public function setCookie(string $name, mixed $value): void {
|
||||
public function setCookie(string $name, string $value): void {
|
||||
$this->cookies[$name] = $value;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpResponseBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -15,6 +15,8 @@ use Index\Performance\Timings;
|
|||
class HttpResponseBuilder extends HttpMessageBuilder {
|
||||
private int $statusCode = -1;
|
||||
private ?string $statusText;
|
||||
|
||||
/** @var string[] */
|
||||
private array $vary = [];
|
||||
|
||||
/**
|
||||
|
@ -73,7 +75,7 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
* Adds a cookie to this response.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param mixed $value Value of the cookie.
|
||||
* @param string $value Value of the cookie.
|
||||
* @param DateTimeInterface|int|null $expires Point at which the cookie should expire.
|
||||
* @param string $path Path to which to apply the cookie.
|
||||
* @param string $domain Domain name to which to apply the cookie.
|
||||
|
@ -83,7 +85,7 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
*/
|
||||
public function addCookie(
|
||||
string $name,
|
||||
mixed $value,
|
||||
string $value,
|
||||
DateTimeInterface|int|null $expires = null,
|
||||
string $path = '',
|
||||
string $domain = '',
|
||||
|
@ -147,7 +149,7 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
/**
|
||||
* Sets a Vary header.
|
||||
*
|
||||
* @param string|array $headers Header or headers that will vary.
|
||||
* @param string|string[] $headers Header or headers that will vary.
|
||||
*/
|
||||
public function addVary(string|array $headers): void {
|
||||
if(!is_array($headers))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpUploadedFile.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -17,6 +17,8 @@ class HttpUploadedFile implements ICloseable {
|
|||
private bool $hasMoved = false;
|
||||
private ?Stream $stream = null;
|
||||
|
||||
private MediaType $suggestedMediaType;
|
||||
|
||||
/**
|
||||
* @param int $errorCode PHP file upload error code.
|
||||
* @param int $size Size, as provided by the client.
|
||||
|
@ -29,10 +31,11 @@ class HttpUploadedFile implements ICloseable {
|
|||
private int $size,
|
||||
private string $localFileName,
|
||||
private string $suggestedFileName,
|
||||
private MediaType|string $suggestedMediaType
|
||||
MediaType|string $suggestedMediaType
|
||||
) {
|
||||
if(!($suggestedMediaType instanceof MediaType))
|
||||
$suggestedMediaType = MediaType::parse($suggestedMediaType);
|
||||
$this->suggestedMediaType = $suggestedMediaType instanceof MediaType
|
||||
? $suggestedMediaType
|
||||
: MediaType::parse($suggestedMediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,22 +163,24 @@ class HttpUploadedFile implements ICloseable {
|
|||
/**
|
||||
* Creates a HttpUploadedFile instance from an entry in the $_FILES superglobal.
|
||||
*
|
||||
* @param array<string, int|string> $file File info array.
|
||||
* @return HttpUploadedFile Uploaded file info.
|
||||
*/
|
||||
public static function createFromFILE(array $file): self {
|
||||
return new HttpUploadedFile(
|
||||
$file['error'] ?? UPLOAD_ERR_NO_FILE,
|
||||
$file['size'] ?? -1,
|
||||
$file['tmp_name'] ?? '',
|
||||
$file['name'] ?? '',
|
||||
$file['type'] ?? ''
|
||||
(int)($file['error'] ?? UPLOAD_ERR_NO_FILE),
|
||||
(int)($file['size'] ?? -1),
|
||||
(string)($file['tmp_name'] ?? ''),
|
||||
(string)($file['name'] ?? ''),
|
||||
(string)($file['type'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection of HttpUploadedFile instances from the $_FILES superglobal.
|
||||
*
|
||||
* @return array<string, mixed> Uploaded files.
|
||||
* @param array<string, mixed> $files Value of a $_FILES superglobal.
|
||||
* @return array<string, HttpUploadedFile|array<string, mixed>> Uploaded files.
|
||||
*/
|
||||
public static function createFromFILES(array $files): array {
|
||||
if(empty($files))
|
||||
|
@ -184,6 +189,10 @@ class HttpUploadedFile implements ICloseable {
|
|||
return self::createObjectInstances(self::normalizeFILES($files));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $files
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function traverseFILES(array $files, string $keyName): array {
|
||||
$arr = [];
|
||||
|
||||
|
@ -200,11 +209,15 @@ class HttpUploadedFile implements ICloseable {
|
|||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $files
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function normalizeFILES(array $files): array {
|
||||
$out = [];
|
||||
|
||||
foreach($files as $key => $arr) {
|
||||
if(empty($arr))
|
||||
if(!is_array($arr) || !isset($arr['error']))
|
||||
continue;
|
||||
|
||||
$key = '_' . $key;
|
||||
|
@ -217,9 +230,8 @@ class HttpUploadedFile implements ICloseable {
|
|||
if(is_array($arr['error'])) {
|
||||
$keys = array_keys($arr);
|
||||
|
||||
foreach($keys as $keyName) {
|
||||
$out[$key] = array_merge_recursive($out[$key] ?? [], self::traverseFILES($arr[$keyName], $keyName));
|
||||
}
|
||||
foreach($keys as $keyName)
|
||||
$out[$key] = array_merge_recursive($out[$key] ?? [], self::traverseFILES($arr[$keyName], (string)$keyName));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -227,17 +239,21 @@ class HttpUploadedFile implements ICloseable {
|
|||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $files
|
||||
* @return array<string, HttpUploadedFile|array<string, mixed>>
|
||||
*/
|
||||
private static function createObjectInstances(array $files): array {
|
||||
$coll = [];
|
||||
|
||||
foreach($files as $key => $val) {
|
||||
$key = substr($key, 1);
|
||||
if(!is_array($val))
|
||||
continue;
|
||||
|
||||
if(isset($val['error'])) {
|
||||
$coll[$key] = self::createFromFILE($val);
|
||||
} else {
|
||||
$coll[$key] = self::createObjectInstances($val);
|
||||
}
|
||||
$key = substr($key, 1);
|
||||
$coll[$key] = isset($val['error'])
|
||||
? self::createFromFILE($val)
|
||||
: self::createObjectInstances($val);
|
||||
}
|
||||
|
||||
return $coll;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRouter.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -15,11 +15,16 @@ use Index\Http\ErrorHandling\{HtmlErrorHandler,IErrorHandler,PlainErrorHandler};
|
|||
class HttpRouter implements IRouter {
|
||||
use RouterTrait;
|
||||
|
||||
/** @var object[] */
|
||||
private array $middlewares = [];
|
||||
|
||||
/** @var array<string, array<string, callable>> */
|
||||
private array $staticRoutes = [];
|
||||
|
||||
/** @var array<string, array<string, callable>> */
|
||||
private array $dynamicRoutes = [];
|
||||
|
||||
/** @var IContentHandler[] */
|
||||
private array $contentHandlers = [];
|
||||
|
||||
private IErrorHandler $errorHandler;
|
||||
|
@ -46,8 +51,14 @@ class HttpRouter implements IRouter {
|
|||
* @return string Normalised character set name.
|
||||
*/
|
||||
public function getCharSet(): string {
|
||||
if($this->charSet === '')
|
||||
return strtolower(mb_preferred_mime_name(mb_internal_encoding()));
|
||||
if($this->charSet === '') {
|
||||
$charSet = mb_preferred_mime_name(mb_internal_encoding());
|
||||
if($charSet === false)
|
||||
$charSet = 'UTF-8';
|
||||
|
||||
return strtolower($charSet);
|
||||
}
|
||||
|
||||
return $this->charSet;
|
||||
}
|
||||
|
||||
|
@ -196,19 +207,19 @@ class HttpRouter implements IRouter {
|
|||
$middlewares = [];
|
||||
|
||||
foreach($this->middlewares as $mwInfo) {
|
||||
if($mwInfo->dynamic) {
|
||||
if(preg_match($mwInfo->match, $path, $args) !== 1)
|
||||
if($mwInfo->dynamic ?? false) {
|
||||
if(preg_match($mwInfo->match ?? '', $path, $args) !== 1)
|
||||
continue;
|
||||
|
||||
array_shift($args);
|
||||
} else {
|
||||
if(!str_starts_with($path, $mwInfo->prefix))
|
||||
if(!str_starts_with($path, $mwInfo->prefix ?? ''))
|
||||
continue;
|
||||
|
||||
$args = [];
|
||||
}
|
||||
|
||||
$middlewares[] = [$mwInfo->handler, $args];
|
||||
$middlewares[] = [$mwInfo->handler ?? null, $args];
|
||||
}
|
||||
|
||||
$methods = [];
|
||||
|
@ -241,7 +252,7 @@ class HttpRouter implements IRouter {
|
|||
* Dispatches a route based on a given HTTP request message with additional prefix arguments and output to stdout.
|
||||
*
|
||||
* @param ?HttpRequest $request HTTP request message to handle, null to use the current request.
|
||||
* @param array $args Additional arguments to prepend to the argument list sent to the middleware and route handlers.
|
||||
* @param mixed[] $args Additional arguments to prepend to the argument list sent to the middleware and route handlers.
|
||||
*/
|
||||
public function dispatch(?HttpRequest $request = null, array $args = []): void {
|
||||
$request ??= HttpRequest::fromRequest();
|
||||
|
@ -275,7 +286,7 @@ class HttpRouter implements IRouter {
|
|||
break;
|
||||
}
|
||||
|
||||
if(!$response->hasContent() && $result !== null) {
|
||||
if(!$response->hasContent() && is_scalar($result)) {
|
||||
$result = (string)$result;
|
||||
$response->setContent(new StringContent($result));
|
||||
|
||||
|
@ -283,7 +294,10 @@ class HttpRouter implements IRouter {
|
|||
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
|
||||
$response->setTypeHTML($this->getCharSet());
|
||||
else {
|
||||
$charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result)));
|
||||
$charset = mb_detect_encoding($result);
|
||||
if($charset !== false)
|
||||
$charset = mb_preferred_mime_name($charset);
|
||||
$charset = $charset === false ? 'utf-8' : strtolower($charset);
|
||||
|
||||
if(strtolower(substr($result, 0, 5)) === '<?xml')
|
||||
$response->setTypeXML($charset);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ResolvedRouteInfo.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -10,10 +10,10 @@ namespace Index\Http\Routing;
|
|||
*/
|
||||
class ResolvedRouteInfo {
|
||||
/**
|
||||
* @param array $middlewares Middlewares that should be run prior to the route handler.
|
||||
* @param array $supportedMethods HTTP methods that this route accepts.
|
||||
* @param mixed $handler Route handler.
|
||||
* @param array $args Argument list to pass to the middleware and route handlers.
|
||||
* @param array{callable, mixed[]}[] $middlewares Middlewares that should be run prior to the route handler.
|
||||
* @param string[] $supportedMethods HTTP methods that this route accepts.
|
||||
* @param (callable(): mixed)|null $handler Route handler.
|
||||
* @param mixed[] $args Argument list to pass to the middleware and route handlers.
|
||||
*/
|
||||
public function __construct(
|
||||
private array $middlewares,
|
||||
|
@ -25,7 +25,7 @@ class ResolvedRouteInfo {
|
|||
/**
|
||||
* Run middleware handlers.
|
||||
*
|
||||
* @param array $args Additional arguments to pass to the middleware handlers.
|
||||
* @param mixed[] $args Additional arguments to pass to the middleware handlers.
|
||||
* @return mixed Return value from the first middleware to return anything non-null, otherwise null.
|
||||
*/
|
||||
public function runMiddleware(array $args): mixed {
|
||||
|
@ -44,7 +44,7 @@ class ResolvedRouteInfo {
|
|||
* @return bool true if it does.
|
||||
*/
|
||||
public function hasHandler(): bool {
|
||||
return $this->handler !== null;
|
||||
return is_callable($this->handler);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,10 +68,13 @@ class ResolvedRouteInfo {
|
|||
/**
|
||||
* Dispatches this route.
|
||||
*
|
||||
* @param array $args Additional arguments to pass to the route handler.
|
||||
* @param mixed[] $args Additional arguments to pass to the route handler.
|
||||
* @return mixed Return value of the route handler.
|
||||
*/
|
||||
public function dispatch(array $args): mixed {
|
||||
if(!is_callable($this->handler))
|
||||
return null;
|
||||
|
||||
return ($this->handler)(...array_merge($args, $this->args));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<?php
|
||||
// GenericStream.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents any stream that can be handled using the C-like f* functions.
|
||||
|
@ -32,8 +33,8 @@ class GenericStream extends Stream {
|
|||
'x+', 'x+b', 'c', 'cb', 'c+', 'c+b',
|
||||
];
|
||||
|
||||
// resource
|
||||
// can't seem to turn this one into a constructor property thing?
|
||||
/** @var resource */
|
||||
protected $stream;
|
||||
|
||||
private bool $canRead;
|
||||
|
@ -51,7 +52,7 @@ class GenericStream extends Stream {
|
|||
$metaData = $this->metaData();
|
||||
$this->canRead = in_array($metaData['mode'], self::READABLE);
|
||||
$this->canWrite = in_array($metaData['mode'], self::WRITEABLE);
|
||||
$this->canSeek = $metaData['seekable'];
|
||||
$this->canSeek = is_bool($metaData['seekable']) ? $metaData['seekable'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,23 +64,41 @@ class GenericStream extends Stream {
|
|||
return $this->stream;
|
||||
}
|
||||
|
||||
/** @return array<string|int, mixed> */
|
||||
private function stat(): array {
|
||||
return fstat($this->stream);
|
||||
$stat = fstat($this->stream);
|
||||
if($stat === false)
|
||||
throw new RuntimeException('fstat returned false');
|
||||
|
||||
return $stat;
|
||||
}
|
||||
|
||||
public function getPosition(): int {
|
||||
return ftell($this->stream);
|
||||
$tell = ftell($this->stream);
|
||||
if($tell === false)
|
||||
return -1;
|
||||
|
||||
return $tell;
|
||||
}
|
||||
|
||||
public function getLength(): int {
|
||||
if(!$this->canSeek())
|
||||
return -1;
|
||||
return $this->stat()['size'];
|
||||
|
||||
$size = $this->stat()['size'];
|
||||
if(!is_int($size))
|
||||
return -1;
|
||||
|
||||
return $size;
|
||||
}
|
||||
public function setLength(int $length): void {
|
||||
if($length < 0)
|
||||
throw new InvalidArgumentException('$length must be a positive integer');
|
||||
|
||||
ftruncate($this->stream, $length);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function metaData(): array {
|
||||
return stream_get_meta_data($this->stream);
|
||||
}
|
||||
|
@ -93,10 +112,18 @@ class GenericStream extends Stream {
|
|||
return $this->canSeek;
|
||||
}
|
||||
public function hasTimedOut(): bool {
|
||||
return $this->metaData()['timed_out'];
|
||||
$timedOut = $this->metaData()['timed_out'];
|
||||
if(!is_bool($timedOut))
|
||||
return false;
|
||||
|
||||
return $timedOut;
|
||||
}
|
||||
public function isBlocking(): bool {
|
||||
return $this->metaData()['blocked'];
|
||||
$blocked = $this->metaData()['blocked'];
|
||||
if(!is_bool($blocked))
|
||||
return false;
|
||||
|
||||
return $blocked;
|
||||
}
|
||||
|
||||
public function isEnded(): bool {
|
||||
|
@ -116,9 +143,13 @@ class GenericStream extends Stream {
|
|||
}
|
||||
|
||||
public function read(int $length): ?string {
|
||||
if($length < 1)
|
||||
throw new InvalidArgumentException('$length must be greater than 0');
|
||||
|
||||
$buffer = fread($this->stream, $length);
|
||||
if($buffer === false)
|
||||
return null;
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
|
@ -155,6 +186,10 @@ class GenericStream extends Stream {
|
|||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return stream_get_contents($this->stream);
|
||||
$contents = stream_get_contents($this->stream);
|
||||
if($contents === false)
|
||||
return '';
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<?php
|
||||
// MemoryStream.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents an in-memory stream.
|
||||
*/
|
||||
class MemoryStream extends GenericStream {
|
||||
public function __construct() {
|
||||
parent::__construct(fopen('php://memory', 'r+b'));
|
||||
$stream = fopen('php://memory', 'r+b');
|
||||
if($stream === false)
|
||||
throw new RuntimeException('failed to fopen a memory stream');
|
||||
|
||||
parent::__construct($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<?php
|
||||
// ProcessStream.php
|
||||
// Created: 2023-01-25
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents a stream to a running sub-process.
|
||||
*/
|
||||
class ProcessStream extends Stream {
|
||||
/** @var resource */
|
||||
private $handle;
|
||||
|
||||
private bool $canRead;
|
||||
private bool $canWrite;
|
||||
|
||||
|
@ -21,7 +24,11 @@ class ProcessStream extends Stream {
|
|||
* @throws RuntimeException If we were unable to spawn the process.
|
||||
*/
|
||||
public function __construct(string $command, string $mode) {
|
||||
$this->handle = popen($command, $mode);
|
||||
$handle = popen($command, $mode);
|
||||
if($handle === false)
|
||||
throw new RuntimeException('Failed to create process.');
|
||||
|
||||
$this->handle = $handle;
|
||||
$this->canRead = strpos($mode, 'r') !== false;
|
||||
$this->canWrite = strpos($mode, 'w') !== false;
|
||||
|
||||
|
@ -75,9 +82,13 @@ class ProcessStream extends Stream {
|
|||
}
|
||||
|
||||
public function read(int $length): ?string {
|
||||
if($length < 1)
|
||||
throw new InvalidArgumentException('$length must be greater than 0');
|
||||
|
||||
$buffer = fread($this->handle, $length);
|
||||
if($buffer === false)
|
||||
return null;
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
|
@ -99,9 +110,7 @@ class ProcessStream extends Stream {
|
|||
public function flush(): void {}
|
||||
|
||||
public function close(): void {
|
||||
if(is_resource($this->handle)) {
|
||||
if(is_resource($this->handle))
|
||||
pclose($this->handle);
|
||||
$this->handle = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Stream.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
|
@ -186,8 +186,13 @@ abstract class Stream implements Stringable, ICloseable {
|
|||
if($this->canSeek())
|
||||
$this->seek(0);
|
||||
|
||||
while(!$this->isEnded())
|
||||
$other->write($this->read(8192));
|
||||
while(!$this->isEnded()) {
|
||||
$buffer = $this->read(8192);
|
||||
if($buffer === null)
|
||||
break;
|
||||
|
||||
$other->write($buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<?php
|
||||
// TempFileStream.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents a temporary file stream. Will remain in memory if the size is below a given threshold.
|
||||
*/
|
||||
|
@ -21,7 +23,11 @@ class TempFileStream extends GenericStream {
|
|||
* @param int $maxMemory Maximum filesize until the file is moved out of memory and to disk.
|
||||
*/
|
||||
public function __construct(?string $body = null, int $maxMemory = self::MAX_MEMORY) {
|
||||
parent::__construct(fopen("php://temp/maxmemory:{$maxMemory}", 'r+b'));
|
||||
$stream = fopen("php://temp/maxmemory:{$maxMemory}", 'r+b');
|
||||
if($stream === false)
|
||||
throw new RuntimeException('failed to fopen a temporary file stream');
|
||||
|
||||
parent::__construct($stream);
|
||||
|
||||
if($body !== null) {
|
||||
$this->write($body);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MediaType.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2024-08-02
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -79,10 +79,10 @@ class MediaType implements Stringable, IComparable, IEquatable {
|
|||
*
|
||||
* @param string $name Media type parameter name.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @param array<string, mixed>|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the parameter, null if not present.
|
||||
*/
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, $options = null): mixed {
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
if(!isset($this->params[$name]))
|
||||
return null;
|
||||
return filter_var($this->params[$name], $filter, $options);
|
||||
|
@ -94,7 +94,8 @@ class MediaType implements Stringable, IComparable, IEquatable {
|
|||
* @return string Character specified in media type.
|
||||
*/
|
||||
public function getCharset(): string {
|
||||
return $this->getParam('charset', FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? 'utf-8';
|
||||
$charset = $this->getParam('charset', FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
|
||||
return is_string($charset) ? $charset : 'utf-8';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,7 +104,11 @@ class MediaType implements Stringable, IComparable, IEquatable {
|
|||
* @return float Quality specified for the media type.
|
||||
*/
|
||||
public function getQuality(): float {
|
||||
return max(min(round($this->getParam('q', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION) ?? 1, 2), 1), 0);
|
||||
$quality = $this->getParam('q', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
|
||||
if(!is_float($quality) && !is_int($quality))
|
||||
return 1;
|
||||
|
||||
return max(min(round($quality, 2), 1), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -169,12 +174,16 @@ class MediaType implements Stringable, IComparable, IEquatable {
|
|||
}
|
||||
|
||||
public function compare(mixed $other): int {
|
||||
if(!($other instanceof MediaType))
|
||||
if(!($other instanceof MediaType)) {
|
||||
if(!is_scalar($other))
|
||||
return -1;
|
||||
|
||||
try {
|
||||
$other = self::parse((string)$other);
|
||||
} catch(InvalidArgumentException $ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if(($match = self::compareCategory($other->category)) !== 0)
|
||||
return $match;
|
||||
|
@ -185,12 +194,16 @@ class MediaType implements Stringable, IComparable, IEquatable {
|
|||
}
|
||||
|
||||
public function equals(mixed $other): bool {
|
||||
if(!($other instanceof MediaType))
|
||||
if(!($other instanceof MediaType)) {
|
||||
if(!is_scalar($other))
|
||||
return false;
|
||||
|
||||
try {
|
||||
$other = self::parse((string)$other);
|
||||
} catch(InvalidArgumentException $ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$this->matchCategory($other->category))
|
||||
return false;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DnsEndPoint.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -55,6 +55,7 @@ class DnsEndPoint extends EndPoint {
|
|||
return [$this->host, $this->port];
|
||||
}
|
||||
|
||||
/** @param array{string,int} $serialized */
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->host = $serialized[0];
|
||||
$this->port = $serialized[1];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// IPAddress.php
|
||||
// Created: 2021-04-26
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -76,7 +76,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
* @return string Safe string IP address.
|
||||
*/
|
||||
public function getAddress(): string {
|
||||
return sprintf($this->isV6() ? '[%s]' : '%s', inet_ntop($this->raw));
|
||||
return sprintf($this->isV6() ? '[%s]' : '%s', $this->getCleanAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,7 +85,11 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
* @return string String IP address.
|
||||
*/
|
||||
public function getCleanAddress(): string {
|
||||
return inet_ntop($this->raw);
|
||||
$string = inet_ntop($this->raw);
|
||||
if($string === false)
|
||||
return '';
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,7 +124,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return inet_ntop($this->raw);
|
||||
return $this->getCleanAddress();
|
||||
}
|
||||
|
||||
public function equals(mixed $other): bool {
|
||||
|
@ -129,13 +133,14 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
|
||||
#[\Override]
|
||||
public function jsonSerialize(): mixed {
|
||||
return inet_ntop($this->raw);
|
||||
return $this->getCleanAddress();
|
||||
}
|
||||
|
||||
public function __serialize(): array {
|
||||
return [$this->raw];
|
||||
}
|
||||
|
||||
/** @param array{string} $serialized */
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->raw = $serialized[0];
|
||||
$this->width = strlen($this->raw);
|
||||
|
@ -152,6 +157,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
$parsed = inet_pton($string);
|
||||
if($parsed === false)
|
||||
throw new InvalidArgumentException('$string is not a valid IP address.');
|
||||
|
||||
return new static($parsed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// IPAddressRange.php
|
||||
// Created: 2021-04-26
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -109,6 +109,7 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
|||
];
|
||||
}
|
||||
|
||||
/** @param array{b: string, m: int} $serialized */
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->base = new IPAddress($serialized['b']);
|
||||
$this->mask = $serialized['m'];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// IPEndPoint.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -56,6 +56,7 @@ class IPEndPoint extends EndPoint {
|
|||
return [$this->address, $this->port];
|
||||
}
|
||||
|
||||
/** @param array{IPAddress, int} $serialized */
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->address = $serialized[0];
|
||||
$this->port = $serialized[1];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// UnixEndPoint.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -29,6 +29,7 @@ class UnixEndPoint extends EndPoint {
|
|||
return [$this->socketPath];
|
||||
}
|
||||
|
||||
/** @param array{string} $serialized */
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->socketPath = $serialized[0];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Timings.php
|
||||
// Created: 2022-02-16
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
|
@ -13,6 +13,8 @@ use InvalidArgumentException;
|
|||
class Timings {
|
||||
private Stopwatch $sw;
|
||||
private int $lastLapTicks;
|
||||
|
||||
/** @var TimingPoint[] */
|
||||
private array $laps;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// WString.php
|
||||
// Created: 2021-06-22
|
||||
// Updated: 2024-01-04
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -55,12 +55,16 @@ final class WString {
|
|||
|
||||
private static function trimInternal(Stringable|string $string, string $chars, ?string $encoding, ?string $charsEncoding, int $flags): string {
|
||||
$encoding = $encoding === null ? mb_internal_encoding() : mb_preferred_mime_name($encoding);
|
||||
if($encoding === false) $encoding = 'utf-8';
|
||||
$charsEncoding = $charsEncoding === null ? self::TRIM_CHARS_CHARSET : mb_preferred_mime_name($charsEncoding);
|
||||
if($charsEncoding === false) $charsEncoding = 'utf-8';
|
||||
|
||||
// this fucks, i hate character sets
|
||||
if($encoding !== $charsEncoding) {
|
||||
$questionMarkCharsEnc = mb_convert_encoding('?', $charsEncoding, 'utf-8');
|
||||
if($questionMarkCharsEnc) $questionMarkCharsEnc = 'utf-8';
|
||||
$questionMarkStrEnc = mb_convert_encoding('?', $encoding, 'utf-8');
|
||||
if($questionMarkStrEnc) $questionMarkStrEnc = 'utf-8';
|
||||
$hasQuestionMark = mb_strpos($chars, $questionMarkCharsEnc, encoding: $charsEncoding) !== false;
|
||||
$chars = mb_convert_encoding($chars, $encoding, $charsEncoding);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// XArray.php
|
||||
// Created: 2022-02-02
|
||||
// Updated: 2023-11-09
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -39,6 +39,7 @@ final class XArray {
|
|||
/**
|
||||
* Retrieves the amount of items in a collection.
|
||||
*
|
||||
* @param mixed[] $iterable
|
||||
* @return int
|
||||
*/
|
||||
public static function count(iterable $iterable): int {
|
||||
|
@ -56,6 +57,7 @@ final class XArray {
|
|||
/**
|
||||
* Checks if a collection has no items.
|
||||
*
|
||||
* @param mixed[] $iterable
|
||||
* @return bool
|
||||
*/
|
||||
public static function empty(iterable $iterable): bool {
|
||||
|
@ -73,7 +75,7 @@ final class XArray {
|
|||
/**
|
||||
* Checks if an item occurs in a collection
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @param mixed $value
|
||||
* @param bool $strict
|
||||
* @return bool
|
||||
|
@ -98,12 +100,12 @@ final class XArray {
|
|||
/**
|
||||
* Checks if a key occurs in a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @param mixed $key
|
||||
* @return bool
|
||||
*/
|
||||
public static function containsKey(iterable $iterable, mixed $key): bool {
|
||||
if(is_array($iterable))
|
||||
if(is_array($iterable) && (is_string($key) || is_int($key)))
|
||||
return array_key_exists($key, $iterable);
|
||||
|
||||
foreach($iterable as $k => $_)
|
||||
|
@ -116,7 +118,7 @@ final class XArray {
|
|||
/**
|
||||
* Retrieves the first key in a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @return int|string|null
|
||||
*/
|
||||
public static function firstKey(iterable $iterable): int|string|null {
|
||||
|
@ -132,7 +134,7 @@ final class XArray {
|
|||
/**
|
||||
* Retrieves the last key in a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @return int|string|null
|
||||
*/
|
||||
public static function lastKey(iterable $iterable): int|string|null {
|
||||
|
@ -148,7 +150,7 @@ final class XArray {
|
|||
/**
|
||||
* Gets the index of a value in a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @param mixed $value
|
||||
* @param bool $strict
|
||||
* @return int|string|false
|
||||
|
@ -177,9 +179,9 @@ final class XArray {
|
|||
/**
|
||||
* Extracts unique values from a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @param int $type
|
||||
* @return array
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function unique(iterable $iterable, int $type = self::UNIQUE_VALUE): array {
|
||||
if(is_array($iterable))
|
||||
|
@ -197,8 +199,8 @@ final class XArray {
|
|||
/**
|
||||
* Takes values from a collection and discards the keys.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @return array
|
||||
* @param mixed[] $iterable
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function reflow(iterable $iterable): array {
|
||||
if(is_array($iterable))
|
||||
|
@ -215,8 +217,8 @@ final class XArray {
|
|||
/**
|
||||
* Puts a collection in reverse order.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @return array
|
||||
* @param mixed[] $iterable
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function reverse(iterable $iterable): array {
|
||||
if(is_array($iterable))
|
||||
|
@ -233,9 +235,9 @@ final class XArray {
|
|||
/**
|
||||
* Merges two collections.
|
||||
*
|
||||
* @param iterable $iterable1
|
||||
* @param iterable $iterable2
|
||||
* @return array
|
||||
* @param mixed[] $iterable1
|
||||
* @param mixed[] $iterable2
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function merge(iterable $iterable1, iterable $iterable2): array {
|
||||
return array_merge(self::toArray($iterable1), self::toArray($iterable2));
|
||||
|
@ -244,9 +246,10 @@ final class XArray {
|
|||
/**
|
||||
* Sorts a collection according to a comparer.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @template T
|
||||
* @param T[] $iterable
|
||||
* @param callable $comparer
|
||||
* @return array
|
||||
* @return T[]
|
||||
*/
|
||||
public static function sort(iterable $iterable, callable $comparer): array {
|
||||
if(is_array($iterable)) {
|
||||
|
@ -267,10 +270,10 @@ final class XArray {
|
|||
/**
|
||||
* Takes a subsection of a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @param int $offset
|
||||
* @param int|null $length
|
||||
* @return array
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function slice(iterable $iterable, int $offset, int|null $length = null): array {
|
||||
if(is_array($iterable))
|
||||
|
@ -284,6 +287,12 @@ final class XArray {
|
|||
return array_slice($items, $offset, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any iterable to a PHP array.
|
||||
*
|
||||
* @param mixed[] $iterable
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function toArray(iterable $iterable): array {
|
||||
if(is_array($iterable))
|
||||
return $iterable;
|
||||
|
@ -299,8 +308,8 @@ final class XArray {
|
|||
/**
|
||||
* Checks if any value in the collection matches a given predicate.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param callable $predicate
|
||||
* @param mixed[] $iterable
|
||||
* @param callable(mixed): bool $predicate
|
||||
* @return bool
|
||||
*/
|
||||
public static function any(iterable $iterable, callable $predicate): bool {
|
||||
|
@ -314,8 +323,8 @@ final class XArray {
|
|||
/**
|
||||
* Checks if all values in the collection match a given predicate.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param callable $predicate
|
||||
* @param mixed[] $iterable
|
||||
* @param callable(mixed): bool $predicate
|
||||
* @return bool
|
||||
*/
|
||||
public static function all(iterable $iterable, callable $predicate): bool {
|
||||
|
@ -329,9 +338,9 @@ final class XArray {
|
|||
/**
|
||||
* Gets a subset of a collection based on a given predicate.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param callable $predicate
|
||||
* @return array
|
||||
* @param mixed[] $iterable
|
||||
* @param callable(mixed): bool $predicate
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function where(iterable $iterable, callable $predicate): array {
|
||||
$array = [];
|
||||
|
@ -346,8 +355,8 @@ final class XArray {
|
|||
/**
|
||||
* Gets the first item in a collection that matches a given predicate.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param callable|null $predicate
|
||||
* @param mixed[] $iterable
|
||||
* @param (callable(mixed): bool)|null $predicate
|
||||
* @return mixed
|
||||
*/
|
||||
public static function first(iterable $iterable, ?callable $predicate = null): mixed {
|
||||
|
@ -375,8 +384,8 @@ final class XArray {
|
|||
/**
|
||||
* Gets the last item in a collection that matches a given predicate.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param callable|null $predicate
|
||||
* @param mixed[] $iterable
|
||||
* @param (callable(mixed): bool)|null $predicate
|
||||
* @return mixed
|
||||
*/
|
||||
public static function last(iterable $iterable, ?callable $predicate = null): mixed {
|
||||
|
@ -406,9 +415,11 @@ final class XArray {
|
|||
/**
|
||||
* Applies a modifier on a collection.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param callable $selector
|
||||
* @return array
|
||||
* @template T1
|
||||
* @template T2
|
||||
* @param T1[] $iterable
|
||||
* @param callable(T1): T2 $selector
|
||||
* @return T2[]
|
||||
*/
|
||||
public static function select(iterable $iterable, callable $selector): array {
|
||||
if(is_array($iterable))
|
||||
|
@ -416,8 +427,8 @@ final class XArray {
|
|||
|
||||
$array = [];
|
||||
|
||||
foreach($iterable as $key => $value)
|
||||
$array[] = $selector($value, $key);
|
||||
foreach($iterable as $value)
|
||||
$array[] = $selector($value);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
@ -425,7 +436,7 @@ final class XArray {
|
|||
/**
|
||||
* Tries to extract an instance of Iterator from any iterable type.
|
||||
*
|
||||
* @param iterable $iterable
|
||||
* @param mixed[] $iterable
|
||||
* @return Iterator
|
||||
*/
|
||||
public static function extractIterator(iterable &$iterable): Iterator {
|
||||
|
@ -442,14 +453,11 @@ final class XArray {
|
|||
/**
|
||||
* Checks if two collections are equal in both keys and values.
|
||||
*
|
||||
* @param iterable $iterable1
|
||||
* @param iterable $iterable2
|
||||
* @param mixed[] $iterable1
|
||||
* @param mixed[] $iterable2
|
||||
* @return bool
|
||||
*/
|
||||
public static function sequenceEquals(iterable $iterable1, iterable $iterable2): bool {
|
||||
if(count($iterable1) !== count($iterable2))
|
||||
return false;
|
||||
|
||||
$iterator1 = self::extractIterator($iterable1);
|
||||
$iterator2 = self::extractIterator($iterable2);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// XDateTime.php
|
||||
// Created: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
// Updated: 2024-08-03
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -15,6 +15,11 @@ use DateTimeZone;
|
|||
final class XDateTime {
|
||||
private const COMPARE_FORMAT = 'YmdHisu';
|
||||
|
||||
/**
|
||||
* Returns the current date and time.
|
||||
*
|
||||
* @return DateTimeInterface Current date and time.
|
||||
*/
|
||||
public static function now(): DateTimeInterface {
|
||||
return new DateTimeImmutable('now');
|
||||
}
|
||||
|
@ -25,10 +30,19 @@ final class XDateTime {
|
|||
|
||||
if(is_string($dt))
|
||||
$dt = strtotime($dt);
|
||||
if($dt === false)
|
||||
$dt = null;
|
||||
|
||||
return gmdate(self::COMPARE_FORMAT, $dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two timestamps.
|
||||
*
|
||||
* @param DateTimeInterface|string|int $dt1 Base timestamp.
|
||||
* @param DateTimeInterface|string|int $dt2 Timestamp to compare against.
|
||||
* @return int 0 if they are equal, less than 0 if $dt1 is less than $dt2, greater than 0 if $dt1 is greater than $dt2.
|
||||
*/
|
||||
public static function compare(
|
||||
DateTimeInterface|string|int $dt1,
|
||||
DateTimeInterface|string|int $dt2
|
||||
|
@ -39,6 +53,13 @@ final class XDateTime {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two timezones for sorting.
|
||||
*
|
||||
* @param DateTimeZone|string $tz1 Base time zone.
|
||||
* @param DateTimeZone|string $tz2 Time zone to compare against.
|
||||
* @return int 0 if they are equal, less than 0 if $tz1 is less than $tz2, greater than 0 if $tz1 is greater than $tz2.
|
||||
*/
|
||||
public static function compareTimeZone(
|
||||
DateTimeZone|string $tz1,
|
||||
DateTimeZone|string $tz2
|
||||
|
@ -58,6 +79,12 @@ final class XDateTime {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a time zone as a string for a list.
|
||||
*
|
||||
* @param DateTimeZone $dtz Time zone to format.
|
||||
* @return string Formatted time zone string.
|
||||
*/
|
||||
public static function timeZoneListName(DateTimeZone $dtz): string {
|
||||
$offset = $dtz->getOffset(self::now());
|
||||
return sprintf(
|
||||
|
@ -68,6 +95,14 @@ final class XDateTime {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of time zones.
|
||||
*
|
||||
* @param bool $sort true if the list should be sorted.
|
||||
* @param int $group Group of time zones to fetch.
|
||||
* @param ?string $countryCode Country code of country of which to fetch time zones.
|
||||
* @return DateTimeZone[] Collection of DateTimeZone instances.
|
||||
*/
|
||||
public static function listTimeZones(bool $sort = false, int $group = DateTimeZone::ALL, string|null $countryCode = null): array {
|
||||
$list = DateTimeZone::listIdentifiers($group, $countryCode);
|
||||
$list = XArray::select($list, fn($id) => new DateTimeZone($id));
|
||||
|
@ -78,6 +113,13 @@ final class XDateTime {
|
|||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date and time as a string.
|
||||
*
|
||||
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
|
||||
* @param string $format DateTimeInterface::format date formatting string.
|
||||
* @return string Provided date, formatted as requested.
|
||||
*/
|
||||
public static function format(DateTimeInterface|string|int|null $dt, string $format): string {
|
||||
if($dt === null) {
|
||||
$dt = time();
|
||||
|
@ -89,17 +131,38 @@ final class XDateTime {
|
|||
$dt = strtotime($dt);
|
||||
}
|
||||
|
||||
if($dt === false)
|
||||
$dt = null;
|
||||
|
||||
return gmdate($format, $dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date and time as an ISO-8601 string.
|
||||
*
|
||||
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
|
||||
* @return string ISO-8601 date time string.
|
||||
*/
|
||||
public static function toISO8601String(DateTimeInterface|string|int|null $dt): string {
|
||||
return self::format($dt, DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date and time as a Cookie date/time string.
|
||||
*
|
||||
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
|
||||
* @return string Cookie date time string.
|
||||
*/
|
||||
public static function toCookieString(DateTimeInterface|string|int|null $dt): string {
|
||||
return self::format($dt, DateTimeInterface::COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date and time as an RFC 822 date/time string.
|
||||
*
|
||||
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
|
||||
* @return string RFC 822 date time string.
|
||||
*/
|
||||
public static function toRFC822String(DateTimeInterface|string|int|null $dt): string {
|
||||
return self::format($dt, DateTimeInterface::RFC822);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue