Raised PHPStan analysis level and fixed issues.

This commit is contained in:
flash 2024-08-03 20:27:50 +00:00
parent 5be38a7090
commit 6e3e86f416
63 changed files with 600 additions and 280 deletions

View file

@ -1 +1 @@
0.2408.20020 0.2408.32027

View file

@ -1,4 +1,4 @@
parameters: parameters:
level: 5 # Raise this eventually level: 9
paths: paths:
- src - src

View file

@ -1,7 +1,7 @@
<?php <?php
// ArrayIterator.php // ArrayIterator.php
// Created: 2022-02-03 // Created: 2022-02-03
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index; namespace Index;
@ -9,12 +9,14 @@ use Iterator;
/** /**
* Provides an Iterator implementation for normal arrays. * Provides an Iterator implementation for normal arrays.
*
* @implements Iterator<mixed, mixed>
*/ */
class ArrayIterator implements Iterator { class ArrayIterator implements Iterator {
private bool $wasValid = true; private bool $wasValid = true;
/** /**
* @param array $array Array to iterate upon. * @param array<mixed, mixed> $array Array to iterate upon.
*/ */
public function __construct( public function __construct(
private array $array private array $array

View file

@ -1,7 +1,7 @@
<?php <?php
// CSRFP.php // CSRFP.php
// Created: 2021-06-11 // Created: 2021-06-11
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index; namespace Index;
@ -74,7 +74,10 @@ class CSRFP {
return false; return false;
$unpacked = unpack('Vts', $token); $unpacked = unpack('Vts', $token);
$uTime = (int)$unpacked['ts']; if($unpacked === false)
return false;
$uTime = (int)($unpacked['ts'] ?? 0);
if($uTime < 0) if($uTime < 0)
return false; return false;

View file

@ -1,7 +1,7 @@
<?php <?php
// ArrayCacheBackend.php // ArrayCacheBackend.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\ArrayCache; namespace Index\Cache\ArrayCache;
@ -34,7 +34,7 @@ class ArrayCacheBackend implements ICacheBackend {
* *
* ArrayCache has no parameters that can be controlled using the DSN. * 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. * @return ArrayCacheProviderInfo Dummy provider info instance.
*/ */
public function parseDsn(string|array $dsn): ICacheProviderInfo { public function parseDsn(string|array $dsn): ICacheProviderInfo {

View file

@ -1,7 +1,7 @@
<?php <?php
// ArrayCacheProvider.php // ArrayCacheProvider.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\ArrayCache; namespace Index\Cache\ArrayCache;
@ -12,6 +12,7 @@ use Index\Cache\ICacheProvider;
* Represents a dummy cache provider. * Represents a dummy cache provider.
*/ */
class ArrayCacheProvider implements ICacheProvider { class ArrayCacheProvider implements ICacheProvider {
/** @var array<string, array{ttl: int, value: string}> */
private array $items = []; private array $items = [];
public function get(string $key): mixed { public function get(string $key): mixed {

View file

@ -1,7 +1,7 @@
<?php <?php
// CacheTools.php // CacheTools.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache; namespace Index\Cache;
@ -34,6 +34,7 @@ final class CacheTools {
'keydb' => Valkey\ValkeyBackend::class, 'keydb' => Valkey\ValkeyBackend::class,
]; ];
/** @return array<string, int|string> */
private static function parseDsnUri(string $dsn): array { private static function parseDsnUri(string $dsn): array {
$uri = parse_url($dsn); $uri = parse_url($dsn);
if($uri === false) if($uri === false)
@ -41,6 +42,7 @@ final class CacheTools {
return $uri; return $uri;
} }
/** @param array<string, int|string> $uri */
private static function resolveBackend(array $uri): ICacheBackend { private static function resolveBackend(array $uri): ICacheBackend {
static $backends = []; static $backends = [];
@ -54,7 +56,7 @@ final class CacheTools {
if(array_key_exists($scheme, self::CACHE_PROTOS)) if(array_key_exists($scheme, self::CACHE_PROTOS))
$name = self::CACHE_PROTOS[$scheme]; $name = self::CACHE_PROTOS[$scheme];
else else
$name = str_replace('-', '\\', $scheme); $name = str_replace('-', '\\', (string)$scheme);
if(class_exists($name) && is_subclass_of($name, ICacheBackend::class)) { if(class_exists($name) && is_subclass_of($name, ICacheBackend::class)) {
$backend = new $name; $backend = new $name;

View file

@ -1,7 +1,7 @@
<?php <?php
// ICacheBackend.php // ICacheBackend.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache; namespace Index\Cache;
@ -29,7 +29,7 @@ interface ICacheBackend {
/** /**
* Constructs a cache info instance from a dsn. * 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. * @return ICacheProviderInfo Provider info based on the dsn.
*/ */
function parseDsn(string|array $dsn): ICacheProviderInfo; function parseDsn(string|array $dsn): ICacheProviderInfo;

View file

@ -1,7 +1,7 @@
<?php <?php
// MemcachedBackend.php // MemcachedBackend.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
@ -71,7 +71,7 @@ class MemcachedBackend implements ICacheBackend {
* *
* Why is the legacy memcache extension supported? cuz I felt like it. * 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. * @return MemcachedProviderInfo Memcached provider info instance.
*/ */
public function parseDsn(string|array $dsn): ICacheProviderInfo { public function parseDsn(string|array $dsn): ICacheProviderInfo {
@ -92,13 +92,13 @@ class MemcachedBackend implements ICacheBackend {
if(isset($dsn['port'])) if(isset($dsn['port']))
$host .= ':' . $dsn['port']; $host .= ':' . $dsn['port'];
$endPoints[] = [EndPoint::parse($host), 0]; $endPoints[] = [EndPoint::parse((string)$host), 0];
} }
$endPoint = $isPool ? null : EndPoint::parse($host); $endPoint = $isPool ? null : EndPoint::parse((string)$host);
$prefixKey = str_replace('/', ':', ltrim($dsn['path'] ?? '', '/')); $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(isset($query['server'])) {
if(is_string($query['server'])) if(is_string($query['server']))

View file

@ -1,17 +1,26 @@
<?php <?php
// MemcachedProviderInfo.php // MemcachedProviderInfo.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
use InvalidArgumentException; use InvalidArgumentException;
use Index\Cache\ICacheProviderInfo; use Index\Cache\ICacheProviderInfo;
use Index\Net\EndPoint;
/** /**
* Represents Memcached provider info. * Represents Memcached provider info.
*/ */
class MemcachedProviderInfo implements ICacheProviderInfo { 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( public function __construct(
private array $endPoints, private array $endPoints,
private string $prefixKey, private string $prefixKey,
@ -24,7 +33,7 @@ class MemcachedProviderInfo implements ICacheProviderInfo {
/** /**
* Gets server endpoints. * Gets server endpoints.
* *
* @return array * @return array<int, array{EndPoint, int}>
*/ */
public function getEndPoints(): array { public function getEndPoints(): array {
return $this->endPoints; return $this->endPoints;

View file

@ -1,7 +1,7 @@
<?php <?php
// MemcachedProviderLegacy.php // MemcachedProviderLegacy.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
@ -42,7 +42,7 @@ class MemcachedProviderLegacy extends MemcachedProvider {
public function get(string $key): mixed { public function get(string $key): mixed {
$value = $this->memcache->get($this->prefix . $key); $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 { public function set(string $key, mixed $value, int $ttl = 0): void {

View file

@ -1,7 +1,7 @@
<?php <?php
// MemcachedProviderModern.php // MemcachedProviderModern.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
@ -17,7 +17,7 @@ class MemcachedProviderModern extends MemcachedProvider {
private Memcached $memcached; private Memcached $memcached;
public function __construct(MemcachedProviderInfo $providerInfo) { 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_BINARY_PROTOCOL, $providerInfo->shouldUseBinaryProtocol());
$this->memcached->setOption(Memcached::OPT_COMPRESSION, $providerInfo->shouldEnableCompression()); $this->memcached->setOption(Memcached::OPT_COMPRESSION, $providerInfo->shouldEnableCompression());
$this->memcached->setOption(Memcached::OPT_TCP_NODELAY, $providerInfo->shouldTcpNoDelay()); $this->memcached->setOption(Memcached::OPT_TCP_NODELAY, $providerInfo->shouldTcpNoDelay());

View file

@ -1,7 +1,7 @@
<?php <?php
// ValkeyBackend.php // ValkeyBackend.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-04-10 // Updated: 2024-08-03
namespace Index\Cache\Valkey; namespace Index\Cache\Valkey;
@ -52,7 +52,7 @@ class ValkeyBackend implements ICacheBackend {
* - `db=<number>`: Allows you to select a different database. * - `db=<number>`: Allows you to select a different database.
* - `persist`: Uses a persistent connection. * - `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. * @return ValkeyProviderInfo Valkey provider info instance.
*/ */
public function parseDsn(string|array $dsn): ICacheProviderInfo { public function parseDsn(string|array $dsn): ICacheProviderInfo {
@ -84,11 +84,11 @@ class ValkeyBackend implements ICacheBackend {
if(isset($dsn['port'])) if(isset($dsn['port']))
$host .= ':' . $dsn['port']; $host .= ':' . $dsn['port'];
$endPoint = EndPoint::parse($host); $endPoint = EndPoint::parse((string)$host);
} }
$prefix = str_replace('/', ':', ltrim($dsn['path'] ?? '', '/')); $prefix = str_replace('/', ':', ltrim((string)($dsn['path'] ?? ''), '/'));
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query); parse_str(str_replace('+', '%2B', (string)($dsn['query'] ?? '')), $query);
$unixPath = isset($query['socket']) && is_string($query['socket']) ? $query['socket'] : ''; $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; $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); $endPoint = new UnixEndPoint($unixPath);
} }
return new ValkeyProviderInfo($endPoint, $prefix, $persist, $username, $password, $dbNumber); return new ValkeyProviderInfo($endPoint, $prefix, $persist, (string)$username, (string)$password, $dbNumber);
} }
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// Colour.php // Colour.php
// Created: 2023-01-02 // Created: 2023-01-02
// Updated: 2023-11-09 // Updated: 2024-08-03
namespace Index\Colour; namespace Index\Colour;
@ -194,9 +194,9 @@ abstract class Colour implements Stringable {
} }
if($length === 6) if($length === 6)
return ColourRGB::fromRawRGB(hexdec($value)); return ColourRGB::fromRawRGB((int)hexdec($value));
if($length === 8) if($length === 8)
return ColourRGB::fromRawRGBA(hexdec($value)); return ColourRGB::fromRawRGBA((int)hexdec($value));
} }
return self::$none; return self::$none;
@ -223,10 +223,12 @@ abstract class Colour implements Stringable {
if($parts !== 3 && $parts !== 4) if($parts !== 3 && $parts !== 4)
return self::$none; return self::$none;
$value[0] = (int)trim($value[0]); $value = [
$value[1] = (int)trim($value[1]); (int)trim($value[0]),
$value[2] = (int)trim($value[2]); (int)trim($value[1]),
$value[3] = (float)trim($value[3] ?? '1'); (int)trim($value[2]),
(float)trim($value[3] ?? '1'),
];
} }
return new ColourRGB(...$value); return new ColourRGB(...$value);
@ -256,41 +258,46 @@ abstract class Colour implements Stringable {
for($i = 0; $i < $parts; ++$i) for($i = 0; $i < $parts; ++$i)
$value[$i] = trim($value[$i]); $value[$i] = trim($value[$i]);
if(str_ends_with($value[1], '%')) $hue = $value[0];
$value[1] = substr($value[1], 0, -1); $saturation = $value[1];
$lightness = $value[2];
$alpha = $value[3] ?? null;
if(str_ends_with($value[2], '%')) if(str_ends_with($saturation, '%'))
$value[2] = substr($value[2], 0, -1); $saturation = substr($saturation, 0, -1);
$value[1] = (float)$value[1]; if(str_ends_with($lightness, '%'))
$value[2] = (float)$value[2]; $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; return self::$none;
$value[1] /= 100.0; $saturation /= 100.0;
$value[2] /= 100.0; $lightness /= 100.0;
if(ctype_digit($value[0])) { if(ctype_digit($hue)) {
$value[0] = (float)$value[0]; $hue = (float)$hue;
} else { } else {
if(str_ends_with($value[0], 'deg')) { if(str_ends_with($hue, 'deg')) {
$value[0] = (float)substr($value[0], 0, -3); $hue = (float)substr($hue, 0, -3);
} elseif(str_ends_with($value[0], 'grad')) { } elseif(str_ends_with($hue, 'grad')) {
$value[0] = 0.9 * (float)substr($value[0], 0, -4); $hue = 0.9 * (float)substr($hue, 0, -4);
} elseif(str_ends_with($value[0], 'rad')) { } elseif(str_ends_with($hue, 'rad')) {
$value[0] = round(rad2deg((float)substr($value[0], 0, -3))); $hue = round(rad2deg((float)substr($hue, 0, -3)));
} elseif(str_ends_with($value[0], 'turn')) { } elseif(str_ends_with($hue, 'turn')) {
$value[0] = 360.0 * ((float)substr($value[0], 0, -4)); $hue = 360.0 * ((float)substr($hue, 0, -4));
} else { } else {
return self::$none; 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; return self::$none;

View file

@ -1,7 +1,7 @@
<?php <?php
// DbResultIterator.php // DbResultIterator.php
// Created: 2024-02-06 // Created: 2024-02-06
// Updated: 2024-02-06 // Updated: 2024-08-03
namespace Index\Data; namespace Index\Data;
@ -10,6 +10,8 @@ use Iterator;
/** /**
* Implements an iterator and constructor wrapper for IDbResult. * Implements an iterator and constructor wrapper for IDbResult.
*
* @implements Iterator<int, object>
*/ */
class DbResultIterator implements Iterator { class DbResultIterator implements Iterator {
private bool $wasValid; private bool $wasValid;

View file

@ -1,7 +1,7 @@
<?php <?php
// DbResultTrait.php // DbResultTrait.php
// Created: 2023-11-09 // Created: 2023-11-09
// Updated: 2024-02-06 // Updated: 2024-08-03
namespace Index\Data; namespace Index\Data;
@ -10,27 +10,30 @@ namespace Index\Data;
*/ */
trait DbResultTrait { trait DbResultTrait {
public function getString(int|string $index): string { 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 { 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 { 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 { 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 { 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 { 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 { public function getBoolean(int|string $index): bool {
@ -38,7 +41,7 @@ trait DbResultTrait {
} }
public function getBooleanOrNull(int|string $index): ?bool { 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 { public function getIterator(callable $construct): DbResultIterator {

View file

@ -1,7 +1,7 @@
<?php <?php
// DbStatementCache.php // DbStatementCache.php
// Created: 2023-07-21 // Created: 2023-07-21
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data; namespace Index\Data;
@ -9,6 +9,7 @@ namespace Index\Data;
* Container to avoid having many prepared instances of the same query. * Container to avoid having many prepared instances of the same query.
*/ */
class DbStatementCache { class DbStatementCache {
/** @var array<string, IDbStatement> */
private array $stmts = []; private array $stmts = [];
/** /**

View file

@ -1,7 +1,7 @@
<?php <?php
// DbTools.php // DbTools.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data; namespace Index\Data;
@ -33,6 +33,7 @@ final class DbTools {
'sqlite3' => SQLite\SQLiteBackend::class, 'sqlite3' => SQLite\SQLiteBackend::class,
]; ];
/** @return array<string, string|int> */
private static function parseDsnUri(string $dsn): array { private static function parseDsnUri(string $dsn): array {
$uri = parse_url($dsn); $uri = parse_url($dsn);
if($uri === false) if($uri === false)
@ -40,6 +41,7 @@ final class DbTools {
return $uri; return $uri;
} }
/** @param array<string, string|int> $uri */
private static function resolveBackend(array $uri): IDbBackend { private static function resolveBackend(array $uri): IDbBackend {
static $backends = []; static $backends = [];
@ -53,7 +55,7 @@ final class DbTools {
if(array_key_exists($scheme, self::DB_PROTOS)) if(array_key_exists($scheme, self::DB_PROTOS))
$name = self::DB_PROTOS[$scheme]; $name = self::DB_PROTOS[$scheme];
else else
$name = str_replace('-', '\\', $scheme); $name = str_replace('-', '\\', (string)$scheme);
if(class_exists($name) && is_subclass_of($name, IDbBackend::class)) { if(class_exists($name) && is_subclass_of($name, IDbBackend::class)) {
$backend = new $name; $backend = new $name;
@ -161,7 +163,7 @@ final class DbTools {
/** /**
* Constructs a partial query for prepared statements of lists. * 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 $repeat String to repeat.
* @param string $glue Glue character. * @param string $glue Glue character.
* @return string Glued string ready for being thrown into your prepared statement. * @return string Glued string ready for being thrown into your prepared statement.

View file

@ -1,7 +1,7 @@
<?php <?php
// IDbBackend.php // IDbBackend.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2022-02-28 // Updated: 2024-08-03
namespace Index\Data; namespace Index\Data;
@ -28,7 +28,7 @@ interface IDbBackend {
/** /**
* Constructs a connection info instance from a dsn. * 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. * @return IDbConnectionInfo Connection info based on the dsn.
*/ */
function parseDsn(string|array $dsn): IDbConnectionInfo; function parseDsn(string|array $dsn): IDbConnectionInfo;

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDBBackend.php // MariaDBBackend.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
@ -77,7 +77,7 @@ class MariaDBBackend implements IDbBackend {
* Previously supported query parameters: * Previously supported query parameters:
* - `enc_verify=<anything>`: Enabled verification of server certificate. Replaced with `enc_no_verify` as it now defaults to on. * - `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. * @return MariaDBConnectionInfo MariaDB connection info.
*/ */
public function parseDsn(string|array $dsn): IDbConnectionInfo { public function parseDsn(string|array $dsn): IDbConnectionInfo {
@ -98,21 +98,37 @@ class MariaDBBackend implements IDbBackend {
if(!$needsUnix && isset($dsn['port'])) if(!$needsUnix && isset($dsn['port']))
$host .= ':' . $dsn['port']; $host .= ':' . $dsn['port'];
$user = $dsn['user'] ?? ''; $user = (string)($dsn['user'] ?? '');
$pass = $dsn['pass'] ?? ''; $pass = (string)($dsn['pass'] ?? '');
$endPoint = $needsUnix ? null : EndPoint::parse($host); $endPoint = $needsUnix ? null : EndPoint::parse((string)$host);
$dbName = str_replace('/', '_', trim($dsn['path'], '/')); // cute for table prefixes i think $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; $unixPath = $query['socket'] ?? null;
if(!is_string($unixPath)) $unixPath = null;
$charSet = $query['charset'] ?? null; $charSet = $query['charset'] ?? null;
if(!is_string($charSet)) $charSet = null;
$initCommand = $query['init'] ?? null; $initCommand = $query['init'] ?? null;
if(!is_string($initCommand)) $initCommand = null;
$keyPath = $query['enc_key'] ?? null; $keyPath = $query['enc_key'] ?? null;
if(!is_string($keyPath)) $keyPath = null;
$certPath = $query['enc_cert'] ?? null; $certPath = $query['enc_cert'] ?? null;
if(!is_string($certPath)) $certPath = null;
$certAuthPath = $query['enc_authority'] ?? null; $certAuthPath = $query['enc_authority'] ?? null;
if(!is_string($certAuthPath)) $certAuthPath = null;
$trustedCertsPath = $query['enc_trusted_certs'] ?? null; $trustedCertsPath = $query['enc_trusted_certs'] ?? null;
if(!is_string($trustedCertsPath)) $trustedCertsPath = null;
$cipherAlgos = $query['enc_ciphers'] ?? null; $cipherAlgos = $query['enc_ciphers'] ?? null;
if(!is_string($cipherAlgos)) $cipherAlgos = null;
$verifyCert = !isset($query['enc_no_verify']); $verifyCert = !isset($query['enc_no_verify']);
$useCompression = isset($query['compress']); $useCompression = isset($query['compress']);
@ -122,6 +138,9 @@ class MariaDBBackend implements IDbBackend {
$endPoint = new UnixEndPoint($unixPath); $endPoint = new UnixEndPoint($unixPath);
} }
if($endPoint === null)
throw new InvalidArgumentException('No end point could be determined from the provided DSN.');
return new MariaDBConnectionInfo( return new MariaDBConnectionInfo(
$endPoint, $user, $pass, $dbName, $endPoint, $user, $pass, $dbName,
$charSet, $initCommand, $keyPath, $certPath, $charSet, $initCommand, $keyPath, $certPath,

View file

@ -1,12 +1,10 @@
<?php <?php
// MariaDBCharacterSetInfo.php // MariaDBCharacterSetInfo.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
use stdClass;
/** /**
* Contains information about the character set. * Contains information about the character set.
* *
@ -16,11 +14,11 @@ class MariaDBCharacterSetInfo {
/** /**
* Creates a new character set info instance. * 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. * @return MariaDBCharacterSetInfo Character set information class.
*/ */
public function __construct( public function __construct(
private stdClass $charSet private object $charSet
) {} ) {}
/** /**
@ -29,7 +27,7 @@ class MariaDBCharacterSetInfo {
* @return string Character set name. * @return string Character set name.
*/ */
public function getCharacterSet(): string { public function getCharacterSet(): string {
return $this->charSet->charset; return $this->charSet->charset ?? '';
} }
/** /**
@ -38,7 +36,7 @@ class MariaDBCharacterSetInfo {
* @return string Default collation name. * @return string Default collation name.
*/ */
public function getDefaultCollation(): string { public function getDefaultCollation(): string {
return $this->charSet->collation; return $this->charSet->collation ?? '';
} }
/** /**
@ -48,7 +46,7 @@ class MariaDBCharacterSetInfo {
* @return string Source directory. * @return string Source directory.
*/ */
public function getDirectory(): string { 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. * @return int Minimum character width in bytes.
*/ */
public function getMinimumWidth(): int { 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. * @return int Maximum character width in bytes.
*/ */
public function getMaximumWidth(): int { 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. * @return int Character set identifier.
*/ */
public function getId(): int { public function getId(): int {
return $this->charSet->number; return $this->charSet->number ?? 0;
} }
/** /**
@ -86,6 +84,6 @@ class MariaDBCharacterSetInfo {
* @return int Character set status. * @return int Character set status.
*/ */
public function getState(): int { public function getState(): int {
return $this->charSet->state; return $this->charSet->state ?? 0;
} }
} }

View file

@ -1,12 +1,13 @@
<?php <?php
// MariaDBConnection.php // MariaDBConnection.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
use mysqli; use mysqli;
use mysqli_sql_exception; use mysqli_sql_exception;
use stdClass;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Data\{IDbConnection,IDbTransactions}; use Index\Data\{IDbConnection,IDbTransactions};
@ -87,14 +88,18 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
// nothing suggests otherwise too. // nothing suggests otherwise too.
// The output of mysqli_init is just an object anyway so we can safely use it instead // 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. // 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); $this->connection->options(MYSQLI_OPT_LOCAL_INFILE, 0);
if($connectionInfo->hasCharacterSet()) if($connectionInfo->hasCharacterSet())
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet()); $this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet() ?? '');
if($connectionInfo->hasInitCommand()) 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; $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. * 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 { public function getLastErrors(): array {
return MariaDBWarning::fromLastErrors($this->connection->error_list); return MariaDBWarning::fromLastErrors($this->connection->error_list);
@ -262,7 +267,7 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
* *
* The result of SHOW WARNINGS; * The result of SHOW WARNINGS;
* *
* @return array List of warnings. * @return MariaDBWarning[] List of warnings.
*/ */
public function getWarnings(): array { public function getWarnings(): array {
return MariaDBWarning::fromGetWarnings($this->connection->get_warnings()); return MariaDBWarning::fromGetWarnings($this->connection->get_warnings());
@ -370,8 +375,14 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
} catch(mysqli_sql_exception $ex) { } catch(mysqli_sql_exception $ex) {
throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex); throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex);
} }
if($result === false) if($result === false)
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); 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 // Yes, this always uses Native, for some reason the stupid limitation in libmysql only applies to preparing
return new MariaDBResultNative($result); return new MariaDBResultNative($result);
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDBResult.php // MariaDBResult.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
@ -17,6 +17,7 @@ use Index\IO\{Stream,TempFileStream};
abstract class MariaDBResult implements IDbResult { abstract class MariaDBResult implements IDbResult {
use DbResultTrait; use DbResultTrait;
/** @var array<int|string, mixed> */
protected array $currentRow = []; protected array $currentRow = [];
/** /**
@ -70,7 +71,12 @@ abstract class MariaDBResult implements IDbResult {
public function getStream(int|string $index): ?Stream { public function getStream(int|string $index): ?Stream {
if($this->isNull($index)) if($this->isNull($index))
return null; 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; abstract function close(): void;

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDBResultLib.php // MariaDBResultLib.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
@ -14,7 +14,10 @@ use RuntimeException;
* @internal * @internal
*/ */
class MariaDBResultLib extends MariaDBResult { class MariaDBResultLib extends MariaDBResult {
/** @var mixed[] */
private array $fields = []; private array $fields = [];
/** @var mixed[] */
private array $data = []; private array $data = [];
public function __construct(mysqli_stmt $statement) { public function __construct(mysqli_stmt $statement) {
@ -24,6 +27,9 @@ class MariaDBResultLib extends MariaDBResult {
throw new RuntimeException($statement->error, $statement->errno); throw new RuntimeException($statement->error, $statement->errno);
$metadata = $statement->result_metadata(); $metadata = $statement->result_metadata();
if($metadata === false)
throw new RuntimeException('result_metadata output was false');
while($field = $metadata->fetch_field()) while($field = $metadata->fetch_field())
$this->fields[] = &$this->data[$field->name]; $this->fields[] = &$this->data[$field->name];
@ -31,6 +37,9 @@ class MariaDBResultLib extends MariaDBResult {
} }
public function next(): bool { public function next(): bool {
if(!($this->result instanceof mysqli_stmt))
return false;
$result = $this->result->fetch(); $result = $this->result->fetch();
if($result === false) if($result === false)
throw new RuntimeException($this->result->error, $this->result->errno); throw new RuntimeException($this->result->error, $this->result->errno);

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDBResultNative.php // MariaDBResultNative.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
@ -27,9 +27,13 @@ class MariaDBResultNative extends MariaDBResult {
} }
public function next(): bool { public function next(): bool {
if(!($this->result instanceof mysqli_result))
return false;
$result = $this->result->fetch_array(MYSQLI_BOTH); $result = $this->result->fetch_array(MYSQLI_BOTH);
if($result === null) if($result === null)
return false; return false;
$this->currentRow = $result; $this->currentRow = $result;
return true; return true;
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDBStatement.php // MariaDBStatement.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
@ -15,6 +15,7 @@ use Index\IO\Stream;
* Represents a MariaDB/MySQL prepared statement. * Represents a MariaDB/MySQL prepared statement.
*/ */
class MariaDBStatement implements IDbStatement { class MariaDBStatement implements IDbStatement {
/** @var array<int, MariaDBParameter> */
private array $params = []; private array $params = [];
/** /**
@ -28,6 +29,8 @@ class MariaDBStatement implements IDbStatement {
) {} ) {}
private static bool $constructed = false; private static bool $constructed = false;
/** @var class-string<MariaDBResult> */
private static string $resultImplementation; private static string $resultImplementation;
/** /**
@ -84,7 +87,7 @@ class MariaDBStatement implements IDbStatement {
/** /**
* Gets a list of errors from the last command. * 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 { public function getLastErrors(): array {
return MariaDBWarning::fromLastErrors($this->statement->error_list); return MariaDBWarning::fromLastErrors($this->statement->error_list);
@ -95,7 +98,7 @@ class MariaDBStatement implements IDbStatement {
* *
* The result of SHOW WARNINGS; * The result of SHOW WARNINGS;
* *
* @return array List of warnings. * @return MariaDBWarning[] List of warnings.
*/ */
public function getWarnings(): array { public function getWarnings(): array {
return MariaDBWarning::fromGetWarnings($this->statement->get_warnings()); return MariaDBWarning::fromGetWarnings($this->statement->get_warnings());
@ -126,7 +129,7 @@ class MariaDBStatement implements IDbStatement {
} elseif(is_resource($value)) { } elseif(is_resource($value)) {
while($data = fread($value, 8192)) while($data = fread($value, 8192))
$this->statement->send_long_data($key, $data); $this->statement->send_long_data($key, $data);
} else } elseif(is_scalar($value))
$this->statement->send_long_data($key, (string)$value); $this->statement->send_long_data($key, (string)$value);
$value = null; $value = null;

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDBWarning.php // MariaDBWarning.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\MariaDB; namespace Index\Data\MariaDB;
@ -61,8 +61,8 @@ class MariaDBWarning implements Stringable {
/** /**
* Creates an array of warning objects from the output of $last_errors. * Creates an array of warning objects from the output of $last_errors.
* *
* @param array $errors Array of warning arrays. * @param array<int, array{errno: int, sqlstate: string, error: string}> $errors Array of warning arrays.
* @return array Array of warnings objects. * @return MariaDBWarning[] Array of warnings objects.
*/ */
public static function fromLastErrors(array $errors): array { public static function fromLastErrors(array $errors): array {
return XArray::select($errors, fn($error) => new MariaDBWarning($error['error'], $error['sqlstate'], $error['errno'])); 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. * Creates an array of warning objects from a mysqli_warning instance.
* *
* @param mysqli_warning|false $warnings Warning object. * @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 { public static function fromGetWarnings(mysqli_warning|false $warnings): array {
$array = []; $array = [];

View file

@ -1,7 +1,7 @@
<?php <?php
// DbMigrationManager.php // DbMigrationManager.php
// Created: 2023-01-07 // Created: 2023-01-07
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\Migration; namespace Index\Data\Migration;
@ -200,7 +200,7 @@ EOF;
* Runs all migrations present in a migration repository. This process is irreversible! * Runs all migrations present in a migration repository. This process is irreversible!
* *
* @param IDbMigrationRepo $migrations Migrations repository to fetch migrations from. * @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 { public function processMigrations(IDbMigrationRepo $migrations): array {
$migrations = $migrations->getMigrations(); $migrations = $migrations->getMigrations();

View file

@ -1,7 +1,7 @@
<?php <?php
// FsDbMigrationInfo.php // FsDbMigrationInfo.php
// Created: 2023-01-07 // Created: 2023-01-07
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\Migration; namespace Index\Data\Migration;
@ -41,6 +41,9 @@ class FsDbMigrationInfo implements IDbMigrationInfo {
public function migrate(IDbConnection $conn): void { public function migrate(IDbConnection $conn): void {
require_once $this->path; require_once $this->path;
(new $this->className)->migrate($conn);
$migration = new $this->className;
if($migration instanceof IDbMigration)
$migration->migrate($conn);
} }
} }

View file

@ -1,10 +1,12 @@
<?php <?php
// FsDbMigrationRepo.php // FsDbMigrationRepo.php
// Created: 2023-01-07 // Created: 2023-01-07
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\Migration; namespace Index\Data\Migration;
use RuntimeException;
class FsDbMigrationRepo implements IDbMigrationRepo { class FsDbMigrationRepo implements IDbMigrationRepo {
/** /**
* @param string $path Filesystem path to the directory containing the migration files. * @param string $path Filesystem path to the directory containing the migration files.
@ -16,7 +18,11 @@ class FsDbMigrationRepo implements IDbMigrationRepo {
public function getMigrations(): array { public function getMigrations(): array {
if(!is_dir($this->path)) if(!is_dir($this->path))
return []; return [];
$files = glob(realpath($this->path) . '/*.php'); $files = glob(realpath($this->path) . '/*.php');
if($files === false)
throw new RuntimeException('Failed to glob migration files.');
$migrations = []; $migrations = [];
foreach($files as $file) foreach($files as $file)

View file

@ -1,7 +1,7 @@
<?php <?php
// NullDbBackend.php // NullDbBackend.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\NullDb; namespace Index\Data\NullDb;
@ -30,7 +30,7 @@ class NullDbBackend implements IDbBackend {
* *
* NullDb has no parameters that can be controlled using the DSN. * 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. * @return NullDbConnectionInfo Dummy connection info instance.
*/ */
public function parseDsn(string|array $dsn): IDbConnectionInfo { public function parseDsn(string|array $dsn): IDbConnectionInfo {

View file

@ -1,7 +1,7 @@
<?php <?php
// SQLiteBackend.php // SQLiteBackend.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\SQLite; namespace Index\Data\SQLite;
@ -51,7 +51,7 @@ class SQLiteBackend implements IDbBackend {
* - `readOnly` to open a database in read-only mode. * - `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. * - `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. * @return SQLiteConnectionInfo SQLite connection info.
*/ */
public function parseDsn(string|array $dsn): IDbConnectionInfo { public function parseDsn(string|array $dsn): IDbConnectionInfo {
@ -63,14 +63,16 @@ class SQLiteBackend implements IDbBackend {
$encKey = ''; $encKey = '';
$path = $dsn['path'] ?? ''; $path = (string)($dsn['path'] ?? '');
if($path === 'memory:') if($path === 'memory:')
$path = ':memory:'; $path = ':memory:';
parse_str(str_replace('+', '%2B', $dsn['query'] ?? ''), $query); parse_str(str_replace('+', '%2B', (string)($dsn['query'] ?? '')), $query);
$encKey = $query['key'] ?? ''; $encKey = $query['key'] ?? '';
if(!is_string($encKey)) $encKey = '';
$readOnly = isset($query['readOnly']); $readOnly = isset($query['readOnly']);
$create = !isset($query['openOnly']); $create = !isset($query['openOnly']);

View file

@ -1,7 +1,7 @@
<?php <?php
// SQLiteConnection.php // SQLiteConnection.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\SQLite; namespace Index\Data\SQLite;
@ -477,7 +477,8 @@ class SQLiteConnection implements IDbConnection, IDbTransactions {
$handle = $this->connection->openBlob($table, $column, $rowId); $handle = $this->connection->openBlob($table, $column, $rowId);
if(!is_resource($handle)) if(!is_resource($handle))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode()); 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 { public function getLastInsertId(): int|string {

View file

@ -1,7 +1,7 @@
<?php <?php
// SQLiteResult.php // SQLiteResult.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Data\SQLite; namespace Index\Data\SQLite;
@ -16,6 +16,7 @@ use Index\IO\{Stream,TempFileStream};
class SQLiteResult implements IDbResult { class SQLiteResult implements IDbResult {
use DbResultTrait; use DbResultTrait;
/** @var array<int|string, mixed> */
private array $currentRow = []; private array $currentRow = [];
/** /**
@ -60,7 +61,12 @@ class SQLiteResult implements IDbResult {
public function getStream(int|string $index): ?Stream { public function getStream(int|string $index): ?Stream {
if($this->isNull($index)) if($this->isNull($index))
return null; 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 {} public function close(): void {}

View file

@ -1,7 +1,7 @@
<?php <?php
// FormContent.php // FormContent.php
// Created: 2022-02-10 // Created: 2022-02-10
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\Content; namespace Index\Http\Content;
@ -14,7 +14,7 @@ use Index\Http\HttpUploadedFile;
class FormContent implements IHttpContent { class FormContent implements IHttpContent {
/** /**
* @param array<string, mixed> $postFields Form fields. * @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( public function __construct(
private array $postFields, private array $postFields,
@ -26,7 +26,7 @@ class FormContent implements IHttpContent {
* *
* @param string $name Name of the form field. * @param string $name Name of the form field.
* @param int $filter A PHP filter extension filter constant. * @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. * @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 { 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 { public function getUploadedFile(string $name): HttpUploadedFile {
if(!isset($this->uploadedFiles[$name])) if(!isset($this->uploadedFiles[$name]))
throw new RuntimeException('No file with name $name present.'); 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]; return $this->uploadedFiles[$name];
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// JsonContent.php // JsonContent.php
// Created: 2022-02-10 // Created: 2022-02-10
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\Content; namespace Index\Http\Content;
@ -38,7 +38,11 @@ class JsonContent implements IHttpContent, JsonSerializable {
* @return string JSON encoded string. * @return string JSON encoded string.
*/ */
public function encode(): 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 { public function __toString(): string {
@ -48,10 +52,10 @@ class JsonContent implements IHttpContent, JsonSerializable {
/** /**
* Creates an instance from encoded content. * 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. * @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)); return new JsonContent(json_decode($encoded));
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// StringContent.php // StringContent.php
// Created: 2022-02-10 // Created: 2022-02-10
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\Content; namespace Index\Http\Content;
@ -48,7 +48,11 @@ class StringContent implements IHttpContent {
* @return StringContent Instance representing the provided path. * @return StringContent Instance representing the provided path.
*/ */
public static function fromFile(string $path): StringContent { 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);
} }
/** /**

View file

@ -1,7 +1,7 @@
<?php <?php
// StreamContentHandler.php // StreamContentHandler.php
// Created: 2024-03-28 // Created: 2024-03-28
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\ContentHandling; namespace Index\Http\ContentHandling;
@ -17,6 +17,7 @@ class StreamContentHandler implements IContentHandler {
return $content instanceof Stream; return $content instanceof Stream;
} }
/** @param Stream $content */
public function handle(HttpResponseBuilder $response, mixed $content): void { public function handle(HttpResponseBuilder $response, mixed $content): void {
if(!$response->hasContentType()) if(!$response->hasContentType())
$response->setTypeStream(); $response->setTypeStream();

View file

@ -1,7 +1,7 @@
<?php <?php
// HtmlErrorHandler.php // HtmlErrorHandler.php
// Created: 2024-03-28 // Created: 2024-03-28
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\ErrorHandling; namespace Index\Http\ErrorHandling;
@ -28,8 +28,13 @@ class HtmlErrorHandler implements IErrorHandler {
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void { public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
$response->setTypeHTML(); $response->setTypeHTML();
$charSet = mb_preferred_mime_name(mb_internal_encoding());
if($charSet === false)
$charSet = 'UTF-8';
$response->setContent(strtr(self::TEMPLATE, [ $response->setContent(strtr(self::TEMPLATE, [
':charset' => strtolower(mb_preferred_mime_name(mb_internal_encoding())), ':charset' => strtolower($charSet),
':code' => sprintf('%03d', $code), ':code' => sprintf('%03d', $code),
':message' => $message, ':message' => $message,
])); ]));

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpHeader.php // HttpHeader.php
// Created: 2022-02-14 // Created: 2022-02-14
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; namespace Index\Http;
@ -11,6 +11,7 @@ use Stringable;
* Represents a generic HTTP header. * Represents a generic HTTP header.
*/ */
class HttpHeader implements Stringable { class HttpHeader implements Stringable {
/** @var string[] */
private array $lines; private array $lines;
/** /**

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpHeadersBuilder.php // HttpHeadersBuilder.php
// Created: 2022-02-08 // Created: 2022-02-08
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; namespace Index\Http;
@ -11,6 +11,7 @@ use InvalidArgumentException;
* Represents a HTTP message header builder. * Represents a HTTP message header builder.
*/ */
class HttpHeadersBuilder { class HttpHeadersBuilder {
/** @var array<string, string[]> */
private array $headers = []; 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. * 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 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); $nameLower = strtolower($name);
if(!isset($this->headers[$nameLower])) if(!isset($this->headers[$nameLower]))
$this->headers[$nameLower] = [$name]; $this->headers[$nameLower] = [$name];
@ -32,9 +33,9 @@ class HttpHeadersBuilder {
* If a header with the same name is already present, it will be overwritten. * If a header with the same name is already present, it will be overwritten.
* *
* @param string $name Name of the header to set. * @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]; $this->headers[strtolower($name)] = [$name, $value];
} }
@ -66,7 +67,7 @@ class HttpHeadersBuilder {
$headers = []; $headers = [];
foreach($this->headers as $index => $lines) foreach($this->headers as $index => $lines)
$headers[] = new HttpHeader(array_shift($lines), ...$lines); $headers[] = new HttpHeader(array_shift($lines) ?? '', ...$lines);
return new HttpHeaders($headers); return new HttpHeaders($headers);
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpMessageBuilder.php // HttpMessageBuilder.php
// Created: 2022-02-08 // Created: 2022-02-08
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; 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. * 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 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); $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. * If a header with the same name is already present, it will be overwritten.
* *
* @param string $name Name of the header to set. * @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); $this->headers->setHeader($name, $value);
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpRequest.php // HttpRequest.php
// Created: 2022-02-08 // Created: 2022-02-08
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; namespace Index\Http;
@ -77,7 +77,7 @@ class HttpRequest extends HttpMessage {
* *
* @param string $name Name of the request query field. * @param string $name Name of the request query field.
* @param int $filter A PHP filter extension filter constant. * @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. * @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 { 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 string $name Name of the request cookie.
* @param int $filter A PHP filter extension filter constant. * @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. * @return mixed Value of the cookie, null if not present.
*/ */
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed { 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 $build->toRequest();
} }
/** @return array<string, string> */
private static function getRawRequestHeaders(): array { private static function getRawRequestHeaders(): array {
if(function_exists('getallheaders')) if(function_exists('getallheaders'))
return getallheaders(); return getallheaders();

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpRequestBuilder.php // HttpRequestBuilder.php
// Created: 2022-02-08 // Created: 2022-02-08
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; namespace Index\Http;
@ -11,7 +11,11 @@ namespace Index\Http;
class HttpRequestBuilder extends HttpMessageBuilder { class HttpRequestBuilder extends HttpMessageBuilder {
private string $method = 'GET'; private string $method = 'GET';
private string $path = '/'; private string $path = '/';
/** @var array<string, mixed> */
private array $params = []; private array $params = [];
/** @var array<string, string> */
private array $cookies = []; private array $cookies = [];
/** /**
@ -109,9 +113,9 @@ class HttpRequestBuilder extends HttpMessageBuilder {
* Sets a HTTP request cookie. * Sets a HTTP request cookie.
* *
* @param string $name Name of the 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; $this->cookies[$name] = $value;
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpResponseBuilder.php // HttpResponseBuilder.php
// Created: 2022-02-08 // Created: 2022-02-08
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; namespace Index\Http;
@ -15,6 +15,8 @@ use Index\Performance\Timings;
class HttpResponseBuilder extends HttpMessageBuilder { class HttpResponseBuilder extends HttpMessageBuilder {
private int $statusCode = -1; private int $statusCode = -1;
private ?string $statusText; private ?string $statusText;
/** @var string[] */
private array $vary = []; private array $vary = [];
/** /**
@ -73,7 +75,7 @@ class HttpResponseBuilder extends HttpMessageBuilder {
* Adds a cookie to this response. * Adds a cookie to this response.
* *
* @param string $name Name of the cookie. * @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 DateTimeInterface|int|null $expires Point at which the cookie should expire.
* @param string $path Path to which to apply the cookie. * @param string $path Path to which to apply the cookie.
* @param string $domain Domain name 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( public function addCookie(
string $name, string $name,
mixed $value, string $value,
DateTimeInterface|int|null $expires = null, DateTimeInterface|int|null $expires = null,
string $path = '', string $path = '',
string $domain = '', string $domain = '',
@ -147,7 +149,7 @@ class HttpResponseBuilder extends HttpMessageBuilder {
/** /**
* Sets a Vary header. * 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 { public function addVary(string|array $headers): void {
if(!is_array($headers)) if(!is_array($headers))

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpUploadedFile.php // HttpUploadedFile.php
// Created: 2022-02-10 // Created: 2022-02-10
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http; namespace Index\Http;
@ -17,6 +17,8 @@ class HttpUploadedFile implements ICloseable {
private bool $hasMoved = false; private bool $hasMoved = false;
private ?Stream $stream = null; private ?Stream $stream = null;
private MediaType $suggestedMediaType;
/** /**
* @param int $errorCode PHP file upload error code. * @param int $errorCode PHP file upload error code.
* @param int $size Size, as provided by the client. * @param int $size Size, as provided by the client.
@ -29,10 +31,11 @@ class HttpUploadedFile implements ICloseable {
private int $size, private int $size,
private string $localFileName, private string $localFileName,
private string $suggestedFileName, private string $suggestedFileName,
private MediaType|string $suggestedMediaType MediaType|string $suggestedMediaType
) { ) {
if(!($suggestedMediaType instanceof MediaType)) $this->suggestedMediaType = $suggestedMediaType instanceof MediaType
$suggestedMediaType = MediaType::parse($suggestedMediaType); ? $suggestedMediaType
: MediaType::parse($suggestedMediaType);
} }
/** /**
@ -160,22 +163,24 @@ class HttpUploadedFile implements ICloseable {
/** /**
* Creates a HttpUploadedFile instance from an entry in the $_FILES superglobal. * 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. * @return HttpUploadedFile Uploaded file info.
*/ */
public static function createFromFILE(array $file): self { public static function createFromFILE(array $file): self {
return new HttpUploadedFile( return new HttpUploadedFile(
$file['error'] ?? UPLOAD_ERR_NO_FILE, (int)($file['error'] ?? UPLOAD_ERR_NO_FILE),
$file['size'] ?? -1, (int)($file['size'] ?? -1),
$file['tmp_name'] ?? '', (string)($file['tmp_name'] ?? ''),
$file['name'] ?? '', (string)($file['name'] ?? ''),
$file['type'] ?? '' (string)($file['type'] ?? '')
); );
} }
/** /**
* Creates a collection of HttpUploadedFile instances from the $_FILES superglobal. * 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 { public static function createFromFILES(array $files): array {
if(empty($files)) if(empty($files))
@ -184,6 +189,10 @@ class HttpUploadedFile implements ICloseable {
return self::createObjectInstances(self::normalizeFILES($files)); return self::createObjectInstances(self::normalizeFILES($files));
} }
/**
* @param array<string, mixed> $files
* @return array<string, mixed>
*/
private static function traverseFILES(array $files, string $keyName): array { private static function traverseFILES(array $files, string $keyName): array {
$arr = []; $arr = [];
@ -200,11 +209,15 @@ class HttpUploadedFile implements ICloseable {
return $arr; return $arr;
} }
/**
* @param array<string, mixed> $files
* @return array<string, mixed>
*/
private static function normalizeFILES(array $files): array { private static function normalizeFILES(array $files): array {
$out = []; $out = [];
foreach($files as $key => $arr) { foreach($files as $key => $arr) {
if(empty($arr)) if(!is_array($arr) || !isset($arr['error']))
continue; continue;
$key = '_' . $key; $key = '_' . $key;
@ -217,9 +230,8 @@ class HttpUploadedFile implements ICloseable {
if(is_array($arr['error'])) { if(is_array($arr['error'])) {
$keys = array_keys($arr); $keys = array_keys($arr);
foreach($keys as $keyName) { foreach($keys as $keyName)
$out[$key] = array_merge_recursive($out[$key] ?? [], self::traverseFILES($arr[$keyName], $keyName)); $out[$key] = array_merge_recursive($out[$key] ?? [], self::traverseFILES($arr[$keyName], (string)$keyName));
}
continue; continue;
} }
} }
@ -227,17 +239,21 @@ class HttpUploadedFile implements ICloseable {
return $out; return $out;
} }
/**
* @param array<string, mixed> $files
* @return array<string, HttpUploadedFile|array<string, mixed>>
*/
private static function createObjectInstances(array $files): array { private static function createObjectInstances(array $files): array {
$coll = []; $coll = [];
foreach($files as $key => $val) { foreach($files as $key => $val) {
$key = substr($key, 1); if(!is_array($val))
continue;
if(isset($val['error'])) { $key = substr($key, 1);
$coll[$key] = self::createFromFILE($val); $coll[$key] = isset($val['error'])
} else { ? self::createFromFILE($val)
$coll[$key] = self::createObjectInstances($val); : self::createObjectInstances($val);
}
} }
return $coll; return $coll;

View file

@ -1,7 +1,7 @@
<?php <?php
// HttpRouter.php // HttpRouter.php
// Created: 2024-03-28 // Created: 2024-03-28
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\Routing; namespace Index\Http\Routing;
@ -15,11 +15,16 @@ use Index\Http\ErrorHandling\{HtmlErrorHandler,IErrorHandler,PlainErrorHandler};
class HttpRouter implements IRouter { class HttpRouter implements IRouter {
use RouterTrait; use RouterTrait;
/** @var object[] */
private array $middlewares = []; private array $middlewares = [];
/** @var array<string, array<string, callable>> */
private array $staticRoutes = []; private array $staticRoutes = [];
/** @var array<string, array<string, callable>> */
private array $dynamicRoutes = []; private array $dynamicRoutes = [];
/** @var IContentHandler[] */
private array $contentHandlers = []; private array $contentHandlers = [];
private IErrorHandler $errorHandler; private IErrorHandler $errorHandler;
@ -46,8 +51,14 @@ class HttpRouter implements IRouter {
* @return string Normalised character set name. * @return string Normalised character set name.
*/ */
public function getCharSet(): string { public function getCharSet(): string {
if($this->charSet === '') if($this->charSet === '') {
return strtolower(mb_preferred_mime_name(mb_internal_encoding())); $charSet = mb_preferred_mime_name(mb_internal_encoding());
if($charSet === false)
$charSet = 'UTF-8';
return strtolower($charSet);
}
return $this->charSet; return $this->charSet;
} }
@ -196,19 +207,19 @@ class HttpRouter implements IRouter {
$middlewares = []; $middlewares = [];
foreach($this->middlewares as $mwInfo) { foreach($this->middlewares as $mwInfo) {
if($mwInfo->dynamic) { if($mwInfo->dynamic ?? false) {
if(preg_match($mwInfo->match, $path, $args) !== 1) if(preg_match($mwInfo->match ?? '', $path, $args) !== 1)
continue; continue;
array_shift($args); array_shift($args);
} else { } else {
if(!str_starts_with($path, $mwInfo->prefix)) if(!str_starts_with($path, $mwInfo->prefix ?? ''))
continue; continue;
$args = []; $args = [];
} }
$middlewares[] = [$mwInfo->handler, $args]; $middlewares[] = [$mwInfo->handler ?? null, $args];
} }
$methods = []; $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. * 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 ?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 { public function dispatch(?HttpRequest $request = null, array $args = []): void {
$request ??= HttpRequest::fromRequest(); $request ??= HttpRequest::fromRequest();
@ -275,7 +286,7 @@ class HttpRouter implements IRouter {
break; break;
} }
if(!$response->hasContent() && $result !== null) { if(!$response->hasContent() && is_scalar($result)) {
$result = (string)$result; $result = (string)$result;
$response->setContent(new StringContent($result)); $response->setContent(new StringContent($result));
@ -283,7 +294,10 @@ class HttpRouter implements IRouter {
if(strtolower(substr($result, 0, 14)) === '<!doctype html') if(strtolower(substr($result, 0, 14)) === '<!doctype html')
$response->setTypeHTML($this->getCharSet()); $response->setTypeHTML($this->getCharSet());
else { 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') if(strtolower(substr($result, 0, 5)) === '<?xml')
$response->setTypeXML($charset); $response->setTypeXML($charset);

View file

@ -1,7 +1,7 @@
<?php <?php
// ResolvedRouteInfo.php // ResolvedRouteInfo.php
// Created: 2024-03-28 // Created: 2024-03-28
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Http\Routing; namespace Index\Http\Routing;
@ -10,10 +10,10 @@ namespace Index\Http\Routing;
*/ */
class ResolvedRouteInfo { class ResolvedRouteInfo {
/** /**
* @param array $middlewares Middlewares that should be run prior to the route handler. * @param array{callable, mixed[]}[] $middlewares Middlewares that should be run prior to the route handler.
* @param array $supportedMethods HTTP methods that this route accepts. * @param string[] $supportedMethods HTTP methods that this route accepts.
* @param mixed $handler Route handler. * @param (callable(): mixed)|null $handler Route handler.
* @param array $args Argument list to pass to the middleware and route handlers. * @param mixed[] $args Argument list to pass to the middleware and route handlers.
*/ */
public function __construct( public function __construct(
private array $middlewares, private array $middlewares,
@ -25,7 +25,7 @@ class ResolvedRouteInfo {
/** /**
* Run middleware handlers. * 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. * @return mixed Return value from the first middleware to return anything non-null, otherwise null.
*/ */
public function runMiddleware(array $args): mixed { public function runMiddleware(array $args): mixed {
@ -44,7 +44,7 @@ class ResolvedRouteInfo {
* @return bool true if it does. * @return bool true if it does.
*/ */
public function hasHandler(): bool { public function hasHandler(): bool {
return $this->handler !== null; return is_callable($this->handler);
} }
/** /**
@ -68,10 +68,13 @@ class ResolvedRouteInfo {
/** /**
* Dispatches this route. * 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. * @return mixed Return value of the route handler.
*/ */
public function dispatch(array $args): mixed { public function dispatch(array $args): mixed {
if(!is_callable($this->handler))
return null;
return ($this->handler)(...array_merge($args, $this->args)); return ($this->handler)(...array_merge($args, $this->args));
} }
} }

View file

@ -1,11 +1,12 @@
<?php <?php
// GenericStream.php // GenericStream.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\IO; namespace Index\IO;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException;
/** /**
* Represents any stream that can be handled using the C-like f* functions. * 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', 'x+', 'x+b', 'c', 'cb', 'c+', 'c+b',
]; ];
// resource
// can't seem to turn this one into a constructor property thing? // can't seem to turn this one into a constructor property thing?
/** @var resource */
protected $stream; protected $stream;
private bool $canRead; private bool $canRead;
@ -51,7 +52,7 @@ class GenericStream extends Stream {
$metaData = $this->metaData(); $metaData = $this->metaData();
$this->canRead = in_array($metaData['mode'], self::READABLE); $this->canRead = in_array($metaData['mode'], self::READABLE);
$this->canWrite = in_array($metaData['mode'], self::WRITEABLE); $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 $this->stream;
} }
/** @return array<string|int, mixed> */
private function stat(): array { 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 { public function getPosition(): int {
return ftell($this->stream); $tell = ftell($this->stream);
if($tell === false)
return -1;
return $tell;
} }
public function getLength(): int { public function getLength(): int {
if(!$this->canSeek()) if(!$this->canSeek())
return -1; return -1;
return $this->stat()['size'];
$size = $this->stat()['size'];
if(!is_int($size))
return -1;
return $size;
} }
public function setLength(int $length): void { public function setLength(int $length): void {
if($length < 0)
throw new InvalidArgumentException('$length must be a positive integer');
ftruncate($this->stream, $length); ftruncate($this->stream, $length);
} }
/** @return array<string, mixed> */
private function metaData(): array { private function metaData(): array {
return stream_get_meta_data($this->stream); return stream_get_meta_data($this->stream);
} }
@ -93,10 +112,18 @@ class GenericStream extends Stream {
return $this->canSeek; return $this->canSeek;
} }
public function hasTimedOut(): bool { 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 { public function isBlocking(): bool {
return $this->metaData()['blocked']; $blocked = $this->metaData()['blocked'];
if(!is_bool($blocked))
return false;
return $blocked;
} }
public function isEnded(): bool { public function isEnded(): bool {
@ -116,9 +143,13 @@ class GenericStream extends Stream {
} }
public function read(int $length): ?string { public function read(int $length): ?string {
if($length < 1)
throw new InvalidArgumentException('$length must be greater than 0');
$buffer = fread($this->stream, $length); $buffer = fread($this->stream, $length);
if($buffer === false) if($buffer === false)
return null; return null;
return $buffer; return $buffer;
} }
@ -155,6 +186,10 @@ class GenericStream extends Stream {
} }
public function __toString(): string { public function __toString(): string {
return stream_get_contents($this->stream); $contents = stream_get_contents($this->stream);
if($contents === false)
return '';
return $contents;
} }
} }

View file

@ -1,16 +1,22 @@
<?php <?php
// MemoryStream.php // MemoryStream.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\IO; namespace Index\IO;
use RuntimeException;
/** /**
* Represents an in-memory stream. * Represents an in-memory stream.
*/ */
class MemoryStream extends GenericStream { class MemoryStream extends GenericStream {
public function __construct() { 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);
} }
/** /**

View file

@ -1,17 +1,20 @@
<?php <?php
// ProcessStream.php // ProcessStream.php
// Created: 2023-01-25 // Created: 2023-01-25
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\IO; namespace Index\IO;
use InvalidArgumentException;
use RuntimeException; use RuntimeException;
/** /**
* Represents a stream to a running sub-process. * Represents a stream to a running sub-process.
*/ */
class ProcessStream extends Stream { class ProcessStream extends Stream {
/** @var resource */
private $handle; private $handle;
private bool $canRead; private bool $canRead;
private bool $canWrite; private bool $canWrite;
@ -21,7 +24,11 @@ class ProcessStream extends Stream {
* @throws RuntimeException If we were unable to spawn the process. * @throws RuntimeException If we were unable to spawn the process.
*/ */
public function __construct(string $command, string $mode) { 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->canRead = strpos($mode, 'r') !== false;
$this->canWrite = strpos($mode, 'w') !== false; $this->canWrite = strpos($mode, 'w') !== false;
@ -75,9 +82,13 @@ class ProcessStream extends Stream {
} }
public function read(int $length): ?string { public function read(int $length): ?string {
if($length < 1)
throw new InvalidArgumentException('$length must be greater than 0');
$buffer = fread($this->handle, $length); $buffer = fread($this->handle, $length);
if($buffer === false) if($buffer === false)
return null; return null;
return $buffer; return $buffer;
} }
@ -99,9 +110,7 @@ class ProcessStream extends Stream {
public function flush(): void {} public function flush(): void {}
public function close(): void { public function close(): void {
if(is_resource($this->handle)) { if(is_resource($this->handle))
pclose($this->handle); pclose($this->handle);
$this->handle = null;
}
} }
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// Stream.php // Stream.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\IO; namespace Index\IO;
@ -186,8 +186,13 @@ abstract class Stream implements Stringable, ICloseable {
if($this->canSeek()) if($this->canSeek())
$this->seek(0); $this->seek(0);
while(!$this->isEnded()) while(!$this->isEnded()) {
$other->write($this->read(8192)); $buffer = $this->read(8192);
if($buffer === null)
break;
$other->write($buffer);
}
} }
/** /**

View file

@ -1,10 +1,12 @@
<?php <?php
// TempFileStream.php // TempFileStream.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\IO; namespace Index\IO;
use RuntimeException;
/** /**
* Represents a temporary file stream. Will remain in memory if the size is below a given threshold. * 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. * @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) { 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) { if($body !== null) {
$this->write($body); $this->write($body);

View file

@ -1,7 +1,7 @@
<?php <?php
// MediaType.php // MediaType.php
// Created: 2022-02-10 // Created: 2022-02-10
// Updated: 2024-08-02 // Updated: 2024-08-03
namespace Index; namespace Index;
@ -79,10 +79,10 @@ class MediaType implements Stringable, IComparable, IEquatable {
* *
* @param string $name Media type parameter name. * @param string $name Media type parameter name.
* @param int $filter A PHP filter extension filter constant. * @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. * @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])) if(!isset($this->params[$name]))
return null; return null;
return filter_var($this->params[$name], $filter, $options); 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. * @return string Character specified in media type.
*/ */
public function getCharset(): string { 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. * @return float Quality specified for the media type.
*/ */
public function getQuality(): float { 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 { public function compare(mixed $other): int {
if(!($other instanceof MediaType)) if(!($other instanceof MediaType)) {
if(!is_scalar($other))
return -1;
try { try {
$other = self::parse((string)$other); $other = self::parse((string)$other);
} catch(InvalidArgumentException $ex) { } catch(InvalidArgumentException $ex) {
return -1; return -1;
} }
}
if(($match = self::compareCategory($other->category)) !== 0) if(($match = self::compareCategory($other->category)) !== 0)
return $match; return $match;
@ -185,12 +194,16 @@ class MediaType implements Stringable, IComparable, IEquatable {
} }
public function equals(mixed $other): bool { public function equals(mixed $other): bool {
if(!($other instanceof MediaType)) if(!($other instanceof MediaType)) {
if(!is_scalar($other))
return false;
try { try {
$other = self::parse((string)$other); $other = self::parse((string)$other);
} catch(InvalidArgumentException $ex) { } catch(InvalidArgumentException $ex) {
return false; return false;
} }
}
if(!$this->matchCategory($other->category)) if(!$this->matchCategory($other->category))
return false; return false;

View file

@ -1,7 +1,7 @@
<?php <?php
// DnsEndPoint.php // DnsEndPoint.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Net; namespace Index\Net;
@ -55,6 +55,7 @@ class DnsEndPoint extends EndPoint {
return [$this->host, $this->port]; return [$this->host, $this->port];
} }
/** @param array{string,int} $serialized */
public function __unserialize(array $serialized): void { public function __unserialize(array $serialized): void {
$this->host = $serialized[0]; $this->host = $serialized[0];
$this->port = $serialized[1]; $this->port = $serialized[1];

View file

@ -1,7 +1,7 @@
<?php <?php
// IPAddress.php // IPAddress.php
// Created: 2021-04-26 // Created: 2021-04-26
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Net; namespace Index\Net;
@ -76,7 +76,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
* @return string Safe string IP address. * @return string Safe string IP address.
*/ */
public function getAddress(): string { 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. * @return string String IP address.
*/ */
public function getCleanAddress(): string { 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 { public function __toString(): string {
return inet_ntop($this->raw); return $this->getCleanAddress();
} }
public function equals(mixed $other): bool { public function equals(mixed $other): bool {
@ -129,13 +133,14 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
#[\Override] #[\Override]
public function jsonSerialize(): mixed { public function jsonSerialize(): mixed {
return inet_ntop($this->raw); return $this->getCleanAddress();
} }
public function __serialize(): array { public function __serialize(): array {
return [$this->raw]; return [$this->raw];
} }
/** @param array{string} $serialized */
public function __unserialize(array $serialized): void { public function __unserialize(array $serialized): void {
$this->raw = $serialized[0]; $this->raw = $serialized[0];
$this->width = strlen($this->raw); $this->width = strlen($this->raw);
@ -152,6 +157,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
$parsed = inet_pton($string); $parsed = inet_pton($string);
if($parsed === false) if($parsed === false)
throw new InvalidArgumentException('$string is not a valid IP address.'); throw new InvalidArgumentException('$string is not a valid IP address.');
return new static($parsed); return new static($parsed);
} }
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// IPAddressRange.php // IPAddressRange.php
// Created: 2021-04-26 // Created: 2021-04-26
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Net; 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 { public function __unserialize(array $serialized): void {
$this->base = new IPAddress($serialized['b']); $this->base = new IPAddress($serialized['b']);
$this->mask = $serialized['m']; $this->mask = $serialized['m'];

View file

@ -1,7 +1,7 @@
<?php <?php
// IPEndPoint.php // IPEndPoint.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Net; namespace Index\Net;
@ -56,6 +56,7 @@ class IPEndPoint extends EndPoint {
return [$this->address, $this->port]; return [$this->address, $this->port];
} }
/** @param array{IPAddress, int} $serialized */
public function __unserialize(array $serialized): void { public function __unserialize(array $serialized): void {
$this->address = $serialized[0]; $this->address = $serialized[0];
$this->port = $serialized[1]; $this->port = $serialized[1];

View file

@ -1,7 +1,7 @@
<?php <?php
// UnixEndPoint.php // UnixEndPoint.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Net; namespace Index\Net;
@ -29,6 +29,7 @@ class UnixEndPoint extends EndPoint {
return [$this->socketPath]; return [$this->socketPath];
} }
/** @param array{string} $serialized */
public function __unserialize(array $serialized): void { public function __unserialize(array $serialized): void {
$this->socketPath = $serialized[0]; $this->socketPath = $serialized[0];
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// Timings.php // Timings.php
// Created: 2022-02-16 // Created: 2022-02-16
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index\Performance; namespace Index\Performance;
@ -13,6 +13,8 @@ use InvalidArgumentException;
class Timings { class Timings {
private Stopwatch $sw; private Stopwatch $sw;
private int $lastLapTicks; private int $lastLapTicks;
/** @var TimingPoint[] */
private array $laps; private array $laps;
/** /**

View file

@ -1,7 +1,7 @@
<?php <?php
// WString.php // WString.php
// Created: 2021-06-22 // Created: 2021-06-22
// Updated: 2024-01-04 // Updated: 2024-08-03
namespace Index; 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 { 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); $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); $charsEncoding = $charsEncoding === null ? self::TRIM_CHARS_CHARSET : mb_preferred_mime_name($charsEncoding);
if($charsEncoding === false) $charsEncoding = 'utf-8';
// this fucks, i hate character sets // this fucks, i hate character sets
if($encoding !== $charsEncoding) { if($encoding !== $charsEncoding) {
$questionMarkCharsEnc = mb_convert_encoding('?', $charsEncoding, 'utf-8'); $questionMarkCharsEnc = mb_convert_encoding('?', $charsEncoding, 'utf-8');
if($questionMarkCharsEnc) $questionMarkCharsEnc = 'utf-8';
$questionMarkStrEnc = mb_convert_encoding('?', $encoding, 'utf-8'); $questionMarkStrEnc = mb_convert_encoding('?', $encoding, 'utf-8');
if($questionMarkStrEnc) $questionMarkStrEnc = 'utf-8';
$hasQuestionMark = mb_strpos($chars, $questionMarkCharsEnc, encoding: $charsEncoding) !== false; $hasQuestionMark = mb_strpos($chars, $questionMarkCharsEnc, encoding: $charsEncoding) !== false;
$chars = mb_convert_encoding($chars, $encoding, $charsEncoding); $chars = mb_convert_encoding($chars, $encoding, $charsEncoding);

View file

@ -1,7 +1,7 @@
<?php <?php
// XArray.php // XArray.php
// Created: 2022-02-02 // Created: 2022-02-02
// Updated: 2023-11-09 // Updated: 2024-08-03
namespace Index; namespace Index;
@ -39,6 +39,7 @@ final class XArray {
/** /**
* Retrieves the amount of items in a collection. * Retrieves the amount of items in a collection.
* *
* @param mixed[] $iterable
* @return int * @return int
*/ */
public static function count(iterable $iterable): int { public static function count(iterable $iterable): int {
@ -56,6 +57,7 @@ final class XArray {
/** /**
* Checks if a collection has no items. * Checks if a collection has no items.
* *
* @param mixed[] $iterable
* @return bool * @return bool
*/ */
public static function empty(iterable $iterable): bool { public static function empty(iterable $iterable): bool {
@ -73,7 +75,7 @@ final class XArray {
/** /**
* Checks if an item occurs in a collection * Checks if an item occurs in a collection
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param mixed $value * @param mixed $value
* @param bool $strict * @param bool $strict
* @return bool * @return bool
@ -98,12 +100,12 @@ final class XArray {
/** /**
* Checks if a key occurs in a collection. * Checks if a key occurs in a collection.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param mixed $key * @param mixed $key
* @return bool * @return bool
*/ */
public static function containsKey(iterable $iterable, mixed $key): 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); return array_key_exists($key, $iterable);
foreach($iterable as $k => $_) foreach($iterable as $k => $_)
@ -116,7 +118,7 @@ final class XArray {
/** /**
* Retrieves the first key in a collection. * Retrieves the first key in a collection.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @return int|string|null * @return int|string|null
*/ */
public static function firstKey(iterable $iterable): 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. * Retrieves the last key in a collection.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @return int|string|null * @return int|string|null
*/ */
public static function lastKey(iterable $iterable): 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. * Gets the index of a value in a collection.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param mixed $value * @param mixed $value
* @param bool $strict * @param bool $strict
* @return int|string|false * @return int|string|false
@ -177,9 +179,9 @@ final class XArray {
/** /**
* Extracts unique values from a collection. * Extracts unique values from a collection.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param int $type * @param int $type
* @return array * @return mixed[]
*/ */
public static function unique(iterable $iterable, int $type = self::UNIQUE_VALUE): array { public static function unique(iterable $iterable, int $type = self::UNIQUE_VALUE): array {
if(is_array($iterable)) if(is_array($iterable))
@ -197,8 +199,8 @@ final class XArray {
/** /**
* Takes values from a collection and discards the keys. * Takes values from a collection and discards the keys.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @return array * @return mixed[]
*/ */
public static function reflow(iterable $iterable): array { public static function reflow(iterable $iterable): array {
if(is_array($iterable)) if(is_array($iterable))
@ -215,8 +217,8 @@ final class XArray {
/** /**
* Puts a collection in reverse order. * Puts a collection in reverse order.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @return array * @return mixed[]
*/ */
public static function reverse(iterable $iterable): array { public static function reverse(iterable $iterable): array {
if(is_array($iterable)) if(is_array($iterable))
@ -233,9 +235,9 @@ final class XArray {
/** /**
* Merges two collections. * Merges two collections.
* *
* @param iterable $iterable1 * @param mixed[] $iterable1
* @param iterable $iterable2 * @param mixed[] $iterable2
* @return array * @return mixed[]
*/ */
public static function merge(iterable $iterable1, iterable $iterable2): array { public static function merge(iterable $iterable1, iterable $iterable2): array {
return array_merge(self::toArray($iterable1), self::toArray($iterable2)); return array_merge(self::toArray($iterable1), self::toArray($iterable2));
@ -244,9 +246,10 @@ final class XArray {
/** /**
* Sorts a collection according to a comparer. * Sorts a collection according to a comparer.
* *
* @param iterable $iterable * @template T
* @param T[] $iterable
* @param callable $comparer * @param callable $comparer
* @return array * @return T[]
*/ */
public static function sort(iterable $iterable, callable $comparer): array { public static function sort(iterable $iterable, callable $comparer): array {
if(is_array($iterable)) { if(is_array($iterable)) {
@ -267,10 +270,10 @@ final class XArray {
/** /**
* Takes a subsection of a collection. * Takes a subsection of a collection.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param int $offset * @param int $offset
* @param int|null $length * @param int|null $length
* @return array * @return mixed[]
*/ */
public static function slice(iterable $iterable, int $offset, int|null $length = null): array { public static function slice(iterable $iterable, int $offset, int|null $length = null): array {
if(is_array($iterable)) if(is_array($iterable))
@ -284,6 +287,12 @@ final class XArray {
return array_slice($items, $offset, $length); 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 { public static function toArray(iterable $iterable): array {
if(is_array($iterable)) if(is_array($iterable))
return $iterable; return $iterable;
@ -299,8 +308,8 @@ final class XArray {
/** /**
* Checks if any value in the collection matches a given predicate. * Checks if any value in the collection matches a given predicate.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param callable $predicate * @param callable(mixed): bool $predicate
* @return bool * @return bool
*/ */
public static function any(iterable $iterable, callable $predicate): 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. * Checks if all values in the collection match a given predicate.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param callable $predicate * @param callable(mixed): bool $predicate
* @return bool * @return bool
*/ */
public static function all(iterable $iterable, callable $predicate): 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. * Gets a subset of a collection based on a given predicate.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param callable $predicate * @param callable(mixed): bool $predicate
* @return array * @return mixed[]
*/ */
public static function where(iterable $iterable, callable $predicate): array { public static function where(iterable $iterable, callable $predicate): array {
$array = []; $array = [];
@ -346,8 +355,8 @@ final class XArray {
/** /**
* Gets the first item in a collection that matches a given predicate. * Gets the first item in a collection that matches a given predicate.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param callable|null $predicate * @param (callable(mixed): bool)|null $predicate
* @return mixed * @return mixed
*/ */
public static function first(iterable $iterable, ?callable $predicate = null): 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. * Gets the last item in a collection that matches a given predicate.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @param callable|null $predicate * @param (callable(mixed): bool)|null $predicate
* @return mixed * @return mixed
*/ */
public static function last(iterable $iterable, ?callable $predicate = null): mixed { public static function last(iterable $iterable, ?callable $predicate = null): mixed {
@ -406,9 +415,11 @@ final class XArray {
/** /**
* Applies a modifier on a collection. * Applies a modifier on a collection.
* *
* @param iterable $iterable * @template T1
* @param callable $selector * @template T2
* @return array * @param T1[] $iterable
* @param callable(T1): T2 $selector
* @return T2[]
*/ */
public static function select(iterable $iterable, callable $selector): array { public static function select(iterable $iterable, callable $selector): array {
if(is_array($iterable)) if(is_array($iterable))
@ -416,8 +427,8 @@ final class XArray {
$array = []; $array = [];
foreach($iterable as $key => $value) foreach($iterable as $value)
$array[] = $selector($value, $key); $array[] = $selector($value);
return $array; return $array;
} }
@ -425,7 +436,7 @@ final class XArray {
/** /**
* Tries to extract an instance of Iterator from any iterable type. * Tries to extract an instance of Iterator from any iterable type.
* *
* @param iterable $iterable * @param mixed[] $iterable
* @return Iterator * @return Iterator
*/ */
public static function extractIterator(iterable &$iterable): 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. * Checks if two collections are equal in both keys and values.
* *
* @param iterable $iterable1 * @param mixed[] $iterable1
* @param iterable $iterable2 * @param mixed[] $iterable2
* @return bool * @return bool
*/ */
public static function sequenceEquals(iterable $iterable1, iterable $iterable2): bool { public static function sequenceEquals(iterable $iterable1, iterable $iterable2): bool {
if(count($iterable1) !== count($iterable2))
return false;
$iterator1 = self::extractIterator($iterable1); $iterator1 = self::extractIterator($iterable1);
$iterator2 = self::extractIterator($iterable2); $iterator2 = self::extractIterator($iterable2);

View file

@ -1,7 +1,7 @@
<?php <?php
// XDateTime.php // XDateTime.php
// Created: 2024-07-31 // Created: 2024-07-31
// Updated: 2024-08-01 // Updated: 2024-08-03
namespace Index; namespace Index;
@ -15,6 +15,11 @@ use DateTimeZone;
final class XDateTime { final class XDateTime {
private const COMPARE_FORMAT = 'YmdHisu'; private const COMPARE_FORMAT = 'YmdHisu';
/**
* Returns the current date and time.
*
* @return DateTimeInterface Current date and time.
*/
public static function now(): DateTimeInterface { public static function now(): DateTimeInterface {
return new DateTimeImmutable('now'); return new DateTimeImmutable('now');
} }
@ -25,10 +30,19 @@ final class XDateTime {
if(is_string($dt)) if(is_string($dt))
$dt = strtotime($dt); $dt = strtotime($dt);
if($dt === false)
$dt = null;
return gmdate(self::COMPARE_FORMAT, $dt); 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( public static function compare(
DateTimeInterface|string|int $dt1, DateTimeInterface|string|int $dt1,
DateTimeInterface|string|int $dt2 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( public static function compareTimeZone(
DateTimeZone|string $tz1, DateTimeZone|string $tz1,
DateTimeZone|string $tz2 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 { public static function timeZoneListName(DateTimeZone $dtz): string {
$offset = $dtz->getOffset(self::now()); $offset = $dtz->getOffset(self::now());
return sprintf( 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 { public static function listTimeZones(bool $sort = false, int $group = DateTimeZone::ALL, string|null $countryCode = null): array {
$list = DateTimeZone::listIdentifiers($group, $countryCode); $list = DateTimeZone::listIdentifiers($group, $countryCode);
$list = XArray::select($list, fn($id) => new DateTimeZone($id)); $list = XArray::select($list, fn($id) => new DateTimeZone($id));
@ -78,6 +113,13 @@ final class XDateTime {
return $list; 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 { public static function format(DateTimeInterface|string|int|null $dt, string $format): string {
if($dt === null) { if($dt === null) {
$dt = time(); $dt = time();
@ -89,17 +131,38 @@ final class XDateTime {
$dt = strtotime($dt); $dt = strtotime($dt);
} }
if($dt === false)
$dt = null;
return gmdate($format, $dt); 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 { public static function toISO8601String(DateTimeInterface|string|int|null $dt): string {
return self::format($dt, DateTimeInterface::ATOM); 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 { public static function toCookieString(DateTimeInterface|string|int|null $dt): string {
return self::format($dt, DateTimeInterface::COOKIE); 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 { public static function toRFC822String(DateTimeInterface|string|int|null $dt): string {
return self::format($dt, DateTimeInterface::RFC822); return self::format($dt, DateTimeInterface::RFC822);
} }