update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-18 20:32:18 +07:00
parent 4554035227
commit dcaf267458
3359 changed files with 153185 additions and 205489 deletions

View File

@@ -20,7 +20,9 @@
"require-dev": {
"nette/tester": "^2.5",
"tracy/tracy": "^2.9",
"phpstan/phpstan-nette": "^2.0@stable",
"phpstan/phpstan": "^2.1@stable",
"phpstan/extension-installer": "^1.4@stable",
"nette/phpstan-rules": "^1.0",
"jetbrains/phpstorm-attributes": "^1.2"
},
"conflict": {
@@ -50,5 +52,10 @@
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
}
}

View File

@@ -10,10 +10,13 @@
namespace Nette;
/**
* Represents object convertible to HTML string.
*/
interface HtmlStringable
{
/**
* Returns string in HTML format
* Returns string in HTML format.
*/
function __toString(): string;
}

View File

@@ -13,16 +13,19 @@
/**
* Smarter caching iterator.
* Enhanced caching iterator with first/last/counter tracking.
*
* @template TKey
* @template TValue
* @extends \CachingIterator<TKey, TValue, \Iterator<TKey, TValue>>
* @property-read bool $first
* @property-read bool $last
* @property-read bool $empty
* @property-read bool $odd
* @property-read bool $even
* @property-read int $counter
* @property-read mixed $nextKey
* @property-read mixed $nextValue
* @property-read TKey $nextKey
* @property-read TValue $nextValue
*/
class CachingIterator extends \CachingIterator implements \Countable
{
@@ -31,6 +34,7 @@ class CachingIterator extends \CachingIterator implements \Countable
private int $counter = 0;
/** @param iterable<TKey, TValue>|\stdClass $iterable */
public function __construct(iterable|\stdClass $iterable)
{
$iterable = $iterable instanceof \stdClass
@@ -58,45 +62,30 @@ public function isLast(?int $gridWidth = null): bool
}
/**
* Is the iterator empty?
*/
public function isEmpty(): bool
{
return $this->counter === 0;
}
/**
* Is the counter odd?
*/
public function isOdd(): bool
{
return $this->counter % 2 === 1;
}
/**
* Is the counter even?
*/
public function isEven(): bool
{
return $this->counter % 2 === 0;
}
/**
* Returns the counter.
*/
public function getCounter(): int
{
return $this->counter;
}
/**
* Returns the count of elements.
*/
public function count(): int
{
$inner = $this->getInnerIterator();
@@ -131,18 +120,14 @@ public function rewind(): void
}
/**
* Returns the next key.
*/
/** @return TKey */
public function getNextKey(): mixed
{
return $this->getInnerIterator()->key();
}
/**
* Returns the next element.
*/
/** @return TValue */
public function getNextValue(): mixed
{
return $this->getInnerIterator()->current();

View File

@@ -15,14 +15,13 @@
*/
class Mapper extends \IteratorIterator
{
/** @var callable */
private $callback;
private \Closure $callback;
public function __construct(\Traversable $iterator, callable $callback)
{
parent::__construct($iterator);
$this->callback = $callback;
$this->callback = $callback(...);
}

View File

@@ -22,6 +22,7 @@
trait SmartObject
{
/**
* @param mixed[] $args
* @return mixed
* @throws MemberAccessException
*/
@@ -47,6 +48,8 @@ public function __call(string $name, array $args)
/**
* @param mixed[] $args
* @return never
* @throws MemberAccessException
*/
public static function __callStatic(string $name, array $args)

View File

@@ -11,7 +11,7 @@
/**
* Static class.
* Prevents instantiation.
*/
trait StaticClass
{

View File

@@ -11,7 +11,7 @@
/**
* Translator adapter.
* Translation provider.
*/
interface Translator
{

View File

@@ -14,7 +14,7 @@
/**
* Provides objects to work as array.
* Array-like object with property access.
* @template T
* @implements \IteratorAggregate<array-key, T>
* @implements \ArrayAccess<array-key, T>
@@ -39,7 +39,6 @@ public static function from(array $array, bool $recursive = true): static
/**
* Returns an iterator over all items.
* @return \Iterator<array-key, T>
*/
public function &getIterator(): \Iterator
@@ -50,9 +49,6 @@ public function &getIterator(): \Iterator
}
/**
* Returns items count.
*/
public function count(): int
{
return count((array) $this);

View File

@@ -14,13 +14,14 @@
/**
* Provides the base class for a generic list (items can be accessed by index).
* Generic list with integer indices.
* @template T
* @implements \IteratorAggregate<int, T>
* @implements \ArrayAccess<int, T>
*/
class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
{
/** @var list<T> */
private array $list = [];
@@ -41,7 +42,6 @@ public static function from(array $array): static
/**
* Returns an iterator over all items.
* @return \Iterator<int, T>
*/
public function &getIterator(): \Iterator
@@ -52,9 +52,6 @@ public function &getIterator(): \Iterator
}
/**
* Returns items count.
*/
public function count(): int
{
return count($this->list);
@@ -63,7 +60,7 @@ public function count(): int
/**
* Replaces or appends an item.
* @param int|null $index
* @param ?int $index
* @param T $value
* @throws Nette\OutOfRangeException
*/

View File

@@ -79,7 +79,7 @@ public static function &getRef(array &$array, string|int|array $key): mixed
* @template T2
* @param array<T1> $array1
* @param array<T2> $array2
* @return array<T1|T2>
* @return array<T1|T2|array<mixed>>
*/
public static function mergeTree(array $array1, array $array2): array
{
@@ -96,6 +96,7 @@ public static function mergeTree(array $array1, array $array2): array
/**
* Returns zero-indexed position of given array key. Returns null if key is not found.
* @param array<mixed> $array
*/
public static function getKeyOffset(array $array, string|int $key): ?int
{
@@ -104,9 +105,10 @@ public static function getKeyOffset(array $array, string|int $key): ?int
/**
* @param array<mixed> $array
* @deprecated use getKeyOffset()
*/
public static function searchKey(array $array, $key): ?int
public static function searchKey(array $array, string|int $key): ?int
{
return self::getKeyOffset($array, $key);
}
@@ -114,10 +116,11 @@ public static function searchKey(array $array, $key): ?int
/**
* Tests an array for the presence of value.
* @param array<mixed> $array
*/
public static function contains(array $array, mixed $value): bool
{
return in_array($value, $array, true);
return in_array($value, $array, strict: true);
}
@@ -125,9 +128,11 @@ public static function contains(array $array, mixed $value): bool
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K of int|string
* @template V
* @template E
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
* @param ?callable(): E $else
* @return ($else is null ? ?V : V|E)
*/
public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -142,9 +147,11 @@ public static function first(array $array, ?callable $predicate = null, ?callabl
* Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K of int|string
* @template V
* @template E
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
* @param ?callable(): E $else
* @return ($else is null ? ?V : V|E)
*/
public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -196,6 +203,8 @@ public static function lastKey(array $array, ?callable $predicate = null): int|s
/**
* Inserts the contents of the $inserted array into the $array immediately after the $key.
* If $key is null (or does not exist), it is inserted at the beginning.
* @param array<mixed> $array
* @param array<mixed> $inserted
*/
public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
{
@@ -209,6 +218,8 @@ public static function insertBefore(array &$array, string|int|null $key, array $
/**
* Inserts the contents of the $inserted array into the $array before the $key.
* If $key is null (or does not exist), it is inserted at the end.
* @param array<mixed> $array
* @param array<mixed> $inserted
*/
public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
{
@@ -224,6 +235,7 @@ public static function insertAfter(array &$array, string|int|null $key, array $i
/**
* Renames key in array.
* @param array<mixed> $array
*/
public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
{
@@ -260,6 +272,8 @@ public static function grep(
/**
* Transforms multidimensional array to flat array.
* @param array<mixed> $array
* @return array<mixed>
*/
public static function flatten(array $array, bool $preserveKeys = false): array
{
@@ -284,16 +298,18 @@ public static function isList(mixed $value): bool
/**
* Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
* @param string|string[] $path
* @param array<mixed> $array
* @param string|list<string> $path
* @return array<mixed>|\stdClass
*/
public static function associate(array $array, $path): array|\stdClass
public static function associate(array $array, string|array $path): array|\stdClass
{
$parts = is_array($path)
? $path
: preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') {
throw new Nette\InvalidArgumentException("Invalid path '$path'.");
throw new Nette\InvalidArgumentException("Invalid path '" . (is_array($path) ? implode('', $path) : $path) . "'.");
}
$res = $parts[0] === '->' ? new \stdClass : [];
@@ -312,6 +328,8 @@ public static function associate(array $array, $path): array|\stdClass
$x = $row[$parts[$i]];
$row = null;
}
break; // '=' is always the final operation
} elseif ($part === '->') {
if (isset($parts[++$i])) {
if ($x === null) {
@@ -338,6 +356,8 @@ public static function associate(array $array, $path): array|\stdClass
/**
* Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
* @param array<mixed> $array
* @return array<string, mixed>
*/
public static function normalize(array $array, mixed $filling = null): array
{
@@ -481,8 +501,9 @@ public static function mapWithKeys(array $array, callable $transformer): array
/**
* Invokes all callbacks and returns array of results.
* @param callable[] $callbacks
* @return array<mixed>
*/
public static function invoke(iterable $callbacks, ...$args): array
public static function invoke(iterable $callbacks, mixed ...$args): array
{
$res = [];
foreach ($callbacks as $k => $cb) {
@@ -496,8 +517,9 @@ public static function invoke(iterable $callbacks, ...$args): array
/**
* Invokes method on every object in an array and returns array of results.
* @param object[] $objects
* @return array<mixed>
*/
public static function invokeMethod(iterable $objects, string $method, ...$args): array
public static function invokeMethod(iterable $objects, string $method, mixed ...$args): array
{
$res = [];
foreach ($objects as $k => $obj) {
@@ -511,6 +533,7 @@ public static function invokeMethod(iterable $objects, string $method, ...$args)
/**
* Copies the elements of the $array array to the $object object and then returns it.
* @template T of object
* @param iterable<mixed> $array
* @param T $object
* @return T
*/

View File

@@ -22,21 +22,24 @@ final class Callback
/**
* Invokes internal PHP function with own error handler.
* @param callable-string $function
* @param list<mixed> $args
* @param callable(string, int): (bool|void|null) $onError
*/
public static function invokeSafe(string $function, array $args, callable $onError): mixed
{
$prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
$prev = set_error_handler(function (int $severity, string $message, string $file, int $line) use ($onError, &$prev, $function): bool {
if ($file === __FILE__) {
$msg = ini_get('html_errors')
? Html::htmlToText($message)
: $message;
$msg = preg_replace("#^$function\\(.*?\\): #", '', $msg);
$msg = (string) preg_replace("#^$function\\(.*?\\): #", '', $msg);
if ($onError($msg, $severity) !== false) {
return null;
return true;
}
}
return $prev ? $prev(...func_get_args()) : false;
return $prev ? $prev(...func_get_args()) !== false : false;
});
try {
@@ -53,7 +56,7 @@ public static function invokeSafe(string $function, array $args, callable $onErr
* @return callable
* @throws Nette\InvalidArgumentException
*/
public static function check(mixed $callable, bool $syntax = false)
public static function check(mixed $callable, bool $syntax = false): mixed
{
if (!is_callable($callable, $syntax)) {
throw new Nette\InvalidArgumentException(
@@ -87,7 +90,7 @@ public static function toString(mixed $callable): string
* @param callable $callable type check is escalated to ReflectionException
* @throws \ReflectionException if callback is not valid
*/
public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
public static function toReflection(mixed $callable): \ReflectionMethod|\ReflectionFunction
{
if ($callable instanceof \Closure) {
$callable = self::unwrap($callable);
@@ -100,6 +103,7 @@ public static function toReflection($callable): \ReflectionMethod|\ReflectionFun
} elseif (is_object($callable) && !$callable instanceof \Closure) {
return new ReflectionMethod($callable, '__invoke');
} else {
assert($callable instanceof \Closure || is_string($callable));
return new \ReflectionFunction($callable);
}
}
@@ -116,8 +120,9 @@ public static function isStatic(callable $callable): bool
/**
* Unwraps closure created by Closure::fromCallable().
* @return callable|array{object|class-string, string}|string
*/
public static function unwrap(\Closure $closure): callable|array
public static function unwrap(\Closure $closure): callable|array|string
{
$r = new \ReflectionFunction($closure);
$class = $r->getClosureScopeClass()?->name;

View File

@@ -142,7 +142,7 @@ public static function relativeToSeconds(string $relativeTime): int
}
private function apply(string $datetime, $timezone = null, bool $ctr = false): void
private function apply(string $datetime, ?\DateTimeZone $timezone = null, bool $ctr = false): void
{
$relPart = '';
$absPart = preg_replace_callback(

View File

@@ -19,14 +19,12 @@
*/
final class FileInfo extends \SplFileInfo
{
private readonly string $relativePath;
public function __construct(string $file, string $relativePath = '')
{
public function __construct(
string $file,
private readonly string $relativePath = '',
) {
parent::__construct($file);
$this->setInfoClass(static::class);
$this->relativePath = $relativePath;
$this->setInfoClass(self::class);
}

View File

@@ -52,14 +52,15 @@ public static function copy(string $origin, string $target, bool $overwrite = tr
} elseif (is_dir($origin)) {
static::createDir($target);
foreach (new \FilesystemIterator($target) as $item) {
\assert($item instanceof \SplFileInfo);
static::delete($item->getPathname());
}
foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
if ($item->isDir()) {
static::createDir($target . '/' . $iterator->getSubPathName());
static::createDir($target . '/' . $iterator->getSubPathname());
} else {
static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName());
static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathname());
}
}
} else {
@@ -112,6 +113,7 @@ public static function delete(string $path): void
}
} elseif (is_dir($path)) {
foreach (new \FilesystemIterator($path) as $item) {
\assert($item instanceof \SplFileInfo);
static::delete($item->getPathname());
}
@@ -251,6 +253,7 @@ public static function makeWritable(string $path, int $dirMode = 0o777, int $fil
}
} elseif (is_dir($path)) {
foreach (new \FilesystemIterator($path) as $item) {
\assert($item instanceof \SplFileInfo);
static::makeWritable($item->getPathname(), $dirMode, $fileMode);
}

View File

@@ -32,24 +32,25 @@ class Finder implements \IteratorAggregate
/** @var string[] */
private array $in = [];
/** @var \Closure[] */
/** @var array<\Closure(FileInfo): bool> */
private array $filters = [];
/** @var \Closure[] */
/** @var array<\Closure(FileInfo): bool> */
private array $descentFilters = [];
/** @var array<string|self> */
private array $appends = [];
private bool $childFirst = false;
/** @var ?callable */
private $sort;
/** @var ?(\Closure(FileInfo, FileInfo): int) */
private ?\Closure $sort = null;
private int $maxDepth = -1;
private bool $ignoreUnreadableDirs = true;
/**
* Begins search for files and directories matching mask.
* @param string|list<string> $masks
*/
public static function find(string|array $masks = ['*']): static
{
@@ -60,6 +61,7 @@ public static function find(string|array $masks = ['*']): static
/**
* Begins search for files matching mask.
* @param string|list<string> $masks
*/
public static function findFiles(string|array $masks = ['*']): static
{
@@ -70,6 +72,7 @@ public static function findFiles(string|array $masks = ['*']): static
/**
* Begins search for directories matching mask.
* @param string|list<string> $masks
*/
public static function findDirectories(string|array $masks = ['*']): static
{
@@ -80,6 +83,7 @@ public static function findDirectories(string|array $masks = ['*']): static
/**
* Finds files matching the specified masks.
* @param string|list<string> $masks
*/
public function files(string|array $masks = ['*']): static
{
@@ -89,6 +93,7 @@ public function files(string|array $masks = ['*']): static
/**
* Finds directories matching the specified masks.
* @param string|list<string> $masks
*/
public function directories(string|array $masks = ['*']): static
{
@@ -96,6 +101,7 @@ public function directories(string|array $masks = ['*']): static
}
/** @param list<string> $masks */
private function addMask(array $masks, string $mode): static
{
foreach ($masks as $mask) {
@@ -117,6 +123,7 @@ private function addMask(array $masks, string $mode): static
/**
* Searches in the given directories. Wildcards are allowed.
* @param string|list<string> $paths
*/
public function in(string|array $paths): static
{
@@ -128,6 +135,7 @@ public function in(string|array $paths): static
/**
* Searches recursively from the given directories. Wildcards are allowed.
* @param string|list<string> $paths
*/
public function from(string|array $paths): static
{
@@ -137,6 +145,7 @@ public function from(string|array $paths): static
}
/** @param list<string> $paths */
private function addLocation(array $paths, string $ext): void
{
foreach ($paths as $path) {
@@ -175,7 +184,7 @@ public function ignoreUnreadableDirs(bool $state = true): static
*/
public function sortBy(callable $callback): static
{
$this->sort = $callback;
$this->sort = $callback(...);
return $this;
}
@@ -192,6 +201,7 @@ public function sortByName(): static
/**
* Adds the specified paths or appends a new finder that returns.
* @param string|list<string>|null $paths
*/
public function append(string|array|null $paths = null): static
{
@@ -209,6 +219,7 @@ public function append(string|array|null $paths = null): static
/**
* Skips entries that matches the given masks relative to the ones defined with the in() or from() methods.
* @param string|list<string> $masks
*/
public function exclude(string|array $masks): static
{
@@ -239,7 +250,7 @@ public function exclude(string|array $masks): static
*/
public function filter(callable $callback): static
{
$this->filters[] = \Closure::fromCallable($callback);
$this->filters[] = $callback(...);
return $this;
}
@@ -250,7 +261,7 @@ public function filter(callable $callback): static
*/
public function descentFilter(callable $callback): static
{
$this->descentFilters[] = \Closure::fromCallable($callback);
$this->descentFilters[] = $callback(...);
return $this;
}
@@ -267,6 +278,7 @@ public function limitDepth(?int $depth): static
/**
* Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB
* @param '>'|'>='|'<'|'<='|'='|'=='|'==='|'!='|'!=='|'<>' $operator or predicate string
*/
public function size(string $operator, ?int $size = null): static
{
@@ -277,7 +289,7 @@ public function size(string $operator, ?int $size = null): static
[, $operator, $size, $unit] = $matches;
$units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
$size *= $units[strtolower($unit)];
$size = (float) $size * $units[strtolower($unit)];
$operator = $operator ?: '=';
}
@@ -287,6 +299,7 @@ public function size(string $operator, ?int $size = null): static
/**
* Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23
* @param '>'|'>='|'<'|'<='|'='|'=='|'==='|'!='|'!=='|'<>' $operator or predicate string
*/
public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static
{
@@ -401,6 +414,7 @@ private function traverseDir(string $dir, array $searches, array $subdirs = []):
}
/** @param iterable<string> $pathNames */
private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator
{
foreach ($pathNames as $pathName) {
@@ -413,6 +427,10 @@ private function convertToFiles(iterable $pathNames, string $relativePath, bool
}
/**
* @param (\Closure(FileInfo): bool)[] $filters
* @param array<int, bool> $cache
*/
private function proveFilters(array $filters, FileInfo $file, array &$cache): bool
{
foreach ($filters as $filter) {
@@ -468,6 +486,7 @@ private function buildPlan(): array
/**
* Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal.
* @return array{string, string, bool}
*/
private static function splitRecursivePart(string $path): array
{

View File

@@ -14,6 +14,9 @@
use const PHP_OS_FAMILY;
/**
* Miscellaneous utilities.
*/
class Helpers
{
public const IsWindows = PHP_OS_FAMILY === 'Windows';
@@ -21,6 +24,7 @@ class Helpers
/**
* Executes a callback and returns the captured output as a string.
* @param callable(): void $func
*/
public static function capture(callable $func): string
{
@@ -37,7 +41,7 @@ public static function capture(callable $func): string
/**
* Returns the last occurred PHP error or an empty string if no error occurred. Unlike error_get_last(),
* it is nit affected by the PHP directive html_errors and always returns text, not HTML.
* it is not affected by the PHP directive html_errors and always returns text, not HTML.
*/
public static function getLastError(): string
{
@@ -59,6 +63,7 @@ public static function falseToNull(mixed $value): mixed
/**
* Returns value clamped to the inclusive range of min and max.
* @return ($value is float ? float : ($min is float ? float : ($max is float ? float : int)))
*/
public static function clamp(int|float $value, int|float $min, int|float $max): int|float
{
@@ -91,6 +96,7 @@ public static function getSuggestion(array $possibilities, string $value): ?stri
/**
* Compares two values in the same way that PHP does. Recognizes operators: >, >=, <, <=, =, ==, ===, !=, !==, <>
* @param '>'|'>='|'<'|'<='|'='|'=='|'==='|'!='|'!=='|'<>' $operator
*/
public static function compare(mixed $left, string $operator, mixed $right): bool
{

View File

@@ -17,113 +17,113 @@
/**
* HTML helper.
*
* @property string|null $accept
* @property string|null $accesskey
* @property string|null $action
* @property string|null $align
* @property string|null $allow
* @property string|null $alt
* @property bool|null $async
* @property string|null $autocapitalize
* @property string|null $autocomplete
* @property bool|null $autofocus
* @property bool|null $autoplay
* @property string|null $charset
* @property bool|null $checked
* @property string|null $cite
* @property string|null $class
* @property int|null $cols
* @property int|null $colspan
* @property string|null $content
* @property bool|null $contenteditable
* @property bool|null $controls
* @property string|null $coords
* @property string|null $crossorigin
* @property string|null $data
* @property string|null $datetime
* @property string|null $decoding
* @property bool|null $default
* @property bool|null $defer
* @property string|null $dir
* @property string|null $dirname
* @property bool|null $disabled
* @property bool|null $download
* @property string|null $draggable
* @property string|null $dropzone
* @property string|null $enctype
* @property string|null $for
* @property string|null $form
* @property string|null $formaction
* @property string|null $formenctype
* @property string|null $formmethod
* @property bool|null $formnovalidate
* @property string|null $formtarget
* @property string|null $headers
* @property int|null $height
* @property bool|null $hidden
* @property float|null $high
* @property string|null $href
* @property string|null $hreflang
* @property string|null $id
* @property string|null $integrity
* @property string|null $inputmode
* @property bool|null $ismap
* @property string|null $itemprop
* @property string|null $kind
* @property string|null $label
* @property string|null $lang
* @property string|null $list
* @property bool|null $loop
* @property float|null $low
* @property float|null $max
* @property int|null $maxlength
* @property int|null $minlength
* @property string|null $media
* @property string|null $method
* @property float|null $min
* @property bool|null $multiple
* @property bool|null $muted
* @property string|null $name
* @property bool|null $novalidate
* @property bool|null $open
* @property float|null $optimum
* @property string|null $pattern
* @property string|null $ping
* @property string|null $placeholder
* @property string|null $poster
* @property string|null $preload
* @property string|null $radiogroup
* @property bool|null $readonly
* @property string|null $rel
* @property bool|null $required
* @property bool|null $reversed
* @property int|null $rows
* @property int|null $rowspan
* @property string|null $sandbox
* @property string|null $scope
* @property bool|null $selected
* @property string|null $shape
* @property int|null $size
* @property string|null $sizes
* @property string|null $slot
* @property int|null $span
* @property string|null $spellcheck
* @property string|null $src
* @property string|null $srcdoc
* @property string|null $srclang
* @property string|null $srcset
* @property int|null $start
* @property float|null $step
* @property string|null $style
* @property int|null $tabindex
* @property string|null $target
* @property string|null $title
* @property string|null $translate
* @property string|null $type
* @property string|null $usemap
* @property string|null $value
* @property int|null $width
* @property string|null $wrap
* @property ?string $accept
* @property ?string $accesskey
* @property ?string $action
* @property ?string $align
* @property ?string $allow
* @property ?string $alt
* @property ?bool $async
* @property ?string $autocapitalize
* @property ?string $autocomplete
* @property ?bool $autofocus
* @property ?bool $autoplay
* @property ?string $charset
* @property ?bool $checked
* @property ?string $cite
* @property ?string $class
* @property ?int $cols
* @property ?int $colspan
* @property ?string $content
* @property ?bool $contenteditable
* @property ?bool $controls
* @property ?string $coords
* @property ?string $crossorigin
* @property ?string $data
* @property ?string $datetime
* @property ?string $decoding
* @property ?bool $default
* @property ?bool $defer
* @property ?string $dir
* @property ?string $dirname
* @property ?bool $disabled
* @property ?bool $download
* @property ?string $draggable
* @property ?string $dropzone
* @property ?string $enctype
* @property ?string $for
* @property ?string $form
* @property ?string $formaction
* @property ?string $formenctype
* @property ?string $formmethod
* @property ?bool $formnovalidate
* @property ?string $formtarget
* @property ?string $headers
* @property ?int $height
* @property ?bool $hidden
* @property ?float $high
* @property ?string $href
* @property ?string $hreflang
* @property ?string $id
* @property ?string $integrity
* @property ?string $inputmode
* @property ?bool $ismap
* @property ?string $itemprop
* @property ?string $kind
* @property ?string $label
* @property ?string $lang
* @property ?string $list
* @property ?bool $loop
* @property ?float $low
* @property ?float $max
* @property ?int $maxlength
* @property ?int $minlength
* @property ?string $media
* @property ?string $method
* @property ?float $min
* @property ?bool $multiple
* @property ?bool $muted
* @property ?string $name
* @property ?bool $novalidate
* @property ?bool $open
* @property ?float $optimum
* @property ?string $pattern
* @property ?string $ping
* @property ?string $placeholder
* @property ?string $poster
* @property ?string $preload
* @property ?string $radiogroup
* @property ?bool $readonly
* @property ?string $rel
* @property ?bool $required
* @property ?bool $reversed
* @property ?int $rows
* @property ?int $rowspan
* @property ?string $sandbox
* @property ?string $scope
* @property ?bool $selected
* @property ?string $shape
* @property ?int $size
* @property ?string $sizes
* @property ?string $slot
* @property ?int $span
* @property ?string $spellcheck
* @property ?string $src
* @property ?string $srcdoc
* @property ?string $srclang
* @property ?string $srcset
* @property ?int $start
* @property ?float $step
* @property ?string $style
* @property ?int $tabindex
* @property ?string $target
* @property ?string $title
* @property ?string $translate
* @property ?string $type
* @property ?string $usemap
* @property ?string $value
* @property ?int $width
* @property ?string $wrap
*
* @method self accept(?string $val)
* @method self accesskey(?string $val, bool $state = null)
@@ -230,20 +230,23 @@
* @method self value(?string $val)
* @method self width(?int $val)
* @method self wrap(?string $val)
*
* @implements \IteratorAggregate<int, self|string>
* @implements \ArrayAccess<int, self|string>
*/
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
{
/** @var array<string, mixed> element's attributes */
public array $attrs = [];
/** void elements */
/** @var array<string, int> void elements */
public static array $emptyElements = [
'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
];
/** @var array<int, HtmlStringable|string> nodes */
/** @var array<int, self|string> nodes */
protected array $children = [];
/** element's name */
@@ -254,7 +257,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Constructs new HTML element.
* @param array|string $attrs element's attributes or plain text content
* @param array<string, mixed>|string|null $attrs element's attributes or plain text content
*/
public static function el(?string $name = null, array|string|null $attrs = null): static
{
@@ -355,6 +358,7 @@ final public function isEmpty(): bool
/**
* Sets multiple attributes.
* @param array<string, mixed> $attrs
*/
public function addAttributes(array $attrs): static
{
@@ -417,6 +421,7 @@ public function removeAttribute(string $name): static
/**
* Unsets element's attributes.
* @param list<string> $attributes
*/
public function removeAttributes(array $attributes): static
{
@@ -466,6 +471,7 @@ final public function __unset(string $name): void
/**
* Overloaded setter for element's attribute.
* @param mixed[] $args
*/
final public function __call(string $m, array $args): mixed
{
@@ -496,6 +502,7 @@ final public function __call(string $m, array $args): mixed
/**
* Special setter for element's attribute.
* @param array<string, mixed> $query
*/
final public function href(string $path, array $query = []): static
{
@@ -594,6 +601,7 @@ public function addText(\Stringable|string|int|null $text): static
/**
* Creates and adds a new Html child.
* @param array<string, mixed>|string|null $attrs
*/
final public function create(string $name, array|string|null $attrs = null): static
{
@@ -621,7 +629,7 @@ public function insert(?int $index, HtmlStringable|string $child, bool $replace
/**
* Inserts (replaces) child node (\ArrayAccess implementation).
* @param int|null $index position or null for appending
* @param ?int $index position or null for appending
* @param Html|string $child Html node or raw HTML string
*/
final public function offsetSet($index, $child): void
@@ -634,7 +642,7 @@ final public function offsetSet($index, $child): void
* Returns child node (\ArrayAccess implementation).
* @param int $index
*/
final public function offsetGet($index): HtmlStringable|string
final public function offsetGet($index): self|string
{
return $this->children[$index];
}
@@ -682,7 +690,7 @@ public function removeChildren(): void
/**
* Iterates over elements.
* @return \ArrayIterator<int, HtmlStringable|string>
* @return \ArrayIterator<int, self|string>
*/
final public function getIterator(): \ArrayIterator
{
@@ -692,6 +700,7 @@ final public function getIterator(): \ArrayIterator
/**
* Returns all children.
* @return array<int, self|string>
*/
final public function getChildren(): array
{
@@ -764,10 +773,6 @@ final public function endTag(): string
*/
final public function attributes(): string
{
if (!is_array($this->attrs)) {
return '';
}
$s = '';
$attrs = $this->attrs;
foreach ($attrs as $key => $value) {
@@ -780,7 +785,7 @@ final public function attributes(): string
continue;
} elseif (is_array($value)) {
if (strncmp($key, 'data-', 5) === 0) {
if (str_starts_with($key, 'data-')) {
$value = Json::encode($value);
} else {

View File

@@ -24,7 +24,7 @@
* $image->send();
* </code>
*
* @method Image affine(array $affine, ?array $clip = null)
* @method Image affine(array<int, float|int> $affine, ?array{x: int, y: int, width: int, height: int} $clip = null)
* @method void alphaBlending(bool $enable)
* @method void antialias(bool $enable)
* @method void arc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color)
@@ -41,51 +41,51 @@
* @method int colorResolve(int $red, int $green, int $blue)
* @method int colorResolveAlpha(int $red, int $green, int $blue, int $alpha)
* @method void colorSet(int $index, int $red, int $green, int $blue, int $alpha = 0)
* @method array colorsForIndex(int $color)
* @method array{red: int, green: int, blue: int, alpha: int} colorsForIndex(int $color)
* @method int colorsTotal()
* @method int colorTransparent(?int $color = null)
* @method void convolution(array $matrix, float $div, float $offset)
* @method void convolution(array<int, array<int, float>> $matrix, float $div, float $offset)
* @method void copy(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH)
* @method void copyMerge(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
* @method void copyMergeGray(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
* @method void copyResampled(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
* @method void copyResized(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
* @method Image cropAuto(int $mode = IMG_CROP_DEFAULT, float $threshold = .5, ?ImageColor $color = null)
* @method Image cropAuto(int $mode = 0, float $threshold = .5, ?ImageColor $color = null)
* @method void ellipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
* @method void fill(int $x, int $y, ImageColor $color)
* @method void filledArc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color, int $style)
* @method void filledEllipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
* @method void filledPolygon(array $points, ImageColor $color)
* @method void filledPolygon(array<int, int> $points, ImageColor $color)
* @method void filledRectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void fillToBorder(int $x, int $y, ImageColor $borderColor, ImageColor $color)
* @method void filter(int $filter, ...$args)
* @method void flip(int $mode)
* @method array ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array $options = [])
* @method array<int, int> ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array<string, mixed> $options = [])
* @method void gammaCorrect(float $inputgamma, float $outputgamma)
* @method array getClip()
* @method array{int, int, int, int} getClip()
* @method int getInterpolation()
* @method int interlace(?bool $enable = null)
* @method bool isTrueColor()
* @method void layerEffect(int $effect)
* @method void line(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void openPolygon(array $points, ImageColor $color)
* @method void openPolygon(array<int, int> $points, ImageColor $color)
* @method void paletteCopy(Image $source)
* @method void paletteToTrueColor()
* @method void polygon(array $points, ImageColor $color)
* @method void polygon(array<int, int> $points, ImageColor $color)
* @method void rectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method mixed resolution(?int $resolutionX = null, ?int $resolutionY = null)
* @method Image rotate(float $angle, ImageColor $backgroundColor)
* @method void saveAlpha(bool $enable)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = 3)
* @method void setBrush(Image $brush)
* @method void setClip(int $x1, int $y1, int $x2, int $y2)
* @method void setInterpolation(int $method = IMG_BILINEAR_FIXED)
* @method void setInterpolation(int $method = 3)
* @method void setPixel(int $x, int $y, ImageColor $color)
* @method void setStyle(array $style)
* @method void setStyle(array<int, int> $style)
* @method void setThickness(int $thickness)
* @method void setTile(Image $tile)
* @method void trueColorToPalette(bool $dither, int $ncolors)
* @method array ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array $options = [])
* @method array<int, int> ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array<string, mixed> $options = [])
* @property-read positive-int $width
* @property-read positive-int $height
* @property-read \GdImage $imageResource
@@ -146,6 +146,7 @@ class Image
/**
* Returns RGB color (0..255) and transparency (0..127).
* @deprecated use ImageColor::rgb()
* @return array{red: int, green: int, blue: int, alpha: int}
*/
public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
{
@@ -192,6 +193,7 @@ public static function fromString(string $s, ?int &$type = null): static
}
/** @param callable-string $func */
private static function invokeSafe(string $func, string $arg, string $message, string $callee): static
{
$errors = [];
@@ -213,6 +215,7 @@ private static function invokeSafe(string $func, string $arg, string $message, s
* Creates a new true color image of the given dimensions. The default color is black.
* @param positive-int $width
* @param positive-int $height
* @param ImageColor|array{red: int, green: int, blue: int, alpha?: int}|null $color
* @throws Nette\NotSupportedException if gd extension is not loaded
*/
public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
@@ -224,9 +227,9 @@ public static function fromBlank(int $width, int $height, ImageColor|array|null
$image = new static(imagecreatetruecolor($width, $height));
if ($color) {
$image->alphablending(false);
$image->filledrectangle(0, 0, $width - 1, $height - 1, $color);
$image->alphablending(true);
$image->alphaBlending(false);
$image->filledRectangle(0, 0, $width - 1, $height - 1, self::normalizeColor($color));
$image->alphaBlending(true);
}
return $image;
@@ -235,9 +238,11 @@ public static function fromBlank(int $width, int $height, ImageColor|array|null
/**
* Returns the type of image from file.
* @return ImageType::*|null
* @param-out ?int $width
* @param-out ?int $height
* @return ?ImageType::*
*/
public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int
public static function detectTypeFromFile(string $file, mixed &$width = null, mixed &$height = null): ?int
{
[$width, $height, $type] = Helpers::falseToNull(@getimagesize($file)); // @ - files smaller than 12 bytes causes read error
return $type && isset(self::Formats[$type]) ? $type : null;
@@ -246,9 +251,11 @@ public static function detectTypeFromFile(string $file, &$width = null, &$height
/**
* Returns the type of image from string.
* @return ImageType::*|null
* @param-out ?int $width
* @param-out ?int $height
* @return ?ImageType::*
*/
public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int
public static function detectTypeFromString(string $s, mixed &$width = null, mixed &$height = null): ?int
{
[$width, $height, $type] = Helpers::falseToNull(@getimagesizefromstring($s)); // @ - strings smaller than 12 bytes causes read error
return $type && isset(self::Formats[$type]) ? $type : null;
@@ -314,7 +321,7 @@ public static function isTypeSupported(int $type): bool
}
/** @return ImageType[] */
/** @return ImageType::*[] */
public static function getSupportedTypes(): array
{
self::ensureExtension();
@@ -386,6 +393,10 @@ public function getImageResource(): \GdImage
public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static
{
if ($mode & self::Cover) {
if ($width === null || $height === null) {
throw new Nette\InvalidArgumentException('Both width and height must be set for Cover mode.');
}
return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height);
}
@@ -419,12 +430,13 @@ public function resize(int|string|null $width, int|string|null $height, int $mod
/**
* Calculates dimensions of resized image. Width and height accept pixels or percent.
* @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode
* @return array{int<1, max>, int<1, max>}
*/
public static function calculateSize(
int $srcWidth,
int $srcHeight,
$newWidth,
$newHeight,
int|string|null $newWidth,
int|string|null $newHeight,
int $mode = self::OrSmaller,
): array
{
@@ -468,19 +480,19 @@ public static function calculateSize(
}
if ($mode & self::OrBigger) {
$scale = [max($scale)];
$scale = [max($scale ?: [1])];
}
if ($mode & self::ShrinkOnly) {
$scale[] = 1;
}
$scale = min($scale);
$scale = min($scale ?: [1]);
$newWidth = (int) round($srcWidth * $scale);
$newHeight = (int) round($srcHeight * $scale);
}
return [max($newWidth, 1), max($newHeight, 1)];
return [max((int) $newWidth, 1), max((int) $newHeight, 1)];
}
@@ -495,7 +507,7 @@ public function crop(int|string $left, int|string $top, int|string $width, int|s
$this->image = imagecrop($this->image, $r);
imagesavealpha($this->image, true);
} else {
$newImage = static::fromBlank($r['width'], $r['height'], ImageColor::rgb(0, 0, 0, 0))->getImageResource();
$newImage = static::fromBlank(max(1, $r['width']), max(1, $r['height']), ImageColor::rgb(0, 0, 0, 0))->getImageResource();
imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
$this->image = $newImage;
}
@@ -506,6 +518,7 @@ public function crop(int|string $left, int|string $top, int|string $width, int|s
/**
* Calculates dimensions of cutout in image. Arguments accepts pixels or percent.
* @return array{int, int, int, int}
*/
public static function calculateCutout(
int $srcWidth,
@@ -516,21 +529,10 @@ public static function calculateCutout(
int|string $newHeight,
): array
{
if (self::isPercent($newWidth)) {
$newWidth = (int) round($srcWidth / 100 * $newWidth);
}
if (self::isPercent($newHeight)) {
$newHeight = (int) round($srcHeight / 100 * $newHeight);
}
if (self::isPercent($left)) {
$left = (int) round(($srcWidth - $newWidth) / 100 * $left);
}
if (self::isPercent($top)) {
$top = (int) round(($srcHeight - $newHeight) / 100 * $top);
}
$newWidth = (int) (self::isPercent($newWidth) ? round($srcWidth / 100 * $newWidth) : $newWidth);
$newHeight = (int) (self::isPercent($newHeight) ? round($srcHeight / 100 * $newHeight) : $newHeight);
$left = (int) (self::isPercent($left) ? round(($srcWidth - $newWidth) / 100 * $left) : $left);
$top = (int) (self::isPercent($top) ? round(($srcHeight - $newHeight) / 100 * $top) : $top);
if ($left < 0) {
$newWidth += $left;
@@ -575,14 +577,8 @@ public function place(self $image, int|string $left = 0, int|string $top = 0, in
$width = $image->getWidth();
$height = $image->getHeight();
if (self::isPercent($left)) {
$left = (int) round(($this->getWidth() - $width) / 100 * $left);
}
if (self::isPercent($top)) {
$top = (int) round(($this->getHeight() - $height) / 100 * $top);
}
$left = (int) (self::isPercent($left) ? round(($this->getWidth() - $width) / 100 * $left) : $left);
$top = (int) (self::isPercent($top) ? round(($this->getHeight() - $height) / 100 * $top) : $top);
$output = $input = $image->image;
if ($opacity < 100) {
@@ -595,7 +591,7 @@ public function place(self $image, int|string $left = 0, int|string $top = 0, in
imagealphablending($output, false);
if (!$image->isTrueColor()) {
$input = $output;
imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127));
imagefilledrectangle($output, 0, 0, $width, $height, (int) imagecolorallocatealpha($output, 0, 0, 0, 127));
imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
}
@@ -626,6 +622,8 @@ public function place(self $image, int|string $left = 0, int|string $top = 0, in
/**
* Calculates the bounding box for a TrueType text. Returns keys left, top, width and height.
* @param array<string, mixed> $options
* @return array{left: int, top: int, width: int, height: int}
*/
public static function calculateTextBox(
string $text,
@@ -670,7 +668,7 @@ public function filledRectangleWH(int $x, int $y, int $width, int $height, Image
/**
* Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9).
* @param ImageType::*|null $type
* @param ?ImageType::* $type
* @throws ImageException
*/
public function save(string $file, ?int $quality = null, ?int $type = null): void
@@ -746,6 +744,7 @@ private function output(int $type, ?int $quality, ?string $file = null): void
/**
* Call to undefined method.
* @param mixed[] $args
* @throws Nette\MemberAccessException
*/
public function __call(string $name, array $args): mixed
@@ -760,6 +759,7 @@ public function __call(string $name, array $args): mixed
$args[$key] = $value->getImageResource();
} elseif ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
/** @var ImageColor|array{red: int, green: int, blue: int, alpha?: int} $value */
$args[$key] = $this->resolveColor($value);
}
}
@@ -775,10 +775,11 @@ public function __clone()
{
ob_start(fn() => '');
imagepng($this->image, null, 0);
$this->setImageResource(imagecreatefromstring(ob_get_clean()));
$this->setImageResource(imagecreatefromstring(ob_get_clean()) ?: throw new Nette\ShouldNotHappenException);
}
/** @param-out int|float $num */
private static function isPercent(int|string &$num): bool
{
if (is_string($num) && str_ends_with($num, '%')) {
@@ -802,13 +803,30 @@ public function __serialize(): array
}
/**
* @param ImageColor|array{red: int, green: int, blue: int, alpha?: int} $color
*/
public function resolveColor(ImageColor|array $color): int
{
$color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color);
$color = self::normalizeColor($color)->toRGBA();
return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color);
}
/** @param ImageColor|array{red: int, green: int, blue: int, alpha?: int} $color */
private static function normalizeColor(ImageColor|array $color): ImageColor
{
return $color instanceof ImageColor
? $color
: ImageColor::rgb(
$color['red'],
$color['green'],
$color['blue'],
(127 - ($color['alpha'] ?? 0)) / 127,
);
}
private static function ensureExtension(): void
{
if (!extension_loaded('gd')) {

View File

@@ -64,6 +64,10 @@ private function __construct(
}
/**
* Returns GD-compatible color array [R, G, B, alpha].
* @return array{int<0, 255>, int<0, 255>, int<0, 255>, int<0, 127>}
*/
public function toRGBA(): array
{
return [

View File

@@ -22,6 +22,7 @@ final class Iterables
/**
* Tests for the presence of value.
* @param iterable<mixed> $iterable
*/
public static function contains(iterable $iterable, mixed $value): bool
{
@@ -36,6 +37,7 @@ public static function contains(iterable $iterable, mixed $value): bool
/**
* Tests for the presence of key.
* @param iterable<mixed> $iterable
*/
public static function containsKey(iterable $iterable, mixed $key): bool
{
@@ -52,9 +54,11 @@ public static function containsKey(iterable $iterable, mixed $key): bool
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K
* @template V
* @template E
* @param iterable<K, V> $iterable
* @param ?callable(V, K, iterable<K, V>): bool $predicate
* @return ?V
* @param ?callable(): E $else
* @return ($else is null ? ?V : V|E)
*/
public static function first(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -71,9 +75,11 @@ public static function first(iterable $iterable, ?callable $predicate = null, ?c
* Returns the key of first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K
* @template V
* @template E
* @param iterable<K, V> $iterable
* @param ?callable(V, K, iterable<K, V>): bool $predicate
* @return ?K
* @param ?callable(): E $else
* @return ($else is null ? ?K : K|E)
*/
public static function firstKey(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -161,11 +167,11 @@ public static function map(iterable $iterable, callable $transformer): \Generato
* Iterator that transforms keys and values by calling $transformer. If it returns null, the element is skipped.
* @template K
* @template V
* @template ResV
* @template ResK
* @template ResV
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): ?array{ResV, ResK} $transformer
* @return \Generator<ResV, ResK>
* @param callable(V, K, iterable<K, V>): ?array{ResK, ResV} $transformer
* @return \Generator<ResK, ResV>
*/
public static function mapWithKeys(iterable $iterable, callable $transformer): \Generator
{
@@ -188,9 +194,10 @@ public static function mapWithKeys(iterable $iterable, callable $transformer): \
*/
public static function repeatable(callable $factory): \IteratorAggregate
{
return new class ($factory) implements \IteratorAggregate {
return new class ($factory(...)) implements \IteratorAggregate {
public function __construct(
private $factory,
/** @var \Closure(): iterable<mixed, mixed> */
private \Closure $factory,
) {
}
@@ -215,7 +222,8 @@ public static function memoize(iterable $iterable): \IteratorAggregate
{
return new class (self::toIterator($iterable)) implements \IteratorAggregate {
public function __construct(
private \Iterator $iterator,
private readonly \Iterator $iterator,
/** @var array<array{mixed, mixed}> */
private array $cache = [],
) {
}

View File

@@ -17,17 +17,17 @@
*
* @property int $page
* @property-read int $firstPage
* @property-read int|null $lastPage
* @property-read ?int $lastPage
* @property-read int<0,max> $firstItemOnPage
* @property-read int<0,max> $lastItemOnPage
* @property int $base
* @property-read bool $first
* @property-read bool $last
* @property-read int<0,max>|null $pageCount
* @property-read ?int<0,max> $pageCount
* @property positive-int $itemsPerPage
* @property int<0,max>|null $itemCount
* @property ?int<0,max> $itemCount
* @property-read int<0,max> $offset
* @property-read int<0,max>|null $countdownOffset
* @property-read ?int<0,max> $countdownOffset
* @property-read int<0,max> $length
*/
class Paginator
@@ -41,13 +41,10 @@ class Paginator
private int $page = 1;
/** @var int<0, max>|null */
/** @var ?int<0, max> */
private ?int $itemCount = null;
/**
* Sets current page number.
*/
public function setPage(int $page): static
{
$this->page = $page;
@@ -55,27 +52,18 @@ public function setPage(int $page): static
}
/**
* Returns current page number.
*/
public function getPage(): int
{
return $this->base + $this->getPageIndex();
}
/**
* Returns first page number.
*/
public function getFirstPage(): int
{
return $this->base;
}
/**
* Returns last page number.
*/
public function getLastPage(): ?int
{
return $this->itemCount === null
@@ -106,9 +94,6 @@ public function getLastItemOnPage(): int
}
/**
* Sets first page (base) number.
*/
public function setBase(int $base): static
{
$this->base = $base;
@@ -116,9 +101,6 @@ public function setBase(int $base): static
}
/**
* Returns first page (base) number.
*/
public function getBase(): int
{
return $this->base;
@@ -138,18 +120,12 @@ protected function getPageIndex(): int
}
/**
* Is the current page the first one?
*/
public function isFirst(): bool
{
return $this->getPageIndex() === 0;
}
/**
* Is the current page the last one?
*/
public function isLast(): bool
{
return $this->itemCount === null
@@ -159,20 +135,16 @@ public function isLast(): bool
/**
* Returns the total number of pages.
* @return int<0, max>|null
* @return ?int<0, max>
*/
public function getPageCount(): ?int
{
return $this->itemCount === null
? null
: (int) ceil($this->itemCount / $this->itemsPerPage);
: max(0, (int) ceil($this->itemCount / $this->itemsPerPage));
}
/**
* Sets the number of items to display on a single page.
*/
public function setItemsPerPage(int $itemsPerPage): static
{
$this->itemsPerPage = max(1, $itemsPerPage);
@@ -181,7 +153,6 @@ public function setItemsPerPage(int $itemsPerPage): static
/**
* Returns the number of items to display on a single page.
* @return positive-int
*/
public function getItemsPerPage(): int
@@ -190,9 +161,6 @@ public function getItemsPerPage(): int
}
/**
* Sets the total number of items.
*/
public function setItemCount(?int $itemCount = null): static
{
$this->itemCount = $itemCount === null ? null : max(0, $itemCount);
@@ -201,8 +169,7 @@ public function setItemCount(?int $itemCount = null): static
/**
* Returns the total number of items.
* @return int<0, max>|null
* @return ?int<0, max>
*/
public function getItemCount(): ?int
{
@@ -222,7 +189,7 @@ public function getOffset(): int
/**
* Returns the absolute index of the first item on current page in countdown paging.
* @return int<0, max>|null
* @return ?int<0, max>
*/
public function getCountdownOffset(): ?int
{
@@ -240,6 +207,6 @@ public function getLength(): int
{
return $this->itemCount === null
? $this->itemsPerPage
: min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage);
: max(0, min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage));
}
}

View File

@@ -38,7 +38,7 @@ public static function isClassKeyword(string $name): bool
public static function getParameterDefaultValue(\ReflectionParameter $param): mixed
{
if ($param->isDefaultValueConstant()) {
$const = $orig = $param->getDefaultValueConstantName();
$const = $orig = $param->getDefaultValueConstantName() ?? throw new Nette\ShouldNotHappenException;
$pair = explode('::', $const);
if (isset($pair[1])) {
$pair[0] = Type::resolve($pair[0], $param);
@@ -68,6 +68,7 @@ public static function getParameterDefaultValue(\ReflectionParameter $param): mi
/**
* Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait.
* @return \ReflectionClass<object>
*/
public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass
{
@@ -151,6 +152,7 @@ public static function toString(\Reflector $ref): string
/**
* Expands the name of the class to full name in the given context of given class.
* Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context.
* @param \ReflectionClass<object> $context
* @throws Nette\InvalidArgumentException
*/
public static function expandClassName(string $name, \ReflectionClass $context): string
@@ -189,7 +191,10 @@ public static function expandClassName(string $name, \ReflectionClass $context):
}
/** @return array<string, class-string> of [alias => class] */
/**
* @param \ReflectionClass<object> $class
* @return array<string, class-string> of [alias => class]
*/
public static function getUseStatements(\ReflectionClass $class): array
{
if ($class->isAnonymous()) {
@@ -201,7 +206,7 @@ public static function getUseStatements(\ReflectionClass $class): array
if ($class->isInternal()) {
$cache[$name] = [];
} else {
$code = file_get_contents($class->getFileName());
$code = (string) file_get_contents((string) $class->getFileName());
$cache = self::parseUseStatements($code, $name) + $cache;
}
}
@@ -212,6 +217,7 @@ public static function getUseStatements(\ReflectionClass $class): array
/**
* Parses PHP code to [class => [alias => class, ...]]
* @return array<string, array<string, string>>
*/
private static function parseUseStatements(string $code, ?string $forClass = null): array
{
@@ -256,8 +262,8 @@ private static function parseUseStatements(string $code, ?string $forClass = nul
$name = ltrim($name, '\\');
if (self::fetch($tokens, '{')) {
while ($suffix = self::fetch($tokens, $nameTokens)) {
if (self::fetch($tokens, T_AS)) {
$uses[self::fetch($tokens, T_STRING)] = $name . $suffix;
if (self::fetch($tokens, T_AS) && ($alias = self::fetch($tokens, T_STRING))) {
$uses[$alias] = $name . $suffix;
} else {
$tmp = explode('\\', $suffix);
$uses[end($tmp)] = $name . $suffix;
@@ -267,8 +273,8 @@ private static function parseUseStatements(string $code, ?string $forClass = nul
break;
}
}
} elseif (self::fetch($tokens, T_AS)) {
$uses[self::fetch($tokens, T_STRING)] = $name;
} elseif (self::fetch($tokens, T_AS) && ($alias = self::fetch($tokens, T_STRING))) {
$uses[$alias] = $name;
} else {
$tmp = explode('\\', $name);
@@ -301,6 +307,10 @@ private static function parseUseStatements(string $code, ?string $forClass = nul
}
/**
* @param \PhpToken[] $tokens
* @param string|int|int[] $take
*/
private static function fetch(array &$tokens, string|int|array $take): ?string
{
$res = null;

View File

@@ -18,9 +18,11 @@
*/
final class ReflectionMethod extends \ReflectionMethod
{
private \ReflectionClass $originalClass;
/** @var \ReflectionClass<object> */
private readonly \ReflectionClass $originalClass;
/** @param class-string|object $objectOrMethod */
public function __construct(object|string $objectOrMethod, ?string $method = null)
{
if (is_string($objectOrMethod) && str_contains($objectOrMethod, '::')) {
@@ -31,6 +33,7 @@ public function __construct(object|string $objectOrMethod, ?string $method = nul
}
/** @return \ReflectionClass<object> */
public function getOriginalClass(): \ReflectionClass
{
return $this->originalClass;

View File

@@ -59,7 +59,8 @@ public static function chr(int $code): string
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
return iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code));
$res = iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code));
return $res === false ? throw new Nette\ShouldNotHappenException : $res;
}
@@ -73,11 +74,11 @@ public static function ord(string $c): int
}
$tmp = iconv('UTF-8', 'UTF-32BE//IGNORE', $c);
if (!$tmp) {
if ($tmp === false || $tmp === '') {
throw new Nette\InvalidArgumentException('Invalid UTF-8 character "' . ($c === '' ? '' : '\x' . strtoupper(bin2hex($c))) . '".');
}
return unpack('N', $tmp)[1];
return unpack('N', $tmp)[1] ?? throw new Nette\ShouldNotHappenException;
}
@@ -124,7 +125,8 @@ public static function substring(string $s, int $start, ?int $length = null): st
$start += self::length($s); // unifies iconv_substr behavior with mb_substr
}
return iconv_substr($s, $start, $length, 'UTF-8');
$res = iconv_substr($s, $start, $length, 'UTF-8');
return $res === false ? throw new Nette\InvalidStateException('iconv_substr() failed.') : $res;
}
@@ -135,7 +137,7 @@ public static function substring(string $s, int $start, ?int $length = null): st
public static function normalize(string $s): string
{
// convert to compressed normal form (NFC)
if (class_exists('Normalizer', false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) {
if (class_exists('Normalizer', autoload: false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) {
$s = $n;
}
@@ -201,14 +203,23 @@ public static function toAscii(string $s): string
$s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ € ™ ← ↑ → ↓ ↔
}
$s = \Transliterator::create('Any-Latin; Latin-ASCII')->transliterate($s);
$s = \Transliterator::create('Any-Latin; Latin-ASCII')?->transliterate($s)
?? throw new Nette\InvalidStateException('Transliterator::transliterate() failed.');
// use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ
if ($iconv === 'glibc') {
$s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
if ($s === false) {
throw new Nette\InvalidStateException('iconv() failed.');
}
$s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters
} elseif ($iconv === 'libiconv') {
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
if ($s === false) {
throw new Nette\InvalidStateException('iconv() failed.');
}
} else { // null or 'unknown' (#216)
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
}
@@ -323,7 +334,7 @@ public static function capitalize(string $s): string
*/
public static function compare(string $left, string $right, ?int $length = null): bool
{
if (class_exists('Normalizer', false)) {
if (class_exists('Normalizer', autoload: false)) {
$left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster
$right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster
}
@@ -347,6 +358,10 @@ public static function compare(string $left, string $right, ?int $length = null)
public static function findPrefix(array $strings): string
{
$first = array_shift($strings);
if ($first === null) {
return '';
}
for ($i = 0; $i < strlen($first); $i++) {
foreach ($strings as $s) {
if (!isset($s[$i]) || $first[$i] !== $s[$i]) {
@@ -370,8 +385,8 @@ public static function findPrefix(array $strings): string
public static function length(string $s): int
{
return match (true) {
extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'),
extension_loaded('iconv') => iconv_strlen($s, 'UTF-8'),
extension_loaded('mbstring') => (int) mb_strlen($s, 'UTF-8'),
extension_loaded('iconv') => (int) iconv_strlen($s, 'UTF-8'),
default => strlen(@utf8_decode($s)), // deprecated
};
}
@@ -420,7 +435,10 @@ public static function reverse(string $s): string
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', $s)));
$tmp = iconv('UTF-8', 'UTF-32BE', $s);
return $tmp === false
? throw new Nette\InvalidStateException('iconv() failed.')
: (string) iconv('UTF-32LE', 'UTF-8', strrev($tmp));
}
@@ -499,6 +517,7 @@ private static function pos(string $haystack, string $needle, int $nth = 1): ?in
/**
* Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well.
* @return list<string>
*/
public static function split(
string $subject,
@@ -525,6 +544,7 @@ public static function split(
/**
* Searches the string for the part matching the regular expression and returns
* an array with the found expression and individual subexpressions, or `null`.
* @return ?array<string>
*/
public static function match(
string $subject,
@@ -545,6 +565,7 @@ public static function match(
$pattern .= 'u';
}
$m = [];
if ($offset > strlen($subject)) {
return null;
} elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) {
@@ -560,7 +581,7 @@ public static function match(
/**
* Searches the string for all occurrences matching the regular expression and
* returns an array of arrays containing the found expression and each subexpression.
* @return ($lazy is true ? \Generator<int, array> : array[])
* @return ($lazy is true ? \Generator<int, array<string>> : list<array<string>>)
*/
public static function matchAll(
string $subject,
@@ -583,10 +604,12 @@ public static function matchAll(
$flags = PREG_OFFSET_CAPTURE | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
return (function () use ($utf8, $captureOffset, $flags, $subject, $pattern, $offset) {
$counter = 0;
$m = [];
while (
$offset <= strlen($subject) - ($counter ? 1 : 0)
&& self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])
) {
/** @var list<array{string, int}> $m */
$offset = $m[0][1] + max(1, strlen($m[0][0]));
if (!$captureOffset) {
$m = array_map(fn($item) => $item[0], $m);
@@ -606,6 +629,7 @@ public static function matchAll(
? $captureOffset
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0);
$m = [];
self::pcre('preg_match_all', [
$pattern, $subject, &$m,
($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER),
@@ -619,6 +643,7 @@ public static function matchAll(
/**
* Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`.
* @param string|array<string, string> $pattern
*/
public static function replace(
string $subject,
@@ -638,7 +663,7 @@ public static function replace(
$flags = ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
if ($utf8) {
$pattern .= 'u';
$pattern = is_array($pattern) ? array_map(fn($item) => $item . 'u', $pattern) : $pattern . 'u';
if ($captureOffset) {
$replacement = fn($m) => $replacement(self::bytesToChars($subject, [$m])[0]);
}
@@ -659,6 +684,10 @@ public static function replace(
}
/**
* @param list<array<array{string, int}>> $groups
* @return list<array<array{string, int}>>
*/
private static function bytesToChars(string $s, array $groups): array
{
$lastBytes = $lastChars = 0;
@@ -679,8 +708,12 @@ private static function bytesToChars(string $s, array $groups): array
}
/** @internal */
public static function pcre(string $func, array $args)
/**
* @param callable-string $func
* @param list<mixed> $args
* @internal
*/
public static function pcre(string $func, array $args): mixed
{
$res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void {
// compile-time error, not detectable by preg_last_error
@@ -688,7 +721,7 @@ public static function pcre(string $func, array $args)
});
if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars
&& ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true))
&& ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], strict: true))
) {
throw new RegexpException(preg_last_error_msg()
. ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code);

View File

@@ -10,7 +10,7 @@
namespace Nette\Utils;
use Nette;
use function array_map, array_search, array_splice, count, explode, implode, is_a, is_resource, is_string, strcasecmp, strtolower, substr, trim;
use function array_map, array_search, array_splice, array_values, count, explode, implode, is_a, is_resource, is_string, strcasecmp, strtolower, substr, trim;
/**
@@ -18,9 +18,9 @@
*/
final readonly class Type
{
/** @var array<int, string|self> */
/** @var list<string|self> */
private array $types;
private bool $simple;
private ?string $singleName;
private string $kind; // | &
@@ -40,7 +40,12 @@ public static function fromReflection(
}
private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string
/** @return ($asObject is true ? self : self|string) */
private static function fromReflectionType(
\ReflectionType $type,
\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of,
bool $asObject,
): self|string
{
if ($type instanceof \ReflectionNamedType) {
$name = self::resolve($type->getName(), $of);
@@ -107,34 +112,40 @@ public static function fromValue(mixed $value): self
*/
public static function resolve(
string $type,
\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of,
\ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $of,
): string
{
$lower = strtolower($type);
if ($of instanceof \ReflectionFunction) {
return $type;
}
$class = $of->getDeclaringClass();
if ($class === null) {
return $type;
} elseif ($lower === 'self') {
return $of->getDeclaringClass()->name;
return $class->name;
} elseif ($lower === 'static') {
return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $of->getDeclaringClass())->name;
} elseif ($lower === 'parent' && $of->getDeclaringClass()->getParentClass()) {
return $of->getDeclaringClass()->getParentClass()->name;
return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $class)->name;
} elseif ($lower === 'parent' && $class->getParentClass()) {
return $class->getParentClass()->name;
} else {
return $type;
}
}
/** @param array<string|self> $types */
private function __construct(array $types, string $kind = '|')
{
$o = array_search('null', $types, strict: true);
if ($o !== false) { // null as last
array_splice($types, $o, 1);
array_splice($types, (int) $o, 1);
$types[] = 'null';
}
$this->types = $types;
$this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null';
$this->types = array_values($types);
$this->singleName = is_string($types[0]) && ($types[1] ?? 'null') === 'null' ? $types[0] : null;
$this->kind = count($types) > 1 ? $kind : '';
}
@@ -142,8 +153,8 @@ private function __construct(array $types, string $kind = '|')
public function __toString(): string
{
$multi = count($this->types) > 1;
if ($this->simple) {
return ($multi ? '?' : '') . $this->types[0];
if ($this->singleName !== null) {
return ($multi ? '?' : '') . $this->singleName;
}
$res = [];
@@ -173,7 +184,7 @@ public function with(string|self $type): self
/**
* Returns the array of subtypes that make up the compound type as strings.
* @return array<int, string|string[]>
* @return list<string|array<string|array<mixed>>>
*/
public function getNames(): array
{
@@ -182,8 +193,8 @@ public function getNames(): array
/**
* Returns the array of subtypes that make up the compound type as Type objects:
* @return self[]
* Returns the array of subtypes that make up the compound type as Type objects.
* @return list<self>
*/
public function getTypes(): array
{
@@ -196,9 +207,7 @@ public function getTypes(): array
*/
public function getSingleName(): ?string
{
return $this->simple
? $this->types[0]
: null;
return $this->singleName;
}
@@ -225,14 +234,14 @@ public function isIntersection(): bool
*/
public function isSimple(): bool
{
return $this->simple;
return $this->singleName !== null;
}
#[\Deprecated('use isSimple()')]
public function isSingle(): bool
{
return $this->simple;
return $this->singleName !== null;
}
@@ -241,7 +250,7 @@ public function isSingle(): bool
*/
public function isBuiltin(): bool
{
return $this->simple && Validators::isBuiltinType($this->types[0]);
return $this->singleName !== null && Validators::isBuiltinType($this->singleName);
}
@@ -250,7 +259,7 @@ public function isBuiltin(): bool
*/
public function isClass(): bool
{
return $this->simple && !Validators::isBuiltinType($this->types[0]);
return $this->singleName !== null && !Validators::isBuiltinType($this->singleName);
}
@@ -259,7 +268,7 @@ public function isClass(): bool
*/
public function isClassKeyword(): bool
{
return $this->simple && Validators::isClassKeyword($this->types[0]);
return $this->singleName !== null && Validators::isClassKeyword($this->singleName);
}
@@ -279,6 +288,7 @@ public function allows(string|self $type): bool
}
/** @param array<string> $givenTypes */
private function allowsAny(array $givenTypes): bool
{
return $this->isUnion()
@@ -287,13 +297,17 @@ private function allowsAny(array $givenTypes): bool
}
/**
* @param array<string> $ourTypes
* @param array<string> $givenTypes
*/
private function allowsAll(array $ourTypes, array $givenTypes): bool
{
return Arrays::every(
$ourTypes,
fn($ourType) => Arrays::some(
fn(string $ourType) => Arrays::some(
$givenTypes,
fn($givenType) => Validators::isBuiltinType($ourType)
fn(string $givenType) => Validators::isBuiltinType($ourType)
? strcasecmp($ourType, $givenType) === 0
: is_a($givenType, $ourType, allow_string: true),
),

View File

@@ -26,7 +26,7 @@ class Validators
'never' => 1, 'true' => 1,
];
/** @var array<string,?callable> */
/** @var array<string, ?(callable(mixed): bool)> */
protected static $validators = [
// PHP types
'array' => 'is_array',
@@ -76,7 +76,7 @@ class Validators
'type' => [self::class, 'isType'],
];
/** @var array<string,callable> */
/** @var array<string, callable(mixed): int> */
protected static $counters = [
'string' => 'strlen',
'unicode' => [Strings::class, 'length'],
@@ -120,16 +120,16 @@ public static function assert(mixed $value, string $expected, string $label = 'v
*/
public static function assertField(
array $array,
$key,
int|string $key,
?string $expected = null,
string $label = "item '%' in array",
): void
{
if (!array_key_exists($key, $array)) {
throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.');
throw new AssertionException('Missing ' . str_replace('%', (string) $key, $label) . '.');
} elseif ($expected) {
static::assert($array[$key], $expected, str_replace('%', $key, $label));
static::assert($array[$key], $expected, str_replace('%', (string) $key, $label));
}
}
@@ -159,7 +159,7 @@ public static function is(mixed $value, string $expected): bool
if (!static::$validators[$type]($value)) {
continue;
}
} catch (\TypeError $e) {
} catch (\TypeError) {
continue;
}
} elseif ($type === 'pattern') {
@@ -261,7 +261,7 @@ public static function isUnicode(mixed $value): bool
/**
* Checks if the value is 0, '', false or null.
* @return ($value is 0|''|false|null ? true : false)
* @return ($value is 0|0.0|''|false|null ? true : false)
*/
public static function isNone(mixed $value): bool
{
@@ -290,6 +290,7 @@ public static function isList(mixed $value): bool
/**
* Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null).
* Numbers, strings and DateTime objects can be compared.
* @param array{int|float|string|\DateTimeInterface|null, int|float|string|\DateTimeInterface|null} $range
*/
public static function isInRange(mixed $value, array $range): bool
{