From 6e3e86f4166e02df8e7a42a446a1caed8a2cd0ce Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 3 Aug 2024 20:27:50 +0000 Subject: [PATCH] Raised PHPStan analysis level and fixed issues. --- VERSION | 2 +- phpstan.neon | 2 +- src/ArrayIterator.php | 6 +- src/CSRFP.php | 7 +- src/Cache/ArrayCache/ArrayCacheBackend.php | 4 +- src/Cache/ArrayCache/ArrayCacheProvider.php | 3 +- src/Cache/CacheTools.php | 6 +- src/Cache/ICacheBackend.php | 4 +- src/Cache/Memcached/MemcachedBackend.php | 12 +-- src/Cache/Memcached/MemcachedProviderInfo.php | 13 ++- .../Memcached/MemcachedProviderLegacy.php | 4 +- .../Memcached/MemcachedProviderModern.php | 4 +- src/Cache/Valkey/ValkeyBackend.php | 12 +-- src/Colour/Colour.php | 63 +++++++------ src/Data/DbResultIterator.php | 4 +- src/Data/DbResultTrait.php | 19 ++-- src/Data/DbStatementCache.php | 3 +- src/Data/DbTools.php | 8 +- src/Data/IDbBackend.php | 4 +- src/Data/MariaDB/MariaDBBackend.php | 33 +++++-- src/Data/MariaDB/MariaDBCharacterSetInfo.php | 22 ++--- src/Data/MariaDB/MariaDBConnection.php | 23 +++-- src/Data/MariaDB/MariaDBResult.php | 10 +- src/Data/MariaDB/MariaDBResultLib.php | 11 ++- src/Data/MariaDB/MariaDBResultNative.php | 6 +- src/Data/MariaDB/MariaDBStatement.php | 11 ++- src/Data/MariaDB/MariaDBWarning.php | 8 +- src/Data/Migration/DbMigrationManager.php | 4 +- src/Data/Migration/FsDbMigrationInfo.php | 7 +- src/Data/Migration/FsDbMigrationRepo.php | 8 +- src/Data/NullDb/NullDbBackend.php | 4 +- src/Data/SQLite/SQLiteBackend.php | 10 +- src/Data/SQLite/SQLiteConnection.php | 5 +- src/Data/SQLite/SQLiteResult.php | 10 +- src/Http/Content/FormContent.php | 9 +- src/Http/Content/JsonContent.php | 12 ++- src/Http/Content/StringContent.php | 8 +- .../ContentHandling/StreamContentHandler.php | 3 +- src/Http/ErrorHandling/HtmlErrorHandler.php | 9 +- src/Http/HttpHeader.php | 3 +- src/Http/HttpHeadersBuilder.php | 13 +-- src/Http/HttpMessageBuilder.php | 10 +- src/Http/HttpRequest.php | 7 +- src/Http/HttpRequestBuilder.php | 10 +- src/Http/HttpResponseBuilder.php | 10 +- src/Http/HttpUploadedFile.php | 56 +++++++---- src/Http/Routing/HttpRouter.php | 34 +++++-- src/Http/Routing/ResolvedRouteInfo.php | 19 ++-- src/IO/GenericStream.php | 53 +++++++++-- src/IO/MemoryStream.php | 10 +- src/IO/ProcessStream.php | 19 +++- src/IO/Stream.php | 11 ++- src/IO/TempFileStream.php | 10 +- src/MediaType.php | 27 ++++-- src/Net/DnsEndPoint.php | 3 +- src/Net/IPAddress.php | 16 +++- src/Net/IPAddressRange.php | 3 +- src/Net/IPEndPoint.php | 3 +- src/Net/UnixEndPoint.php | 3 +- src/Performance/Timings.php | 4 +- src/WString.php | 6 +- src/XArray.php | 92 ++++++++++--------- src/XDateTime.php | 65 ++++++++++++- 63 files changed, 600 insertions(+), 280 deletions(-) diff --git a/VERSION b/VERSION index aea044f..5d42e00 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2408.20020 +0.2408.32027 diff --git a/phpstan.neon b/phpstan.neon index 0b360bb..ffdbd91 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,4 @@ parameters: - level: 5 # Raise this eventually + level: 9 paths: - src diff --git a/src/ArrayIterator.php b/src/ArrayIterator.php index 6f4870d..5845349 100644 --- a/src/ArrayIterator.php +++ b/src/ArrayIterator.php @@ -1,7 +1,7 @@ */ class ArrayIterator implements Iterator { private bool $wasValid = true; /** - * @param array $array Array to iterate upon. + * @param array $array Array to iterate upon. */ public function __construct( private array $array diff --git a/src/CSRFP.php b/src/CSRFP.php index f387bd6..37b4cfd 100644 --- a/src/CSRFP.php +++ b/src/CSRFP.php @@ -1,7 +1,7 @@ $dsn DSN with provider information. * @return ArrayCacheProviderInfo Dummy provider info instance. */ public function parseDsn(string|array $dsn): ICacheProviderInfo { diff --git a/src/Cache/ArrayCache/ArrayCacheProvider.php b/src/Cache/ArrayCache/ArrayCacheProvider.php index ce15022..e66d1a8 100644 --- a/src/Cache/ArrayCache/ArrayCacheProvider.php +++ b/src/Cache/ArrayCache/ArrayCacheProvider.php @@ -1,7 +1,7 @@ */ private array $items = []; public function get(string $key): mixed { diff --git a/src/Cache/CacheTools.php b/src/Cache/CacheTools.php index b403d35..383a414 100644 --- a/src/Cache/CacheTools.php +++ b/src/Cache/CacheTools.php @@ -1,7 +1,7 @@ Valkey\ValkeyBackend::class, ]; + /** @return array */ 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 $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; diff --git a/src/Cache/ICacheBackend.php b/src/Cache/ICacheBackend.php index ed775b6..51e9300 100644 --- a/src/Cache/ICacheBackend.php +++ b/src/Cache/ICacheBackend.php @@ -1,7 +1,7 @@ $dsn DSN with provider information. * @return ICacheProviderInfo Provider info based on the dsn. */ function parseDsn(string|array $dsn): ICacheProviderInfo; diff --git a/src/Cache/Memcached/MemcachedBackend.php b/src/Cache/Memcached/MemcachedBackend.php index 27cbd4b..5dde3c2 100644 --- a/src/Cache/Memcached/MemcachedBackend.php +++ b/src/Cache/Memcached/MemcachedBackend.php @@ -1,7 +1,7 @@ $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'])) diff --git a/src/Cache/Memcached/MemcachedProviderInfo.php b/src/Cache/Memcached/MemcachedProviderInfo.php index 47d027d..4cac3a3 100644 --- a/src/Cache/Memcached/MemcachedProviderInfo.php +++ b/src/Cache/Memcached/MemcachedProviderInfo.php @@ -1,17 +1,26 @@ $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 */ public function getEndPoints(): array { return $this->endPoints; diff --git a/src/Cache/Memcached/MemcachedProviderLegacy.php b/src/Cache/Memcached/MemcachedProviderLegacy.php index 5c8cfdb..3ce97fc 100644 --- a/src/Cache/Memcached/MemcachedProviderLegacy.php +++ b/src/Cache/Memcached/MemcachedProviderLegacy.php @@ -1,7 +1,7 @@ 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 { diff --git a/src/Cache/Memcached/MemcachedProviderModern.php b/src/Cache/Memcached/MemcachedProviderModern.php index 05a0f34..bcb6e10 100644 --- a/src/Cache/Memcached/MemcachedProviderModern.php +++ b/src/Cache/Memcached/MemcachedProviderModern.php @@ -1,7 +1,7 @@ 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()); diff --git a/src/Cache/Valkey/ValkeyBackend.php b/src/Cache/Valkey/ValkeyBackend.php index 8fcd177..02cfb23 100644 --- a/src/Cache/Valkey/ValkeyBackend.php +++ b/src/Cache/Valkey/ValkeyBackend.php @@ -1,7 +1,7 @@ `: Allows you to select a different database. * - `persist`: Uses a persistent connection. * - * @param string|array $dsn DSN with provider information. + * @param string|array $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); } } diff --git a/src/Colour/Colour.php b/src/Colour/Colour.php index 37a78a8..1cd2059 100644 --- a/src/Colour/Colour.php +++ b/src/Colour/Colour.php @@ -1,7 +1,7 @@ 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; diff --git a/src/Data/DbResultIterator.php b/src/Data/DbResultIterator.php index a212bac..eb2bb49 100644 --- a/src/Data/DbResultIterator.php +++ b/src/Data/DbResultIterator.php @@ -1,7 +1,7 @@ */ class DbResultIterator implements Iterator { private bool $wasValid; diff --git a/src/Data/DbResultTrait.php b/src/Data/DbResultTrait.php index 966e2cc..7c62d03 100644 --- a/src/Data/DbResultTrait.php +++ b/src/Data/DbResultTrait.php @@ -1,7 +1,7 @@ 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 { diff --git a/src/Data/DbStatementCache.php b/src/Data/DbStatementCache.php index 173190d..4908255 100644 --- a/src/Data/DbStatementCache.php +++ b/src/Data/DbStatementCache.php @@ -1,7 +1,7 @@ */ private array $stmts = []; /** diff --git a/src/Data/DbTools.php b/src/Data/DbTools.php index a13e19f..710196d 100644 --- a/src/Data/DbTools.php +++ b/src/Data/DbTools.php @@ -1,7 +1,7 @@ SQLite\SQLiteBackend::class, ]; + /** @return array */ 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 $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. diff --git a/src/Data/IDbBackend.php b/src/Data/IDbBackend.php index 559e7ae..4f1a1f6 100644 --- a/src/Data/IDbBackend.php +++ b/src/Data/IDbBackend.php @@ -1,7 +1,7 @@ $dsn DSN with connection information. * @return IDbConnectionInfo Connection info based on the dsn. */ function parseDsn(string|array $dsn): IDbConnectionInfo; diff --git a/src/Data/MariaDB/MariaDBBackend.php b/src/Data/MariaDB/MariaDBBackend.php index 2e8c305..00b91ff 100644 --- a/src/Data/MariaDB/MariaDBBackend.php +++ b/src/Data/MariaDB/MariaDBBackend.php @@ -1,7 +1,7 @@ `: 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 $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, diff --git a/src/Data/MariaDB/MariaDBCharacterSetInfo.php b/src/Data/MariaDB/MariaDBCharacterSetInfo.php index 369e4f3..1a24448 100644 --- a/src/Data/MariaDB/MariaDBCharacterSetInfo.php +++ b/src/Data/MariaDB/MariaDBCharacterSetInfo.php @@ -1,12 +1,10 @@ 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; } } diff --git a/src/Data/MariaDB/MariaDBConnection.php b/src/Data/MariaDB/MariaDBConnection.php index 43354e3..378b841 100644 --- a/src/Data/MariaDB/MariaDBConnection.php +++ b/src/Data/MariaDB/MariaDBConnection.php @@ -1,12 +1,13 @@ 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); } diff --git a/src/Data/MariaDB/MariaDBResult.php b/src/Data/MariaDB/MariaDBResult.php index 99abc9f..9faf1ed 100644 --- a/src/Data/MariaDB/MariaDBResult.php +++ b/src/Data/MariaDB/MariaDBResult.php @@ -1,7 +1,7 @@ */ 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; diff --git a/src/Data/MariaDB/MariaDBResultLib.php b/src/Data/MariaDB/MariaDBResultLib.php index 0ad9d65..8af124a 100644 --- a/src/Data/MariaDB/MariaDBResultLib.php +++ b/src/Data/MariaDB/MariaDBResultLib.php @@ -1,7 +1,7 @@ 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); diff --git a/src/Data/MariaDB/MariaDBResultNative.php b/src/Data/MariaDB/MariaDBResultNative.php index 86cc51d..469ec93 100644 --- a/src/Data/MariaDB/MariaDBResultNative.php +++ b/src/Data/MariaDB/MariaDBResultNative.php @@ -1,7 +1,7 @@ result instanceof mysqli_result)) + return false; + $result = $this->result->fetch_array(MYSQLI_BOTH); if($result === null) return false; + $this->currentRow = $result; return true; } diff --git a/src/Data/MariaDB/MariaDBStatement.php b/src/Data/MariaDB/MariaDBStatement.php index 7499b1c..a9db378 100644 --- a/src/Data/MariaDB/MariaDBStatement.php +++ b/src/Data/MariaDB/MariaDBStatement.php @@ -1,7 +1,7 @@ */ private array $params = []; /** @@ -28,6 +29,8 @@ class MariaDBStatement implements IDbStatement { ) {} private static bool $constructed = false; + + /** @var class-string */ 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; diff --git a/src/Data/MariaDB/MariaDBWarning.php b/src/Data/MariaDB/MariaDBWarning.php index a7def43..346c4d1 100644 --- a/src/Data/MariaDB/MariaDBWarning.php +++ b/src/Data/MariaDB/MariaDBWarning.php @@ -1,7 +1,7 @@ $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 = []; diff --git a/src/Data/Migration/DbMigrationManager.php b/src/Data/Migration/DbMigrationManager.php index ead87d4..d03fcfe 100644 --- a/src/Data/Migration/DbMigrationManager.php +++ b/src/Data/Migration/DbMigrationManager.php @@ -1,7 +1,7 @@ getMigrations(); diff --git a/src/Data/Migration/FsDbMigrationInfo.php b/src/Data/Migration/FsDbMigrationInfo.php index 70219e0..3504099 100644 --- a/src/Data/Migration/FsDbMigrationInfo.php +++ b/src/Data/Migration/FsDbMigrationInfo.php @@ -1,7 +1,7 @@ path; - (new $this->className)->migrate($conn); + + $migration = new $this->className; + if($migration instanceof IDbMigration) + $migration->migrate($conn); } } diff --git a/src/Data/Migration/FsDbMigrationRepo.php b/src/Data/Migration/FsDbMigrationRepo.php index 4e792aa..2b23550 100644 --- a/src/Data/Migration/FsDbMigrationRepo.php +++ b/src/Data/Migration/FsDbMigrationRepo.php @@ -1,10 +1,12 @@ path)) return []; + $files = glob(realpath($this->path) . '/*.php'); + if($files === false) + throw new RuntimeException('Failed to glob migration files.'); + $migrations = []; foreach($files as $file) diff --git a/src/Data/NullDb/NullDbBackend.php b/src/Data/NullDb/NullDbBackend.php index eaf0b49..d6a4a8b 100644 --- a/src/Data/NullDb/NullDbBackend.php +++ b/src/Data/NullDb/NullDbBackend.php @@ -1,7 +1,7 @@ $dsn DSN with connection information. * @return NullDbConnectionInfo Dummy connection info instance. */ public function parseDsn(string|array $dsn): IDbConnectionInfo { diff --git a/src/Data/SQLite/SQLiteBackend.php b/src/Data/SQLite/SQLiteBackend.php index 788d00c..2bdd3b4 100644 --- a/src/Data/SQLite/SQLiteBackend.php +++ b/src/Data/SQLite/SQLiteBackend.php @@ -1,7 +1,7 @@ $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']); diff --git a/src/Data/SQLite/SQLiteConnection.php b/src/Data/SQLite/SQLiteConnection.php index 3cb0293..976e91f 100644 --- a/src/Data/SQLite/SQLiteConnection.php +++ b/src/Data/SQLite/SQLiteConnection.php @@ -1,7 +1,7 @@ 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 { diff --git a/src/Data/SQLite/SQLiteResult.php b/src/Data/SQLite/SQLiteResult.php index 315764a..c727ebb 100644 --- a/src/Data/SQLite/SQLiteResult.php +++ b/src/Data/SQLite/SQLiteResult.php @@ -1,7 +1,7 @@ */ 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 {} diff --git a/src/Http/Content/FormContent.php b/src/Http/Content/FormContent.php index ce8e906..c9006b6 100644 --- a/src/Http/Content/FormContent.php +++ b/src/Http/Content/FormContent.php @@ -1,7 +1,7 @@ $postFields Form fields. - * @param array $uploadedFiles Uploaded files. + * @param array> $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|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]; } diff --git a/src/Http/Content/JsonContent.php b/src/Http/Content/JsonContent.php index 6e0db07..e4e3ef2 100644 --- a/src/Http/Content/JsonContent.php +++ b/src/Http/Content/JsonContent.php @@ -1,7 +1,7 @@ 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)); } diff --git a/src/Http/Content/StringContent.php b/src/Http/Content/StringContent.php index 24cba80..9997a6c 100644 --- a/src/Http/Content/StringContent.php +++ b/src/Http/Content/StringContent.php @@ -1,7 +1,7 @@ hasContentType()) $response->setTypeStream(); diff --git a/src/Http/ErrorHandling/HtmlErrorHandler.php b/src/Http/ErrorHandling/HtmlErrorHandler.php index 97d6279..94b8892 100644 --- a/src/Http/ErrorHandling/HtmlErrorHandler.php +++ b/src/Http/ErrorHandling/HtmlErrorHandler.php @@ -1,7 +1,7 @@ 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, ])); diff --git a/src/Http/HttpHeader.php b/src/Http/HttpHeader.php index 74357a4..1b905f2 100644 --- a/src/Http/HttpHeader.php +++ b/src/Http/HttpHeader.php @@ -1,7 +1,7 @@ */ 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); } diff --git a/src/Http/HttpMessageBuilder.php b/src/Http/HttpMessageBuilder.php index 63c1911..6cebcb0 100644 --- a/src/Http/HttpMessageBuilder.php +++ b/src/Http/HttpMessageBuilder.php @@ -1,7 +1,7 @@ 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); } diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index 4c1633a..23474f7 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -1,7 +1,7 @@ |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 */ private static function getRawRequestHeaders(): array { if(function_exists('getallheaders')) return getallheaders(); diff --git a/src/Http/HttpRequestBuilder.php b/src/Http/HttpRequestBuilder.php index 0fcc4e8..3974870 100644 --- a/src/Http/HttpRequestBuilder.php +++ b/src/Http/HttpRequestBuilder.php @@ -1,7 +1,7 @@ */ private array $params = []; + + /** @var array */ 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; } diff --git a/src/Http/HttpResponseBuilder.php b/src/Http/HttpResponseBuilder.php index 266effb..cba1f1e 100644 --- a/src/Http/HttpResponseBuilder.php +++ b/src/Http/HttpResponseBuilder.php @@ -1,7 +1,7 @@ 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 $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 Uploaded files. + * @param array $files Value of a $_FILES superglobal. + * @return array> 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 $files + * @return array + */ private static function traverseFILES(array $files, string $keyName): array { $arr = []; @@ -200,11 +209,15 @@ class HttpUploadedFile implements ICloseable { return $arr; } + /** + * @param array $files + * @return array + */ 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 $files + * @return array> + */ 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; diff --git a/src/Http/Routing/HttpRouter.php b/src/Http/Routing/HttpRouter.php index 834eda5..19f6c98 100644 --- a/src/Http/Routing/HttpRouter.php +++ b/src/Http/Routing/HttpRouter.php @@ -1,7 +1,7 @@ > */ private array $staticRoutes = []; + + /** @var array> */ 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)) === '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)) === 'setTypeXML($charset); diff --git a/src/Http/Routing/ResolvedRouteInfo.php b/src/Http/Routing/ResolvedRouteInfo.php index d74509d..8398359 100644 --- a/src/Http/Routing/ResolvedRouteInfo.php +++ b/src/Http/Routing/ResolvedRouteInfo.php @@ -1,7 +1,7 @@ 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)); } } diff --git a/src/IO/GenericStream.php b/src/IO/GenericStream.php index 6368d92..5716a2f 100644 --- a/src/IO/GenericStream.php +++ b/src/IO/GenericStream.php @@ -1,11 +1,12 @@ 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 */ 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 */ 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; } } diff --git a/src/IO/MemoryStream.php b/src/IO/MemoryStream.php index 1ebb217..eb1f75a 100644 --- a/src/IO/MemoryStream.php +++ b/src/IO/MemoryStream.php @@ -1,16 +1,22 @@ 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; - } } } diff --git a/src/IO/Stream.php b/src/IO/Stream.php index f39591a..6766bb1 100644 --- a/src/IO/Stream.php +++ b/src/IO/Stream.php @@ -1,7 +1,7 @@ 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); + } } /** diff --git a/src/IO/TempFileStream.php b/src/IO/TempFileStream.php index 5ab89da..c7137e0 100644 --- a/src/IO/TempFileStream.php +++ b/src/IO/TempFileStream.php @@ -1,10 +1,12 @@ write($body); diff --git a/src/MediaType.php b/src/MediaType.php index b04a708..cd5a170 100644 --- a/src/MediaType.php +++ b/src/MediaType.php @@ -1,7 +1,7 @@ |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; diff --git a/src/Net/DnsEndPoint.php b/src/Net/DnsEndPoint.php index c544c0a..dcae019 100644 --- a/src/Net/DnsEndPoint.php +++ b/src/Net/DnsEndPoint.php @@ -1,7 +1,7 @@ host, $this->port]; } + /** @param array{string,int} $serialized */ public function __unserialize(array $serialized): void { $this->host = $serialized[0]; $this->port = $serialized[1]; diff --git a/src/Net/IPAddress.php b/src/Net/IPAddress.php index fc554c5..91ff91a 100644 --- a/src/Net/IPAddress.php +++ b/src/Net/IPAddress.php @@ -1,7 +1,7 @@ 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); } } diff --git a/src/Net/IPAddressRange.php b/src/Net/IPAddressRange.php index 04b2b33..8d8569d 100644 --- a/src/Net/IPAddressRange.php +++ b/src/Net/IPAddressRange.php @@ -1,7 +1,7 @@ base = new IPAddress($serialized['b']); $this->mask = $serialized['m']; diff --git a/src/Net/IPEndPoint.php b/src/Net/IPEndPoint.php index 04c5055..c745296 100644 --- a/src/Net/IPEndPoint.php +++ b/src/Net/IPEndPoint.php @@ -1,7 +1,7 @@ address, $this->port]; } + /** @param array{IPAddress, int} $serialized */ public function __unserialize(array $serialized): void { $this->address = $serialized[0]; $this->port = $serialized[1]; diff --git a/src/Net/UnixEndPoint.php b/src/Net/UnixEndPoint.php index 463e701..15f1a03 100644 --- a/src/Net/UnixEndPoint.php +++ b/src/Net/UnixEndPoint.php @@ -1,7 +1,7 @@ socketPath]; } + /** @param array{string} $serialized */ public function __unserialize(array $serialized): void { $this->socketPath = $serialized[0]; } diff --git a/src/Performance/Timings.php b/src/Performance/Timings.php index 5382f1f..b6b61ed 100644 --- a/src/Performance/Timings.php +++ b/src/Performance/Timings.php @@ -1,7 +1,7 @@ $_) @@ -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); diff --git a/src/XDateTime.php b/src/XDateTime.php index 0cc3252..dde5366 100644 --- a/src/XDateTime.php +++ b/src/XDateTime.php @@ -1,7 +1,7 @@ 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); }