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:
level: 5 # Raise this eventually
level: 9
paths:
- src

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,26 @@
<?php
// MemcachedProviderInfo.php
// Created: 2024-04-10
// Updated: 2024-04-10
// Updated: 2024-08-03
namespace Index\Cache\Memcached;
use InvalidArgumentException;
use Index\Cache\ICacheProviderInfo;
use Index\Net\EndPoint;
/**
* Represents Memcached provider info.
*/
class MemcachedProviderInfo implements ICacheProviderInfo {
/**
* @param array<int, array{EndPoint, int}> $endPoints Memcached end points.
* @param string $prefixKey Prefix to apply to keys.
* @param string $persistName Persistent connection name.
* @param bool $useBinaryProto Use the binary protocol.
* @param bool $enableCompression Use compression.
* @param bool $tcpNoDelay Disable Nagle's algorithm.
*/
public function __construct(
private array $endPoints,
private string $prefixKey,
@ -24,7 +33,7 @@ class MemcachedProviderInfo implements ICacheProviderInfo {
/**
* Gets server endpoints.
*
* @return array
* @return array<int, array{EndPoint, int}>
*/
public function getEndPoints(): array {
return $this->endPoints;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?php
// DbResultTrait.php
// Created: 2023-11-09
// Updated: 2024-02-06
// Updated: 2024-08-03
namespace Index\Data;
@ -10,27 +10,30 @@ namespace Index\Data;
*/
trait DbResultTrait {
public function getString(int|string $index): string {
return (string)$this->getValue($index);
$value = $this->getValue($index);
return is_scalar($value) ? (string)$value : '';
}
public function getStringOrNull(int|string $index): ?string {
return $this->isNull($index) ? null : (string)$this->getValue($index);
return $this->isNull($index) ? null : $this->getString($index);
}
public function getInteger(int|string $index): int {
return (int)$this->getValue($index);
$value = $this->getValue($index);
return is_scalar($value) ? (int)$value : 0;
}
public function getIntegerOrNull(int|string $index): ?int {
return $this->isNull($index) ? null : (int)$this->getValue($index);
return $this->isNull($index) ? null : $this->getInteger($index);
}
public function getFloat(int|string $index): float {
return (float)$this->getValue($index);
$value = $this->getValue($index);
return is_scalar($value) ? (float)$value : 0.0;
}
public function getFloatOrNull(int|string $index): ?float {
return $this->isNull($index) ? null : (float)$this->getValue($index);
return $this->isNull($index) ? null : $this->getFloat($index);
}
public function getBoolean(int|string $index): bool {
@ -38,7 +41,7 @@ trait DbResultTrait {
}
public function getBooleanOrNull(int|string $index): ?bool {
return $this->isNull($index) ? null : ($this->getInteger($index) !== 0);
return $this->isNull($index) ? null : $this->getBoolean($index);
}
public function getIterator(callable $construct): DbResultIterator {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,13 @@
<?php
// MariaDBConnection.php
// Created: 2021-04-30
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\Data\MariaDB;
use mysqli;
use mysqli_sql_exception;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\{IDbConnection,IDbTransactions};
@ -87,14 +88,18 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
// nothing suggests otherwise too.
// The output of mysqli_init is just an object anyway so we can safely use it instead
// but continue to use it as an object.
$this->connection = mysqli_init();
$connection = mysqli_init();
if($connection === false)
throw new RuntimeException('mysqli_init() returned false!');
$this->connection = $connection;
$this->connection->options(MYSQLI_OPT_LOCAL_INFILE, 0);
if($connectionInfo->hasCharacterSet())
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet());
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet() ?? '');
if($connectionInfo->hasInitCommand())
$this->connection->options(MYSQLI_INIT_COMMAND, $connectionInfo->getInitCommand());
$this->connection->options(MYSQLI_INIT_COMMAND, $connectionInfo->getInitCommand() ?? '');
$flags = $connectionInfo->shouldUseCompression() ? MYSQLI_CLIENT_COMPRESS : 0;
@ -242,7 +247,7 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
/**
* Gets a list of errors from the last command.
*
* @return array List of last errors.
* @return MariaDBWarning[] List of last errors.
*/
public function getLastErrors(): array {
return MariaDBWarning::fromLastErrors($this->connection->error_list);
@ -262,7 +267,7 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
*
* The result of SHOW WARNINGS;
*
* @return array List of warnings.
* @return MariaDBWarning[] List of warnings.
*/
public function getWarnings(): array {
return MariaDBWarning::fromGetWarnings($this->connection->get_warnings());
@ -370,8 +375,14 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
} catch(mysqli_sql_exception $ex) {
throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex);
}
if($result === false)
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode());
// the mysql library is very adorable
if($result === true)
throw new RuntimeException('Query succeeded but you should have use the execute method for that query.');
// Yes, this always uses Native, for some reason the stupid limitation in libmysql only applies to preparing
return new MariaDBResultNative($result);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?php
// FormContent.php
// Created: 2022-02-10
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\Http\Content;
@ -14,7 +14,7 @@ use Index\Http\HttpUploadedFile;
class FormContent implements IHttpContent {
/**
* @param array<string, mixed> $postFields Form fields.
* @param array<string, HttpUploadedFile> $uploadedFiles Uploaded files.
* @param array<string, HttpUploadedFile|array<string, mixed>> $uploadedFiles Uploaded files.
*/
public function __construct(
private array $postFields,
@ -26,7 +26,7 @@ class FormContent implements IHttpContent {
*
* @param string $name Name of the form field.
* @param int $filter A PHP filter extension filter constant.
* @param array|int $options Options for the PHP filter.
* @param array<string, mixed>|int $options Options for the PHP filter.
* @return mixed Value of the form field, null if not present.
*/
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
@ -84,6 +84,9 @@ class FormContent implements IHttpContent {
public function getUploadedFile(string $name): HttpUploadedFile {
if(!isset($this->uploadedFiles[$name]))
throw new RuntimeException('No file with name $name present.');
if(is_array($this->uploadedFiles[$name]))
throw new RuntimeException('Array based accessors are unsupported currently.');
return $this->uploadedFiles[$name];
}

View file

@ -1,7 +1,7 @@
<?php
// JsonContent.php
// Created: 2022-02-10
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\Http\Content;
@ -38,7 +38,11 @@ class JsonContent implements IHttpContent, JsonSerializable {
* @return string JSON encoded string.
*/
public function encode(): string {
return json_encode($this->content);
$encoded = json_encode($this->content);
if($encoded === false)
return '';
return $encoded;
}
public function __toString(): string {
@ -48,10 +52,10 @@ class JsonContent implements IHttpContent, JsonSerializable {
/**
* Creates an instance from encoded content.
*
* @param mixed $encoded JSON encoded content.
* @param string $encoded JSON encoded content.
* @return JsonContent Instance representing the provided content.
*/
public static function fromEncoded(mixed $encoded): JsonContent {
public static function fromEncoded(string $encoded): JsonContent {
return new JsonContent(json_decode($encoded));
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?php
// HttpMessageBuilder.php
// Created: 2022-02-08
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\Http;
@ -61,9 +61,9 @@ class HttpMessageBuilder {
* If a header with the same name is already present, it will be appended with a new line.
*
* @param string $name Name of the header to add.
* @param mixed $value Value to apply for this header.
* @param string $value Value to apply for this header.
*/
public function addHeader(string $name, mixed $value): void {
public function addHeader(string $name, string $value): void {
$this->headers->addHeader($name, $value);
}
@ -72,9 +72,9 @@ class HttpMessageBuilder {
* If a header with the same name is already present, it will be overwritten.
*
* @param string $name Name of the header to set.
* @param mixed $value Value to apply for this header.
* @param string $value Value to apply for this header.
*/
public function setHeader(string $name, mixed $value): void {
public function setHeader(string $name, string $value): void {
$this->headers->setHeader($name, $value);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?php
// HttpRouter.php
// Created: 2024-03-28
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\Http\Routing;
@ -15,11 +15,16 @@ use Index\Http\ErrorHandling\{HtmlErrorHandler,IErrorHandler,PlainErrorHandler};
class HttpRouter implements IRouter {
use RouterTrait;
/** @var object[] */
private array $middlewares = [];
/** @var array<string, array<string, callable>> */
private array $staticRoutes = [];
/** @var array<string, array<string, callable>> */
private array $dynamicRoutes = [];
/** @var IContentHandler[] */
private array $contentHandlers = [];
private IErrorHandler $errorHandler;
@ -46,8 +51,14 @@ class HttpRouter implements IRouter {
* @return string Normalised character set name.
*/
public function getCharSet(): string {
if($this->charSet === '')
return strtolower(mb_preferred_mime_name(mb_internal_encoding()));
if($this->charSet === '') {
$charSet = mb_preferred_mime_name(mb_internal_encoding());
if($charSet === false)
$charSet = 'UTF-8';
return strtolower($charSet);
}
return $this->charSet;
}
@ -196,19 +207,19 @@ class HttpRouter implements IRouter {
$middlewares = [];
foreach($this->middlewares as $mwInfo) {
if($mwInfo->dynamic) {
if(preg_match($mwInfo->match, $path, $args) !== 1)
if($mwInfo->dynamic ?? false) {
if(preg_match($mwInfo->match ?? '', $path, $args) !== 1)
continue;
array_shift($args);
} else {
if(!str_starts_with($path, $mwInfo->prefix))
if(!str_starts_with($path, $mwInfo->prefix ?? ''))
continue;
$args = [];
}
$middlewares[] = [$mwInfo->handler, $args];
$middlewares[] = [$mwInfo->handler ?? null, $args];
}
$methods = [];
@ -241,7 +252,7 @@ class HttpRouter implements IRouter {
* Dispatches a route based on a given HTTP request message with additional prefix arguments and output to stdout.
*
* @param ?HttpRequest $request HTTP request message to handle, null to use the current request.
* @param array $args Additional arguments to prepend to the argument list sent to the middleware and route handlers.
* @param mixed[] $args Additional arguments to prepend to the argument list sent to the middleware and route handlers.
*/
public function dispatch(?HttpRequest $request = null, array $args = []): void {
$request ??= HttpRequest::fromRequest();
@ -275,7 +286,7 @@ class HttpRouter implements IRouter {
break;
}
if(!$response->hasContent() && $result !== null) {
if(!$response->hasContent() && is_scalar($result)) {
$result = (string)$result;
$response->setContent(new StringContent($result));
@ -283,7 +294,10 @@ class HttpRouter implements IRouter {
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
$response->setTypeHTML($this->getCharSet());
else {
$charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result)));
$charset = mb_detect_encoding($result);
if($charset !== false)
$charset = mb_preferred_mime_name($charset);
$charset = $charset === false ? 'utf-8' : strtolower($charset);
if(strtolower(substr($result, 0, 5)) === '<?xml')
$response->setTypeXML($charset);

View file

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

View file

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

View file

@ -1,16 +1,22 @@
<?php
// MemoryStream.php
// Created: 2021-05-02
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\IO;
use RuntimeException;
/**
* Represents an in-memory stream.
*/
class MemoryStream extends GenericStream {
public function __construct() {
parent::__construct(fopen('php://memory', 'r+b'));
$stream = fopen('php://memory', 'r+b');
if($stream === false)
throw new RuntimeException('failed to fopen a memory stream');
parent::__construct($stream);
}
/**

View file

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

View file

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

View file

@ -1,10 +1,12 @@
<?php
// TempFileStream.php
// Created: 2021-05-02
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\IO;
use RuntimeException;
/**
* Represents a temporary file stream. Will remain in memory if the size is below a given threshold.
*/
@ -21,7 +23,11 @@ class TempFileStream extends GenericStream {
* @param int $maxMemory Maximum filesize until the file is moved out of memory and to disk.
*/
public function __construct(?string $body = null, int $maxMemory = self::MAX_MEMORY) {
parent::__construct(fopen("php://temp/maxmemory:{$maxMemory}", 'r+b'));
$stream = fopen("php://temp/maxmemory:{$maxMemory}", 'r+b');
if($stream === false)
throw new RuntimeException('failed to fopen a temporary file stream');
parent::__construct($stream);
if($body !== null) {
$this->write($body);

View file

@ -1,7 +1,7 @@
<?php
// MediaType.php
// Created: 2022-02-10
// Updated: 2024-08-02
// Updated: 2024-08-03
namespace Index;
@ -79,10 +79,10 @@ class MediaType implements Stringable, IComparable, IEquatable {
*
* @param string $name Media type parameter name.
* @param int $filter A PHP filter extension filter constant.
* @param array|int $options Options for the PHP filter.
* @param array<string, mixed>|int $options Options for the PHP filter.
* @return mixed Value of the parameter, null if not present.
*/
public function getParam(string $name, int $filter = FILTER_DEFAULT, $options = null): mixed {
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
if(!isset($this->params[$name]))
return null;
return filter_var($this->params[$name], $filter, $options);
@ -94,7 +94,8 @@ class MediaType implements Stringable, IComparable, IEquatable {
* @return string Character specified in media type.
*/
public function getCharset(): string {
return $this->getParam('charset', FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? 'utf-8';
$charset = $this->getParam('charset', FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
return is_string($charset) ? $charset : 'utf-8';
}
/**
@ -103,7 +104,11 @@ class MediaType implements Stringable, IComparable, IEquatable {
* @return float Quality specified for the media type.
*/
public function getQuality(): float {
return max(min(round($this->getParam('q', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION) ?? 1, 2), 1), 0);
$quality = $this->getParam('q', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
if(!is_float($quality) && !is_int($quality))
return 1;
return max(min(round($quality, 2), 1), 0);
}
/**
@ -169,12 +174,16 @@ class MediaType implements Stringable, IComparable, IEquatable {
}
public function compare(mixed $other): int {
if(!($other instanceof MediaType))
if(!($other instanceof MediaType)) {
if(!is_scalar($other))
return -1;
try {
$other = self::parse((string)$other);
} catch(InvalidArgumentException $ex) {
return -1;
}
}
if(($match = self::compareCategory($other->category)) !== 0)
return $match;
@ -185,12 +194,16 @@ class MediaType implements Stringable, IComparable, IEquatable {
}
public function equals(mixed $other): bool {
if(!($other instanceof MediaType))
if(!($other instanceof MediaType)) {
if(!is_scalar($other))
return false;
try {
$other = self::parse((string)$other);
} catch(InvalidArgumentException $ex) {
return false;
}
}
if(!$this->matchCategory($other->category))
return false;

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?php
// IPAddressRange.php
// Created: 2021-04-26
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index\Net;
@ -109,6 +109,7 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
];
}
/** @param array{b: string, m: int} $serialized */
public function __unserialize(array $serialized): void {
$this->base = new IPAddress($serialized['b']);
$this->mask = $serialized['m'];

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<?php
// WString.php
// Created: 2021-06-22
// Updated: 2024-01-04
// Updated: 2024-08-03
namespace Index;
@ -55,12 +55,16 @@ final class WString {
private static function trimInternal(Stringable|string $string, string $chars, ?string $encoding, ?string $charsEncoding, int $flags): string {
$encoding = $encoding === null ? mb_internal_encoding() : mb_preferred_mime_name($encoding);
if($encoding === false) $encoding = 'utf-8';
$charsEncoding = $charsEncoding === null ? self::TRIM_CHARS_CHARSET : mb_preferred_mime_name($charsEncoding);
if($charsEncoding === false) $charsEncoding = 'utf-8';
// this fucks, i hate character sets
if($encoding !== $charsEncoding) {
$questionMarkCharsEnc = mb_convert_encoding('?', $charsEncoding, 'utf-8');
if($questionMarkCharsEnc) $questionMarkCharsEnc = 'utf-8';
$questionMarkStrEnc = mb_convert_encoding('?', $encoding, 'utf-8');
if($questionMarkStrEnc) $questionMarkStrEnc = 'utf-8';
$hasQuestionMark = mb_strpos($chars, $questionMarkCharsEnc, encoding: $charsEncoding) !== false;
$chars = mb_convert_encoding($chars, $encoding, $charsEncoding);

View file

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

View file

@ -1,7 +1,7 @@
<?php
// XDateTime.php
// Created: 2024-07-31
// Updated: 2024-08-01
// Updated: 2024-08-03
namespace Index;
@ -15,6 +15,11 @@ use DateTimeZone;
final class XDateTime {
private const COMPARE_FORMAT = 'YmdHisu';
/**
* Returns the current date and time.
*
* @return DateTimeInterface Current date and time.
*/
public static function now(): DateTimeInterface {
return new DateTimeImmutable('now');
}
@ -25,10 +30,19 @@ final class XDateTime {
if(is_string($dt))
$dt = strtotime($dt);
if($dt === false)
$dt = null;
return gmdate(self::COMPARE_FORMAT, $dt);
}
/**
* Compares two timestamps.
*
* @param DateTimeInterface|string|int $dt1 Base timestamp.
* @param DateTimeInterface|string|int $dt2 Timestamp to compare against.
* @return int 0 if they are equal, less than 0 if $dt1 is less than $dt2, greater than 0 if $dt1 is greater than $dt2.
*/
public static function compare(
DateTimeInterface|string|int $dt1,
DateTimeInterface|string|int $dt2
@ -39,6 +53,13 @@ final class XDateTime {
);
}
/**
* Compares two timezones for sorting.
*
* @param DateTimeZone|string $tz1 Base time zone.
* @param DateTimeZone|string $tz2 Time zone to compare against.
* @return int 0 if they are equal, less than 0 if $tz1 is less than $tz2, greater than 0 if $tz1 is greater than $tz2.
*/
public static function compareTimeZone(
DateTimeZone|string $tz1,
DateTimeZone|string $tz2
@ -58,6 +79,12 @@ final class XDateTime {
);
}
/**
* Formats a time zone as a string for a list.
*
* @param DateTimeZone $dtz Time zone to format.
* @return string Formatted time zone string.
*/
public static function timeZoneListName(DateTimeZone $dtz): string {
$offset = $dtz->getOffset(self::now());
return sprintf(
@ -68,6 +95,14 @@ final class XDateTime {
);
}
/**
* Generates a list of time zones.
*
* @param bool $sort true if the list should be sorted.
* @param int $group Group of time zones to fetch.
* @param ?string $countryCode Country code of country of which to fetch time zones.
* @return DateTimeZone[] Collection of DateTimeZone instances.
*/
public static function listTimeZones(bool $sort = false, int $group = DateTimeZone::ALL, string|null $countryCode = null): array {
$list = DateTimeZone::listIdentifiers($group, $countryCode);
$list = XArray::select($list, fn($id) => new DateTimeZone($id));
@ -78,6 +113,13 @@ final class XDateTime {
return $list;
}
/**
* Formats a date and time as a string.
*
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
* @param string $format DateTimeInterface::format date formatting string.
* @return string Provided date, formatted as requested.
*/
public static function format(DateTimeInterface|string|int|null $dt, string $format): string {
if($dt === null) {
$dt = time();
@ -89,17 +131,38 @@ final class XDateTime {
$dt = strtotime($dt);
}
if($dt === false)
$dt = null;
return gmdate($format, $dt);
}
/**
* Formats a date and time as an ISO-8601 string.
*
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
* @return string ISO-8601 date time string.
*/
public static function toISO8601String(DateTimeInterface|string|int|null $dt): string {
return self::format($dt, DateTimeInterface::ATOM);
}
/**
* Formats a date and time as a Cookie date/time string.
*
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
* @return string Cookie date time string.
*/
public static function toCookieString(DateTimeInterface|string|int|null $dt): string {
return self::format($dt, DateTimeInterface::COOKIE);
}
/**
* Formats a date and time as an RFC 822 date/time string.
*
* @param DateTimeInterface|string|int|null $dt Date time to format, null for now.
* @return string RFC 822 date time string.
*/
public static function toRFC822String(DateTimeInterface|string|int|null $dt): string {
return self::format($dt, DateTimeInterface::RFC822);
}