This commit is contained in:
2025-05-12 14:25:25 +02:00
parent ab2db755ef
commit 9e378ca2b7
2719 changed files with 46505 additions and 60181 deletions

View File

@@ -15,11 +15,11 @@
}
],
"require": {
"php": "8.1 - 8.3",
"php": "8.1 - 8.4",
"nette/utils": "^4.0"
},
"require-dev": {
"nette/tester": "^2.4",
"nette/tester": "^2.5.2",
"tracy/tracy": "^2.8",
"phpstan/phpstan-nette": "^1.0"
},

View File

@@ -21,7 +21,7 @@ Installation:
composer require nette/schema
```
It requires PHP version 8.1 and supports PHP up to 8.3.
It requires PHP version 8.1 and supports PHP up to 8.4.
[Support Me](https://github.com/sponsors/dg)

View File

@@ -31,12 +31,12 @@ final class Structure implements Schema
/**
* @param Schema[] $items
* @param Schema[] $shape
*/
public function __construct(array $items)
public function __construct(array $shape)
{
(function (Schema ...$items) {})(...array_values($items));
$this->items = $items;
(function (Schema ...$items) {})(...array_values($shape));
$this->items = $shape;
$this->castTo('object');
$this->required = true;
}
@@ -76,6 +76,19 @@ final class Structure implements Schema
}
public function extend(array|self $shape): self
{
$shape = $shape instanceof self ? $shape->items : $shape;
return new self(array_merge($this->items, $shape));
}
public function getShape(): array
{
return $this->items;
}
/********************* processing ****************d*g**/
@@ -117,25 +130,22 @@ final class Structure implements Schema
}
if (is_array($value) && is_array($base)) {
$index = 0;
$index = $this->otherItems === null ? null : 0;
foreach ($value as $key => $val) {
if ($key === $index) {
$base[] = $val;
$index++;
} elseif (array_key_exists($key, $base)) {
$itemSchema = $this->items[$key] ?? $this->otherItems;
$base[$key] = $itemSchema
? $itemSchema->merge($val, $base[$key])
: Helpers::merge($val, $base[$key]);
} else {
$base[$key] = $val;
$base[$key] = array_key_exists($key, $base) && ($itemSchema = $this->items[$key] ?? $this->otherItems)
? $itemSchema->merge($val, $base[$key])
: $val;
}
}
return $base;
}
return Helpers::merge($value, $base);
return $value ?? $base;
}

View File

@@ -75,7 +75,7 @@ final class Type implements Schema
/**
* @internal use arrayOf() or listOf()
*/
public function items(string|Schema $valueType = 'mixed', string|Schema $keyType = null): self
public function items(string|Schema $valueType = 'mixed', string|Schema|null $keyType = null): self
{
$this->itemsValue = $valueType instanceof Schema
? $valueType

View File

@@ -24,7 +24,6 @@ use Nette\Schema\Elements\Type;
* @method static Type float($default = null)
* @method static Type bool($default = null)
* @method static Type null()
* @method static Type array($default = [])
* @method static Type list($default = [])
* @method static Type mixed($default = null)
* @method static Type email($default = null)
@@ -56,11 +55,11 @@ final class Expect
/**
* @param Schema[] $items
* @param Schema[] $shape
*/
public static function structure(array $items): Structure
public static function structure(array $shape): Structure
{
return new Structure($items);
return new Structure($shape);
}
@@ -95,7 +94,18 @@ final class Expect
}
public static function arrayOf(string|Schema $valueType, string|Schema $keyType = null): Type
/**
* @param mixed[] $shape
*/
public static function array(?array $shape = []): Structure|Type
{
return Nette\Utils\Arrays::first($shape ?? []) instanceof Schema
? (new Structure($shape))->castTo('array')
: (new Type('array'))->default($shape);
}
public static function arrayOf(string|Schema $valueType, string|Schema|null $keyType = null): Type
{
return (new Type('array'))->items($valueType, $keyType);
}

View File

@@ -15,7 +15,7 @@
}
],
"require": {
"php": ">=8.0 <8.4"
"php": "8.0 - 8.4"
},
"require-dev": {
"nette/tester": "^2.5",

View File

@@ -1,5 +1,4 @@
Nette Utility Classes
=====================
[![Nette Utils](https://github.com/nette/utils/assets/194960/c33fdb74-0652-4cad-ac6e-c1ce0d29e32a)](https://doc.nette.org/en/utils)
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/utils.svg)](https://packagist.org/packages/nette/utils)
[![Tests](https://github.com/nette/utils/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/utils/actions)
@@ -11,25 +10,27 @@ Nette Utility Classes
Introduction
------------
In package nette/utils you will find a set of [useful classes](https://doc.nette.org/utils) for everyday use:
In package nette/utils you will find a set of useful classes for everyday use:
- [Arrays](https://doc.nette.org/utils/arrays) - manipulate arrays
- [Callback](https://doc.nette.org/utils/callback) - PHP callbacks
- [Date and Time](https://doc.nette.org/utils/datetime) - modify times and dates
- [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …
- [Finder](https://doc.nette.org/utils/finder) - finds files and directories
- [Helper Functions](https://doc.nette.org/utils/helpers)
- [HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML
- [Images](https://doc.nette.org/utils/images) - crop, resize, rotate images
- [JSON](https://doc.nette.org/utils/json) - encoding and decoding
- [Generating Random Strings](https://doc.nette.org/utils/random)
- [Paginator](https://doc.nette.org/utils/paginator) - pagination math
- [PHP Reflection](https://doc.nette.org/utils/reflection)
- [Strings](https://doc.nette.org/utils/strings) - useful text functions
- [SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements
- [Validation](https://doc.nette.org/utils/validators) - validate inputs
- [Type](https://doc.nette.org/utils/type) - PHP data type
[Arrays](https://doc.nette.org/utils/arrays)<br>
[Callback](https://doc.nette.org/utils/callback) - PHP callbacks<br>
[Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …<br>
[Finder](https://doc.nette.org/utils/finder) - finds files and directories<br>
[Floats](https://doc.nette.org/utils/floats) - floating point numbers<br>
[Helper Functions](https://doc.nette.org/utils/helpers)<br>
[HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML<br>
[Images](https://doc.nette.org/utils/images) - crop, resize, rotate images<br>
[Iterables](https://doc.nette.org/utils/iterables) <br>
[JSON](https://doc.nette.org/utils/json) - encoding and decoding<br>
[Generating Random Strings](https://doc.nette.org/utils/random)<br>
[Paginator](https://doc.nette.org/utils/paginator) - pagination math<br>
[PHP Reflection](https://doc.nette.org/utils/reflection)<br>
[Strings](https://doc.nette.org/utils/strings) - useful text functions<br>
[SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements<br>
[Type](https://doc.nette.org/utils/type) - PHP data type<br>
✅ [Validation](https://doc.nette.org/utils/validators) - validate inputs<br>
 <!---->
Installation
------------
@@ -40,11 +41,9 @@ The recommended way to install is via Composer:
composer require nette/utils
```
- Nette Utils 4.0 is compatible with PHP 8.0 to 8.3
- Nette Utils 3.2 is compatible with PHP 7.2 to 8.3
- Nette Utils 3.1 is compatible with PHP 7.1 to 8.0
- Nette Utils 3.0 is compatible with PHP 7.1 to 8.0
- Nette Utils 2.5 is compatible with PHP 5.6 to 8.0
Nette Utils 4.0 is compatible with PHP 8.0 to 8.4.
 <!---->
[Support Me](https://github.com/sponsors/dg)
--------------------------------------------

View File

@@ -31,26 +31,12 @@ class CachingIterator extends \CachingIterator implements \Countable
private int $counter = 0;
public function __construct($iterator)
public function __construct(iterable|\stdClass $iterable)
{
if (is_array($iterator) || $iterator instanceof \stdClass) {
$iterator = new \ArrayIterator($iterator);
} elseif ($iterator instanceof \IteratorAggregate) {
do {
$iterator = $iterator->getIterator();
} while ($iterator instanceof \IteratorAggregate);
assert($iterator instanceof \Iterator);
} elseif ($iterator instanceof \Iterator) {
} elseif ($iterator instanceof \Traversable) {
$iterator = new \IteratorIterator($iterator);
} else {
throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, get_debug_type($iterator)));
}
parent::__construct($iterator, 0);
$iterable = $iterable instanceof \stdClass
? new \ArrayIterator($iterable)
: Nette\Utils\Iterables::toIterator($iterable);
parent::__construct($iterable, 0);
}

View File

@@ -10,9 +10,8 @@ declare(strict_types=1);
namespace Nette\Iterators;
/**
* Applies the callback to the elements of the inner iterator.
* @deprecated use Nette\Utils\Iterables::map()
*/
class Mapper extends \IteratorIterator
{

View File

@@ -122,10 +122,11 @@ class Arrays
/**
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
* @template T
* @param array<T> $array
* @return ?T
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
*/
public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -138,10 +139,11 @@ class Arrays
/**
* Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
* @template T
* @param array<T> $array
* @return ?T
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
*/
public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -154,7 +156,11 @@ class Arrays
/**
* Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
* The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?K
*/
public static function firstKey(array $array, ?callable $predicate = null): int|string|null
{
@@ -172,7 +178,11 @@ class Arrays
/**
* Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
* The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?K
*/
public static function lastKey(array $array, ?callable $predicate = null): int|string|null
{
@@ -267,7 +277,8 @@ class Arrays
*/
public static function isList(mixed $value): bool
{
return is_array($value) && (PHP_VERSION_ID < 80100
return is_array($value) && (
PHP_VERSION_ID < 80100
? !$value || array_keys($value) === range(0, count($value) - 1)
: array_is_list($value)
);
@@ -368,12 +379,11 @@ class Arrays
/**
* Tests whether at least one element in the array passes the test implemented by the provided function,
* which has the signature `function ($value, $key, array $array): bool`.
* @template K
* Tests whether at least one element in the array passes the test implemented by the provided function.
* @template K of int|string
* @template V
* @param iterable<K, V> $array
* @param callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): bool $predicate
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
*/
public static function some(iterable $array, callable $predicate): bool
{
@@ -388,12 +398,11 @@ class Arrays
/**
* Tests whether all elements in the array pass the test implemented by the provided function,
* which has the signature `function ($value, $key, array $array): bool`.
* @template K
* Tests whether all elements in the array pass the test implemented by the provided function.
* @template K of int|string
* @template V
* @param iterable<K, V> $array
* @param callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): bool $predicate
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
*/
public static function every(iterable $array, callable $predicate): bool
{
@@ -409,11 +418,10 @@ class Arrays
/**
* Returns a new array containing all key-value pairs matching the given $predicate.
* The callback has the signature `function (mixed $value, int|string $key, array $array): bool`.
* @template K of array-key
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
* @return array<K, V>
*/
public static function filter(array $array, callable $predicate): array
@@ -430,12 +438,11 @@ class Arrays
/**
* Returns an array containing the original keys and results of applying the given transform function to each element.
* The function has signature `function ($value, $key, array $array): mixed`.
* @template K of array-key
* @template K of int|string
* @template V
* @template R
* @param iterable<K, V> $array
* @param callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): R $transformer
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): R $transformer
* @return array<K, R>
*/
public static function map(iterable $array, callable $transformer): array
@@ -449,6 +456,31 @@ class Arrays
}
/**
* Returns an array containing new keys and values generated by applying the given transform function to each element.
* If the function returns null, the element is skipped.
* @template K of int|string
* @template V
* @template ResK of int|string
* @template ResV
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): ?array{ResK, ResV} $transformer
* @return array<ResK, ResV>
*/
public static function mapWithKeys(array $array, callable $transformer): array
{
$res = [];
foreach ($array as $k => $v) {
$pair = $transformer($v, $k, $array);
if ($pair) {
$res[$pair[0]] = $pair[1];
}
}
return $res;
}
/**
* Invokes all callbacks and returns array of results.
* @param callable[] $callbacks

View File

@@ -94,7 +94,7 @@ final class Callback
}
if (is_string($callable) && str_contains($callable, '::')) {
return new ReflectionMethod($callable);
return new ReflectionMethod(...explode('::', $callable, 2));
} elseif (is_array($callable)) {
return new ReflectionMethod($callable[0], $callable[1]);
} elseif (is_object($callable) && !$callable instanceof \Closure) {

View File

@@ -271,7 +271,7 @@ final class FileSystem
*/
public static function isAbsolute(string $path): bool
{
return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
}
@@ -280,7 +280,7 @@ final class FileSystem
*/
public static function normalizePath(string $path): string
{
$parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path);
$parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
$res = [];
foreach ($parts as $part) {
if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
@@ -305,6 +305,19 @@ final class FileSystem
}
/**
* Resolves a path against a base path. If the path is absolute, returns it directly, if it's relative, joins it with the base path.
*/
public static function resolvePath(string $basePath, string $path): string
{
return match (true) {
self::isAbsolute($path) => self::platformSlashes($path),
$path === '' => self::platformSlashes($basePath),
default => self::joinPaths($basePath, $path),
};
}
/**
* Converts backslashes to slashes.
*/

View File

@@ -505,6 +505,6 @@ class Finder implements \IteratorAggregate
'\-' => '-',
],
);
return '#' . $anchor . $pattern . '$#D' . (defined('PHP_WINDOWS_VERSION_BUILD') ? 'i' : '');
return '#' . $anchor . $pattern . '$#D' . (Helpers::IsWindows ? 'i' : '');
}
}

View File

@@ -14,6 +14,9 @@ use Nette;
class Helpers
{
public const IsWindows = PHP_OS_FAMILY === 'Windows';
/**
* Executes a callback and returns the captured output as a string.
*/

View File

@@ -163,10 +163,7 @@ class Image
*/
public static function fromFile(string $file, ?int &$type = null): static
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
self::ensureExtension();
$type = self::detectTypeFromFile($file);
if (!$type) {
throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
@@ -183,10 +180,7 @@ class Image
*/
public static function fromString(string $s, ?int &$type = null): static
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
self::ensureExtension();
$type = self::detectTypeFromString($s);
if (!$type) {
throw new UnknownImageFileException('Unknown type of image.');
@@ -221,10 +215,7 @@ class Image
*/
public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
self::ensureExtension();
if ($width < 1 || $height < 1) {
throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
}
@@ -308,6 +299,7 @@ class Image
*/
public static function isTypeSupported(int $type): bool
{
self::ensureExtension();
return (bool) (imagetypes() & match ($type) {
ImageType::JPEG => IMG_JPG,
ImageType::PNG => IMG_PNG,
@@ -323,6 +315,7 @@ class Image
/** @return ImageType[] */
public static function getSupportedTypes(): array
{
self::ensureExtension();
$flag = imagetypes();
return array_filter([
$flag & IMG_GIF ? ImageType::GIF : null,
@@ -640,6 +633,7 @@ class Image
array $options = [],
): array
{
self::ensureExtension();
$box = imagettfbbox($size, $angle, $fontFile, $text, $options);
return [
'left' => $minX = min([$box[0], $box[2], $box[4], $box[6]]),
@@ -724,42 +718,27 @@ class Image
*/
private function output(int $type, ?int $quality, ?string $file = null): void
{
switch ($type) {
case ImageType::JPEG:
$quality = $quality === null ? 85 : max(0, min(100, $quality));
$success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception
break;
[$defQuality, $min, $max] = match ($type) {
ImageType::JPEG => [85, 0, 100],
ImageType::PNG => [9, 0, 9],
ImageType::GIF => [null, null, null],
ImageType::WEBP => [80, 0, 100],
ImageType::AVIF => [30, 0, 100],
ImageType::BMP => [null, null, null],
default => throw new Nette\InvalidArgumentException("Unsupported image type '$type'."),
};
case ImageType::PNG:
$quality = $quality === null ? 9 : max(0, min(9, $quality));
$success = @imagepng($this->image, $file, $quality); // @ is escalated to exception
break;
case ImageType::GIF:
$success = @imagegif($this->image, $file); // @ is escalated to exception
break;
case ImageType::WEBP:
$quality = $quality === null ? 80 : max(0, min(100, $quality));
$success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception
break;
case ImageType::AVIF:
$quality = $quality === null ? 30 : max(0, min(100, $quality));
$success = @imageavif($this->image, $file, $quality); // @ is escalated to exception
break;
case ImageType::BMP:
$success = @imagebmp($this->image, $file); // @ is escalated to exception
break;
default:
throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
$args = [$this->image, $file];
if ($defQuality !== null) {
$args[] = $quality === null ? $defQuality : max($min, min($max, $quality));
}
if (!$success) {
throw new ImageException(Helpers::getLastError() ?: 'Unknown error');
}
Callback::invokeSafe('image' . self::Formats[$type], $args, function (string $message) use ($file): void {
if ($file !== null) {
@unlink($file);
}
throw new ImageException($message);
});
}
@@ -826,4 +805,12 @@ class Image
$color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color);
return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color);
}
private static function ensureExtension(): void
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
}
}

View File

@@ -49,10 +49,11 @@ final class Iterables
/**
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* The $predicate has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template T
* @param iterable<T> $iterable
* @return ?T
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param ?callable(V, K, iterable<K, V>): bool $predicate
* @return ?V
*/
public static function first(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -67,10 +68,11 @@ final class Iterables
/**
* 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.
* The $predicate has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template T
* @param iterable<T, mixed> $iterable
* @return ?T
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param ?callable(V, K, iterable<K, V>): bool $predicate
* @return ?K
*/
public static function firstKey(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
@@ -84,11 +86,10 @@ final class Iterables
/**
* Tests whether at least one element in the iterator passes the test implemented by the
* provided callback with signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* Tests whether at least one element in the iterator passes the test implemented by the provided function.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
*/
public static function some(iterable $iterable, callable $predicate): bool
@@ -103,11 +104,10 @@ final class Iterables
/**
* Tests whether all elements in the iterator pass the test implemented by the provided function,
* which has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* Tests whether all elements in the iterator pass the test implemented by the provided function.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
*/
public static function every(iterable $iterable, callable $predicate): bool
@@ -123,11 +123,10 @@ final class Iterables
/**
* Iterator that filters elements according to a given $predicate. Maintains original keys.
* The callback has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
* @return \Generator<K, V>
*/
public static function filter(iterable $iterable, callable $predicate): \Generator
@@ -142,12 +141,11 @@ final class Iterables
/**
* Iterator that transforms values by calling $transformer. Maintains original keys.
* The callback has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template K
* @template V
* @template R
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): R $transformer
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): R $transformer
* @return \Generator<K, R>
*/
public static function map(iterable $iterable, callable $transformer): \Generator
@@ -156,4 +154,86 @@ final class Iterables
yield $k => $transformer($v, $k, $iterable);
}
}
/**
* 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
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): ?array{ResV, ResK} $transformer
* @return \Generator<ResV, ResK>
*/
public static function mapWithKeys(iterable $iterable, callable $transformer): \Generator
{
foreach ($iterable as $k => $v) {
$pair = $transformer($v, $k, $iterable);
if ($pair) {
yield $pair[0] => $pair[1];
}
}
}
/**
* Wraps around iterator and caches its keys and values during iteration.
* This allows the data to be re-iterated multiple times.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @return \IteratorAggregate<K, V>
*/
public static function memoize(iterable $iterable): iterable
{
return new class (self::toIterator($iterable)) implements \IteratorAggregate {
public function __construct(
private \Iterator $iterator,
private array $cache = [],
) {
}
public function getIterator(): \Generator
{
if (!$this->cache) {
$this->iterator->rewind();
}
$i = 0;
while (true) {
if (isset($this->cache[$i])) {
[$k, $v] = $this->cache[$i];
} elseif ($this->iterator->valid()) {
$k = $this->iterator->key();
$v = $this->iterator->current();
$this->iterator->next();
$this->cache[$i] = [$k, $v];
} else {
break;
}
yield $k => $v;
$i++;
}
}
};
}
/**
* Creates an iterator from anything that is iterable.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @return \Iterator<K, V>
*/
public static function toIterator(iterable $iterable): \Iterator
{
return match (true) {
$iterable instanceof \Iterator => $iterable,
$iterable instanceof \IteratorAggregate => self::toIterator($iterable->getIterator()),
is_array($iterable) => new \ArrayIterator($iterable),
default => throw new Nette\ShouldNotHappenException,
};
}
}

View File

@@ -19,14 +19,14 @@ final class Reflection
{
use Nette\StaticClass;
/** @deprecated use Nette\Utils\Validator::isBuiltinType() */
/** @deprecated use Nette\Utils\Validators::isBuiltinType() */
public static function isBuiltinType(string $type): bool
{
return Validators::isBuiltinType($type);
}
/** @deprecated use Nette\Utils\Validator::isClassKeyword() */
/** @deprecated use Nette\Utils\Validators::isClassKeyword() */
public static function isClassKeyword(string $name): bool
{
return Validators::isClassKeyword($name);
@@ -100,7 +100,7 @@ final class Reflection
$hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()];
if (($alias = $decl->getTraitAliases()[$method->name] ?? null)
&& ($m = new \ReflectionMethod($alias))
&& ($m = new \ReflectionMethod(...explode('::', $alias, 2)))
&& $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]
) {
return self::getMethodDeclaringMethod($m);
@@ -125,7 +125,7 @@ final class Reflection
public static function areCommentsAvailable(): bool
{
static $res;
return $res ?? $res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment();
return $res ?? $res = (bool) (new \ReflectionMethod(self::class, __FUNCTION__))->getDocComment();
}
@@ -136,7 +136,9 @@ final class Reflection
} elseif ($ref instanceof \ReflectionMethod) {
return $ref->getDeclaringClass()->name . '::' . $ref->name . '()';
} elseif ($ref instanceof \ReflectionFunction) {
return $ref->name . '()';
return PHP_VERSION_ID >= 80200 && $ref->isAnonymous()
? '{closure}()'
: $ref->name . '()';
} elseif ($ref instanceof \ReflectionProperty) {
return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name;
} elseif ($ref instanceof \ReflectionParameter) {

View File

@@ -28,7 +28,7 @@ class Strings
/**
* @deprecated use Nette\Utils\Validator::isUnicode()
* @deprecated use Nette\Utils\Validators::isUnicode()
*/
public static function checkEncoding(string $s): bool
{
@@ -547,7 +547,6 @@ class Strings
return $utf8 && $captureOffset
? self::bytesToChars($subject, [$m])[0]
: $m;
}
@@ -589,6 +588,7 @@ class Strings
/**
* 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[])
*/
public static function matchAll(
string $subject,
@@ -599,21 +599,41 @@ class Strings
bool $unmatchedAsNull = false,
bool $patternOrder = false,
bool $utf8 = false,
): array
bool $lazy = false,
): array|\Generator
{
$flags = is_int($captureOffset) // back compatibility
? $captureOffset
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0);
if ($utf8) {
$offset = strlen(self::substring($subject, 0, $offset));
$pattern .= 'u';
}
if ($lazy) {
$flags = PREG_OFFSET_CAPTURE | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
return (function () use ($utf8, $captureOffset, $flags, $subject, $pattern, $offset) {
$counter = 0;
while (
$offset <= strlen($subject) - ($counter ? 1 : 0)
&& self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])
) {
$offset = $m[0][1] + max(1, strlen($m[0][0]));
if (!$captureOffset) {
$m = array_map(fn($item) => $item[0], $m);
} elseif ($utf8) {
$m = self::bytesToChars($subject, [$m])[0];
}
yield $counter++ => $m;
}
})();
}
if ($offset > strlen($subject)) {
return [];
}
$flags = is_int($captureOffset) // back compatibility
? $captureOffset
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0);
self::pcre('preg_match_all', [
$pattern, $subject, &$m,
($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER),
@@ -622,7 +642,6 @@ class Strings
return $utf8 && $captureOffset
? self::bytesToChars($subject, $m)
: $m;
}

View File

@@ -260,8 +260,8 @@ final class Type
$subtypes,
fn($subtype) => Validators::isBuiltinType($type)
? strcasecmp($type, $subtype) === 0
: is_a($subtype, $type, allow_string: true)
)
: is_a($subtype, $type, allow_string: true),
),
);
}
}

View File

@@ -11,7 +11,7 @@ namespace Nette\Utils;
/**
* The exception that is thrown when an image error occurs.
* An error occurred while working with the image.
*/
class ImageException extends \Exception
{
@@ -19,7 +19,7 @@ class ImageException extends \Exception
/**
* The exception that indicates invalid image file.
* The image file is invalid or in an unsupported format.
*/
class UnknownImageFileException extends ImageException
{
@@ -27,7 +27,7 @@ class UnknownImageFileException extends ImageException
/**
* The exception that indicates error of JSON encoding/decoding.
* JSON encoding or decoding failed.
*/
class JsonException extends \JsonException
{
@@ -35,7 +35,7 @@ class JsonException extends \JsonException
/**
* The exception that indicates error of the last Regexp execution.
* Regular expression pattern or execution failed.
*/
class RegexpException extends \Exception
{
@@ -43,7 +43,7 @@ class RegexpException extends \Exception
/**
* The exception that indicates assertion error.
* Type validation failed. The value doesn't match the expected type constraints.
*/
class AssertionException extends \Exception
{

View File

@@ -11,8 +11,7 @@ namespace Nette;
/**
* The exception that is thrown when the value of an argument is
* outside the allowable range of values as defined by the invoked method.
* The value is outside the allowed range.
*/
class ArgumentOutOfRangeException extends \InvalidArgumentException
{
@@ -20,8 +19,7 @@ class ArgumentOutOfRangeException extends \InvalidArgumentException
/**
* The exception that is thrown when a method call is invalid for the object's
* current state, method has been invoked at an illegal or inappropriate time.
* The object is in a state that does not allow the requested operation.
*/
class InvalidStateException extends \RuntimeException
{
@@ -29,7 +27,7 @@ class InvalidStateException extends \RuntimeException
/**
* The exception that is thrown when a requested method or operation is not implemented.
* The requested feature is not implemented.
*/
class NotImplementedException extends \LogicException
{
@@ -37,8 +35,7 @@ class NotImplementedException extends \LogicException
/**
* The exception that is thrown when an invoked method is not supported. For scenarios where
* it is sometimes possible to perform the requested operation, see InvalidStateException.
* The requested operation is not supported.
*/
class NotSupportedException extends \LogicException
{
@@ -46,7 +43,7 @@ class NotSupportedException extends \LogicException
/**
* The exception that is thrown when a requested method or operation is deprecated.
* The requested feature is deprecated and no longer available.
*/
class DeprecatedException extends NotSupportedException
{
@@ -54,7 +51,7 @@ class DeprecatedException extends NotSupportedException
/**
* The exception that is thrown when accessing a class member (property or method) fails.
* Cannot access the requested class property or method.
*/
class MemberAccessException extends \Error
{
@@ -62,7 +59,7 @@ class MemberAccessException extends \Error
/**
* The exception that is thrown when an I/O error occurs.
* Failed to read from or write to a file or stream.
*/
class IOException extends \RuntimeException
{
@@ -70,7 +67,7 @@ class IOException extends \RuntimeException
/**
* The exception that is thrown when accessing a file that does not exist on disk.
* The requested file does not exist.
*/
class FileNotFoundException extends IOException
{
@@ -78,7 +75,7 @@ class FileNotFoundException extends IOException
/**
* The exception that is thrown when part of a file or directory cannot be found.
* The requested directory does not exist.
*/
class DirectoryNotFoundException extends IOException
{
@@ -86,7 +83,7 @@ class DirectoryNotFoundException extends IOException
/**
* The exception that is thrown when an argument does not match with the expected value.
* The provided argument has invalid type or format.
*/
class InvalidArgumentException extends \InvalidArgumentException
{
@@ -94,7 +91,7 @@ class InvalidArgumentException extends \InvalidArgumentException
/**
* The exception that is thrown when an illegal index was requested.
* The requested array or collection index does not exist.
*/
class OutOfRangeException extends \OutOfRangeException
{
@@ -102,8 +99,16 @@ class OutOfRangeException extends \OutOfRangeException
/**
* The exception that is thrown when a value (typically returned by function) does not match with the expected value.
* The returned value has unexpected type or format.
*/
class UnexpectedValueException extends \UnexpectedValueException
{
}
/**
* Houston, we have a problem.
*/
class ShouldNotHappenException extends \LogicException
{
}