mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-12 11:11:31 +00:00
resolved conflicts
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Iterators;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Smarter caching iterator.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
class CachingIterator extends \CachingIterator implements \Countable
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
private int $counter = 0;
|
||||
|
||||
|
||||
public function __construct(iterable|\stdClass $iterable)
|
||||
{
|
||||
$iterable = $iterable instanceof \stdClass
|
||||
? new \ArrayIterator($iterable)
|
||||
: Nette\Utils\Iterables::toIterator($iterable);
|
||||
parent::__construct($iterable, 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the current element the first one?
|
||||
*/
|
||||
public function isFirst(?int $gridWidth = null): bool
|
||||
{
|
||||
return $this->counter === 1 || ($gridWidth && $this->counter !== 0 && (($this->counter - 1) % $gridWidth) === 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the current element the last one?
|
||||
*/
|
||||
public function isLast(?int $gridWidth = null): bool
|
||||
{
|
||||
return !$this->hasNext() || ($gridWidth && ($this->counter % $gridWidth) === 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if ($inner instanceof \Countable) {
|
||||
return $inner->count();
|
||||
|
||||
} else {
|
||||
throw new Nette\NotSupportedException('Iterator is not countable.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Forwards to the next element.
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
parent::next();
|
||||
if (parent::valid()) {
|
||||
$this->counter++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rewinds the Iterator.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
parent::rewind();
|
||||
$this->counter = parent::valid() ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next key.
|
||||
*/
|
||||
public function getNextKey(): mixed
|
||||
{
|
||||
return $this->getInnerIterator()->key();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next element.
|
||||
*/
|
||||
public function getNextValue(): mixed
|
||||
{
|
||||
return $this->getInnerIterator()->current();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Iterators;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use Nette\Utils\Iterables::map()
|
||||
*/
|
||||
class Mapper extends \IteratorIterator
|
||||
{
|
||||
/** @var callable */
|
||||
private $callback;
|
||||
|
||||
|
||||
public function __construct(\Traversable $iterator, callable $callback)
|
||||
{
|
||||
parent::__construct($iterator);
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
|
||||
public function current(): mixed
|
||||
{
|
||||
return ($this->callback)(parent::current(), parent::key());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use JetBrains\PhpStorm\Language;
|
||||
use Nette;
|
||||
use function is_array, is_int, is_object, count;
|
||||
|
||||
|
||||
/**
|
||||
* Array tools library.
|
||||
*/
|
||||
class Arrays
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/**
|
||||
* Returns item from array. If it does not exist, it throws an exception, unless a default value is set.
|
||||
* @template T
|
||||
* @param array<T> $array
|
||||
* @param array-key|array-key[] $key
|
||||
* @param ?T $default
|
||||
* @return ?T
|
||||
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
|
||||
*/
|
||||
public static function get(array $array, string|int|array $key, mixed $default = null): mixed
|
||||
{
|
||||
foreach (is_array($key) ? $key : [$key] as $k) {
|
||||
if (is_array($array) && array_key_exists($k, $array)) {
|
||||
$array = $array[$k];
|
||||
} else {
|
||||
if (func_num_args() < 3) {
|
||||
throw new Nette\InvalidArgumentException("Missing item '$k'.");
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns reference to array item. If the index does not exist, new one is created with value null.
|
||||
* @template T
|
||||
* @param array<T> $array
|
||||
* @param array-key|array-key[] $key
|
||||
* @return ?T
|
||||
* @throws Nette\InvalidArgumentException if traversed item is not an array
|
||||
*/
|
||||
public static function &getRef(array &$array, string|int|array $key): mixed
|
||||
{
|
||||
foreach (is_array($key) ? $key : [$key] as $k) {
|
||||
if (is_array($array) || $array === null) {
|
||||
$array = &$array[$k];
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException('Traversed item is not an array.');
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as
|
||||
* the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains
|
||||
* the value from the first array in the case of a key collision.
|
||||
* @template T1
|
||||
* @template T2
|
||||
* @param array<T1> $array1
|
||||
* @param array<T2> $array2
|
||||
* @return array<T1|T2>
|
||||
*/
|
||||
public static function mergeTree(array $array1, array $array2): array
|
||||
{
|
||||
$res = $array1 + $array2;
|
||||
foreach (array_intersect_key($array1, $array2) as $k => $v) {
|
||||
if (is_array($v) && is_array($array2[$k])) {
|
||||
$res[$k] = self::mergeTree($v, $array2[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns zero-indexed position of given array key. Returns null if key is not found.
|
||||
*/
|
||||
public static function getKeyOffset(array $array, string|int $key): ?int
|
||||
{
|
||||
return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use getKeyOffset()
|
||||
*/
|
||||
public static function searchKey(array $array, $key): ?int
|
||||
{
|
||||
return self::getKeyOffset($array, $key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests an array for the presence of value.
|
||||
*/
|
||||
public static function contains(array $array, mixed $value): bool
|
||||
{
|
||||
return in_array($value, $array, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
{
|
||||
$key = self::firstKey($array, $predicate);
|
||||
return $key === null
|
||||
? ($else ? $else() : null)
|
||||
: $array[$key];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
{
|
||||
$key = self::lastKey($array, $predicate);
|
||||
return $key === null
|
||||
? ($else ? $else() : null)
|
||||
: $array[$key];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
|
||||
* @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
|
||||
{
|
||||
if (!$predicate) {
|
||||
return array_key_first($array);
|
||||
}
|
||||
foreach ($array as $k => $v) {
|
||||
if ($predicate($v, $k, $array)) {
|
||||
return $k;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
|
||||
* @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
|
||||
{
|
||||
return $predicate
|
||||
? self::firstKey(array_reverse($array, preserve_keys: true), $predicate)
|
||||
: array_key_last($array);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
|
||||
{
|
||||
$offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key);
|
||||
$array = array_slice($array, 0, $offset, preserve_keys: true)
|
||||
+ $inserted
|
||||
+ array_slice($array, $offset, count($array), preserve_keys: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
|
||||
{
|
||||
if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) {
|
||||
$offset = count($array) - 1;
|
||||
}
|
||||
|
||||
$array = array_slice($array, 0, $offset + 1, preserve_keys: true)
|
||||
+ $inserted
|
||||
+ array_slice($array, $offset + 1, count($array), preserve_keys: true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renames key in array.
|
||||
*/
|
||||
public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
|
||||
{
|
||||
$offset = self::getKeyOffset($array, $oldKey);
|
||||
if ($offset === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$val = &$array[$oldKey];
|
||||
$keys = array_keys($array);
|
||||
$keys[$offset] = $newKey;
|
||||
$array = array_combine($keys, $array);
|
||||
$array[$newKey] = &$val;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns only those array items, which matches a regular expression $pattern.
|
||||
* @param string[] $array
|
||||
* @return string[]
|
||||
*/
|
||||
public static function grep(
|
||||
array $array,
|
||||
#[Language('RegExp')]
|
||||
string $pattern,
|
||||
bool|int $invert = false,
|
||||
): array
|
||||
{
|
||||
$flags = $invert ? PREG_GREP_INVERT : 0;
|
||||
return Strings::pcre('preg_grep', [$pattern, $array, $flags]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transforms multidimensional array to flat array.
|
||||
*/
|
||||
public static function flatten(array $array, bool $preserveKeys = false): array
|
||||
{
|
||||
$res = [];
|
||||
$cb = $preserveKeys
|
||||
? function ($v, $k) use (&$res): void { $res[$k] = $v; }
|
||||
: function ($v) use (&$res): void { $res[] = $v; };
|
||||
array_walk_recursive($array, $cb);
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list.
|
||||
* @return ($value is list ? true : false)
|
||||
*/
|
||||
public static function isList(mixed $value): bool
|
||||
{
|
||||
return is_array($value) && (PHP_VERSION_ID < 80100
|
||||
? !$value || array_keys($value) === range(0, count($value) - 1)
|
||||
: array_is_list($value)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
|
||||
* @param string|string[] $path
|
||||
*/
|
||||
public static function associate(array $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'.");
|
||||
}
|
||||
|
||||
$res = $parts[0] === '->' ? new \stdClass : [];
|
||||
|
||||
foreach ($array as $rowOrig) {
|
||||
$row = (array) $rowOrig;
|
||||
$x = &$res;
|
||||
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
$part = $parts[$i];
|
||||
if ($part === '[]') {
|
||||
$x = &$x[];
|
||||
|
||||
} elseif ($part === '=') {
|
||||
if (isset($parts[++$i])) {
|
||||
$x = $row[$parts[$i]];
|
||||
$row = null;
|
||||
}
|
||||
} elseif ($part === '->') {
|
||||
if (isset($parts[++$i])) {
|
||||
if ($x === null) {
|
||||
$x = new \stdClass;
|
||||
}
|
||||
|
||||
$x = &$x->{$row[$parts[$i]]};
|
||||
} else {
|
||||
$row = is_object($rowOrig) ? $rowOrig : (object) $row;
|
||||
}
|
||||
} elseif ($part !== '|') {
|
||||
$x = &$x[(string) $row[$part]];
|
||||
}
|
||||
}
|
||||
|
||||
if ($x === null) {
|
||||
$x = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
|
||||
*/
|
||||
public static function normalize(array $array, mixed $filling = null): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns and removes the value of an item from an array. If it does not exist, it throws an exception,
|
||||
* or returns $default, if provided.
|
||||
* @template T
|
||||
* @param array<T> $array
|
||||
* @param ?T $default
|
||||
* @return ?T
|
||||
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
|
||||
*/
|
||||
public static function pick(array &$array, string|int $key, mixed $default = null): mixed
|
||||
{
|
||||
if (array_key_exists($key, $array)) {
|
||||
$value = $array[$key];
|
||||
unset($array[$key]);
|
||||
return $value;
|
||||
|
||||
} elseif (func_num_args() < 3) {
|
||||
throw new Nette\InvalidArgumentException("Missing item '$key'.");
|
||||
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): bool $predicate
|
||||
*/
|
||||
public static function some(iterable $array, callable $predicate): bool
|
||||
{
|
||||
foreach ($array as $k => $v) {
|
||||
if ($predicate($v, $k, $array)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests whether all elements in the array pass the test implemented by the provided function.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @param array<K, V> $array
|
||||
* @param callable(V, K, array<K, V>): bool $predicate
|
||||
*/
|
||||
public static function every(iterable $array, callable $predicate): bool
|
||||
{
|
||||
foreach ($array as $k => $v) {
|
||||
if (!$predicate($v, $k, $array)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new array containing all key-value pairs matching the given $predicate.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @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
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
if ($predicate($v, $k, $array)) {
|
||||
$res[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array containing the original keys and results of applying the given transform function to each element.
|
||||
* @template K of int|string
|
||||
* @template V
|
||||
* @template R
|
||||
* @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
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$res[$k] = $transformer($v, $k, $array);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function invoke(iterable $callbacks, ...$args): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($callbacks as $k => $cb) {
|
||||
$res[$k] = $cb(...$args);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invokes method on every object in an array and returns array of results.
|
||||
* @param object[] $objects
|
||||
*/
|
||||
public static function invokeMethod(iterable $objects, string $method, ...$args): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($objects as $k => $obj) {
|
||||
$res[$k] = $obj->$method(...$args);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copies the elements of the $array array to the $object object and then returns it.
|
||||
* @template T of object
|
||||
* @param T $object
|
||||
* @return T
|
||||
*/
|
||||
public static function toObject(iterable $array, object $object): object
|
||||
{
|
||||
foreach ($array as $k => $v) {
|
||||
$object->$k = $v;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts value to array key.
|
||||
*/
|
||||
public static function toKey(mixed $value): int|string
|
||||
{
|
||||
return key([$value => null]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns copy of the $array where every item is converted to string
|
||||
* and prefixed by $prefix and suffixed by $suffix.
|
||||
* @param string[] $array
|
||||
* @return string[]
|
||||
*/
|
||||
public static function wrap(array $array, string $prefix = '', string $suffix = ''): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$res[$k] = $prefix . $v . $suffix;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
use function is_array, is_object, is_string;
|
||||
|
||||
|
||||
/**
|
||||
* PHP callable tools.
|
||||
*/
|
||||
final class Callback
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/**
|
||||
* Invokes internal PHP function with own error handler.
|
||||
*/
|
||||
public static function invokeSafe(string $function, array $args, callable $onError): mixed
|
||||
{
|
||||
$prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
|
||||
if ($file === __FILE__) {
|
||||
$msg = ini_get('html_errors')
|
||||
? Html::htmlToText($message)
|
||||
: $message;
|
||||
$msg = preg_replace("#^$function\\(.*?\\): #", '', $msg);
|
||||
if ($onError($msg, $severity) !== false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $prev ? $prev(...func_get_args()) : false;
|
||||
});
|
||||
|
||||
try {
|
||||
return $function(...$args);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies
|
||||
* that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists.
|
||||
* @return callable
|
||||
* @throws Nette\InvalidArgumentException
|
||||
*/
|
||||
public static function check(mixed $callable, bool $syntax = false)
|
||||
{
|
||||
if (!is_callable($callable, $syntax)) {
|
||||
throw new Nette\InvalidArgumentException(
|
||||
$syntax
|
||||
? 'Given value is not a callable type.'
|
||||
: sprintf("Callback '%s' is not callable.", self::toString($callable)),
|
||||
);
|
||||
}
|
||||
|
||||
return $callable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts PHP callback to textual form. Class or method may not exists.
|
||||
*/
|
||||
public static function toString(mixed $callable): string
|
||||
{
|
||||
if ($callable instanceof \Closure) {
|
||||
$inner = self::unwrap($callable);
|
||||
return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}');
|
||||
} else {
|
||||
is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual);
|
||||
return $textual;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns reflection for method or function used in PHP callback.
|
||||
* @param callable $callable type check is escalated to ReflectionException
|
||||
* @throws \ReflectionException if callback is not valid
|
||||
*/
|
||||
public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
|
||||
{
|
||||
if ($callable instanceof \Closure) {
|
||||
$callable = self::unwrap($callable);
|
||||
}
|
||||
|
||||
if (is_string($callable) && str_contains($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) {
|
||||
return new ReflectionMethod($callable, '__invoke');
|
||||
} else {
|
||||
return new \ReflectionFunction($callable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether PHP callback is function or static method.
|
||||
*/
|
||||
public static function isStatic(callable $callable): bool
|
||||
{
|
||||
return is_string(is_array($callable) ? $callable[0] : $callable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unwraps closure created by Closure::fromCallable().
|
||||
*/
|
||||
public static function unwrap(\Closure $closure): callable|array
|
||||
{
|
||||
$r = new \ReflectionFunction($closure);
|
||||
$class = $r->getClosureScopeClass()?->name;
|
||||
if (str_ends_with($r->name, '}')) {
|
||||
return $closure;
|
||||
|
||||
} elseif (($obj = $r->getClosureThis()) && $obj::class === $class) {
|
||||
return [$obj, $r->name];
|
||||
|
||||
} elseif ($class) {
|
||||
return [$class, $r->name];
|
||||
|
||||
} else {
|
||||
return $r->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,831 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Basic manipulation with images. Supported types are JPEG, PNG, GIF, WEBP, AVIF and BMP.
|
||||
*
|
||||
* <code>
|
||||
* $image = Image::fromFile('nette.jpg');
|
||||
* $image->resize(150, 100);
|
||||
* $image->sharpen();
|
||||
* $image->send();
|
||||
* </code>
|
||||
*
|
||||
* @method Image affine(array $affine, ?array $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)
|
||||
* @method int colorAllocate(int $red, int $green, int $blue)
|
||||
* @method int colorAllocateAlpha(int $red, int $green, int $blue, int $alpha)
|
||||
* @method int colorAt(int $x, int $y)
|
||||
* @method int colorClosest(int $red, int $green, int $blue)
|
||||
* @method int colorClosestAlpha(int $red, int $green, int $blue, int $alpha)
|
||||
* @method int colorClosestHWB(int $red, int $green, int $blue)
|
||||
* @method void colorDeallocate(int $color)
|
||||
* @method int colorExact(int $red, int $green, int $blue)
|
||||
* @method int colorExactAlpha(int $red, int $green, int $blue, int $alpha)
|
||||
* @method void colorMatch(Image $image2)
|
||||
* @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 int colorsTotal()
|
||||
* @method int colorTransparent(?int $color = null)
|
||||
* @method void convolution(array $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 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 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 void gammaCorrect(float $inputgamma, float $outputgamma)
|
||||
* @method array 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 paletteCopy(Image $source)
|
||||
* @method void paletteToTrueColor()
|
||||
* @method void polygon(array $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 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 setPixel(int $x, int $y, ImageColor $color)
|
||||
* @method void setStyle(array $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 = [])
|
||||
* @property-read positive-int $width
|
||||
* @property-read positive-int $height
|
||||
* @property-read \GdImage $imageResource
|
||||
*/
|
||||
class Image
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** Prevent from getting resized to a bigger size than the original */
|
||||
public const ShrinkOnly = 0b0001;
|
||||
|
||||
/** Resizes to a specified width and height without keeping aspect ratio */
|
||||
public const Stretch = 0b0010;
|
||||
|
||||
/** Resizes to fit into a specified width and height and preserves aspect ratio */
|
||||
public const OrSmaller = 0b0000;
|
||||
|
||||
/** Resizes while bounding the smaller dimension to the specified width or height and preserves aspect ratio */
|
||||
public const OrBigger = 0b0100;
|
||||
|
||||
/** Resizes to the smallest possible size to completely cover specified width and height and reserves aspect ratio */
|
||||
public const Cover = 0b1000;
|
||||
|
||||
/** @deprecated use Image::ShrinkOnly */
|
||||
public const SHRINK_ONLY = self::ShrinkOnly;
|
||||
|
||||
/** @deprecated use Image::Stretch */
|
||||
public const STRETCH = self::Stretch;
|
||||
|
||||
/** @deprecated use Image::OrSmaller */
|
||||
public const FIT = self::OrSmaller;
|
||||
|
||||
/** @deprecated use Image::OrBigger */
|
||||
public const FILL = self::OrBigger;
|
||||
|
||||
/** @deprecated use Image::Cover */
|
||||
public const EXACT = self::Cover;
|
||||
|
||||
/** @deprecated use Image::EmptyGIF */
|
||||
public const EMPTY_GIF = self::EmptyGIF;
|
||||
|
||||
/** image types */
|
||||
public const
|
||||
JPEG = ImageType::JPEG,
|
||||
PNG = ImageType::PNG,
|
||||
GIF = ImageType::GIF,
|
||||
WEBP = ImageType::WEBP,
|
||||
AVIF = ImageType::AVIF,
|
||||
BMP = ImageType::BMP;
|
||||
|
||||
public const EmptyGIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
|
||||
|
||||
private const Formats = [ImageType::JPEG => 'jpeg', ImageType::PNG => 'png', ImageType::GIF => 'gif', ImageType::WEBP => 'webp', ImageType::AVIF => 'avif', ImageType::BMP => 'bmp'];
|
||||
|
||||
private \GdImage $image;
|
||||
|
||||
|
||||
/**
|
||||
* Returns RGB color (0..255) and transparency (0..127).
|
||||
* @deprecated use ImageColor::rgb()
|
||||
*/
|
||||
public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
|
||||
{
|
||||
return [
|
||||
'red' => max(0, min(255, $red)),
|
||||
'green' => max(0, min(255, $green)),
|
||||
'blue' => max(0, min(255, $blue)),
|
||||
'alpha' => max(0, min(127, $transparency)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads an image from a file and returns its type in $type.
|
||||
* @throws Nette\NotSupportedException if gd extension is not loaded
|
||||
* @throws UnknownImageFileException if file not found or file type is not known
|
||||
*/
|
||||
public static function fromFile(string $file, ?int &$type = null): static
|
||||
{
|
||||
self::ensureExtension();
|
||||
$type = self::detectTypeFromFile($file);
|
||||
if (!$type) {
|
||||
throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
|
||||
}
|
||||
|
||||
return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads an image from a string and returns its type in $type.
|
||||
* @throws Nette\NotSupportedException if gd extension is not loaded
|
||||
* @throws ImageException
|
||||
*/
|
||||
public static function fromString(string $s, ?int &$type = null): static
|
||||
{
|
||||
self::ensureExtension();
|
||||
$type = self::detectTypeFromString($s);
|
||||
if (!$type) {
|
||||
throw new UnknownImageFileException('Unknown type of image.');
|
||||
}
|
||||
|
||||
return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__);
|
||||
}
|
||||
|
||||
|
||||
private static function invokeSafe(string $func, string $arg, string $message, string $callee): static
|
||||
{
|
||||
$errors = [];
|
||||
$res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void {
|
||||
$errors[] = $message;
|
||||
});
|
||||
|
||||
if (!$res) {
|
||||
throw new ImageException($message . ' Errors: ' . implode(', ', $errors));
|
||||
} elseif ($errors) {
|
||||
trigger_error($callee . '(): ' . implode(', ', $errors), E_USER_WARNING);
|
||||
}
|
||||
|
||||
return new static($res);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new true color image of the given dimensions. The default color is black.
|
||||
* @param positive-int $width
|
||||
* @param positive-int $height
|
||||
* @throws Nette\NotSupportedException if gd extension is not loaded
|
||||
*/
|
||||
public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
|
||||
{
|
||||
self::ensureExtension();
|
||||
if ($width < 1 || $height < 1) {
|
||||
throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
|
||||
}
|
||||
|
||||
$image = new static(imagecreatetruecolor($width, $height));
|
||||
if ($color) {
|
||||
$image->alphablending(false);
|
||||
$image->filledrectangle(0, 0, $width - 1, $height - 1, $color);
|
||||
$image->alphablending(true);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the type of image from file.
|
||||
* @return ImageType::*|null
|
||||
*/
|
||||
public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int
|
||||
{
|
||||
[$width, $height, $type] = @getimagesize($file); // @ - files smaller than 12 bytes causes read error
|
||||
return isset(self::Formats[$type]) ? $type : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the type of image from string.
|
||||
* @return ImageType::*|null
|
||||
*/
|
||||
public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int
|
||||
{
|
||||
[$width, $height, $type] = @getimagesizefromstring($s); // @ - strings smaller than 12 bytes causes read error
|
||||
return isset(self::Formats[$type]) ? $type : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the file extension for the given image type.
|
||||
* @param ImageType::* $type
|
||||
* @return value-of<self::Formats>
|
||||
*/
|
||||
public static function typeToExtension(int $type): string
|
||||
{
|
||||
if (!isset(self::Formats[$type])) {
|
||||
throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
|
||||
}
|
||||
|
||||
return self::Formats[$type];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the image type for given file extension.
|
||||
* @return ImageType::*
|
||||
*/
|
||||
public static function extensionToType(string $extension): int
|
||||
{
|
||||
$extensions = array_flip(self::Formats) + ['jpg' => ImageType::JPEG];
|
||||
$extension = strtolower($extension);
|
||||
if (!isset($extensions[$extension])) {
|
||||
throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'.");
|
||||
}
|
||||
|
||||
return $extensions[$extension];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the mime type for the given image type.
|
||||
* @param ImageType::* $type
|
||||
*/
|
||||
public static function typeToMimeType(int $type): string
|
||||
{
|
||||
return 'image/' . self::typeToExtension($type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param ImageType::* $type
|
||||
*/
|
||||
public static function isTypeSupported(int $type): bool
|
||||
{
|
||||
self::ensureExtension();
|
||||
return (bool) (imagetypes() & match ($type) {
|
||||
ImageType::JPEG => IMG_JPG,
|
||||
ImageType::PNG => IMG_PNG,
|
||||
ImageType::GIF => IMG_GIF,
|
||||
ImageType::WEBP => IMG_WEBP,
|
||||
ImageType::AVIF => 256, // IMG_AVIF,
|
||||
ImageType::BMP => IMG_BMP,
|
||||
default => 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** @return ImageType[] */
|
||||
public static function getSupportedTypes(): array
|
||||
{
|
||||
self::ensureExtension();
|
||||
$flag = imagetypes();
|
||||
return array_filter([
|
||||
$flag & IMG_GIF ? ImageType::GIF : null,
|
||||
$flag & IMG_JPG ? ImageType::JPEG : null,
|
||||
$flag & IMG_PNG ? ImageType::PNG : null,
|
||||
$flag & IMG_WEBP ? ImageType::WEBP : null,
|
||||
$flag & 256 ? ImageType::AVIF : null, // IMG_AVIF
|
||||
$flag & IMG_BMP ? ImageType::BMP : null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wraps GD image.
|
||||
*/
|
||||
public function __construct(\GdImage $image)
|
||||
{
|
||||
$this->setImageResource($image);
|
||||
imagesavealpha($image, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns image width.
|
||||
* @return positive-int
|
||||
*/
|
||||
public function getWidth(): int
|
||||
{
|
||||
return imagesx($this->image);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns image height.
|
||||
* @return positive-int
|
||||
*/
|
||||
public function getHeight(): int
|
||||
{
|
||||
return imagesy($this->image);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets image resource.
|
||||
*/
|
||||
protected function setImageResource(\GdImage $image): static
|
||||
{
|
||||
$this->image = $image;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns image GD resource.
|
||||
*/
|
||||
public function getImageResource(): \GdImage
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scales an image. Width and height accept pixels or percent.
|
||||
* @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode
|
||||
*/
|
||||
public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static
|
||||
{
|
||||
if ($mode & self::Cover) {
|
||||
return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height);
|
||||
}
|
||||
|
||||
[$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode);
|
||||
|
||||
if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
|
||||
$newImage = static::fromBlank($newWidth, $newHeight, ImageColor::rgb(0, 0, 0, 0))->getImageResource();
|
||||
imagecopyresampled(
|
||||
$newImage,
|
||||
$this->image,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$newWidth,
|
||||
$newHeight,
|
||||
$this->getWidth(),
|
||||
$this->getHeight(),
|
||||
);
|
||||
$this->image = $newImage;
|
||||
}
|
||||
|
||||
if ($width < 0 || $height < 0) {
|
||||
imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function calculateSize(
|
||||
int $srcWidth,
|
||||
int $srcHeight,
|
||||
$newWidth,
|
||||
$newHeight,
|
||||
int $mode = self::OrSmaller,
|
||||
): array
|
||||
{
|
||||
if ($newWidth === null) {
|
||||
} elseif (self::isPercent($newWidth)) {
|
||||
$newWidth = (int) round($srcWidth / 100 * abs($newWidth));
|
||||
$percents = true;
|
||||
} else {
|
||||
$newWidth = abs($newWidth);
|
||||
}
|
||||
|
||||
if ($newHeight === null) {
|
||||
} elseif (self::isPercent($newHeight)) {
|
||||
$newHeight = (int) round($srcHeight / 100 * abs($newHeight));
|
||||
$mode |= empty($percents) ? 0 : self::Stretch;
|
||||
} else {
|
||||
$newHeight = abs($newHeight);
|
||||
}
|
||||
|
||||
if ($mode & self::Stretch) { // non-proportional
|
||||
if (!$newWidth || !$newHeight) {
|
||||
throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.');
|
||||
}
|
||||
|
||||
if ($mode & self::ShrinkOnly) {
|
||||
$newWidth = min($srcWidth, $newWidth);
|
||||
$newHeight = min($srcHeight, $newHeight);
|
||||
}
|
||||
} else { // proportional
|
||||
if (!$newWidth && !$newHeight) {
|
||||
throw new Nette\InvalidArgumentException('At least width or height must be specified.');
|
||||
}
|
||||
|
||||
$scale = [];
|
||||
if ($newWidth > 0) { // fit width
|
||||
$scale[] = $newWidth / $srcWidth;
|
||||
}
|
||||
|
||||
if ($newHeight > 0) { // fit height
|
||||
$scale[] = $newHeight / $srcHeight;
|
||||
}
|
||||
|
||||
if ($mode & self::OrBigger) {
|
||||
$scale = [max($scale)];
|
||||
}
|
||||
|
||||
if ($mode & self::ShrinkOnly) {
|
||||
$scale[] = 1;
|
||||
}
|
||||
|
||||
$scale = min($scale);
|
||||
$newWidth = (int) round($srcWidth * $scale);
|
||||
$newHeight = (int) round($srcHeight * $scale);
|
||||
}
|
||||
|
||||
return [max($newWidth, 1), max($newHeight, 1)];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Crops image. Arguments accepts pixels or percent.
|
||||
*/
|
||||
public function crop(int|string $left, int|string $top, int|string $width, int|string $height): static
|
||||
{
|
||||
[$r['x'], $r['y'], $r['width'], $r['height']]
|
||||
= static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
|
||||
if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') {
|
||||
$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();
|
||||
imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
|
||||
$this->image = $newImage;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates dimensions of cutout in image. Arguments accepts pixels or percent.
|
||||
*/
|
||||
public static function calculateCutout(
|
||||
int $srcWidth,
|
||||
int $srcHeight,
|
||||
int|string $left,
|
||||
int|string $top,
|
||||
int|string $newWidth,
|
||||
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);
|
||||
}
|
||||
|
||||
if ($left < 0) {
|
||||
$newWidth += $left;
|
||||
$left = 0;
|
||||
}
|
||||
|
||||
if ($top < 0) {
|
||||
$newHeight += $top;
|
||||
$top = 0;
|
||||
}
|
||||
|
||||
$newWidth = min($newWidth, $srcWidth - $left);
|
||||
$newHeight = min($newHeight, $srcHeight - $top);
|
||||
return [$left, $top, $newWidth, $newHeight];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sharpens image a little bit.
|
||||
*/
|
||||
public function sharpen(): static
|
||||
{
|
||||
imageconvolution($this->image, [ // my magic numbers ;)
|
||||
[-1, -1, -1],
|
||||
[-1, 24, -1],
|
||||
[-1, -1, -1],
|
||||
], 16, 0);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Puts another image into this image. Left and top accepts pixels or percent.
|
||||
* @param int<0, 100> $opacity 0..100
|
||||
*/
|
||||
public function place(self $image, int|string $left = 0, int|string $top = 0, int $opacity = 100): static
|
||||
{
|
||||
$opacity = max(0, min(100, $opacity));
|
||||
if ($opacity === 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$output = $input = $image->image;
|
||||
if ($opacity < 100) {
|
||||
$tbl = [];
|
||||
for ($i = 0; $i < 128; $i++) {
|
||||
$tbl[$i] = round(127 - (127 - $i) * $opacity / 100);
|
||||
}
|
||||
|
||||
$output = imagecreatetruecolor($width, $height);
|
||||
imagealphablending($output, false);
|
||||
if (!$image->isTrueColor()) {
|
||||
$input = $output;
|
||||
imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127));
|
||||
imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
|
||||
}
|
||||
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
$c = \imagecolorat($input, $x, $y);
|
||||
$c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24);
|
||||
\imagesetpixel($output, $x, $y, $c);
|
||||
}
|
||||
}
|
||||
|
||||
imagealphablending($output, true);
|
||||
}
|
||||
|
||||
imagecopy(
|
||||
$this->image,
|
||||
$output,
|
||||
$left,
|
||||
$top,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the bounding box for a TrueType text. Returns keys left, top, width and height.
|
||||
*/
|
||||
public static function calculateTextBox(
|
||||
string $text,
|
||||
string $fontFile,
|
||||
float $size,
|
||||
float $angle = 0,
|
||||
array $options = [],
|
||||
): array
|
||||
{
|
||||
self::ensureExtension();
|
||||
$box = imagettfbbox($size, $angle, $fontFile, $text, $options);
|
||||
return [
|
||||
'left' => $minX = min([$box[0], $box[2], $box[4], $box[6]]),
|
||||
'top' => $minY = min([$box[1], $box[3], $box[5], $box[7]]),
|
||||
'width' => max([$box[0], $box[2], $box[4], $box[6]]) - $minX + 1,
|
||||
'height' => max([$box[1], $box[3], $box[5], $box[7]]) - $minY + 1,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Draw a rectangle.
|
||||
*/
|
||||
public function rectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
|
||||
{
|
||||
if ($width !== 0 && $height !== 0) {
|
||||
$this->rectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Draw a filled rectangle.
|
||||
*/
|
||||
public function filledRectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
|
||||
{
|
||||
if ($width !== 0 && $height !== 0) {
|
||||
$this->filledRectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws ImageException
|
||||
*/
|
||||
public function save(string $file, ?int $quality = null, ?int $type = null): void
|
||||
{
|
||||
$type ??= self::extensionToType(pathinfo($file, PATHINFO_EXTENSION));
|
||||
$this->output($type, $quality, $file);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Outputs image to string. 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::* $type
|
||||
*/
|
||||
public function toString(int $type = ImageType::JPEG, ?int $quality = null): string
|
||||
{
|
||||
return Helpers::capture(function () use ($type, $quality): void {
|
||||
$this->output($type, $quality);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Outputs image to string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Outputs image to browser. 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::* $type
|
||||
* @throws ImageException
|
||||
*/
|
||||
public function send(int $type = ImageType::JPEG, ?int $quality = null): void
|
||||
{
|
||||
header('Content-Type: ' . self::typeToMimeType($type));
|
||||
$this->output($type, $quality);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Outputs image to browser or file.
|
||||
* @param ImageType::* $type
|
||||
* @throws ImageException
|
||||
*/
|
||||
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;
|
||||
|
||||
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'.");
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
throw new ImageException(Helpers::getLastError() ?: 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call to undefined method.
|
||||
* @throws Nette\MemberAccessException
|
||||
*/
|
||||
public function __call(string $name, array $args): mixed
|
||||
{
|
||||
$function = 'image' . $name;
|
||||
if (!function_exists($function)) {
|
||||
ObjectHelpers::strictCall(static::class, $name);
|
||||
}
|
||||
|
||||
foreach ($args as $key => $value) {
|
||||
if ($value instanceof self) {
|
||||
$args[$key] = $value->getImageResource();
|
||||
|
||||
} elseif ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
|
||||
$args[$key] = $this->resolveColor($value);
|
||||
}
|
||||
}
|
||||
|
||||
$res = $function($this->image, ...$args);
|
||||
return $res instanceof \GdImage
|
||||
? $this->setImageResource($res)
|
||||
: $res;
|
||||
}
|
||||
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
ob_start(function () {});
|
||||
imagepng($this->image, null, 0);
|
||||
$this->setImageResource(imagecreatefromstring(ob_get_clean()));
|
||||
}
|
||||
|
||||
|
||||
private static function isPercent(int|string &$num): bool
|
||||
{
|
||||
if (is_string($num) && str_ends_with($num, '%')) {
|
||||
$num = (float) substr($num, 0, -1);
|
||||
return true;
|
||||
} elseif (is_int($num) || $num === (string) (int) $num) {
|
||||
$num = (int) $num;
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Nette\InvalidArgumentException("Expected dimension in int|string, '$num' given.");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prevents serialization.
|
||||
*/
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.');
|
||||
}
|
||||
|
||||
|
||||
public function resolveColor(ImageColor|array $color): int
|
||||
{
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Utilities for iterables.
|
||||
*/
|
||||
final class Iterables
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/**
|
||||
* Tests for the presence of value.
|
||||
*/
|
||||
public static function contains(iterable $iterable, mixed $value): bool
|
||||
{
|
||||
foreach ($iterable as $v) {
|
||||
if ($v === $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests for the presence of key.
|
||||
*/
|
||||
public static function containsKey(iterable $iterable, mixed $key): bool
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
if ($k === $key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
if (!$predicate || $predicate($v, $k, $iterable)) {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
return $else ? $else() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
if (!$predicate || $predicate($v, $k, $iterable)) {
|
||||
return $k;
|
||||
}
|
||||
}
|
||||
return $else ? $else() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 callable(V, K, iterable<K, V>): bool $predicate
|
||||
*/
|
||||
public static function some(iterable $iterable, callable $predicate): bool
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
if ($predicate($v, $k, $iterable)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 callable(V, K, iterable<K, V>): bool $predicate
|
||||
*/
|
||||
public static function every(iterable $iterable, callable $predicate): bool
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
if (!$predicate($v, $k, $iterable)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterator that filters elements according to a given $predicate. Maintains original keys.
|
||||
* @template K
|
||||
* @template V
|
||||
* @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
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
if ($predicate($v, $k, $iterable)) {
|
||||
yield $k => $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterator that transforms values by calling $transformer. Maintains original keys.
|
||||
* @template K
|
||||
* @template V
|
||||
* @template R
|
||||
* @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
|
||||
{
|
||||
foreach ($iterable as $k => $v) {
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* PHP reflection helpers.
|
||||
*/
|
||||
final class Reflection
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
/** @deprecated use Nette\Utils\Validator::isBuiltinType() */
|
||||
public static function isBuiltinType(string $type): bool
|
||||
{
|
||||
return Validators::isBuiltinType($type);
|
||||
}
|
||||
|
||||
|
||||
/** @deprecated use Nette\Utils\Validator::isClassKeyword() */
|
||||
public static function isClassKeyword(string $name): bool
|
||||
{
|
||||
return Validators::isClassKeyword($name);
|
||||
}
|
||||
|
||||
|
||||
/** @deprecated use native ReflectionParameter::getDefaultValue() */
|
||||
public static function getParameterDefaultValue(\ReflectionParameter $param): mixed
|
||||
{
|
||||
if ($param->isDefaultValueConstant()) {
|
||||
$const = $orig = $param->getDefaultValueConstantName();
|
||||
$pair = explode('::', $const);
|
||||
if (isset($pair[1])) {
|
||||
$pair[0] = Type::resolve($pair[0], $param);
|
||||
try {
|
||||
$rcc = new \ReflectionClassConstant($pair[0], $pair[1]);
|
||||
} catch (\ReflectionException $e) {
|
||||
$name = self::toString($param);
|
||||
throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.", 0, $e);
|
||||
}
|
||||
|
||||
return $rcc->getValue();
|
||||
|
||||
} elseif (!defined($const)) {
|
||||
$const = substr((string) strrchr($const, '\\'), 1);
|
||||
if (!defined($const)) {
|
||||
$name = self::toString($param);
|
||||
throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.");
|
||||
}
|
||||
}
|
||||
|
||||
return constant($const);
|
||||
}
|
||||
|
||||
return $param->getDefaultValue();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait.
|
||||
*/
|
||||
public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass
|
||||
{
|
||||
foreach ($prop->getDeclaringClass()->getTraits() as $trait) {
|
||||
if ($trait->hasProperty($prop->name)
|
||||
// doc-comment guessing as workaround for insufficient PHP reflection
|
||||
&& $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment()
|
||||
) {
|
||||
return self::getPropertyDeclaringClass($trait->getProperty($prop->name));
|
||||
}
|
||||
}
|
||||
|
||||
return $prop->getDeclaringClass();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a reflection of a method that contains a declaration of $method.
|
||||
* Usually, each method is its own declaration, but the body of the method can also be in the trait and under a different name.
|
||||
*/
|
||||
public static function getMethodDeclaringMethod(\ReflectionMethod $method): \ReflectionMethod
|
||||
{
|
||||
// file & line guessing as workaround for insufficient PHP reflection
|
||||
$decl = $method->getDeclaringClass();
|
||||
if ($decl->getFileName() === $method->getFileName()
|
||||
&& $decl->getStartLine() <= $method->getStartLine()
|
||||
&& $decl->getEndLine() >= $method->getEndLine()
|
||||
) {
|
||||
return $method;
|
||||
}
|
||||
|
||||
$hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()];
|
||||
if (($alias = $decl->getTraitAliases()[$method->name] ?? null)
|
||||
&& ($m = new \ReflectionMethod(...explode('::', $alias, 2)))
|
||||
&& $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]
|
||||
) {
|
||||
return self::getMethodDeclaringMethod($m);
|
||||
}
|
||||
|
||||
foreach ($decl->getTraits() as $trait) {
|
||||
if ($trait->hasMethod($method->name)
|
||||
&& ($m = $trait->getMethod($method->name))
|
||||
&& $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]
|
||||
) {
|
||||
return self::getMethodDeclaringMethod($m);
|
||||
}
|
||||
}
|
||||
|
||||
return $method;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds out if reflection has access to PHPdoc comments. Comments may not be available due to the opcode cache.
|
||||
*/
|
||||
public static function areCommentsAvailable(): bool
|
||||
{
|
||||
static $res;
|
||||
return $res ?? $res = (bool) (new \ReflectionMethod(self::class, __FUNCTION__))->getDocComment();
|
||||
}
|
||||
|
||||
|
||||
public static function toString(\Reflector $ref): string
|
||||
{
|
||||
if ($ref instanceof \ReflectionClass) {
|
||||
return $ref->name;
|
||||
} elseif ($ref instanceof \ReflectionMethod) {
|
||||
return $ref->getDeclaringClass()->name . '::' . $ref->name . '()';
|
||||
} elseif ($ref instanceof \ReflectionFunction) {
|
||||
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) {
|
||||
return '$' . $ref->name . ' in ' . self::toString($ref->getDeclaringFunction());
|
||||
} else {
|
||||
throw new Nette\InvalidArgumentException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @throws Nette\InvalidArgumentException
|
||||
*/
|
||||
public static function expandClassName(string $name, \ReflectionClass $context): string
|
||||
{
|
||||
$lower = strtolower($name);
|
||||
if (empty($name)) {
|
||||
throw new Nette\InvalidArgumentException('Class name must not be empty.');
|
||||
|
||||
} elseif (Validators::isBuiltinType($lower)) {
|
||||
return $lower;
|
||||
|
||||
} elseif ($lower === 'self' || $lower === 'static') {
|
||||
return $context->name;
|
||||
|
||||
} elseif ($lower === 'parent') {
|
||||
return $context->getParentClass()
|
||||
? $context->getParentClass()->name
|
||||
: 'parent';
|
||||
|
||||
} elseif ($name[0] === '\\') { // fully qualified name
|
||||
return ltrim($name, '\\');
|
||||
}
|
||||
|
||||
$uses = self::getUseStatements($context);
|
||||
$parts = explode('\\', $name, 2);
|
||||
if (isset($uses[$parts[0]])) {
|
||||
$parts[0] = $uses[$parts[0]];
|
||||
return implode('\\', $parts);
|
||||
|
||||
} elseif ($context->inNamespace()) {
|
||||
return $context->getNamespaceName() . '\\' . $name;
|
||||
|
||||
} else {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @return array<string, class-string> of [alias => class] */
|
||||
public static function getUseStatements(\ReflectionClass $class): array
|
||||
{
|
||||
if ($class->isAnonymous()) {
|
||||
throw new Nette\NotImplementedException('Anonymous classes are not supported.');
|
||||
}
|
||||
|
||||
static $cache = [];
|
||||
if (!isset($cache[$name = $class->name])) {
|
||||
if ($class->isInternal()) {
|
||||
$cache[$name] = [];
|
||||
} else {
|
||||
$code = file_get_contents($class->getFileName());
|
||||
$cache = self::parseUseStatements($code, $name) + $cache;
|
||||
}
|
||||
}
|
||||
|
||||
return $cache[$name];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses PHP code to [class => [alias => class, ...]]
|
||||
*/
|
||||
private static function parseUseStatements(string $code, ?string $forClass = null): array
|
||||
{
|
||||
try {
|
||||
$tokens = \PhpToken::tokenize($code, TOKEN_PARSE);
|
||||
} catch (\ParseError $e) {
|
||||
trigger_error($e->getMessage(), E_USER_NOTICE);
|
||||
$tokens = [];
|
||||
}
|
||||
|
||||
$namespace = $class = null;
|
||||
$classLevel = $level = 0;
|
||||
$res = $uses = [];
|
||||
|
||||
$nameTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED];
|
||||
|
||||
while ($token = current($tokens)) {
|
||||
next($tokens);
|
||||
switch ($token->id) {
|
||||
case T_NAMESPACE:
|
||||
$namespace = ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\');
|
||||
$uses = [];
|
||||
break;
|
||||
|
||||
case T_CLASS:
|
||||
case T_INTERFACE:
|
||||
case T_TRAIT:
|
||||
case PHP_VERSION_ID < 80100
|
||||
? T_CLASS
|
||||
: T_ENUM:
|
||||
if ($name = self::fetch($tokens, T_STRING)) {
|
||||
$class = $namespace . $name;
|
||||
$classLevel = $level + 1;
|
||||
$res[$class] = $uses;
|
||||
if ($class === $forClass) {
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case T_USE:
|
||||
while (!$class && ($name = self::fetch($tokens, $nameTokens))) {
|
||||
$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;
|
||||
} else {
|
||||
$tmp = explode('\\', $suffix);
|
||||
$uses[end($tmp)] = $name . $suffix;
|
||||
}
|
||||
|
||||
if (!self::fetch($tokens, ',')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif (self::fetch($tokens, T_AS)) {
|
||||
$uses[self::fetch($tokens, T_STRING)] = $name;
|
||||
|
||||
} else {
|
||||
$tmp = explode('\\', $name);
|
||||
$uses[end($tmp)] = $name;
|
||||
}
|
||||
|
||||
if (!self::fetch($tokens, ',')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case T_CURLY_OPEN:
|
||||
case T_DOLLAR_OPEN_CURLY_BRACES:
|
||||
case ord('{'):
|
||||
$level++;
|
||||
break;
|
||||
|
||||
case ord('}'):
|
||||
if ($level === $classLevel) {
|
||||
$class = $classLevel = 0;
|
||||
}
|
||||
|
||||
$level--;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
private static function fetch(array &$tokens, string|int|array $take): ?string
|
||||
{
|
||||
$res = null;
|
||||
while ($token = current($tokens)) {
|
||||
if ($token->is($take)) {
|
||||
$res .= $token->text;
|
||||
} elseif (!$token->is([T_DOC_COMMENT, T_WHITESPACE, T_COMMENT])) {
|
||||
break;
|
||||
}
|
||||
|
||||
next($tokens);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Utils;
|
||||
|
||||
use JetBrains\PhpStorm\Language;
|
||||
use Nette;
|
||||
use function is_array, is_object, strlen;
|
||||
|
||||
|
||||
/**
|
||||
* String tools library.
|
||||
*/
|
||||
class Strings
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
public const TrimCharacters = " \t\n\r\0\x0B\u{A0}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{200B}";
|
||||
|
||||
/** @deprecated use Strings::TrimCharacters */
|
||||
public const TRIM_CHARACTERS = self::TrimCharacters;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use Nette\Utils\Validator::isUnicode()
|
||||
*/
|
||||
public static function checkEncoding(string $s): bool
|
||||
{
|
||||
return $s === self::fixEncoding($s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all invalid UTF-8 characters from a string.
|
||||
*/
|
||||
public static function fixEncoding(string $s): string
|
||||
{
|
||||
// removes xD800-xDFFF, x110000 and higher
|
||||
return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a specific character in UTF-8 from code point (number in range 0x0000..D7FF or 0xE000..10FFFF).
|
||||
* @throws Nette\InvalidArgumentException if code point is not in valid range
|
||||
*/
|
||||
public static function chr(int $code): string
|
||||
{
|
||||
if ($code < 0 || ($code >= 0xD800 && $code <= 0xDFFF) || $code > 0x10FFFF) {
|
||||
throw new Nette\InvalidArgumentException('Code point must be in range 0x0 to 0xD7FF or 0xE000 to 0x10FFFF.');
|
||||
} elseif (!extension_loaded('iconv')) {
|
||||
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
|
||||
}
|
||||
|
||||
return iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a code point of specific character in UTF-8 (number in range 0x0000..D7FF or 0xE000..10FFFF).
|
||||
*/
|
||||
public static function ord(string $c): int
|
||||
{
|
||||
if (!extension_loaded('iconv')) {
|
||||
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
|
||||
}
|
||||
|
||||
$tmp = iconv('UTF-8', 'UTF-32BE//IGNORE', $c);
|
||||
if (!$tmp) {
|
||||
throw new Nette\InvalidArgumentException('Invalid UTF-8 character "' . ($c === '' ? '' : '\x' . strtoupper(bin2hex($c))) . '".');
|
||||
}
|
||||
|
||||
return unpack('N', $tmp)[1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use str_starts_with()
|
||||
*/
|
||||
public static function startsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
return str_starts_with($haystack, $needle);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use str_ends_with()
|
||||
*/
|
||||
public static function endsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
return str_ends_with($haystack, $needle);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated use str_contains()
|
||||
*/
|
||||
public static function contains(string $haystack, string $needle): bool
|
||||
{
|
||||
return str_contains($haystack, $needle);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a part of UTF-8 string specified by starting position and length. If start is negative,
|
||||
* the returned string will start at the start'th character from the end of string.
|
||||
*/
|
||||
public static function substring(string $s, int $start, ?int $length = null): string
|
||||
{
|
||||
if (function_exists('mb_substr')) {
|
||||
return mb_substr($s, $start, $length, 'UTF-8'); // MB is much faster
|
||||
} elseif (!extension_loaded('iconv')) {
|
||||
throw new Nette\NotSupportedException(__METHOD__ . '() requires extension ICONV or MBSTRING, neither is loaded.');
|
||||
} elseif ($length === null) {
|
||||
$length = self::length($s);
|
||||
} elseif ($start < 0 && $length < 0) {
|
||||
$start += self::length($s); // unifies iconv_substr behavior with mb_substr
|
||||
}
|
||||
|
||||
return iconv_substr($s, $start, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes control characters, normalizes line breaks to `\n`, removes leading and trailing blank lines,
|
||||
* trims end spaces on lines, normalizes UTF-8 to the normal form of NFC.
|
||||
*/
|
||||
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) {
|
||||
$s = $n;
|
||||
}
|
||||
|
||||
$s = self::unixNewLines($s);
|
||||
|
||||
// remove control characters; leave \t + \n
|
||||
$s = self::pcre('preg_replace', ['#[\x00-\x08\x0B-\x1F\x7F-\x9F]+#u', '', $s]);
|
||||
|
||||
// right trim
|
||||
$s = self::pcre('preg_replace', ['#[\t ]+$#m', '', $s]);
|
||||
|
||||
// leading and trailing blank lines
|
||||
$s = trim($s, "\n");
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/** @deprecated use Strings::unixNewLines() */
|
||||
public static function normalizeNewLines(string $s): string
|
||||
{
|
||||
return self::unixNewLines($s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts line endings to \n used on Unix-like systems.
|
||||
* Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator.
|
||||
*/
|
||||
public static function unixNewLines(string $s): string
|
||||
{
|
||||
return preg_replace("~\r\n?|\u{2028}|\u{2029}~", "\n", $s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts line endings to platform-specific, i.e. \r\n on Windows and \n elsewhere.
|
||||
* Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator.
|
||||
*/
|
||||
public static function platformNewLines(string $s): string
|
||||
{
|
||||
return preg_replace("~\r\n?|\n|\u{2028}|\u{2029}~", PHP_EOL, $s);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts UTF-8 string to ASCII, ie removes diacritics etc.
|
||||
*/
|
||||
public static function toAscii(string $s): string
|
||||
{
|
||||
$iconv = defined('ICONV_IMPL') ? trim(ICONV_IMPL, '"\'') : null;
|
||||
static $transliterator = null;
|
||||
if ($transliterator === null) {
|
||||
if (class_exists('Transliterator', false)) {
|
||||
$transliterator = \Transliterator::create('Any-Latin; Latin-ASCII');
|
||||
} else {
|
||||
trigger_error(__METHOD__ . "(): it is recommended to enable PHP extensions 'intl'.", E_USER_NOTICE);
|
||||
$transliterator = false;
|
||||
}
|
||||
}
|
||||
|
||||
// remove control characters and check UTF-8 validity
|
||||
$s = self::pcre('preg_replace', ['#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s]);
|
||||
|
||||
// transliteration (by Transliterator and iconv) is not optimal, replace some characters directly
|
||||
$s = strtr($s, ["\u{201E}" => '"', "\u{201C}" => '"', "\u{201D}" => '"', "\u{201A}" => "'", "\u{2018}" => "'", "\u{2019}" => "'", "\u{B0}" => '^', "\u{42F}" => 'Ya', "\u{44F}" => 'ya', "\u{42E}" => 'Yu', "\u{44E}" => 'yu', "\u{c4}" => 'Ae', "\u{d6}" => 'Oe', "\u{dc}" => 'Ue', "\u{1e9e}" => 'Ss', "\u{e4}" => 'ae', "\u{f6}" => 'oe', "\u{fc}" => 'ue', "\u{df}" => 'ss']); // „ “ ” ‚ ‘ ’ ° Я я Ю ю Ä Ö Ü ẞ ä ö ü ß
|
||||
if ($iconv !== 'libiconv') {
|
||||
$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}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ ` € ™ ℮ ← ↑ → ↓ ↔
|
||||
}
|
||||
|
||||
if ($transliterator) {
|
||||
$s = $transliterator->transliterate($s);
|
||||
// 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);
|
||||
$s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters
|
||||
} elseif ($iconv === 'libiconv') {
|
||||
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
|
||||
} else { // null or 'unknown' (#216)
|
||||
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
|
||||
}
|
||||
} elseif ($iconv === 'glibc' || $iconv === 'libiconv') {
|
||||
// temporarily hide these characters to distinguish them from the garbage that iconv creates
|
||||
$s = strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06");
|
||||
if ($iconv === 'glibc') {
|
||||
// glibc implementation is very limited. transliterate into Windows-1250 and then into ASCII, so most Eastern European characters are preserved
|
||||
$s = iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s);
|
||||
$s = strtr(
|
||||
$s,
|
||||
"\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96\xa0\x8b\x97\x9b\xa6\xad\xb7",
|
||||
'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.',
|
||||
);
|
||||
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]);
|
||||
} else {
|
||||
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
|
||||
}
|
||||
|
||||
// remove garbage that iconv creates during transliteration (eg Ý -> Y')
|
||||
$s = str_replace(['`', "'", '"', '^', '~', '?'], '', $s);
|
||||
// restore temporarily hidden characters
|
||||
$s = strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?');
|
||||
} else {
|
||||
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Modifies the UTF-8 string to the form used in the URL, ie removes diacritics and replaces all characters
|
||||
* except letters of the English alphabet and numbers with a hyphens.
|
||||
*/
|
||||
public static function webalize(string $s, ?string $charlist = null, bool $lower = true): string
|
||||
{
|
||||
$s = self::toAscii($s);
|
||||
if ($lower) {
|
||||
$s = strtolower($s);
|
||||
}
|
||||
|
||||
$s = self::pcre('preg_replace', ['#[^a-z0-9' . ($charlist !== null ? preg_quote($charlist, '#') : '') . ']+#i', '-', $s]);
|
||||
$s = trim($s, '-');
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Truncates a UTF-8 string to given maximal length, while trying not to split whole words. Only if the string is truncated,
|
||||
* an ellipsis (or something else set with third argument) is appended to the string.
|
||||
*/
|
||||
public static function truncate(string $s, int $maxLen, string $append = "\u{2026}"): string
|
||||
{
|
||||
if (self::length($s) > $maxLen) {
|
||||
$maxLen -= self::length($append);
|
||||
if ($maxLen < 1) {
|
||||
return $append;
|
||||
|
||||
} elseif ($matches = self::match($s, '#^.{1,' . $maxLen . '}(?=[\s\x00-/:-@\[-`{-~])#us')) {
|
||||
return $matches[0] . $append;
|
||||
|
||||
} else {
|
||||
return self::substring($s, 0, $maxLen) . $append;
|
||||
}
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indents a multiline text from the left. Second argument sets how many indentation chars should be used,
|
||||
* while the indent itself is the third argument (*tab* by default).
|
||||
*/
|
||||
public static function indent(string $s, int $level = 1, string $chars = "\t"): string
|
||||
{
|
||||
if ($level > 0) {
|
||||
$s = self::replace($s, '#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level));
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts all characters of UTF-8 string to lower case.
|
||||
*/
|
||||
public static function lower(string $s): string
|
||||
{
|
||||
return mb_strtolower($s, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the first character of a UTF-8 string to lower case and leaves the other characters unchanged.
|
||||
*/
|
||||
public static function firstLower(string $s): string
|
||||
{
|
||||
return self::lower(self::substring($s, 0, 1)) . self::substring($s, 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts all characters of a UTF-8 string to upper case.
|
||||
*/
|
||||
public static function upper(string $s): string
|
||||
{
|
||||
return mb_strtoupper($s, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the first character of a UTF-8 string to upper case and leaves the other characters unchanged.
|
||||
*/
|
||||
public static function firstUpper(string $s): string
|
||||
{
|
||||
return self::upper(self::substring($s, 0, 1)) . self::substring($s, 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the first character of every word of a UTF-8 string to upper case and the others to lower case.
|
||||
*/
|
||||
public static function capitalize(string $s): string
|
||||
{
|
||||
return mb_convert_case($s, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares two UTF-8 strings or their parts, without taking character case into account. If length is null, whole strings are compared,
|
||||
* if it is negative, the corresponding number of characters from the end of the strings is compared,
|
||||
* otherwise the appropriate number of characters from the beginning is compared.
|
||||
*/
|
||||
public static function compare(string $left, string $right, ?int $length = null): bool
|
||||
{
|
||||
if (class_exists('Normalizer', false)) {
|
||||
$left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster
|
||||
$right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster
|
||||
}
|
||||
|
||||
if ($length < 0) {
|
||||
$left = self::substring($left, $length, -$length);
|
||||
$right = self::substring($right, $length, -$length);
|
||||
} elseif ($length !== null) {
|
||||
$left = self::substring($left, 0, $length);
|
||||
$right = self::substring($right, 0, $length);
|
||||
}
|
||||
|
||||
return self::lower($left) === self::lower($right);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the common prefix of strings or returns empty string if the prefix was not found.
|
||||
* @param string[] $strings
|
||||
*/
|
||||
public static function findPrefix(array $strings): string
|
||||
{
|
||||
$first = array_shift($strings);
|
||||
for ($i = 0; $i < strlen($first); $i++) {
|
||||
foreach ($strings as $s) {
|
||||
if (!isset($s[$i]) || $first[$i] !== $s[$i]) {
|
||||
while ($i && $first[$i - 1] >= "\x80" && $first[$i] >= "\x80" && $first[$i] < "\xC0") {
|
||||
$i--;
|
||||
}
|
||||
|
||||
return substr($first, 0, $i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $first;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns number of characters (not bytes) in UTF-8 string.
|
||||
* That is the number of Unicode code points which may differ from the number of graphemes.
|
||||
*/
|
||||
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'),
|
||||
default => strlen(@utf8_decode($s)), // deprecated
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string.
|
||||
*/
|
||||
public static function trim(string $s, string $charlist = self::TrimCharacters): string
|
||||
{
|
||||
$charlist = preg_quote($charlist, '#');
|
||||
return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pads a UTF-8 string to given length by prepending the $pad string to the beginning.
|
||||
* @param non-empty-string $pad
|
||||
*/
|
||||
public static function padLeft(string $s, int $length, string $pad = ' '): string
|
||||
{
|
||||
$length = max(0, $length - self::length($s));
|
||||
$padLen = self::length($pad);
|
||||
return str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pads UTF-8 string to given length by appending the $pad string to the end.
|
||||
* @param non-empty-string $pad
|
||||
*/
|
||||
public static function padRight(string $s, int $length, string $pad = ' '): string
|
||||
{
|
||||
$length = max(0, $length - self::length($s));
|
||||
$padLen = self::length($pad);
|
||||
return $s . str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverses UTF-8 string.
|
||||
*/
|
||||
public static function reverse(string $s): string
|
||||
{
|
||||
if (!extension_loaded('iconv')) {
|
||||
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)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns part of $haystack before $nth occurence of $needle or returns null if the needle was not found.
|
||||
* Negative value means searching from the end.
|
||||
*/
|
||||
public static function before(string $haystack, string $needle, int $nth = 1): ?string
|
||||
{
|
||||
$pos = self::pos($haystack, $needle, $nth);
|
||||
return $pos === null
|
||||
? null
|
||||
: substr($haystack, 0, $pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns part of $haystack after $nth occurence of $needle or returns null if the needle was not found.
|
||||
* Negative value means searching from the end.
|
||||
*/
|
||||
public static function after(string $haystack, string $needle, int $nth = 1): ?string
|
||||
{
|
||||
$pos = self::pos($haystack, $needle, $nth);
|
||||
return $pos === null
|
||||
? null
|
||||
: substr($haystack, $pos + strlen($needle));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns position in characters of $nth occurence of $needle in $haystack or null if the $needle was not found.
|
||||
* Negative value of `$nth` means searching from the end.
|
||||
*/
|
||||
public static function indexOf(string $haystack, string $needle, int $nth = 1): ?int
|
||||
{
|
||||
$pos = self::pos($haystack, $needle, $nth);
|
||||
return $pos === null
|
||||
? null
|
||||
: self::length(substr($haystack, 0, $pos));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns position in characters of $nth occurence of $needle in $haystack or null if the needle was not found.
|
||||
*/
|
||||
private static function pos(string $haystack, string $needle, int $nth = 1): ?int
|
||||
{
|
||||
if (!$nth) {
|
||||
return null;
|
||||
} elseif ($nth > 0) {
|
||||
if ($needle === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$pos = 0;
|
||||
while (($pos = strpos($haystack, $needle, $pos)) !== false && --$nth) {
|
||||
$pos++;
|
||||
}
|
||||
} else {
|
||||
$len = strlen($haystack);
|
||||
if ($needle === '') {
|
||||
return $len;
|
||||
} elseif ($len === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pos = $len - 1;
|
||||
while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false && ++$nth) {
|
||||
$pos--;
|
||||
}
|
||||
}
|
||||
|
||||
return Helpers::falseToNull($pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well.
|
||||
*/
|
||||
public static function split(
|
||||
string $subject,
|
||||
#[Language('RegExp')]
|
||||
string $pattern,
|
||||
bool|int $captureOffset = false,
|
||||
bool $skipEmpty = false,
|
||||
int $limit = -1,
|
||||
bool $utf8 = false,
|
||||
): array
|
||||
{
|
||||
$flags = is_int($captureOffset) // back compatibility
|
||||
? $captureOffset
|
||||
: ($captureOffset ? PREG_SPLIT_OFFSET_CAPTURE : 0) | ($skipEmpty ? PREG_SPLIT_NO_EMPTY : 0);
|
||||
|
||||
$pattern .= $utf8 ? 'u' : '';
|
||||
$m = self::pcre('preg_split', [$pattern, $subject, $limit, $flags | PREG_SPLIT_DELIM_CAPTURE]);
|
||||
return $utf8 && $captureOffset
|
||||
? self::bytesToChars($subject, [$m])[0]
|
||||
: $m;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches the string for the part matching the regular expression and returns
|
||||
* an array with the found expression and individual subexpressions, or `null`.
|
||||
*/
|
||||
public static function match(
|
||||
string $subject,
|
||||
#[Language('RegExp')]
|
||||
string $pattern,
|
||||
bool|int $captureOffset = false,
|
||||
int $offset = 0,
|
||||
bool $unmatchedAsNull = false,
|
||||
bool $utf8 = false,
|
||||
): ?array
|
||||
{
|
||||
$flags = is_int($captureOffset) // back compatibility
|
||||
? $captureOffset
|
||||
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
|
||||
|
||||
if ($utf8) {
|
||||
$offset = strlen(self::substring($subject, 0, $offset));
|
||||
$pattern .= 'u';
|
||||
}
|
||||
|
||||
if ($offset > strlen($subject)) {
|
||||
return null;
|
||||
} elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) {
|
||||
return null;
|
||||
} elseif ($utf8 && $captureOffset) {
|
||||
return self::bytesToChars($subject, [$m])[0];
|
||||
} else {
|
||||
return $m;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
#[Language('RegExp')]
|
||||
string $pattern,
|
||||
bool|int $captureOffset = false,
|
||||
int $offset = 0,
|
||||
bool $unmatchedAsNull = false,
|
||||
bool $patternOrder = false,
|
||||
bool $utf8 = false,
|
||||
bool $lazy = false,
|
||||
): array|\Generator
|
||||
{
|
||||
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),
|
||||
$offset,
|
||||
]);
|
||||
return $utf8 && $captureOffset
|
||||
? self::bytesToChars($subject, $m)
|
||||
: $m;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`.
|
||||
*/
|
||||
public static function replace(
|
||||
string $subject,
|
||||
#[Language('RegExp')]
|
||||
string|array $pattern,
|
||||
string|callable $replacement = '',
|
||||
int $limit = -1,
|
||||
bool $captureOffset = false,
|
||||
bool $unmatchedAsNull = false,
|
||||
bool $utf8 = false,
|
||||
): string
|
||||
{
|
||||
if (is_object($replacement) || is_array($replacement)) {
|
||||
if (!is_callable($replacement, false, $textual)) {
|
||||
throw new Nette\InvalidStateException("Callback '$textual' is not callable.");
|
||||
}
|
||||
|
||||
$flags = ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
|
||||
if ($utf8) {
|
||||
$pattern .= 'u';
|
||||
if ($captureOffset) {
|
||||
$replacement = fn($m) => $replacement(self::bytesToChars($subject, [$m])[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit, 0, $flags]);
|
||||
|
||||
} elseif (is_array($pattern) && is_string(key($pattern))) {
|
||||
$replacement = array_values($pattern);
|
||||
$pattern = array_keys($pattern);
|
||||
}
|
||||
|
||||
if ($utf8) {
|
||||
$pattern = array_map(fn($item) => $item . 'u', (array) $pattern);
|
||||
}
|
||||
|
||||
return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]);
|
||||
}
|
||||
|
||||
|
||||
private static function bytesToChars(string $s, array $groups): array
|
||||
{
|
||||
$lastBytes = $lastChars = 0;
|
||||
foreach ($groups as &$matches) {
|
||||
foreach ($matches as &$match) {
|
||||
if ($match[1] > $lastBytes) {
|
||||
$lastChars += self::length(substr($s, $lastBytes, $match[1] - $lastBytes));
|
||||
} elseif ($match[1] < $lastBytes) {
|
||||
$lastChars -= self::length(substr($s, $match[1], $lastBytes - $match[1]));
|
||||
}
|
||||
|
||||
$lastBytes = $match[1];
|
||||
$match[1] = $lastChars;
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function pcre(string $func, array $args)
|
||||
{
|
||||
$res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void {
|
||||
// compile-time error, not detectable by preg_last_error
|
||||
throw new RegexpException($message . ' in pattern: ' . implode(' or ', (array) $args[0]));
|
||||
});
|
||||
|
||||
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))
|
||||
) {
|
||||
throw new RegexpException(preg_last_error_msg()
|
||||
. ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user