diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index a4cd12634..000000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "🐛 Bug Report" -about: "If something isn't working as expected 🤔" - ---- - -Version: ?.?.? - -### Bug Description -... A clear and concise description of what the bug is. A good bug report shouldn't leave others needing to chase you up for more information. - -### Steps To Reproduce -... If possible a minimal demo of the problem ... - -### Expected Behavior -... A clear and concise description of what you expected to happen. - -### Possible Solution -... Only if you have suggestions on a fix for the bug diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index d2e219489..000000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: "🚀 Feature Request" -about: "I have a suggestion (and may want to implement it) 🙂" - ---- - -- Is your feature request related to a problem? Please describe. -- Explain your intentions. -- It's up to you to make a strong case to convince the project's developers of the merits of this feature. diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/Support_question.md deleted file mode 100644 index 75c48b6ed..000000000 --- a/.github/ISSUE_TEMPLATE/Support_question.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: "🤗 Support Question" -about: "If you have a question 💬, please check out our forum!" - ---- - ---------------^ Click "Preview" for a nicer view! -We primarily use GitHub as an issue tracker; for usage and support questions, please check out these resources below. Thanks! 😁. - -* Nette Forum: https://forum.nette.org -* Nette Gitter: https://gitter.im/nette/nette -* Slack (czech): https://pehapkari.slack.com/messages/C2R30BLKA diff --git a/.github/ISSUE_TEMPLATE/Support_us.md b/.github/ISSUE_TEMPLATE/Support_us.md deleted file mode 100644 index 92d8a4c3a..000000000 --- a/.github/ISSUE_TEMPLATE/Support_us.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "❤️ Support us" -about: "If you would like to support our efforts in maintaining this project 🙌" - ---- - ---------------^ Click "Preview" for a nicer view! - -> https://nette.org/donate - -Help support Nette! - -We develop Nette Framework for more than 14 years. In order to make your life more comfortable. Nette cares about the safety of your sites. Nette saves you time. And gives job opportunities. - -Nette earns you money. And is absolutely free. - -To ensure future development and improving the documentation, we need your donation. - -Whether you are chief of IT company which benefits from Nette, or developer who goes for advice on our forum, if you like Nette, [please make a donation now](https://nette.org/donate). - -Thank you! diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index 25adc9520..000000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: dg -custom: "https://nette.org/donate" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index f8aa3f408..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ -- bug fix / new feature? -- BC break? yes/no -- doc PR: nette/docs#??? - - diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 003ba9265..16e97d13c 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.0 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b0692d716..f985b0526 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v2 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.0 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c574b8c9..4470d3019 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - php: ['7.2', '7.3', '7.4', '8.0', '8.1'] + php: ['8.0', '8.1'] fail-fast: false @@ -37,7 +37,7 @@ jobs: - uses: actions/checkout@v2 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.0 extensions: iconv, json, mbstring, xml, gd, intl, tokenizer coverage: none diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index b6594ae55..7810b4cc1 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -14,6 +14,6 @@ expectedArguments(\Nette\Utils\Image::calculateSize(), 4, \Nette\Utils\Image::SHRINK_ONLY, \Nette\Utils\Image::STRETCH, \Nette\Utils\Image::FIT, \Nette\Utils\Image::FILL, \Nette\Utils\Image::EXACT); expectedArguments(\Nette\Utils\Json::encode(), 1, \Nette\Utils\Json::PRETTY); expectedArguments(\Nette\Utils\Json::decode(), 1, \Nette\Utils\Json::FORCE_ARRAY); -expectedArguments(\Nette\Utils\Strings::split(), 2, \PREG_SPLIT_NO_EMPTY); -expectedArguments(\Nette\Utils\Strings::match(), 2, \PREG_OFFSET_CAPTURE); -expectedArguments(\Nette\Utils\Strings::matchAll(), 2, \PREG_OFFSET_CAPTURE, \PREG_SET_ORDER); +expectedArguments(\Nette\Utils\Strings::split(), 2, \PREG_SPLIT_NO_EMPTY | \PREG_OFFSET_CAPTURE); +expectedArguments(\Nette\Utils\Strings::match(), 2, \PREG_OFFSET_CAPTURE | \PREG_UNMATCHED_AS_NULL); +expectedArguments(\Nette\Utils\Strings::matchAll(), 2, \PREG_OFFSET_CAPTURE | \PREG_UNMATCHED_AS_NULL | \PREG_PATTERN_ORDER); diff --git a/composer.json b/composer.json index fa164d856..8548e9731 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,11 @@ } ], "require": { - "php": ">=7.2 <8.2" + "php": ">=8.0 <8.2" }, "require-dev": { - "nette/tester": "~2.0", - "tracy/tracy": "^2.3", + "nette/tester": "^2.4", + "tracy/tracy": "^2.8", "phpstan/phpstan": "^0.12" }, "conflict": { @@ -44,7 +44,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } } } diff --git a/contributing.md b/contributing.md deleted file mode 100644 index 184152c02..000000000 --- a/contributing.md +++ /dev/null @@ -1,33 +0,0 @@ -How to contribute & use the issue tracker -========================================= - -Nette welcomes your contributions. There are several ways to help out: - -* Create an issue on GitHub, if you have found a bug -* Write test cases for open bug issues -* Write fixes for open bug/feature issues, preferably with test cases included -* Contribute to the [documentation](https://nette.org/en/writing) - -Issues ------- - -Please **do not use the issue tracker to ask questions**. We will be happy to help you -on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette). - -A good bug report shouldn't leave others needing to chase you up for more -information. Please try to be as detailed as possible in your report. - -**Feature requests** are welcome. But take a moment to find out whether your idea -fits with the scope and aims of the project. It's up to *you* to make a strong -case to convince the project's developers of the merits of this feature. - -Contributing ------------- - -If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing). - -The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them. - -Please do not fix whitespace, format code, or make a purely cosmetic patch. - -Thanks! :heart: diff --git a/ecs.php b/ecs.php index d655ffcc4..4dd357154 100644 --- a/ecs.php +++ b/ecs.php @@ -9,7 +9,7 @@ return function (Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator $containerConfigurator): void { - $containerConfigurator->import(PRESET_DIR . '/php71.php'); + $containerConfigurator->import(PRESET_DIR . '/php80.php'); $parameters = $containerConfigurator->parameters(); diff --git a/readme.md b/readme.md index 81ed0e919..2508385a3 100644 --- a/readme.md +++ b/readme.md @@ -39,6 +39,7 @@ The recommended way to install is via Composer: composer require nette/utils ``` +- Nette Utils 4.0 is compatible with PHP 8.0 to 8.1 - Nette Utils 3.2 is compatible with PHP 7.2 to 8.1 - Nette Utils 3.1 is compatible with PHP 7.1 to 8.0 - Nette Utils 3.0 is compatible with PHP 7.1 to 8.0 diff --git a/src/Iterators/CachingIterator.php b/src/Iterators/CachingIterator.php index e2e5e4c1a..d4e09f003 100644 --- a/src/Iterators/CachingIterator.php +++ b/src/Iterators/CachingIterator.php @@ -28,8 +28,7 @@ class CachingIterator extends \CachingIterator implements \Countable { use Nette\SmartObject; - /** @var int */ - private $counter = 0; + private int $counter = 0; public function __construct($iterator) @@ -47,7 +46,7 @@ public function __construct($iterator) } elseif ($iterator instanceof \Traversable) { $iterator = new \IteratorIterator($iterator); } else { - throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, is_object($iterator) ? get_class($iterator) : gettype($iterator))); + throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, is_object($iterator) ? $iterator::class : gettype($iterator))); } parent::__construct($iterator, 0); @@ -147,9 +146,8 @@ public function rewind(): void /** * Returns the next key. - * @return mixed */ - public function getNextKey() + public function getNextKey(): mixed { return $this->getInnerIterator()->key(); } @@ -157,9 +155,8 @@ public function getNextKey() /** * Returns the next element. - * @return mixed */ - public function getNextValue() + public function getNextValue(): mixed { return $this->getInnerIterator()->current(); } diff --git a/src/Iterators/Mapper.php b/src/Iterators/Mapper.php index 1a8647c60..87823baf9 100644 --- a/src/Iterators/Mapper.php +++ b/src/Iterators/Mapper.php @@ -27,8 +27,7 @@ public function __construct(\Traversable $iterator, callable $callback) } - #[\ReturnTypeWillChange] - public function current() + public function current(): mixed { return ($this->callback)(parent::current(), parent::key()); } diff --git a/src/SmartObject.php b/src/SmartObject.php index 2128fdfd0..48f938117 100644 --- a/src/SmartObject.php +++ b/src/SmartObject.php @@ -24,7 +24,7 @@ trait SmartObject /** * @throws MemberAccessException */ - public function __call(string $name, array $args) + public function __call(string $name, array $args): mixed { $class = static::class; @@ -37,27 +37,26 @@ public function __call(string $name, array $args) } elseif ($handlers !== null) { throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . gettype($handlers) . ' given.'); } - - } else { - ObjectHelpers::strictCall($class, $name); + return null; } + + ObjectHelpers::strictCall($class, $name); } /** * @throws MemberAccessException */ - public static function __callStatic(string $name, array $args) + public static function __callStatic(string $name, array $args): mixed { ObjectHelpers::strictStaticCall(static::class, $name); } /** - * @return mixed * @throws MemberAccessException if the property is not defined. */ - public function &__get(string $name) + public function &__get(string $name): mixed { $class = static::class; @@ -65,7 +64,14 @@ public function &__get(string $name) if (!($prop & 0b0001)) { throw new MemberAccessException("Cannot read a write-only property $class::\$$name."); } - $m = ($prop & 0b0010 ? 'get' : 'is') . $name; + $m = ($prop & 0b0010 ? 'get' : 'is') . ucfirst($name); + if ($prop & 0b10000) { + $trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call() + $loc = isset($trace['file'], $trace['line']) + ? " in $trace[file] on line $trace[line]" + : ''; + trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED); + } if ($prop & 0b0100) { // return by reference return $this->$m(); } else { @@ -79,11 +85,9 @@ public function &__get(string $name) /** - * @param mixed $value - * @return void * @throws MemberAccessException if the property is not defined or is read-only */ - public function __set(string $name, $value) + public function __set(string $name, mixed $value): void { $class = static::class; @@ -94,7 +98,15 @@ public function __set(string $name, $value) if (!($prop & 0b1000)) { throw new MemberAccessException("Cannot write to a read-only property $class::\$$name."); } - $this->{'set' . $name}($value); + $m = 'set' . ucfirst($name); + if ($prop & 0b10000) { + $trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call() + $loc = isset($trace['file'], $trace['line']) + ? " in $trace[file] on line $trace[line]" + : ''; + trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED); + } + $this->$m($value); } else { ObjectHelpers::strictSet($class, $name); @@ -103,10 +115,9 @@ public function __set(string $name, $value) /** - * @return void * @throws MemberAccessException */ - public function __unset(string $name) + public function __unset(string $name): void { $class = static::class; if (!ObjectHelpers::hasProperty($class, $name)) { diff --git a/src/StaticClass.php b/src/StaticClass.php index 8fed0ef31..c719e3b6f 100644 --- a/src/StaticClass.php +++ b/src/StaticClass.php @@ -27,10 +27,9 @@ final public function __construct() /** * Call to undefined static method. - * @return void * @throws MemberAccessException */ - public static function __callStatic(string $name, array $args) + public static function __callStatic(string $name, array $args): mixed { Utils\ObjectHelpers::strictStaticCall(static::class, $name); } diff --git a/src/Translator.php b/src/Translator.php index 0f343a274..f973f5f15 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -17,10 +17,8 @@ interface Translator { /** * Translates the given string. - * @param mixed $message - * @param mixed ...$parameters */ - function translate($message, ...$parameters): string; + function translate(string|\Stringable $message, mixed ...$parameters): string|\Stringable; } diff --git a/src/Utils/ArrayHash.php b/src/Utils/ArrayHash.php index 5de178e20..f69ab58bb 100644 --- a/src/Utils/ArrayHash.php +++ b/src/Utils/ArrayHash.php @@ -21,9 +21,8 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator /** * Transforms array to ArrayHash. * @param array $array - * @return static */ - public static function from(array $array, bool $recursive = true) + public static function from(array $array, bool $recursive = true): static { $obj = new static; foreach ($array as $key => $value) { @@ -73,8 +72,7 @@ public function offsetSet($key, $value): void * @param string|int $key * @return T */ - #[\ReturnTypeWillChange] - public function offsetGet($key) + public function offsetGet($key): mixed { return $this->$key; } diff --git a/src/Utils/ArrayList.php b/src/Utils/ArrayList.php index 0a694652d..c22c96a46 100644 --- a/src/Utils/ArrayList.php +++ b/src/Utils/ArrayList.php @@ -20,16 +20,14 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate { use Nette\SmartObject; - /** @var mixed[] */ - private $list = []; + private array $list = []; /** * Transforms array to ArrayList. * @param array $array - * @return static */ - public static function from(array $array) + public static function from(array $array): static { if (!Arrays::isList($array)) { throw new Nette\InvalidArgumentException('Array is not valid list.'); @@ -85,8 +83,7 @@ public function offsetSet($index, $value): void * @return T * @throws Nette\OutOfRangeException */ - #[\ReturnTypeWillChange] - public function offsetGet($index) + public function offsetGet($index): mixed { if (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); @@ -123,7 +120,7 @@ public function offsetUnset($index): void * Prepends a item. * @param T $value */ - public function prepend($value): void + public function prepend(mixed $value): void { $first = array_slice($this->list, 0, 1); $this->offsetSet(0, $value); diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index 775b6517b..625bec65e 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -29,7 +29,7 @@ class Arrays * @return ?T * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided */ - public static function get(array $array, $key, $default = null) + 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)) { @@ -53,7 +53,7 @@ public static function get(array $array, $key, $default = null) * @return ?T * @throws Nette\InvalidArgumentException if traversed item is not an array */ - public static function &getRef(array &$array, $key) + 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) { @@ -90,10 +90,8 @@ public static function mergeTree(array $array1, array $array2): array /** * Returns zero-indexed position of given array key. Returns null if key is not found. - * @param array-key $key - * @return int|null offset if it is found, null otherwise */ - public static function getKeyOffset(array $array, $key): ?int + public static function getKeyOffset(array $array, string|int $key): ?int { return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), true)); } @@ -110,9 +108,8 @@ public static function searchKey(array $array, $key): ?int /** * Tests an array for the presence of value. - * @param mixed $value */ - public static function contains(array $array, $value): bool + public static function contains(array $array, mixed $value): bool { return in_array($value, $array, true); } @@ -124,7 +121,7 @@ public static function contains(array $array, $value): bool * @param array $array * @return ?T */ - public static function first(array $array) + public static function first(array $array): mixed { return count($array) ? reset($array) : null; } @@ -136,7 +133,7 @@ public static function first(array $array) * @param array $array * @return ?T */ - public static function last(array $array) + public static function last(array $array): mixed { return count($array) ? end($array) : null; } @@ -145,9 +142,8 @@ public static function last(array $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. - * @param array-key|null $key */ - public static function insertBefore(array &$array, $key, array $inserted): void + 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, true) @@ -159,9 +155,8 @@ public static function insertBefore(array &$array, $key, array $inserted): void /** * Inserts the contents of the $inserted array into the $array before the $key. * If $key is null (or does not exist), it is inserted at the end. - * @param array-key|null $key */ - public static function insertAfter(array &$array, $key, array $inserted): void + 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; @@ -174,10 +169,8 @@ public static function insertAfter(array &$array, $key, array $inserted): void /** * Renames key in array. - * @param array-key $oldKey - * @param array-key $newKey */ - public static function renameKey(array &$array, $oldKey, $newKey): bool + public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool { $offset = self::getKeyOffset($array, $oldKey); if ($offset === null) { @@ -197,8 +190,9 @@ public static function renameKey(array &$array, $oldKey, $newKey): bool * @param string[] $array * @return string[] */ - public static function grep(array $array, string $pattern, int $flags = 0): array + public static function grep(array $array, string $pattern, bool|int $invert = false): array { + $flags = $invert ? PREG_GREP_INVERT : 0; return Strings::pcre('preg_grep', [$pattern, $array, $flags]); } @@ -219,9 +213,8 @@ public static function flatten(array $array, bool $preserveKeys = false): array /** * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list. - * @param mixed $value */ - public static function isList($value): bool + public static function isList(mixed $value): bool { return is_array($value) && (!$value || array_keys($value) === range(0, count($value) - 1)); } @@ -230,9 +223,8 @@ public static function isList($value): bool /** * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'. * @param string|string[] $path - * @return array|\stdClass */ - public static function associate(array $array, $path) + public static function associate(array $array, $path): array|\stdClass { $parts = is_array($path) ? $path @@ -285,9 +277,8 @@ public static function associate(array $array, $path) /** * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling. - * @param mixed $filling */ - public static function normalize(array $array, $filling = null): array + public static function normalize(array $array, mixed $filling = null): array { $res = []; foreach ($array as $k => $v) { @@ -302,12 +293,11 @@ public static function normalize(array $array, $filling = null): array * or returns $default, if provided. * @template T * @param array $array - * @param 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 pick(array &$array, $key, $default = null) + public static function pick(array &$array, string|int $key, mixed $default = null): mixed { if (array_key_exists($key, $array)) { $value = $array[$key]; @@ -401,7 +391,7 @@ public static function invokeMethod(iterable $objects, string $method, ...$args) * @param T $object * @return T */ - public static function toObject(iterable $array, $object) + public static function toObject(iterable $array, object $object): object { foreach ($array as $k => $v) { $object->$k = $v; @@ -412,10 +402,8 @@ public static function toObject(iterable $array, $object) /** * Converts value to array key. - * @param mixed $value - * @return array-key */ - public static function toKey($value) + public static function toKey(mixed $value): int|string { return key([$value => null]); } diff --git a/src/Utils/Callback.php b/src/Utils/Callback.php index 73d5cecd9..3cea432c7 100644 --- a/src/Utils/Callback.php +++ b/src/Utils/Callback.php @@ -20,52 +20,10 @@ final class Callback { use Nette\StaticClass; - /** - * @param string|object|callable $callable class, object, callable - * @deprecated use Closure::fromCallable() - */ - public static function closure($callable, string $method = null): \Closure - { - trigger_error(__METHOD__ . '() is deprecated, use Closure::fromCallable().', E_USER_DEPRECATED); - try { - return \Closure::fromCallable($method === null ? $callable : [$callable, $method]); - } catch (\TypeError $e) { - throw new Nette\InvalidArgumentException($e->getMessage()); - } - } - - - /** - * Invokes callback. - * @return mixed - * @deprecated - */ - public static function invoke($callable, ...$args) - { - trigger_error(__METHOD__ . '() is deprecated, use native invoking.', E_USER_DEPRECATED); - self::check($callable); - return $callable(...$args); - } - - - /** - * Invokes callback with an array of parameters. - * @return mixed - * @deprecated - */ - public static function invokeArgs($callable, array $args = []) - { - trigger_error(__METHOD__ . '() is deprecated, use native invoking.', E_USER_DEPRECATED); - self::check($callable); - return $callable(...$args); - } - - /** * Invokes internal PHP function with own error handler. - * @return mixed */ - public static function invokeSafe(string $function, array $args, callable $onError) + public static function invokeSafe(string $function, array $args, callable $onError): mixed { $prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool { if ($file === __FILE__) { @@ -91,17 +49,16 @@ public static function invokeSafe(string $function, array $args, callable $onErr /** * 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. - * @param mixed $callable * @return callable * @throws Nette\InvalidArgumentException */ - public static function check($callable, bool $syntax = false) + 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)) + : sprintf("Callback '%s' is not callable.", self::toString($callable)), ); } return $callable; @@ -110,15 +67,12 @@ public static function check($callable, bool $syntax = false) /** * Converts PHP callback to textual form. Class or method may not exists. - * @param mixed $callable */ - public static function toString($callable): string + public static function toString(mixed $callable): string { if ($callable instanceof \Closure) { $inner = self::unwrap($callable); return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}'); - } elseif (is_string($callable) && $callable[0] === "\0") { - return '{lambda}'; } else { is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual); return $textual; @@ -129,7 +83,6 @@ public static function toString($callable): string /** * Returns reflection for method or function used in PHP callback. * @param callable $callable type check is escalated to ReflectionException - * @return \ReflectionMethod|\ReflectionFunction * @throws \ReflectionException if callback is not valid */ public static function toReflection($callable): \ReflectionFunctionAbstract @@ -138,7 +91,7 @@ public static function toReflection($callable): \ReflectionFunctionAbstract $callable = self::unwrap($callable); } - if (is_string($callable) && strpos($callable, '::')) { + if (is_string($callable) && str_contains($callable, '::')) { return new \ReflectionMethod($callable); } elseif (is_array($callable)) { return new \ReflectionMethod($callable[0], $callable[1]); @@ -162,10 +115,10 @@ public static function isStatic(callable $callable): bool /** * Unwraps closure created by Closure::fromCallable(). */ - public static function unwrap(\Closure $closure): callable + public static function unwrap(\Closure $closure): \Closure|array|string { $r = new \ReflectionFunction($closure); - if (substr($r->name, -1) === '}') { + if (str_ends_with($r->name, '}')) { return $closure; } elseif ($obj = $r->getClosureThis()) { diff --git a/src/Utils/DateTime.php b/src/Utils/DateTime.php index 320ccc773..48e50334a 100644 --- a/src/Utils/DateTime.php +++ b/src/Utils/DateTime.php @@ -32,19 +32,17 @@ class DateTime extends \DateTime implements \JsonSerializable public const WEEK = 7 * self::DAY; /** average month in seconds */ - public const MONTH = 2629800; + public const MONTH = 2_629_800; /** average year in seconds */ - public const YEAR = 31557600; + public const YEAR = 31_557_600; /** * Creates a DateTime object from a string, UNIX timestamp, or other DateTimeInterface object. - * @param string|int|\DateTimeInterface $time - * @return static * @throws \Exception if the date and time are not valid. */ - public static function from($time) + public static function from(string|int|\DateTimeInterface|null $time): static { if ($time instanceof \DateTimeInterface) { return new static($time->format('Y-m-d H:i:s.u'), $time->getTimezone()); @@ -63,7 +61,6 @@ public static function from($time) /** * Creates DateTime object. - * @return static * @throws Nette\InvalidArgumentException if the date and time are not valid. */ public static function fromParts( @@ -72,8 +69,8 @@ public static function fromParts( int $day, int $hour = 0, int $minute = 0, - float $second = 0.0 - ) { + float $second = 0.0, + ): static { $s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5F', $year, $month, $day, $hour, $minute, $second); if ( !checkdate($month, $day, $year) @@ -92,22 +89,17 @@ public static function fromParts( /** * Returns new DateTime object formatted according to the specified format. - * @param string $format The format the $time parameter should be in - * @param string $time - * @param string|\DateTimeZone $timezone (default timezone is used if null is passed) - * @return static|false */ - #[\ReturnTypeWillChange] - public static function createFromFormat($format, $time, $timezone = null) - { + public static function createFromFormat( + string $format, + string $time, + string|\DateTimeZone $timezone = null, + ): static|false { if ($timezone === null) { $timezone = new \DateTimeZone(date_default_timezone_get()); } elseif (is_string($timezone)) { $timezone = new \DateTimeZone($timezone); - - } elseif (!$timezone instanceof \DateTimeZone) { - throw new Nette\InvalidArgumentException('Invalid timezone given'); } $date = parent::createFromFormat($format, $time, $timezone); @@ -135,9 +127,8 @@ public function __toString(): string /** * Creates a copy with a modified time. - * @return static */ - public function modifyClone(string $modify = '') + public function modifyClone(string $modify = ''): static { $dolly = clone $this; return $modify ? $dolly->modify($modify) : $dolly; diff --git a/src/Utils/Future.php b/src/Utils/Future.php new file mode 100644 index 000000000..3a2ecc764 --- /dev/null +++ b/src/Utils/Future.php @@ -0,0 +1,129 @@ +resolver = $resolver; + } + + + final public function setDefaultValueFactory(callable $cb): static + { + $this->defaultValueFactory = $cb; + return $this; + } + + + final public function bind(int|string|null $key, &$destination): static + { + if ($key !== null) { + $this->destinations[$key][] = &$destination; + } + return $this; + } + + + final public function bindVar(int|string|null &$v): static + { + return $this->bind($v, $v); + } + + + final public function bindArrayValues(array &$values): static + { + foreach ($values as &$destination) { + $this->bind($destination, $destination); + } + return $this; + } + + + final public function bindArrayKeys(array &$keys): static + { + foreach ($keys as $key => &$destination) { + $this->bind($key, $destination); + } + return $this; + } + + + final public function bindArraysKey(string $key, array &$arrays): static + { + foreach ($arrays as &$item) { + assert(is_array($item)); + $this->bind($item[$key], $item[$key]); + } + return $this; + } + + + final public function bindObjectsProperty(string $property, array $objects): static + { + foreach ($objects as $item) { + assert(is_object($item)); + $this->bind($item->{$property}, $item->{$property}); + } + return $this; + } + + + /** + * @throws Nette\UnexpectedValueException if resolver returns value of unexpected type + * @throws FutureException if resolver does not return all required values + */ + final public function resolve(): void + { + if (count($this->destinations) < 1) { + return; + } + + $values = ($this->resolver)(array_keys($this->destinations)); + if ($values instanceof \Traversable) { + $values = iterator_to_array($values); + + } elseif (!is_array($values)) { + throw new Nette\UnexpectedValueException("Resolver returned '" . get_debug_type($values) . "' but array or Traversable expected."); + } + + if ($this->defaultValueFactory === null && count($diff = array_diff_key($this->destinations, $values))) { + throw (new FutureException('Resolver did not return required items.'))->setMissingKeys(array_keys($diff)); + } + + foreach ($this->destinations as $key => $destinations) { + $value = array_key_exists($key, $values) + ? $values[$key] + : ($this->defaultValueFactory)($key); + + foreach ($destinations as &$destination) { + $destination = $value; + } + + unset($this->destinations[$key]); + } + } +} diff --git a/src/Utils/Helpers.php b/src/Utils/Helpers.php index 5e033fe48..1551e1f19 100644 --- a/src/Utils/Helpers.php +++ b/src/Utils/Helpers.php @@ -43,10 +43,8 @@ public static function getLastError(): string /** * Converts false to null, does not change other values. - * @param mixed $value - * @return mixed */ - public static function falseToNull($value) + public static function falseToNull(mixed $value): mixed { return $value === false ? null : $value; } @@ -54,12 +52,8 @@ public static function falseToNull($value) /** * Returns value clamped to the inclusive range of min and max. - * @param int|float $value - * @param int|float $min - * @param int|float $max - * @return int|float */ - public static function clamp($value, $min, $max) + public static function clamp(int|float $value, int|float $min, int|float $max): int|float { if ($min > $max) { throw new \InvalidArgumentException("Minimum ($min) is not less than maximum ($max)."); diff --git a/src/Utils/Html.php b/src/Utils/Html.php index cf6289336..81bad0a4e 100644 --- a/src/Utils/Html.php +++ b/src/Utils/Html.php @@ -238,10 +238,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab /** @var array element's attributes */ public $attrs = []; - /** @var bool use XHTML syntax? */ - public static $xhtml = false; - - /** @var array void elements */ + /** void elements */ public static $emptyElements = [ 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, @@ -251,19 +248,17 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab /** @var array nodes */ protected $children = []; - /** @var string element's name */ - private $name; + /** element's name */ + private string $name = ''; - /** @var bool is element empty? */ - private $isEmpty; + private bool $isEmpty = false; /** * Constructs new HTML element. * @param array|string $attrs element's attributes or plain text content - * @return static */ - public static function el(string $name = null, $attrs = null) + public static function el(string $name = null, array|string $attrs = null): static { $el = new static; $parts = explode(' ', (string) $name, 2); @@ -289,7 +284,7 @@ public static function el(string $name = null, $attrs = null) /** * Returns an object representing HTML text. */ - public static function fromHtml(string $html): self + public static function fromHtml(string $html): static { return (new static)->setHtml($html); } @@ -298,7 +293,7 @@ public static function fromHtml(string $html): self /** * Returns an object representing plain text. */ - public static function fromText(string $text): self + public static function fromText(string $text): static { return (new static)->setText($text); } @@ -333,9 +328,8 @@ public static function htmlToText(string $html): string /** * Changes element's name. - * @return static */ - final public function setName(string $name, bool $isEmpty = null) + final public function setName(string $name, bool $isEmpty = null): static { $this->name = $name; $this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]); @@ -363,9 +357,8 @@ final public function isEmpty(): bool /** * Sets multiple attributes. - * @return static */ - public function addAttributes(array $attrs) + public function addAttributes(array $attrs): static { $this->attrs = array_merge($this->attrs, $attrs); return $this; @@ -374,11 +367,8 @@ public function addAttributes(array $attrs) /** * Appends value to element's attribute. - * @param mixed $value - * @param mixed $option - * @return static */ - public function appendAttribute(string $name, $value, $option = true) + public function appendAttribute(string $name, mixed $value, mixed $option = true): static { if (is_array($value)) { $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : []; @@ -399,10 +389,8 @@ public function appendAttribute(string $name, $value, $option = true) /** * Sets element's attribute. - * @param mixed $value - * @return static */ - public function setAttribute(string $name, $value) + public function setAttribute(string $name, mixed $value): static { $this->attrs[$name] = $value; return $this; @@ -411,9 +399,8 @@ public function setAttribute(string $name, $value) /** * Returns element's attribute. - * @return mixed */ - public function getAttribute(string $name) + public function getAttribute(string $name): mixed { return $this->attrs[$name] ?? null; } @@ -421,9 +408,8 @@ public function getAttribute(string $name) /** * Unsets element's attribute. - * @return static */ - public function removeAttribute(string $name) + public function removeAttribute(string $name): static { unset($this->attrs[$name]); return $this; @@ -432,9 +418,8 @@ public function removeAttribute(string $name) /** * Unsets element's attributes. - * @return static */ - public function removeAttributes(array $attributes) + public function removeAttributes(array $attributes): static { foreach ($attributes as $name) { unset($this->attrs[$name]); @@ -445,9 +430,8 @@ public function removeAttributes(array $attributes) /** * Overloaded setter for element's attribute. - * @param mixed $value */ - final public function __set(string $name, $value): void + final public function __set(string $name, mixed $value): void { $this->attrs[$name] = $value; } @@ -455,9 +439,8 @@ final public function __set(string $name, $value): void /** * Overloaded getter for element's attribute. - * @return mixed */ - final public function &__get(string $name) + final public function &__get(string $name): mixed { return $this->attrs[$name]; } @@ -483,9 +466,8 @@ final public function __unset(string $name): void /** * Overloaded setter for element's attribute. - * @return mixed */ - final public function __call(string $m, array $args) + final public function __call(string $m, array $args): mixed { $p = substr($m, 0, 3); if ($p === 'get' || $p === 'set' || $p === 'add') { @@ -514,9 +496,8 @@ final public function __call(string $m, array $args) /** * Special setter for element's attribute. - * @return static */ - final public function href(string $path, array $query = null) + final public function href(string $path, array $query = null): static { if ($query) { $query = http_build_query($query, '', '&'); @@ -531,10 +512,8 @@ final public function href(string $path, array $query = null) /** * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'. - * @param mixed $value - * @return static */ - public function data(string $name, $value = null) + public function data(string $name, mixed $value = null): static { if (func_num_args() === 1) { $this->attrs['data'] = $name; @@ -549,10 +528,8 @@ public function data(string $name, $value = null) /** * Sets element's HTML content. - * @param HtmlStringable|string $html - * @return static */ - final public function setHtml($html) + final public function setHtml(HtmlStringable|string $html): static { $this->children = [(string) $html]; return $this; @@ -570,10 +547,8 @@ final public function getHtml(): string /** * Sets element's textual content. - * @param HtmlStringable|string|int|float $text - * @return static */ - final public function setText($text) + final public function setText(HtmlStringable|string|int|float $text): static { if (!$text instanceof HtmlStringable) { $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); @@ -594,10 +569,8 @@ final public function getText(): string /** * Adds new element's child. - * @param HtmlStringable|string $child Html node or raw HTML string - * @return static */ - final public function addHtml($child) + final public function addHtml(HtmlStringable|string $child): static { return $this->insert(null, $child); } @@ -605,10 +578,8 @@ final public function addHtml($child) /** * Appends plain-text string to element content. - * @param HtmlStringable|string|int|float $text - * @return static */ - public function addText($text) + public function addText(HtmlStringable|string|int|float $text): static { if (!$text instanceof HtmlStringable) { $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); @@ -619,10 +590,8 @@ public function addText($text) /** * Creates and adds a new Html child. - * @param array|string $attrs element's attributes or raw HTML string - * @return static created element */ - final public function create(string $name, $attrs = null) + final public function create(string $name, array|string $attrs = null): static { $this->insert(null, $child = static::el($name, $attrs)); return $child; @@ -631,10 +600,8 @@ final public function create(string $name, $attrs = null) /** * Inserts child node. - * @param HtmlStringable|string $child Html node or raw HTML string - * @return static */ - public function insert(?int $index, $child, bool $replace = false) + public function insert(?int $index, HtmlStringable|string $child, bool $replace = false): static { $child = $child instanceof self ? $child : (string) $child; if ($index === null) { // append @@ -662,10 +629,8 @@ final public function offsetSet($index, $child): void /** * Returns child node (\ArrayAccess implementation). * @param int $index - * @return HtmlStringable|string */ - #[\ReturnTypeWillChange] - final public function offsetGet($index) + final public function offsetGet($index): HtmlStringable|string { return $this->children[$index]; } @@ -763,15 +728,7 @@ final public function render(int $indent = null): string final public function __toString(): string { - try { - return $this->render(); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } + return $this->render(); } @@ -781,7 +738,7 @@ final public function __toString(): string final public function startTag(): string { return $this->name - ? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>') + ? '<' . $this->name . $this->attributes() . '>' : ''; } @@ -812,11 +769,7 @@ final public function attributes(): string continue; } elseif ($value === true) { - if (static::$xhtml) { - $s .= ' ' . $key . '="' . $key . '"'; - } else { - $s .= ' ' . $key; - } + $s .= ' ' . $key; continue; } elseif (is_array($value)) { @@ -847,14 +800,14 @@ final public function attributes(): string $value = (string) $value; } - $q = strpos($value, '"') === false ? '"' : "'"; + $q = str_contains($value, '"') ? "'" : '"'; $s .= ' ' . $key . '=' . $q . str_replace( ['&', $q, '<'], - ['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'], - $value + ['&', $q === '"' ? '"' : ''', '<'], + $value, ) - . (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '') + . (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '') . $q; } diff --git a/src/Utils/Image.php b/src/Utils/Image.php index 1c305714a..d472120d4 100644 --- a/src/Utils/Image.php +++ b/src/Utils/Image.php @@ -92,13 +92,13 @@ * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) * @property-read int $width * @property-read int $height - * @property-read resource|\GdImage $imageResource + * @property-read \GdImage $imageResource */ class Image { use Nette\SmartObject; - /** {@link resize()} only shrinks images */ + /** @deprecated */ public const SHRINK_ONLY = 0b0001; /** {@link resize()} will ignore aspect ratio */ @@ -125,8 +125,7 @@ class Image private const FORMATS = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::BMP => 'bmp']; - /** @var resource|\GdImage */ - private $image; + private \GdImage $image; /** @@ -147,9 +146,8 @@ public static function rgb(int $red, int $green, int $blue, int $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 - * @return static */ - public static function fromFile(string $file, int &$type = null) + public static function fromFile(string $file, int &$type = null): static { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); @@ -169,11 +167,10 @@ public static function fromFile(string $file, int &$type = null) /** * Reads an image from a string and returns its type in $type. - * @return static * @throws Nette\NotSupportedException if gd extension is not loaded * @throws ImageException */ - public static function fromString(string $s, int &$type = null) + public static function fromString(string $s, int &$type = null): static { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); @@ -192,10 +189,9 @@ public static function fromString(string $s, int &$type = null) /** * Creates a new true color image of the given dimensions. The default color is black. - * @return static * @throws Nette\NotSupportedException if gd extension is not loaded */ - public static function fromBlank(int $width, int $height, array $color = null) + public static function fromBlank(int $width, int $height, array $color = null): static { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); @@ -260,9 +256,8 @@ public static function typeToMimeType(int $type): string /** * Wraps GD image. - * @param resource|\GdImage $image */ - public function __construct($image) + public function __construct(\GdImage $image) { $this->setImageResource($image); imagesavealpha($image, true); @@ -289,14 +284,9 @@ public function getHeight(): int /** * Sets image resource. - * @param resource|\GdImage $image - * @return static */ - protected function setImageResource($image) + protected function setImageResource(\GdImage $image): static { - if (!$image instanceof \GdImage && !(is_resource($image) && get_resource_type($image) === 'gd')) { - throw new Nette\InvalidArgumentException('Image is not valid.'); - } $this->image = $image; return $this; } @@ -304,27 +294,28 @@ protected function setImageResource($image) /** * Returns image GD resource. - * @return resource|\GdImage */ - public function getImageResource() + public function getImageResource(): \GdImage { return $this->image; } /** - * Scales an image. - * @param int|string|null $width in pixels or percent - * @param int|string|null $height in pixels or percent - * @return static + * Scales an image. Width and height accept pixels or percent. + * @param self::FIT|self::FILL|self::STRETCH|self::EXACT $mode */ - public function resize($width, $height, int $flags = self::FIT) - { - if ($flags & self::EXACT) { + public function resize( + int|string|null $width, + int|string|null $height, + int $mode = self::FIT, + bool $shrinkOnly = false, + ): static { + if ($mode & self::EXACT) { return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height); } - [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags); + [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode, $shrinkOnly); if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize $newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource(); @@ -338,7 +329,7 @@ public function resize($width, $height, int $flags = self::FIT) $newWidth, $newHeight, $this->getWidth(), - $this->getHeight() + $this->getHeight(), ); $this->image = $newImage; } @@ -351,17 +342,18 @@ public function resize($width, $height, int $flags = self::FIT) /** - * Calculates dimensions of resized image. - * @param int|string|null $newWidth in pixels or percent - * @param int|string|null $newHeight in pixels or percent + * Calculates dimensions of resized image. Width and height accept pixels or percent. + * @param self::FIT|self::FILL|self::STRETCH $mode */ public static function calculateSize( int $srcWidth, int $srcHeight, $newWidth, $newHeight, - int $flags = self::FIT + int $mode = self::FIT, + bool $shrinkOnly = false, ): array { + $shrinkOnly = $shrinkOnly || ($mode & self::SHRINK_ONLY); // back compatibility if ($newWidth === null) { } elseif (self::isPercent($newWidth)) { $newWidth = (int) round($srcWidth / 100 * abs($newWidth)); @@ -373,17 +365,17 @@ public static function calculateSize( if ($newHeight === null) { } elseif (self::isPercent($newHeight)) { $newHeight = (int) round($srcHeight / 100 * abs($newHeight)); - $flags |= empty($percents) ? 0 : self::STRETCH; + $mode |= empty($percents) ? 0 : self::STRETCH; } else { $newHeight = abs($newHeight); } - if ($flags & self::STRETCH) { // non-proportional + if ($mode & self::STRETCH) { // non-proportional if (!$newWidth || !$newHeight) { throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.'); } - if ($flags & self::SHRINK_ONLY) { + if ($shrinkOnly) { $newWidth = (int) round($srcWidth * min(1, $newWidth / $srcWidth)); $newHeight = (int) round($srcHeight * min(1, $newHeight / $srcHeight)); } @@ -402,11 +394,11 @@ public static function calculateSize( $scale[] = $newHeight / $srcHeight; } - if ($flags & self::FILL) { + if ($mode & self::FILL) { $scale = [max($scale)]; } - if ($flags & self::SHRINK_ONLY) { + if ($shrinkOnly) { $scale[] = 1; } @@ -420,14 +412,9 @@ public static function calculateSize( /** - * Crops image. - * @param int|string $left in pixels or percent - * @param int|string $top in pixels or percent - * @param int|string $width in pixels or percent - * @param int|string $height in pixels or percent - * @return static + * Crops image. Arguments accepts pixels or percent. */ - public function crop($left, $top, $width, $height) + 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); @@ -444,14 +431,16 @@ public function crop($left, $top, $width, $height) /** - * Calculates dimensions of cutout in image. - * @param int|string $left in pixels or percent - * @param int|string $top in pixels or percent - * @param int|string $newWidth in pixels or percent - * @param int|string $newHeight in pixels or percent + * Calculates dimensions of cutout in image. Arguments accepts pixels or percent. */ - public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $top, $newWidth, $newHeight): array - { + 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); } @@ -480,9 +469,8 @@ public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $to /** * Sharpens image a little bit. - * @return static */ - public function sharpen() + public function sharpen(): static { imageconvolution($this->image, [ // my magic numbers ;) [-1, -1, -1], @@ -494,13 +482,10 @@ public function sharpen() /** - * Puts another image into this image. - * @param int|string $left in pixels or percent - * @param int|string $top in pixels or percent + * Puts another image into this image. Left and top accepts pixels or percent. * @param int $opacity 0..100 - * @return static */ - public function place(self $image, $left = 0, $top = 0, int $opacity = 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) { @@ -550,7 +535,7 @@ public function place(self $image, $left = 0, $top = 0, int $opacity = 100) 0, 0, $width, - $height + $height, ); return $this; } @@ -580,7 +565,7 @@ public function save(string $file, int $quality = null, int $type = null): void */ public function toString(int $type = self::JPEG, int $quality = null): string { - return Helpers::capture(function () use ($type, $quality) { + return Helpers::capture(function () use ($type, $quality): void { $this->output($type, $quality); }); } @@ -591,15 +576,7 @@ public function toString(int $type = self::JPEG, int $quality = null): string */ public function __toString(): string { - try { - return $this->toString(); - } catch (\Throwable $e) { - if (func_num_args() || PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } + return $this->toString(); } @@ -655,10 +632,9 @@ private function output(int $type, ?int $quality, string $file = null): void /** * Call to undefined method. - * @return mixed * @throws Nette\MemberAccessException */ - public function __call(string $name, array $args) + public function __call(string $name, array $args): mixed { $function = 'image' . $name; if (!function_exists($function)) { @@ -675,18 +651,18 @@ public function __call(string $name, array $args) $value['red'], $value['green'], $value['blue'], - $value['alpha'] + $value['alpha'], ) ?: imagecolorresolvealpha( $this->image, $value['red'], $value['green'], $value['blue'], - $value['alpha'] + $value['alpha'], ); } } $res = $function($this->image, ...$args); - return $res instanceof \GdImage || (is_resource($res) && get_resource_type($res) === 'gd') + return $res instanceof \GdImage ? $this->setImageResource($res) : $res; } @@ -700,12 +676,9 @@ public function __clone() } - /** - * @param int|string $num in pixels or percent - */ - private static function isPercent(&$num): bool + private static function isPercent(int|string &$num): bool { - if (is_string($num) && substr($num, -1) === '%') { + if (is_string($num) && str_ends_with($num, '%')) { $num = (float) substr($num, 0, -1); return true; } elseif (is_int($num) || $num === (string) (int) $num) { diff --git a/src/Utils/Json.php b/src/Utils/Json.php index 782c1ff2b..6c46d5a48 100644 --- a/src/Utils/Json.php +++ b/src/Utils/Json.php @@ -19,24 +19,32 @@ final class Json { use Nette\StaticClass; + /** @deprecated */ public const FORCE_ARRAY = 0b0001; + /** @deprecated */ public const PRETTY = 0b0010; + /** @deprecated */ public const ESCAPE_UNICODE = 0b0100; /** - * Converts value to JSON format. The flag can be Json::PRETTY, which formats JSON for easier reading and clarity, - * and Json::ESCAPE_UNICODE for ASCII output. - * @param mixed $value + * Converts value to JSON format. Use $pretty for easier reading and clarity and $escapeUnicode for ASCII output. * @throws JsonException */ - public static function encode($value, int $flags = 0): string - { - $flags = ($flags & self::ESCAPE_UNICODE ? 0 : JSON_UNESCAPED_UNICODE) + public static function encode( + mixed $value, + bool|int $pretty = false, + bool $escapeUnicode = false, + ): string { + if (is_int($pretty)) { // back compatibility + $escapeUnicode = $pretty & self::ESCAPE_UNICODE; + $pretty &= self::PRETTY; + } + $flags = ($escapeUnicode ? 0 : JSON_UNESCAPED_UNICODE) | JSON_UNESCAPED_SLASHES - | ($flags & self::PRETTY ? JSON_PRETTY_PRINT : 0) + | ($pretty ? JSON_PRETTY_PRINT : 0) | (defined('JSON_PRESERVE_ZERO_FRACTION') ? JSON_PRESERVE_ZERO_FRACTION : 0); // since PHP 5.6.6 & PECL JSON-C 1.3.7 $json = json_encode($value, $flags); @@ -48,14 +56,12 @@ public static function encode($value, int $flags = 0): string /** - * Parses JSON to PHP value. The flag can be Json::FORCE_ARRAY, which forces an array instead of an object as the return value. - * @return mixed + * Parses JSON to PHP value. Parameter $forceArray forces an array instead of an object as the return value. * @throws JsonException */ - public static function decode(string $json, int $flags = 0) + public static function decode(string $json, bool|int $forceArray = false): mixed { - $forceArray = (bool) ($flags & self::FORCE_ARRAY); - $value = json_decode($json, $forceArray, 512, JSON_BIGINT_AS_STRING); + $value = json_decode($json, (bool) $forceArray, 512, JSON_BIGINT_AS_STRING); if ($error = json_last_error()) { throw new JsonException(json_last_error_msg(), $error); } diff --git a/src/Utils/ObjectHelpers.php b/src/Utils/ObjectHelpers.php index c0fda7d8c..6ec7928d1 100644 --- a/src/Utils/ObjectHelpers.php +++ b/src/Utils/ObjectHelpers.php @@ -28,8 +28,8 @@ public static function strictGet(string $class, string $name): void { $rc = new \ReflectionClass($class); $hint = self::getSuggestion(array_merge( - array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }), - self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m') + array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()), + self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'), ), $name); throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); } @@ -43,8 +43,8 @@ public static function strictSet(string $class, string $name): void { $rc = new \ReflectionClass($class); $hint = self::getSuggestion(array_merge( - array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }), - self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m') + array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()), + self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'), ), $name); throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); } @@ -76,7 +76,7 @@ public static function strictCall(string $class, string $method, array $addition $hint = self::getSuggestion(array_merge( get_class_methods($class), self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:\S+[ \t]+)??(\w+)\(~m'), - $additionalMethods + $additionalMethods, ), $method); throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); } @@ -107,8 +107,8 @@ public static function strictStaticCall(string $class, string $method): void } else { $hint = self::getSuggestion( - array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), function ($m) { return $m->isStatic(); }), - $method + array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), fn($m) => $m->isStatic()), + $method, ); throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); } @@ -130,10 +130,10 @@ public static function getMagicProperties(string $class): array $rc = new \ReflectionClass($class); preg_match_all( - '~^ [ \t*]* @property(|-read|-write) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', + '~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', (string) $rc->getDocComment(), $matches, - PREG_SET_ORDER + PREG_SET_ORDER, ); $props = []; @@ -147,7 +147,7 @@ public static function getMagicProperties(string $class): array && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); if ($read || $write) { - $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3; + $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3 | ($type === '-deprecated') << 4; } } @@ -196,16 +196,16 @@ private static function parseFullDoc(\ReflectionClass $rc, string $pattern): arr $traits += $trait->getTraits(); } } while ($rc = $rc->getParentClass()); - return preg_match_all($pattern, implode($doc), $m) ? $m[1] : []; + return preg_match_all($pattern, implode('', $doc), $m) ? $m[1] : []; } /** * Checks if the public non-static property exists. - * @return bool|string returns 'event' if the property exists and has event like name + * Returns 'event' if the property exists and has event like name * @internal */ - public static function hasProperty(string $class, string $name) + public static function hasProperty(string $class, string $name): bool|string { static $cache; $prop = &$cache[$class][$name]; diff --git a/src/Utils/ObjectMixin.php b/src/Utils/ObjectMixin.php deleted file mode 100644 index 33249509b..000000000 --- a/src/Utils/ObjectMixin.php +++ /dev/null @@ -1,41 +0,0 @@ -page = $page; return $this; @@ -109,9 +104,8 @@ public function getLastItemOnPage(): int /** * Sets first page (base) number. - * @return static */ - public function setBase(int $base) + public function setBase(int $base): static { $this->base = $base; return $this; @@ -172,9 +166,8 @@ public function getPageCount(): ?int /** * Sets the number of items to display on a single page. - * @return static */ - public function setItemsPerPage(int $itemsPerPage) + public function setItemsPerPage(int $itemsPerPage): static { $this->itemsPerPage = max(1, $itemsPerPage); return $this; @@ -192,9 +185,8 @@ public function getItemsPerPage(): int /** * Sets the total number of items. - * @return static */ - public function setItemCount(int $itemCount = null) + public function setItemCount(int $itemCount = null): static { $this->itemCount = $itemCount === null ? null : max(0, $itemCount); return $this; diff --git a/src/Utils/Random.php b/src/Utils/Random.php index 2403b4d3d..0ac9d152c 100644 --- a/src/Utils/Random.php +++ b/src/Utils/Random.php @@ -25,9 +25,12 @@ final class Random */ public static function generate(int $length = 10, string $charlist = '0-9a-z'): string { - $charlist = count_chars(preg_replace_callback('#.-.#', function (array $m): string { - return implode('', range($m[0][0], $m[0][2])); - }, $charlist), 3); + $charlist = preg_replace_callback( + '#.-.#', + fn(array $m): string => implode('', range($m[0][0], $m[0][2])), + $charlist, + ); + $charlist = count_chars($charlist, mode: 3); $chLen = strlen($charlist); if ($length < 1) { diff --git a/src/Utils/Reflection.php b/src/Utils/Reflection.php index 818643f11..bdfa47fc4 100644 --- a/src/Utils/Reflection.php +++ b/src/Utils/Reflection.php @@ -51,93 +51,38 @@ public static function isClassKeyword(string $name): bool /** * Returns the type of return value of given function or method and normalizes `self`, `static`, and `parent` to actual class names. * If the function does not have a return type, it returns null. - * If the function has union or intersection type, it throws Nette\InvalidStateException. */ - public static function getReturnType(\ReflectionFunctionAbstract $func): ?string + public static function getReturnType(\ReflectionFunctionAbstract $func): ?Type { - $type = $func->getReturnType() ?? (PHP_VERSION_ID >= 80100 && $func instanceof \ReflectionMethod ? $func->getTentativeReturnType() : null); - return self::getType($func, $type); - } - - - /** - * @deprecated - */ - public static function getReturnTypes(\ReflectionFunctionAbstract $func): array - { - $type = Type::fromReflection($func); - return $type ? $type->getNames() : []; + return Type::fromReflection($func); } /** * Returns the type of given parameter and normalizes `self` and `parent` to the actual class names. * If the parameter does not have a type, it returns null. - * If the parameter has union or intersection type, it throws Nette\InvalidStateException. */ - public static function getParameterType(\ReflectionParameter $param): ?string + public static function getParameterType(\ReflectionParameter $param): ?Type { - return self::getType($param, $param->getType()); - } - - - /** - * @deprecated - */ - public static function getParameterTypes(\ReflectionParameter $param): array - { - $type = Type::fromReflection($param); - return $type ? $type->getNames() : []; + return Type::fromReflection($param); } /** * Returns the type of given property and normalizes `self` and `parent` to the actual class names. * If the property does not have a type, it returns null. - * If the property has union or intersection type, it throws Nette\InvalidStateException. */ - public static function getPropertyType(\ReflectionProperty $prop): ?string + public static function getPropertyType(\ReflectionProperty $prop): ?Type { - return self::getType($prop, PHP_VERSION_ID >= 70400 ? $prop->getType() : null); - } - - - /** - * @deprecated - */ - public static function getPropertyTypes(\ReflectionProperty $prop): array - { - $type = Type::fromReflection($prop); - return $type ? $type->getNames() : []; - } - - - /** - * @param \ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflection - */ - private static function getType($reflection, ?\ReflectionType $type): ?string - { - if ($type === null) { - return null; - - } elseif ($type instanceof \ReflectionNamedType) { - return Type::resolve($type->getName(), $reflection); - - } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { - throw new Nette\InvalidStateException('The ' . self::toString($reflection) . ' is not expected to have a union or intersection type.'); - - } else { - throw new Nette\InvalidStateException('Unexpected type of ' . self::toString($reflection)); - } + return Type::fromReflection($prop); } /** * Returns the default value of parameter. If it is a constant, it returns its value. - * @return mixed * @throws \ReflectionException If the parameter does not have a default value or the constant cannot be resolved */ - public static function getParameterDefaultValue(\ReflectionParameter $param) + public static function getParameterDefaultValue(\ReflectionParameter $param): mixed { if ($param->isDefaultValueConstant()) { $const = $orig = $param->getDefaultValueConstantName(); @@ -311,7 +256,7 @@ public static function getUseStatements(\ReflectionClass $class): array private static function parseUseStatements(string $code, string $forClass = null): array { try { - $tokens = token_get_all($code, TOKEN_PARSE); + $tokens = \PhpToken::tokenize($code, TOKEN_PARSE); } catch (\ParseError $e) { trigger_error($e->getMessage(), E_USER_NOTICE); $tokens = []; @@ -319,13 +264,11 @@ private static function parseUseStatements(string $code, string $forClass = null $namespace = $class = $classLevel = $level = null; $res = $uses = []; - $nameTokens = PHP_VERSION_ID < 80000 - ? [T_STRING, T_NS_SEPARATOR] - : [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED]; + $nameTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED]; while ($token = current($tokens)) { next($tokens); - switch (is_array($token) ? $token[0] : $token) { + switch ($token->id) { case T_NAMESPACE: $namespace = ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\'); $uses = []; @@ -378,11 +321,11 @@ private static function parseUseStatements(string $code, string $forClass = null case T_CURLY_OPEN: case T_DOLLAR_OPEN_CURLY_BRACES: - case '{': + case ord('{'): $level++; break; - case '}': + case ord('}'): if ($level === $classLevel) { $class = $classLevel = null; } @@ -394,14 +337,13 @@ private static function parseUseStatements(string $code, string $forClass = null } - private static function fetch(array &$tokens, $take): ?string + private static function fetch(array &$tokens, string|int|array $take): ?string { $res = null; while ($token = current($tokens)) { - [$token, $s] = is_array($token) ? $token : [$token, $token]; - if (in_array($token, (array) $take, true)) { - $res .= $s; - } elseif (!in_array($token, [T_DOC_COMMENT, T_WHITESPACE, T_COMMENT], true)) { + if ($token->is($take)) { + $res .= $token->text; + } elseif (!$token->is([T_DOC_COMMENT, T_WHITESPACE, T_COMMENT])) { break; } next($tokens); diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php index e5e626b43..26dfe932a 100644 --- a/src/Utils/Strings.php +++ b/src/Utils/Strings.php @@ -57,12 +57,28 @@ public static function chr(int $code): string } + /** + * 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]; + } + + /** * Starts the $haystack string with the prefix $needle? */ public static function startsWith(string $haystack, string $needle): bool { - return strncmp($haystack, $needle, strlen($needle)) === 0; + return str_starts_with($haystack, $needle); } @@ -71,7 +87,7 @@ public static function startsWith(string $haystack, string $needle): bool */ public static function endsWith(string $haystack, string $needle): bool { - return $needle === '' || substr($haystack, -strlen($needle)) === $needle; + return str_ends_with($haystack, $needle); } @@ -80,7 +96,7 @@ public static function endsWith(string $haystack, string $needle): bool */ public static function contains(string $haystack, string $needle): bool { - return strpos($haystack, $needle) !== false; + return str_contains($haystack, $needle); } @@ -184,7 +200,7 @@ public static function toAscii(string $s): string $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- <->|-.' + 'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.', ); $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); } else { @@ -467,36 +483,79 @@ private static function pos(string $haystack, string $needle, int $nth = 1): ?in /** - * Splits a string into array by the regular expression. - * Argument $flag takes same arguments as preg_split(), but PREG_SPLIT_DELIM_CAPTURE is set by default. + * Splits a string into array by the regular expression. Parenthesized expression in the delimiter are captured. */ - public static function split(string $subject, string $pattern, int $flags = 0): array - { - return self::pcre('preg_split', [$pattern, $subject, -1, $flags | PREG_SPLIT_DELIM_CAPTURE]); + public static function split( + string $subject, + string $pattern, + bool|int $captureOffset = false, + bool $skipEmpty = false, + int $limit = -1, + bool $utf8 = false, + ): array { + $flags = is_int($captureOffset) && $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]); + if ($utf8 && ($flags & PREG_SPLIT_OFFSET_CAPTURE)) { + return self::bytesToChars($subject, [$m])[0]; + } + return $m; } /** * Checks if given string matches a regular expression pattern and returns an array with first found match and each subpattern. - * Argument $flag takes same arguments as function preg_match(). */ - public static function match(string $subject, string $pattern, int $flags = 0, int $offset = 0): ?array - { + public static function match( + string $subject, + string $pattern, + bool|int $captureOffset = false, + int $offset = 0, + bool $unmatchedAsNull = false, + bool $utf8 = false, + ): ?array { + $flags = is_int($captureOffset) && $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; } - return self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset]) - ? $m - : null; + if (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) { + return null; + } + if ($utf8 && ($flags & PREG_OFFSET_CAPTURE)) { + return self::bytesToChars($subject, [$m])[0]; + } + return $m; } /** * Finds all occurrences matching regular expression pattern and returns a two-dimensional array. - * Argument $flag takes same arguments as function preg_match_all(), but PREG_SET_ORDER is set by default. - */ - public static function matchAll(string $subject, string $pattern, int $flags = 0, int $offset = 0): array - { + * Result is array of matches (ie uses by default PREG_SET_ORDER). + */ + public static function matchAll( + string $subject, + string $pattern, + bool|int $captureOffset = false, + int $offset = 0, + bool $unmatchedAsNull = false, + bool $patternOrder = false, + bool $utf8 = false, + ): array { + $flags = is_int($captureOffset) && $captureOffset // back compatibility + ? $captureOffset + : ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0); + if ($utf8) { + $offset = strlen(self::substring($subject, 0, $offset)); + $pattern .= 'u'; + } if ($offset > strlen($subject)) { return []; } @@ -505,32 +564,69 @@ public static function matchAll(string $subject, string $pattern, int $flags = 0 ($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER), $offset, ]); + if ($utf8 && ($flags & PREG_OFFSET_CAPTURE)) { + return self::bytesToChars($subject, $m); + } return $m; } /** * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`. - * @param string|array $pattern - * @param string|callable $replacement */ - public static function replace(string $subject, $pattern, $replacement = '', int $limit = -1): string - { + public static function replace( + string $subject, + 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."); } - return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit]); + $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) { diff --git a/src/Utils/Type.php b/src/Utils/Type.php index 257cc2968..760e4205d 100644 --- a/src/Utils/Type.php +++ b/src/Utils/Type.php @@ -17,26 +17,19 @@ */ final class Type { - /** @var array */ - private $types; - - /** @var bool */ - private $single; - - /** @var string |, & */ - private $kind; + private array $types; + private bool $single; + private string $kind; // | & /** * Creates a Type object based on reflection. Resolves self, static and parent to the actual class name. * If the subject has no type, it returns null. - * @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection */ - public static function fromReflection($reflection): ?self - { - if ($reflection instanceof \ReflectionProperty && PHP_VERSION_ID < 70400) { - return null; - } elseif ($reflection instanceof \ReflectionMethod) { + public static function fromReflection( + \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection, + ): ?self { + if ($reflection instanceof \ReflectionMethod) { $type = $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 ? $reflection->getTentativeReturnType() : null); } else { $type = $reflection instanceof \ReflectionFunctionAbstract @@ -54,10 +47,10 @@ public static function fromReflection($reflection): ?self } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { return new self( array_map( - function ($t) use ($reflection) { return self::resolve($t->getName(), $reflection); }, - $type->getTypes() + fn($t) => self::resolve($t->getName(), $reflection), + $type->getTypes(), ), - $type instanceof \ReflectionUnionType ? '|' : '&' + $type instanceof \ReflectionUnionType ? '|' : '&', ); } else { @@ -90,10 +83,11 @@ public static function fromString(string $type): self /** * Resolves 'self', 'static' and 'parent' to the actual class name. - * @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection */ - public static function resolve(string $type, $reflection): string - { + public static function resolve( + string $type, + \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection, + ): string { $lower = strtolower($type); if ($reflection instanceof \ReflectionFunction) { return $type; @@ -142,7 +136,7 @@ public function getNames(): array */ public function getTypes(): array { - return array_map(function ($name) { return self::fromString($name); }, $this->types); + return array_map(fn($name) => self::fromString($name), $this->types); } @@ -223,27 +217,21 @@ public function allows(string $type): bool $type = self::fromString($type); if ($this->isIntersection()) { - if (!$type->isIntersection()) { - return false; - } - return Arrays::every($this->types, function ($currentType) use ($type) { - $builtin = Reflection::isBuiltinType($currentType); - return Arrays::some($type->types, function ($testedType) use ($currentType, $builtin) { - return $builtin + return $type->isIntersection() + && Arrays::every($this->types, function ($currentType) use ($type) { + $builtin = Reflection::isBuiltinType($currentType); + return Arrays::some($type->types, fn($testedType) => $builtin ? strcasecmp($currentType, $testedType) === 0 - : is_a($testedType, $currentType, true); + : is_a($testedType, $currentType, true)); }); - }); } $method = $type->isIntersection() ? 'some' : 'every'; return Arrays::$method($type->types, function ($testedType) { $builtin = Reflection::isBuiltinType($testedType); - return Arrays::some($this->types, function ($currentType) use ($testedType, $builtin) { - return $builtin - ? strcasecmp($currentType, $testedType) === 0 - : is_a($testedType, $currentType, true); - }); + return Arrays::some($this->types, fn($currentType) => $builtin + ? strcasecmp($currentType, $testedType) === 0 + : is_a($testedType, $currentType, true)); }); } } diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php index 60e0eaab3..b9ee50b20 100644 --- a/src/Utils/Validators.php +++ b/src/Utils/Validators.php @@ -87,10 +87,9 @@ class Validators /** * Verifies that the value is of expected types separated by pipe. - * @param mixed $value * @throws AssertionException */ - public static function assert($value, string $expected, string $label = 'variable'): void + public static function assert(mixed $value, string $expected, string $label = 'variable'): void { if (!static::is($value, $expected)) { $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); @@ -99,7 +98,7 @@ public static function assert($value, string $expected, string $label = 'variabl if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) { $type .= ' ' . var_export($value, true); } elseif (is_object($value)) { - $type .= ' ' . get_class($value); + $type .= ' ' . $value::class; } throw new AssertionException("The $label expects to be $expected, $type given."); } @@ -109,14 +108,13 @@ public static function assert($value, string $expected, string $label = 'variabl /** * Verifies that element $key in array is of expected types separated by pipe. * @param mixed[] $array - * @param int|string $key * @throws AssertionException */ public static function assertField( array $array, $key, string $expected = null, - string $label = "item '%' in array" + string $label = "item '%' in array", ): void { if (!array_key_exists($key, $array)) { throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.'); @@ -129,17 +127,16 @@ public static function assertField( /** * Verifies that the value is of expected types separated by pipe. - * @param mixed $value */ - public static function is($value, string $expected): bool + public static function is(mixed $value, string $expected): bool { foreach (explode('|', $expected) as $item) { - if (substr($item, -2) === '[]') { + if (str_ends_with($item, '[]')) { if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) { return true; } continue; - } elseif (substr($item, 0, 1) === '?') { + } elseif (str_starts_with($item, '?')) { $item = substr($item, 1); if ($value === null) { return true; @@ -200,9 +197,8 @@ public static function everyIs(iterable $values, string $expected): bool /** * Checks if the value is an integer or a float. - * @param mixed $value */ - public static function isNumber($value): bool + public static function isNumber(mixed $value): bool { return is_int($value) || is_float($value); } @@ -210,9 +206,8 @@ public static function isNumber($value): bool /** * Checks if the value is an integer or a integer written in a string. - * @param mixed $value */ - public static function isNumericInt($value): bool + public static function isNumericInt(mixed $value): bool { return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value)); } @@ -220,9 +215,8 @@ public static function isNumericInt($value): bool /** * Checks if the value is a number or a number written in a string. - * @param mixed $value */ - public static function isNumeric($value): bool + public static function isNumeric(mixed $value): bool { return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]*[.]?[0-9]+$#D', $value)); } @@ -230,9 +224,8 @@ public static function isNumeric($value): bool /** * Checks if the value is a syntactically correct callback. - * @param mixed $value */ - public static function isCallable($value): bool + public static function isCallable(mixed $value): bool { return $value && is_callable($value, true); } @@ -240,9 +233,8 @@ public static function isCallable($value): bool /** * Checks if the value is a valid UTF-8 string. - * @param mixed $value */ - public static function isUnicode($value): bool + public static function isUnicode(mixed $value): bool { return is_string($value) && preg_match('##u', $value); } @@ -250,9 +242,8 @@ public static function isUnicode($value): bool /** * Checks if the value is 0, '', false or null. - * @param mixed $value */ - public static function isNone($value): bool + public static function isNone(mixed $value): bool { return $value == null; // intentionally == } @@ -267,10 +258,9 @@ public static function isMixed(): bool /** * Checks if a variable is a zero-based integer indexed array. - * @param mixed $value * @deprecated use Nette\Utils\Arrays::isList */ - public static function isList($value): bool + public static function isList(mixed $value): bool { return Arrays::isList($value); } @@ -279,9 +269,8 @@ public static function isList($value): bool /** * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null). * Numbers, strings and DateTime objects can be compared. - * @param mixed $value */ - public static function isInRange($value, array $range): bool + public static function isInRange(mixed $value, array $range): bool { if ($value === null || !(isset($range[0]) || isset($range[1]))) { return false; @@ -310,14 +299,13 @@ public static function isEmail(string $value): bool $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part $alpha = "a-z\x80-\xFF"; // superset of IDN return (bool) preg_match(<<missingKeys = $keys; + return $this; + } + + + public function getMissingKeys(): array + { + return $this->missingKeys; + } +} diff --git a/tests/Iterators/Mapper.phpt b/tests/Iterators/Mapper.phpt index c521e8d0e..791a238ea 100644 --- a/tests/Iterators/Mapper.phpt +++ b/tests/Iterators/Mapper.phpt @@ -18,9 +18,7 @@ $arr = [ 'David' => 'Grudl', ]; -$callback = function ($item, $key) { - return $key . ': ' . $item; -}; +$callback = fn($item, $key) => $key . ': ' . $item; $iterator = new Iterators\Mapper(new \ArrayIterator($arr), $callback); diff --git a/tests/Utils/Arrays.associate().phpt b/tests/Utils/Arrays.associate().phpt index 80e439e8f..d7cde4a53 100644 --- a/tests/Utils/Arrays.associate().phpt +++ b/tests/Utils/Arrays.associate().phpt @@ -28,12 +28,12 @@ Assert::same( 'Mary' => ['name' => 'Mary', 'age' => null], 'Paul' => ['name' => 'Paul', 'age' => 44], ], - Arrays::associate($arr, 'name') + Arrays::associate($arr, 'name'), ); Assert::same( [], - Arrays::associate([], 'name') + Arrays::associate([], 'name'), ); Assert::same( @@ -42,17 +42,17 @@ Assert::same( 'Mary' => ['name' => 'Mary', 'age' => null], 'Paul' => ['name' => 'Paul', 'age' => 44], ], - Arrays::associate($arr, 'name=') + Arrays::associate($arr, 'name='), ); Assert::same( ['John' => 22, 'Mary' => null, 'Paul' => 44], - Arrays::associate($arr, 'name=age') + Arrays::associate($arr, 'name=age'), ); Assert::same(// path as array ['John' => 22, 'Mary' => null, 'Paul' => 44], - Arrays::associate($arr, ['name', '=', 'age']) + Arrays::associate($arr, ['name', '=', 'age']), ); Assert::equal( @@ -70,7 +70,7 @@ Assert::equal( 'age' => 44, ], ], - Arrays::associate($arr, 'name->') + Arrays::associate($arr, 'name->'), ); Assert::equal( @@ -88,7 +88,7 @@ Assert::equal( 'Paul' => ['name' => 'Paul', 'age' => 44], ], ], - Arrays::associate($arr, 'age->name') + Arrays::associate($arr, 'age->name'), ); Assert::equal( @@ -97,12 +97,12 @@ Assert::equal( 'Mary' => ['name' => 'Mary', 'age' => null], 'Paul' => ['name' => 'Paul', 'age' => 44], ], - Arrays::associate($arr, '->name') + Arrays::associate($arr, '->name'), ); Assert::equal( (object) [], - Arrays::associate([], '->name') + Arrays::associate([], '->name'), ); Assert::same( @@ -118,7 +118,7 @@ Assert::same( 44 => ['name' => 'Paul', 'age' => 44], ], ], - Arrays::associate($arr, 'name|age') + Arrays::associate($arr, 'name|age'), ); Assert::same( @@ -127,7 +127,7 @@ Assert::same( 'Mary' => ['name' => 'Mary', 'age' => null], 'Paul' => ['name' => 'Paul', 'age' => 44], ], - Arrays::associate($arr, 'name|') + Arrays::associate($arr, 'name|'), ); Assert::same( @@ -143,7 +143,7 @@ Assert::same( ['name' => 'Paul', 'age' => 44], ], ], - Arrays::associate($arr, 'name[]') + Arrays::associate($arr, 'name[]'), ); Assert::same( @@ -153,12 +153,12 @@ Assert::same( ['Mary' => ['name' => 'Mary', 'age' => null]], ['Paul' => ['name' => 'Paul', 'age' => 44]], ], - Arrays::associate($arr, '[]name') + Arrays::associate($arr, '[]name'), ); Assert::same( ['John', 'John', 'Mary', 'Paul'], - Arrays::associate($arr, '[]=name') + Arrays::associate($arr, '[]=name'), ); Assert::same( @@ -174,12 +174,12 @@ Assert::same( [44 => ['name' => 'Paul', 'age' => 44]], ], ], - Arrays::associate($arr, 'name[]age') + Arrays::associate($arr, 'name[]age'), ); Assert::same( $arr, - Arrays::associate($arr, '[]') + Arrays::associate($arr, '[]'), ); // converts object to array @@ -190,7 +190,7 @@ Assert::same( (object) ['name' => 'John', 'age' => 22], (object) ['name' => 'Mary', 'age' => null], (object) ['name' => 'Paul', 'age' => 44], - ], '[]') + ], '[]'), ); // allowes objects in keys @@ -198,11 +198,11 @@ Assert::equal( ['2014-02-05 00:00:00' => new DateTime('2014-02-05')], Arrays::associate($arr = [ ['date' => new DateTime('2014-02-05')], - ], 'date=date') + ], 'date=date'), ); Assert::equal( (object) ['2014-02-05 00:00:00' => new DateTime('2014-02-05')], Arrays::associate($arr = [ ['date' => new DateTime('2014-02-05')], - ], '->date=date') + ], '->date=date'), ); diff --git a/tests/Utils/Arrays.every().phpt b/tests/Utils/Arrays.every().phpt index eed82ec59..074bf0b1f 100644 --- a/tests/Utils/Arrays.every().phpt +++ b/tests/Utils/Arrays.every().phpt @@ -18,7 +18,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; }, ); Assert::true($res); Assert::same([], $log); @@ -29,7 +29,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::true($res); Assert::same([], $log); @@ -40,7 +40,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; }, ); Assert::false($res); Assert::same([['a', 0, $arr]], $log); @@ -51,7 +51,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::true($res); Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log); @@ -62,7 +62,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; }, ); Assert::false($res); Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log); @@ -73,7 +73,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::true($res); Assert::same([['a', 'x', $arr], ['b', 'y', $arr]], $log); @@ -84,7 +84,7 @@ test('', function () { $log = []; $res = Arrays::every( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::true($res); Assert::same([['a', 'x', $arr], ['b', 'y', $arr]], $log); diff --git a/tests/Utils/Arrays.get().phpt b/tests/Utils/Arrays.get().phpt index 277af3bf3..9a3195a30 100644 --- a/tests/Utils/Arrays.get().phpt +++ b/tests/Utils/Arrays.get().phpt @@ -22,7 +22,7 @@ $arr = [ ]; test('Single item', function () use ($arr) { - Assert::same('first', Arrays::get($arr, null)); + Assert::same('first', Arrays::get($arr, '')); Assert::same('second', Arrays::get($arr, 1)); Assert::same('second', Arrays::get($arr, 1, 'x')); Assert::same('x', Arrays::get($arr, 'undefined', 'x')); diff --git a/tests/Utils/Arrays.getKeyOffset().phpt b/tests/Utils/Arrays.getKeyOffset().phpt index 357467755..2a2826f00 100644 --- a/tests/Utils/Arrays.getKeyOffset().phpt +++ b/tests/Utils/Arrays.getKeyOffset().phpt @@ -24,6 +24,5 @@ Assert::same(3, Arrays::getKeyOffset($arr, '1')); Assert::same(3, Arrays::getKeyOffset($arr, 1)); Assert::same(2, Arrays::getKeyOffset($arr, 7)); Assert::same(1, Arrays::getKeyOffset($arr, 0)); -Assert::same(0, Arrays::getKeyOffset($arr, null)); Assert::same(0, Arrays::getKeyOffset($arr, '')); Assert::null(Arrays::getKeyOffset($arr, 'undefined')); diff --git a/tests/Utils/Arrays.getRef().phpt b/tests/Utils/Arrays.getRef().phpt index ad242bb90..d45af2b53 100644 --- a/tests/Utils/Arrays.getRef().phpt +++ b/tests/Utils/Arrays.getRef().phpt @@ -23,7 +23,7 @@ $arr = [ test('Single item', function () use ($arr) { $dolly = $arr; - $item = &Arrays::getRef($dolly, null); + $item = &Arrays::getRef($dolly, ''); $item = 'changed'; Assert::same([ '' => 'changed', diff --git a/tests/Utils/Arrays.grep().phpt b/tests/Utils/Arrays.grep().phpt index f356405d3..f8613b8ae 100644 --- a/tests/Utils/Arrays.grep().phpt +++ b/tests/Utils/Arrays.grep().phpt @@ -21,3 +21,8 @@ Assert::same([ 0 => 'a', 2 => 'c', ], Arrays::grep(['a', '1', 'c'], '#\d#', PREG_GREP_INVERT)); + +Assert::same([ + 0 => 'a', + 2 => 'c', +], Arrays::grep(['a', '1', 'c'], '#\d#', invert: true)); diff --git a/tests/Utils/Arrays.invoke.phpt b/tests/Utils/Arrays.invoke.phpt index 173106ce4..19d1094f0 100644 --- a/tests/Utils/Arrays.invoke.phpt +++ b/tests/Utils/Arrays.invoke.phpt @@ -30,10 +30,10 @@ $list['key'] = [new Test, 'fn2']; Assert::same( ['Test::fn1 a,b', 'key' => 'Test::fn2 a,b'], - Arrays::invoke($list, 'a', 'b') + Arrays::invoke($list, 'a', 'b'), ); Assert::same( ['Test::fn1 a,b', 'key' => 'Test::fn2 a,b'], - Arrays::invoke(new ArrayIterator($list), 'a', 'b') + Arrays::invoke(new ArrayIterator($list), 'a', 'b'), ); diff --git a/tests/Utils/Arrays.invokeMethod.phpt b/tests/Utils/Arrays.invokeMethod.phpt index 8e1eec51d..8c42d3186 100644 --- a/tests/Utils/Arrays.invokeMethod.phpt +++ b/tests/Utils/Arrays.invokeMethod.phpt @@ -26,10 +26,10 @@ $list = [new Test1, 'key' => new Test2]; Assert::same( ['Test1 a,b', 'key' => 'Test2 a,b'], - Arrays::invokeMethod($list, 'fn', 'a', 'b') + Arrays::invokeMethod($list, 'fn', 'a', 'b'), ); Assert::same( ['Test1 a,b', 'key' => 'Test2 a,b'], - Arrays::invokeMethod(new ArrayIterator($list), 'fn', 'a', 'b') + Arrays::invokeMethod(new ArrayIterator($list), 'fn', 'a', 'b'), ); diff --git a/tests/Utils/Arrays.map().phpt b/tests/Utils/Arrays.map().phpt index 5cdabdedf..2970e9059 100644 --- a/tests/Utils/Arrays.map().phpt +++ b/tests/Utils/Arrays.map().phpt @@ -18,7 +18,7 @@ test('', function () { $log = []; $res = Arrays::map( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::same([], $res); Assert::same([], $log); @@ -29,7 +29,7 @@ test('', function () { $log = []; $res = Arrays::map( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v . $v; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v . $v; }, ); Assert::same(['aa', 'bb'], $res); Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log); @@ -40,7 +40,7 @@ test('', function () { $log = []; $res = Arrays::map( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v . $v; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v . $v; }, ); Assert::same(['x' => 'aa', 'y' => 'bb'], $res); Assert::same([['a', 'x', $arr], ['b', 'y', $arr]], $log); @@ -51,7 +51,7 @@ test('', function () { $log = []; $res = Arrays::map( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v . $v; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v . $v; }, ); Assert::same(['x' => 'aa', 'y' => 'bb'], $res); Assert::same([['a', 'x', $arr], ['b', 'y', $arr]], $log); diff --git a/tests/Utils/Arrays.normalize.phpt b/tests/Utils/Arrays.normalize.phpt index 0ff385a96..44eab09f7 100644 --- a/tests/Utils/Arrays.normalize.phpt +++ b/tests/Utils/Arrays.normalize.phpt @@ -25,7 +25,7 @@ Assert::same( 'a' => 'second', 'd' => ['third'], 7 => 'fourth', - ]) + ]), ); @@ -37,5 +37,5 @@ Assert::same( Arrays::normalize([ 1 => 'first', '' => 'second', - ], true) + ], true), ); diff --git a/tests/Utils/Arrays.pick().phpt b/tests/Utils/Arrays.pick().phpt index b81ea0d89..787400df3 100644 --- a/tests/Utils/Arrays.pick().phpt +++ b/tests/Utils/Arrays.pick().phpt @@ -20,7 +20,7 @@ $arr = [ ]; test('Single item', function () use ($arr) { - Assert::same('null', Arrays::pick($arr, null)); + Assert::same('null', Arrays::pick($arr, '')); Assert::same('first', Arrays::pick($arr, 1)); Assert::same('x', Arrays::pick($arr, 1, 'x')); Assert::exception(function () use ($arr) { diff --git a/tests/Utils/Arrays.renameKey().phpt b/tests/Utils/Arrays.renameKey().phpt index c852df90f..64f58ee35 100644 --- a/tests/Utils/Arrays.renameKey().phpt +++ b/tests/Utils/Arrays.renameKey().phpt @@ -36,7 +36,7 @@ Assert::same([ 'new1' => 'third', ], $arr); -Arrays::renameKey($arr, null, 'new3'); +Arrays::renameKey($arr, '', 'new3'); Assert::same([ 'new3' => 'first', 'new2' => 'second', diff --git a/tests/Utils/Arrays.some().phpt b/tests/Utils/Arrays.some().phpt index 3cbfadb3a..7a34bc26e 100644 --- a/tests/Utils/Arrays.some().phpt +++ b/tests/Utils/Arrays.some().phpt @@ -18,7 +18,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; }, ); Assert::false($res); Assert::same([], $log); @@ -29,7 +29,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::false($res); Assert::same([], $log); @@ -40,7 +40,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return false; }, ); Assert::false($res); Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log); @@ -51,7 +51,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return true; }, ); Assert::true($res); Assert::same([['a', 0, $arr]], $log); @@ -62,7 +62,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; }, ); Assert::true($res); Assert::same([['a', 0, $arr]], $log); @@ -73,7 +73,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; }, ); Assert::true($res); Assert::same([['a', 'x', $arr]], $log); @@ -84,7 +84,7 @@ test('', function () { $log = []; $res = Arrays::some( $arr, - function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; } + function ($v, $k, $arr) use (&$log) { $log[] = func_get_args(); return $v === 'a'; }, ); Assert::true($res); Assert::same([['a', 'x', $arr]], $log); diff --git a/tests/Utils/Callback.closure.phpt b/tests/Utils/Callback.closure.phpt index fb05a74b3..1495b970c 100644 --- a/tests/Utils/Callback.closure.phpt +++ b/tests/Utils/Callback.closure.phpt @@ -45,15 +45,15 @@ class Test } - public function __call($nm, $args) + public function createPrivateClosure(): Closure { - return __METHOD__ . " $nm $args[0]"; + return Closure::fromCallable([$this, 'privateFun']); } - public static function __callStatic($nm, $args) + public static function createPrivateStaticClosure(): Closure { - return __METHOD__ . " $nm $args[0]"; + return Closure::fromCallable([self::class, 'privateStatic']); } @@ -64,6 +64,21 @@ class Test } } + +class TestDynamic +{ + public function __call($nm, $args) + { + return __METHOD__ . " $nm $args[0]"; + } + + + public static function __callStatic($nm, $args) + { + return __METHOD__ . " $nm $args[0]"; + } +} + class TestChild extends Test { } @@ -134,15 +149,16 @@ test('object methods', function () { Assert::same('Test::publicFun*', Closure::fromCallable([$test, 'publicFun'])->__invoke('*')); - Assert::same([$test, 'privateFun'], Callback::unwrap(Closure::fromCallable([$test, 'privateFun']))); + Assert::same([$test, 'privateFun'], Callback::unwrap($test->createPrivateClosure())); Assert::same('Test::privateFun', Callback::toString([$test, 'privateFun'])); - Assert::same('{closure Test::privateFun}', Callback::toString(Closure::fromCallable([$test, 'privateFun']))); + Assert::same('{closure Test::privateFun}', Callback::toString($test->createPrivateClosure())); Assert::same('Test::privateFun', getName(Callback::toReflection([$test, 'privateFun']))); - Assert::same('Test::privateFun', getName(Callback::toReflection(Closure::fromCallable([$test, 'privateFun'])))); + Assert::same('Test::privateFun', getName(Callback::toReflection($test->createPrivateClosure()))); + Assert::same('Test::privateFun', getName(Callback::toReflection((new TestChild)->createPrivateClosure()))); - Assert::same('Test::__call privateFun *', Closure::fromCallable([$test, 'privateFun'])->__invoke('*')); + Assert::same('Test::privateFun*', $test->createPrivateClosure()->__invoke('*')); Assert::same('Test::ref', Closure::fromCallable([$test, 'ref'])(...[&$res])); Assert::same('Test::ref', $res); @@ -168,35 +184,36 @@ test('static methods', function () { Assert::same('Test::publicStatic*', Closure::fromCallable([$test, 'publicStatic'])->__invoke('*')); - Assert::same(['Test', 'privateStatic'], Callback::unwrap(Closure::fromCallable('Test::privateStatic'))); + Assert::same(['Test', 'privateStatic'], Callback::unwrap(Test::createPrivateStaticClosure())); Assert::same('Test::privateStatic', Callback::toString('Test::privateStatic')); - Assert::same('{closure Test::privateStatic}', Callback::toString(Closure::fromCallable('Test::privateStatic'))); + Assert::same('{closure Test::privateStatic}', Callback::toString(Test::createPrivateStaticClosure())); Assert::same('Test::privateStatic', getName(Callback::toReflection('Test::privateStatic'))); - Assert::same('Test::privateStatic', getName(Callback::toReflection(Closure::fromCallable('Test::privateStatic')))); + Assert::same('Test::privateStatic', getName(Callback::toReflection(Test::createPrivateStaticClosure()))); + Assert::same('Test::privateStatic', getName(Callback::toReflection(TestChild::createPrivateStaticClosure()))); - Assert::same('Test::__callStatic privateStatic *', Closure::fromCallable('Test::privateStatic')->__invoke('*')); + Assert::same('Test::privateStatic*', Test::createPrivateStaticClosure()->__invoke('*')); }); test('magic methods', function () { - $test = new Test; + $test = new TestDynamic; Assert::same([$test, 'magic'], Callback::unwrap(Closure::fromCallable([$test, 'magic']))); - Assert::same('Test::magic', Callback::toString([$test, 'magic'])); - Assert::same('{closure Test::magic}', Callback::toString(Closure::fromCallable([$test, 'magic']))); - Assert::same('Test::__call magic *', Closure::fromCallable([$test, 'magic'])->__invoke('*')); + Assert::same('TestDynamic::magic', Callback::toString([$test, 'magic'])); + Assert::same('{closure TestDynamic::magic}', Callback::toString(Closure::fromCallable([$test, 'magic']))); + Assert::same('TestDynamic::__call magic *', Closure::fromCallable([$test, 'magic'])->__invoke('*')); - Assert::same(['Test', 'magic'], Callback::unwrap(Closure::fromCallable('Test::magic'))); - Assert::same('Test::magic', Callback::toString('Test::magic')); - Assert::same('{closure Test::magic}', Callback::toString(Closure::fromCallable('Test::magic'))); - Assert::same('Test::__callStatic magic *', Closure::fromCallable('Test::magic')->__invoke('*')); + Assert::same(['TestDynamic', 'magic'], Callback::unwrap(Closure::fromCallable('TestDynamic::magic'))); + Assert::same('TestDynamic::magic', Callback::toString('TestDynamic::magic')); + Assert::same('{closure TestDynamic::magic}', Callback::toString(Closure::fromCallable('TestDynamic::magic'))); + Assert::same('TestDynamic::__callStatic magic *', Closure::fromCallable('TestDynamic::magic')->__invoke('*')); Assert::exception(function () { - Callback::toReflection([new Test, 'magic']); - }, ReflectionException::class, 'Method Test::magic() does not exist'); + Callback::toReflection([new TestDynamic, 'magic']); + }, ReflectionException::class, 'Method TestDynamic::magic() does not exist'); Assert::exception(function () { - Callback::toReflection(Closure::fromCallable([new Test, 'magic'])); - }, ReflectionException::class, 'Method Test::magic() does not exist'); + Callback::toReflection(Closure::fromCallable([new TestDynamic, 'magic'])); + }, ReflectionException::class, 'Method TestDynamic::magic() does not exist'); }); diff --git a/tests/Utils/Callback.invoke.phpt b/tests/Utils/Callback.invoke.phpt deleted file mode 100644 index 9d072f29f..000000000 --- a/tests/Utils/Callback.invoke.phpt +++ /dev/null @@ -1,49 +0,0 @@ - false); Assert::same('preg_match(): Delimiter must not be alphanumeric or backslash', $res); diff --git a/tests/Utils/DateTime.JSON.phpt b/tests/Utils/DateTime.JSON.phpt index 90761833a..5683bd981 100644 --- a/tests/Utils/DateTime.JSON.phpt +++ b/tests/Utils/DateTime.JSON.phpt @@ -14,4 +14,4 @@ require __DIR__ . '/../bootstrap.php'; date_default_timezone_set('Europe/Prague'); -Assert::same('"1978-01-23T11:40:00+01:00"', json_encode(DateTime::from(254400000))); +Assert::same('"1978-01-23T11:40:00+01:00"', json_encode(DateTime::from(254_400_000))); diff --git a/tests/Utils/DateTime.createFromFormat.phpt b/tests/Utils/DateTime.createFromFormat.phpt index 08ffdd709..321d43f99 100644 --- a/tests/Utils/DateTime.createFromFormat.phpt +++ b/tests/Utils/DateTime.createFromFormat.phpt @@ -22,8 +22,4 @@ Assert::same('2050-08-13 11:40:00.123450', DateTime::createFromFormat('Y-m-d H:i Assert::same('Europe/Prague', DateTime::createFromFormat('Y', '2050')->getTimezone()->getName()); Assert::same('Europe/Bratislava', DateTime::createFromFormat('Y', '2050', 'Europe/Bratislava')->getTimezone()->getName()); -Assert::error(function () { - DateTime::createFromFormat('Y-m-d H:i:s', '2050-08-13 11:40:00', 5); -}, Nette\InvalidArgumentException::class, 'Invalid timezone given'); - Assert::false(DateTime::createFromFormat('Y-m-d', '2014-10')); diff --git a/tests/Utils/DateTime.from.phpt b/tests/Utils/DateTime.from.phpt index ab7a1e275..c8a6f95cd 100644 --- a/tests/Utils/DateTime.from.phpt +++ b/tests/Utils/DateTime.from.phpt @@ -14,14 +14,14 @@ require __DIR__ . '/../bootstrap.php'; date_default_timezone_set('Europe/Prague'); -Assert::same('1978-01-23 11:40:00', (string) DateTime::from(254400000)); -Assert::same('1978-01-23 11:40:00', (string) (new DateTime)->setTimestamp(254400000)); -Assert::same(254400000, DateTime::from(254400000)->getTimestamp()); +Assert::same('1978-01-23 11:40:00', (string) DateTime::from(254_400_000)); +Assert::same('1978-01-23 11:40:00', (string) (new DateTime)->setTimestamp(254_400_000)); +Assert::same(254_400_000, DateTime::from(254_400_000)->getTimestamp()); Assert::same(time() + 60, (int) DateTime::from(60)->format('U')); -Assert::same(PHP_VERSION_ID < 80100 ? '2050-08-13 11:40:00' : '2050-08-13 12:40:00', (string) DateTime::from(2544000000)); -Assert::same(PHP_VERSION_ID < 80100 ? '2050-08-13 11:40:00' : '2050-08-13 12:40:00', (string) (new DateTime)->setTimestamp(2544000000)); -Assert::same(is_int(2544000000) ? 2544000000 : '2544000000', DateTime::from(2544000000)->getTimestamp()); // 64 bit +Assert::same(PHP_VERSION_ID < 80100 ? '2050-08-13 11:40:00' : '2050-08-13 12:40:00', (string) DateTime::from(2_544_000_000)); +Assert::same(PHP_VERSION_ID < 80100 ? '2050-08-13 11:40:00' : '2050-08-13 12:40:00', (string) (new DateTime)->setTimestamp(2_544_000_000)); +Assert::same(is_int(2_544_000_000) ? 2_544_000_000 : '2544000000', DateTime::from(2_544_000_000)->getTimestamp()); // 64 bit Assert::same('1978-05-05 00:00:00', (string) DateTime::from('1978-05-05')); diff --git a/tests/Utils/DateTime.modifyClone.phpt b/tests/Utils/DateTime.modifyClone.phpt index 7e7fbb34b..b3ed3fbc2 100644 --- a/tests/Utils/DateTime.modifyClone.phpt +++ b/tests/Utils/DateTime.modifyClone.phpt @@ -14,7 +14,7 @@ require __DIR__ . '/../bootstrap.php'; date_default_timezone_set('Europe/Prague'); -$date = DateTime::from(254400000); +$date = DateTime::from(254_400_000); $dolly = $date->modifyClone(); Assert::type(DateTime::class, $dolly); Assert::notSame($date, $dolly); diff --git a/tests/Utils/Future.phpt b/tests/Utils/Future.phpt new file mode 100644 index 000000000..2dfeaf437 --- /dev/null +++ b/tests/Utils/Future.phpt @@ -0,0 +1,216 @@ + 'one', + 'a' => 'AAA', + }; + } + return $result; +}; + + +test('bind()', function () use ($resolver) { + (new Future($resolver)) + ->bind(1, $n) + ->bind('a', $a) + ->resolve(); + + Assert::same('one', $n); + Assert::same('AAA', $a); +}); + + +test('bindVar()', function () use ($resolver) { + $n = 1; + $a = 'a'; + + (new Future($resolver)) + ->bindVar($n) + ->bindVar($a) + ->resolve(); + + Assert::same('one', $n); + Assert::same('AAA', $a); +}); + + +test('bindArrayValues()', function () use ($resolver) { + $a = [1, 'a']; + + (new Future($resolver)) + ->bindArrayValues($a) + ->resolve(); + + Assert::same(['one', 'AAA'], $a);; +}); + + +test('bindArrayKeys()', function () use ($resolver) { + $a = [ + 1 => null, + 'a' => null, + ]; + + (new Future($resolver)) + ->bindArrayKeys($a) + ->resolve(); + + Assert::same([ + 1 => 'one', + 'a' => 'AAA', + ], $a); +}); + + +test('bindArraysKey()', function () use ($resolver) { + $a = [ + ['k' => 1], + ['k' => 'a'], + ]; + + (new Future($resolver)) + ->bindArraysKey('k', $a) + ->resolve(); + + Assert::same([ + ['k' => 'one'], + ['k' => 'AAA'], + ], $a); +}); + + +test('bindObjectsProperty()', function () use ($resolver) { + $a = [ + (object) ['p' => 1], + (object) ['p' => 'a'], + ]; + + (new Future($resolver)) + ->bindObjectsProperty('p', $a) + ->resolve(); + + Assert::equal([ + (object) ['p' => 'one'], + (object) ['p' => 'AAA'], + ], $a); +}); + + +test('Resolver returns Traversable.', function () { + (new Future(fn (array $keys) => new \ArrayIterator([1 => 'one']))) + ->bind(1, $n) + ->resolve(); + + Assert::same('one', $n); +}); + + + +test('bindArraysKey() invalid item', function () { + Assert::throws(function () { + $a = [ + 123 # this should be an array + ]; + + (new Future(fn() => null)) + ->bindArraysKey('k', $a) + ->resolve(); + + }, \AssertionError::class, 'assert(is_array($item))'); +}); + + +test('bindObjectsProperty() invalid item', function () { + Assert::throws(function () { + $a = [ + 123 # this should be an object + ]; + + (new Future(fn() => null)) + ->bindObjectsProperty('p', $a) + ->resolve(); + + }, \AssertionError::class, 'assert(is_object($item))'); +}); + + +test('Invalid resolver return type.', function () { + Assert::throws(function () { + (new Future(fn() => null)) + ->bind(1, $n) + ->resolve(); + + }, Nette\UnexpectedValueException::class, "Resolver returned 'null' but array or Traversable expected."); +}); + + +test('Resolver does not return all values.', function () { + $a = 123; + $b = 456; + $c = 789; + + try { + (new Future(fn() => ['a' => 'AAA', 'c' => 'CCC'])) + ->bind('a', $a) + ->bind('b', $b) + ->bind('c', $c) + ->resolve(); + + throw new Exception('Should not be there.'); + + } catch (Nette\Utils\FutureException $e) { + Assert::same($e->getMessage(), 'Resolver did not return required items.'); + } + + Assert::same(['b'], $e->getMissingKeys()); + + # Bound variables stay unchanged + Assert::same(123, $a); + Assert::same(456, $b); + Assert::same(789, $c); +}); + + +test('Default value for missing resolver items.', function () { + $a = 123; + $b = 456; + $c = 789; + + (new Future(fn() => ['a' => 'AAA', 'c' => 'CCC'])) + ->setDefaultValueFactory(fn($key) => "DEFAULT-$key") + ->bind('a', $a) + ->bind('b', $b) + ->bind('c', $c) + ->resolve(); + + + Assert::same('AAA', $a); + Assert::same('DEFAULT-b', $b); + Assert::same('CCC', $c); +}); + + +test('Resolver is not called when no variable is bound.', function () { + $resolver = function () { + throw new Exception('Should not be called.'); + }; + + (new Future($resolver)) + ->resolve(); +}); diff --git a/tests/Utils/Html.basic.phpt b/tests/Utils/Html.basic.phpt index 6e4f5c1a7..064405e5f 100644 --- a/tests/Utils/Html.basic.phpt +++ b/tests/Utils/Html.basic.phpt @@ -14,63 +14,55 @@ require __DIR__ . '/../bootstrap.php'; test('', function () { - Html::$xhtml = true; $el = Html::el('img')->src('image.gif')->alt(''); - Assert::same('', (string) $el); - Assert::same('', $el->toHtml()); - Assert::same('', $el->startTag()); + Assert::same('', (string) $el); + Assert::same('', $el->toHtml()); + Assert::same('', $el->startTag()); Assert::same('', $el->endTag()); }); test('', function () { - Html::$xhtml = true; $el = Html::el('img')->setAttribute('src', 'image.gif')->setAttribute('alt', ''); - Assert::same('', (string) $el); - Assert::same('', $el->startTag()); + Assert::same('', (string) $el); + Assert::same('', $el->startTag()); Assert::same('', $el->endTag()); }); test('', function () { - Html::$xhtml = true; $el = Html::el('img')->accesskey(0, true)->alt('alt', false); - Assert::same('', (string) $el); - Assert::same('', (string) $el->accesskey(1, true)); - Assert::same('', (string) $el->accesskey(1, false)); - Assert::same('', (string) $el->accesskey(0, true)); - Assert::same('', (string) $el->accesskey(0)); + Assert::same('', (string) $el); + Assert::same('', (string) $el->accesskey(1, true)); + Assert::same('', (string) $el->accesskey(1, false)); + Assert::same('', (string) $el->accesskey(0, true)); + Assert::same('', (string) $el->accesskey(0)); unset($el->accesskey); - Assert::same('', (string) $el); + Assert::same('', (string) $el); }); test('', function () { - Html::$xhtml = true; $el = Html::el('img')->appendAttribute('accesskey', 0)->setAttribute('alt', false); - Assert::same('', (string) $el); - Assert::same('', (string) $el->appendAttribute('accesskey', 1)); - Assert::same('', (string) $el->appendAttribute('accesskey', 1, false)); - Assert::same('', (string) $el->appendAttribute('accesskey', 0)); - Assert::same('', (string) $el->setAttribute('accesskey', 0)); - Assert::same('', (string) $el->removeAttribute('accesskey')); + Assert::same('', (string) $el); + Assert::same('', (string) $el->appendAttribute('accesskey', 1)); + Assert::same('', (string) $el->appendAttribute('accesskey', 1, false)); + Assert::same('', (string) $el->appendAttribute('accesskey', 0)); + Assert::same('', (string) $el->setAttribute('accesskey', 0)); + Assert::same('', (string) $el->removeAttribute('accesskey')); }); test('', function () { $el = Html::el('img')->src('image.gif')->alt('')->setText('any content'); - Assert::same('', (string) $el); - Assert::same('', $el->startTag()); - Assert::same('', $el->endTag()); - - Html::$xhtml = false; Assert::same('', (string) $el); + Assert::same('', $el->startTag()); + Assert::same('', $el->endTag()); }); test('', function () { - Html::$xhtml = false; $el = Html::el('img')->setSrc('image.gif')->setAlt('alt1')->setAlt('alt2'); Assert::same('alt2', (string) $el); Assert::same('image.gif', $el->getSrc()); @@ -104,10 +96,6 @@ test('small & big numbers', function () { test('attributes escaping', function () { - Html::$xhtml = true; - Assert::same('', (string) Html::el('a')->one('"')->two("'")->three('<>')->four('&')); - - Html::$xhtml = false; Assert::same('', (string) Html::el('a')->one('"')->two("'")->three('<>')->four('&')); Assert::same('', (string) Html::el('a')->one('``xx')); // mXSS }); diff --git a/tests/Utils/Image.drawing.phpt b/tests/Utils/Image.drawing.phpt index 47117ff33..31978da89 100644 --- a/tests/Utils/Image.drawing.phpt +++ b/tests/Utils/Image.drawing.phpt @@ -36,7 +36,7 @@ $image->filledEllipse(187, 125, $radius, $radius, Image::rgb(0, 0, 255, 75)); $image->copyResampled($image, 200, 200, 0, 0, 80, 80, $size, $size); -$file = defined('PHP_WINDOWS_VERSION_BUILD') && PHP_VERSION_ID >= 80100 +$file = defined('PHP_WINDOWS_VERSION_BUILD') && PHP_VERSION_ID >= 70424 ? '/expected/Image.drawing.1b.gd2' : '/expected/Image.drawing.1.gd2'; Assert::same(file_get_contents(__DIR__ . $file), toGd2($image)); diff --git a/tests/Utils/Image.resize.phpt b/tests/Utils/Image.resize.phpt index ef8a79887..7b3663cce 100644 --- a/tests/Utils/Image.resize.phpt +++ b/tests/Utils/Image.resize.phpt @@ -52,6 +52,14 @@ test('resizing Y shrink', function () use ($main) { }); +test('resizing Y shrink', function () use ($main) { + $image = clone $main; + $image->resize(null, 150, shrinkOnly: true); + Assert::same(176, $image->width); + Assert::same(104, $image->height); +}); + + test('resizing X Y shrink', function () use ($main) { $image = clone $main; $image->resize(300, 150, Image::SHRINK_ONLY); @@ -60,6 +68,14 @@ test('resizing X Y shrink', function () use ($main) { }); +test('resizing X Y shrink', function () use ($main) { + $image = clone $main; + $image->resize(300, 150, shrinkOnly: true); + Assert::same(176, $image->width); + Assert::same(104, $image->height); +}); + + test('resizing X Y', function () use ($main) { $image = clone $main; $image->resize(300, 150); @@ -84,6 +100,14 @@ test('resizing X Y shrink stretch', function () use ($main) { }); +test('resizing X Y shrink stretch', function () use ($main) { + $image = clone $main; + $image->resize(300, 100, Image::STRETCH, shrinkOnly: true); + Assert::same(176, $image->width); + Assert::same(100, $image->height); +}); + + test('resizing X%', function () use ($main) { $image = clone $main; $image->resize('110%', null); @@ -116,6 +140,14 @@ test('flipping Y shrink', function () use ($main) { }); +test('flipping Y shrink', function () use ($main) { + $image = clone $main; + $image->resize(null, -150, shrinkOnly: true); + Assert::same(176, $image->width); + Assert::same(104, $image->height); +}); + + test('flipping X Y shrink', function () use ($main) { $image = clone $main; $image->resize(-300, -150, Image::SHRINK_ONLY); @@ -124,6 +156,14 @@ test('flipping X Y shrink', function () use ($main) { }); +test('flipping X Y shrink', function () use ($main) { + $image = clone $main; + $image->resize(-300, -150, shrinkOnly: true); + Assert::same(176, $image->width); + Assert::same(104, $image->height); +}); + + test('exact resize', function () use ($main) { $image = clone $main; $image->resize(300, 150, Image::EXACT); diff --git a/tests/Utils/Json.decode().phpt b/tests/Utils/Json.decode().phpt index e5b48af6a..b8ffabe43 100644 --- a/tests/Utils/Json.decode().phpt +++ b/tests/Utils/Json.decode().phpt @@ -20,6 +20,7 @@ Assert::null(Json::decode(' null')); Assert::equal((object) ['a' => 1], Json::decode('{"a":1}')); Assert::same(['a' => 1], Json::decode('{"a":1}', Json::FORCE_ARRAY)); +Assert::same(['a' => 1], Json::decode('{"a":1}', forceArray: true)); Assert::exception(function () { @@ -66,7 +67,7 @@ Assert::exception(function () { if (defined('JSON_C_VERSION')) { if (PHP_INT_SIZE > 4) { // 64-bit - Assert::same([9223372036854775807], Json::decode('[12345678901234567890]')); // trimmed to max 64-bit integer + Assert::same([9_223_372_036_854_775_807], Json::decode('[12345678901234567890]')); // trimmed to max 64-bit integer } else { // 32-bit Assert::same(['9223372036854775807'], Json::decode('[12345678901234567890]')); // trimmed to max 64-bit integer diff --git a/tests/Utils/Json.encode().phpt b/tests/Utils/Json.encode().phpt index 9ba173388..5f072c366 100644 --- a/tests/Utils/Json.encode().phpt +++ b/tests/Utils/Json.encode().phpt @@ -36,10 +36,12 @@ Assert::same('"\u2028\u2029"', Json::encode("\u{2028}\u{2029}")); // ESCAPE_UNICODE Assert::same('"/I\u00f1t\u00ebrn\u00e2ti\u00f4n\u00e0liz\u00e6ti\u00f8n"', Json::encode("/I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n", Json::ESCAPE_UNICODE)); Assert::same('"\u2028\u2029"', Json::encode("\u{2028}\u{2029}", Json::ESCAPE_UNICODE)); +Assert::same('"\u2028\u2029"', Json::encode("\u{2028}\u{2029}", escapeUnicode: true)); // JSON_PRETTY_PRINT Assert::same("[\n 1,\n 2,\n 3\n]", Json::encode([1, 2, 3], Json::PRETTY)); +Assert::same("[\n 1,\n 2,\n 3\n]", Json::encode([1, 2, 3], pretty: true)); Assert::exception(function () { diff --git a/tests/Utils/Reflection.expandClassName.phpt b/tests/Utils/Reflection.expandClassName.phpt index 904c1b125..389ff39d3 100644 --- a/tests/Utils/Reflection.expandClassName.phpt +++ b/tests/Utils/Reflection.expandClassName.phpt @@ -140,21 +140,21 @@ foreach ($cases as $alias => $fqn) { Assert::same( ['C' => 'A\B'], - Reflection::getUseStatements(new ReflectionClass('Test')) + Reflection::getUseStatements(new ReflectionClass('Test')), ); Assert::same( [], - Reflection::getUseStatements(new ReflectionClass('Test\Space\Foo')) + Reflection::getUseStatements(new ReflectionClass('Test\Space\Foo')), ); Assert::same( ['AAA' => 'AAA', 'B' => 'BBB', 'DDD' => 'CCC\DDD', 'F' => 'EEE\FFF', 'G' => 'GGG'], - Reflection::getUseStatements(new ReflectionClass('Test\Space\Bar')) + Reflection::getUseStatements(new ReflectionClass('Test\Space\Bar')), ); Assert::same( [], - Reflection::getUseStatements(new ReflectionClass('stdClass')) + Reflection::getUseStatements(new ReflectionClass('stdClass')), ); Assert::exception(function () use ($rcTest) { diff --git a/tests/Utils/Reflection.getParameterType.80.phpt b/tests/Utils/Reflection.getParameterType.80.phpt deleted file mode 100644 index 57261ac51..000000000 --- a/tests/Utils/Reflection.getParameterType.80.phpt +++ /dev/null @@ -1,70 +0,0 @@ -getParameters(); - -Assert::same('Undeclared', Reflection::getParameterType($params[0])); -Assert::same('Test\B', Reflection::getParameterType($params[1])); -Assert::same('array', Reflection::getParameterType($params[2])); -Assert::same('callable', Reflection::getParameterType($params[3])); -Assert::same('A', Reflection::getParameterType($params[4])); -Assert::null(Reflection::getParameterType($params[5])); -Assert::same('Test\B', Reflection::getParameterType($params[6])); -Assert::same(['Test\B', 'null'], Reflection::getParameterTypes($params[6])); -Assert::same('mixed', Reflection::getParameterType($params[7])); -Assert::same(['mixed'], Reflection::getParameterTypes($params[7])); -Assert::same(['A', 'array'], Reflection::getParameterTypes($params[8])); -Assert::same(['A', 'array', 'null'], Reflection::getParameterTypes($params[9])); - -Assert::exception(function () use ($params) { - Reflection::getParameterType($params[8]); -}, Nette\InvalidStateException::class, 'The $union in A::method() is not expected to have a union or intersection type.'); - -Assert::exception(function () use ($params) { - Reflection::getParameterType($params[9]); -}, Nette\InvalidStateException::class, 'The $nullableUnion in A::method() is not expected to have a union or intersection type.'); - - -$method = new ReflectionMethod('AExt', 'methodExt'); -$params = $method->getParameters(); - -Assert::same('A', Reflection::getParameterType($params[0])); diff --git a/tests/Utils/Reflection.getParameterType.81.phpt b/tests/Utils/Reflection.getParameterType.81.phpt index fc6afab8e..5adb2643c 100644 --- a/tests/Utils/Reflection.getParameterType.81.phpt +++ b/tests/Utils/Reflection.getParameterType.81.phpt @@ -43,32 +43,17 @@ class AExt extends A $method = new ReflectionMethod('A', 'method'); $params = $method->getParameters(); -Assert::same('Undeclared', Reflection::getParameterType($params[0])); -Assert::same('Test\B', Reflection::getParameterType($params[1])); -Assert::same('array', Reflection::getParameterType($params[2])); -Assert::same('callable', Reflection::getParameterType($params[3])); -Assert::same('A', Reflection::getParameterType($params[4])); +Assert::same('Undeclared', (string) Reflection::getParameterType($params[0])); +Assert::same('Test\B', (string) Reflection::getParameterType($params[1])); +Assert::same('array', (string) Reflection::getParameterType($params[2])); +Assert::same('callable', (string) Reflection::getParameterType($params[3])); +Assert::same('A', (string) Reflection::getParameterType($params[4])); Assert::null(Reflection::getParameterType($params[5])); -Assert::same('Test\B', Reflection::getParameterType($params[6])); -Assert::same(['Test\B', 'null'], Reflection::getParameterTypes($params[6])); -Assert::same('mixed', Reflection::getParameterType($params[7])); -Assert::same(['mixed'], Reflection::getParameterTypes($params[7])); -Assert::same(['A', 'array'], Reflection::getParameterTypes($params[8])); -Assert::same(['A', 'array', 'null'], Reflection::getParameterTypes($params[9])); - -Assert::exception(function () use ($params) { - Reflection::getParameterType($params[8]); -}, Nette\InvalidStateException::class, 'The $union in A::method() is not expected to have a union or intersection type.'); - -Assert::exception(function () use ($params) { - Reflection::getParameterType($params[9]); -}, Nette\InvalidStateException::class, 'The $nullableUnion in A::method() is not expected to have a union or intersection type.'); - -Assert::exception(function () use ($params) { - Reflection::getParameterType($params[10]); -}, Nette\InvalidStateException::class, 'The $intersection in A::method() is not expected to have a union or intersection type.'); - +Assert::same('?Test\B', (string) Reflection::getParameterType($params[6])); +Assert::same('mixed', (string) Reflection::getParameterType($params[7])); +Assert::same('A|array', (string) Reflection::getParameterType($params[8], false)); +Assert::same('A|array|null', (string) Reflection::getParameterType($params[9], false)); $method = new ReflectionMethod('AExt', 'methodExt'); $params = $method->getParameters(); -Assert::same('A', Reflection::getParameterType($params[0])); +Assert::same('A', (string) Reflection::getParameterType($params[0])); diff --git a/tests/Utils/Reflection.getParameterType.phpt b/tests/Utils/Reflection.getParameterType.phpt index 869266982..616c67325 100644 --- a/tests/Utils/Reflection.getParameterType.phpt +++ b/tests/Utils/Reflection.getParameterType.phpt @@ -16,8 +16,18 @@ require __DIR__ . '/../bootstrap.php'; class A { - public function method(Undeclared $undeclared, B $b, array $array, callable $callable, self $self, $none, ?B $nullable) - { + public function method( + Undeclared $undeclared, + B $b, + array $array, + callable $callable, + self $self, + $none, + ?B $nullable, + mixed $mixed, + array|self $union, + array|self|null $nullableUnion, + ) { } } @@ -31,15 +41,16 @@ class AExt extends A $method = new ReflectionMethod('A', 'method'); $params = $method->getParameters(); -Assert::same('Undeclared', Reflection::getParameterType($params[0])); -Assert::same('Test\B', Reflection::getParameterType($params[1])); -Assert::same('array', Reflection::getParameterType($params[2])); -Assert::same('callable', Reflection::getParameterType($params[3])); -Assert::same('A', Reflection::getParameterType($params[4])); +Assert::same('Undeclared', (string) Reflection::getParameterType($params[0])); +Assert::same('Test\B', (string) Reflection::getParameterType($params[1])); +Assert::same('array', (string) Reflection::getParameterType($params[2])); +Assert::same('callable', (string) Reflection::getParameterType($params[3])); +Assert::same('A', (string) Reflection::getParameterType($params[4])); Assert::null(Reflection::getParameterType($params[5])); -Assert::same('Test\B', Reflection::getParameterType($params[6])); +Assert::same('?Test\B', (string) Reflection::getParameterType($params[6])); +Assert::same('mixed', (string) Reflection::getParameterType($params[7])); $method = new ReflectionMethod('AExt', 'methodExt'); $params = $method->getParameters(); -Assert::same('A', Reflection::getParameterType($params[0])); +Assert::same('A', (string) Reflection::getParameterType($params[0])); diff --git a/tests/Utils/Reflection.getPropertyType.74.phpt b/tests/Utils/Reflection.getPropertyType.74.phpt deleted file mode 100644 index b822be995..000000000 --- a/tests/Utils/Reflection.getPropertyType.74.phpt +++ /dev/null @@ -1,46 +0,0 @@ -getProperties(); - -Assert::same('Undeclared', Reflection::getPropertyType($props[0])); -Assert::same('Test\B', Reflection::getPropertyType($props[1])); -Assert::same('array', Reflection::getPropertyType($props[2])); -Assert::same('A', Reflection::getPropertyType($props[3])); -Assert::null(Reflection::getPropertyType($props[4])); -Assert::same('Test\B', Reflection::getPropertyType($props[5])); - -$class = new ReflectionClass('AExt'); -$props = $class->getProperties(); - -Assert::same('A', Reflection::getPropertyType($props[0])); diff --git a/tests/Utils/Reflection.getPropertyType.80.phpt b/tests/Utils/Reflection.getPropertyType.80.phpt deleted file mode 100644 index 00378ce66..000000000 --- a/tests/Utils/Reflection.getPropertyType.80.phpt +++ /dev/null @@ -1,62 +0,0 @@ -getProperties(); - -Assert::same('Undeclared', Reflection::getPropertyType($props[0])); -Assert::same('Test\B', Reflection::getPropertyType($props[1])); -Assert::same('array', Reflection::getPropertyType($props[2])); -Assert::same('A', Reflection::getPropertyType($props[3])); -Assert::null(Reflection::getPropertyType($props[4])); -Assert::same('Test\B', Reflection::getPropertyType($props[5])); -Assert::same(['Test\B', 'null'], Reflection::getPropertyTypes($props[5])); -Assert::same('mixed', Reflection::getPropertyType($props[6])); -Assert::same(['mixed'], Reflection::getPropertyTypes($props[6])); -Assert::same(['A', 'array'], Reflection::getPropertyTypes($props[7])); -Assert::same(['A', 'array', 'null'], Reflection::getPropertyTypes($props[8])); - -Assert::exception(function () use ($props) { - Reflection::getPropertyType($props[7]); -}, Nette\InvalidStateException::class, 'The A::$union is not expected to have a union or intersection type.'); - -Assert::exception(function () use ($props) { - Reflection::getPropertyType($props[8]); -}, Nette\InvalidStateException::class, 'The A::$nullableUnion is not expected to have a union or intersection type.'); - -$class = new ReflectionClass('AExt'); -$props = $class->getProperties(); - -Assert::same('A', Reflection::getPropertyType($props[0])); diff --git a/tests/Utils/Reflection.getPropertyType.81.phpt b/tests/Utils/Reflection.getPropertyType.81.phpt index 7e62bb60e..38d42c4e0 100644 --- a/tests/Utils/Reflection.getPropertyType.81.phpt +++ b/tests/Utils/Reflection.getPropertyType.81.phpt @@ -37,31 +37,16 @@ class AExt extends A $class = new ReflectionClass('A'); $props = $class->getProperties(); -Assert::same('Undeclared', Reflection::getPropertyType($props[0])); -Assert::same('Test\B', Reflection::getPropertyType($props[1])); -Assert::same('array', Reflection::getPropertyType($props[2])); -Assert::same('A', Reflection::getPropertyType($props[3])); +Assert::same('Undeclared', (string) Reflection::getPropertyType($props[0])); +Assert::same('Test\B', (string) Reflection::getPropertyType($props[1])); +Assert::same('array', (string) Reflection::getPropertyType($props[2])); +Assert::same('A', (string) Reflection::getPropertyType($props[3])); Assert::null(Reflection::getPropertyType($props[4])); -Assert::same('Test\B', Reflection::getPropertyType($props[5])); -Assert::same(['Test\B', 'null'], Reflection::getPropertyTypes($props[5])); -Assert::same('mixed', Reflection::getPropertyType($props[6])); -Assert::same(['mixed'], Reflection::getPropertyTypes($props[6])); -Assert::same(['A', 'array'], Reflection::getPropertyTypes($props[7])); -Assert::same(['A', 'array', 'null'], Reflection::getPropertyTypes($props[8])); - -Assert::exception(function () use ($props) { - Reflection::getPropertyType($props[7]); -}, Nette\InvalidStateException::class, 'The A::$union is not expected to have a union or intersection type.'); - -Assert::exception(function () use ($props) { - Reflection::getPropertyType($props[8]); -}, Nette\InvalidStateException::class, 'The A::$nullableUnion is not expected to have a union or intersection type.'); - -Assert::exception(function () use ($props) { - Reflection::getPropertyType($props[9]); -}, Nette\InvalidStateException::class, 'The A::$intersection is not expected to have a union or intersection type.'); - +Assert::same('?Test\B', (string) Reflection::getPropertyType($props[5])); +Assert::same('mixed', (string) Reflection::getPropertyType($props[6])); +Assert::same('A|array', (string) Reflection::getPropertyType($props[7], false)); +Assert::same('A|array|null', (string) Reflection::getPropertyType($props[8], false)); $class = new ReflectionClass('AExt'); $props = $class->getProperties(); -Assert::same('A', Reflection::getPropertyType($props[0])); +Assert::same('A', (string) Reflection::getPropertyType($props[0])); diff --git a/tests/Utils/Reflection.getPropertyType.phpt b/tests/Utils/Reflection.getPropertyType.phpt index 8b15eb85e..44abc0630 100644 --- a/tests/Utils/Reflection.getPropertyType.phpt +++ b/tests/Utils/Reflection.getPropertyType.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\Utils\Reflection; +use Test\B; // for testing purposes use Tester\Assert; @@ -15,11 +16,33 @@ require __DIR__ . '/../bootstrap.php'; class A { + public Undeclared $undeclared; + public B $b; + public array $array; + public self $self; public $none; + public ?B $nullable; + public mixed $mixed; + public array|self $union; + public array|self|null $nullableUnion; +} + +class AExt extends A +{ + public parent $parent; } $class = new ReflectionClass('A'); $props = $class->getProperties(); -Assert::null(Reflection::getPropertyType($props[0])); -Assert::same([], Reflection::getPropertyTypes($props[0])); +Assert::same('Undeclared', (string) Reflection::getPropertyType($props[0])); +Assert::same('Test\B', (string) Reflection::getPropertyType($props[1])); +Assert::same('array', (string) Reflection::getPropertyType($props[2])); +Assert::same('A', (string) Reflection::getPropertyType($props[3])); +Assert::null(Reflection::getPropertyType($props[4])); +Assert::same('?Test\B', (string) Reflection::getPropertyType($props[5])); +Assert::same('mixed', (string) Reflection::getPropertyType($props[6])); +$class = new ReflectionClass('AExt'); +$props = $class->getProperties(); + +Assert::same('A', (string) Reflection::getPropertyType($props[0])); diff --git a/tests/Utils/Reflection.getReturnType.80.phpt b/tests/Utils/Reflection.getReturnType.80.phpt deleted file mode 100644 index 96c251ac9..000000000 --- a/tests/Utils/Reflection.getReturnType.80.phpt +++ /dev/null @@ -1,137 +0,0 @@ - 'A\B\A', 'C' => 'A\B\B\C', 'D' => 'A\B\C', 'E' => 'D\E'], - Reflection::getUseStatements(new ReflectionClass('GroupUseTest')) + Reflection::getUseStatements(new ReflectionClass('GroupUseTest')), ); diff --git a/tests/Utils/Reflection.nonClassUseStatements.phpt b/tests/Utils/Reflection.nonClassUseStatements.phpt index c6ce04430..bad5b7ace 100644 --- a/tests/Utils/Reflection.nonClassUseStatements.phpt +++ b/tests/Utils/Reflection.nonClassUseStatements.phpt @@ -16,5 +16,5 @@ require __DIR__ . '/fixtures.reflection/expandClass.nonClassUse.php'; Assert::same( [], - Reflection::getUseStatements(new ReflectionClass('NonClassUseTest')) + Reflection::getUseStatements(new ReflectionClass('NonClassUseTest')), ); diff --git a/tests/Utils/SmartObject.arrayProperty.phpt b/tests/Utils/SmartObject.arrayProperty.phpt index bc6f0ba1c..f935ff943 100644 --- a/tests/Utils/SmartObject.arrayProperty.phpt +++ b/tests/Utils/SmartObject.arrayProperty.phpt @@ -18,7 +18,7 @@ class TestClass { use Nette\SmartObject; - private $items = []; + private array $items = []; public function &getItems() diff --git a/tests/Utils/SmartObject.events.74.phpt b/tests/Utils/SmartObject.events.74.phpt deleted file mode 100644 index cc46b62e6..000000000 --- a/tests/Utils/SmartObject.events.74.phpt +++ /dev/null @@ -1,26 +0,0 @@ -onEvent(); -}); diff --git a/tests/Utils/SmartObject.events.phpt b/tests/Utils/SmartObject.events.phpt index 4d3899627..d7952fadf 100644 --- a/tests/Utils/SmartObject.events.phpt +++ b/tests/Utils/SmartObject.events.phpt @@ -19,6 +19,8 @@ class TestClass public static $onPublicStatic; + public array $onEvent; + protected $onProtected; private $onPrivate; @@ -81,3 +83,8 @@ Assert::exception(function () use ($obj) { $obj->onPublic = 'string'; $obj->onPublic(); }, Nette\UnexpectedValueException::class, 'Property TestClass::$onPublic must be iterable or null, string given.'); + +Assert::noError(function () { + $obj = new TestClass; + $obj->onEvent(); +}); diff --git a/tests/Utils/SmartObject.property.phpt b/tests/Utils/SmartObject.property.phpt index 66237506f..6d258e835 100644 --- a/tests/Utils/SmartObject.property.phpt +++ b/tests/Utils/SmartObject.property.phpt @@ -16,6 +16,7 @@ require __DIR__ . '/../bootstrap.php'; * @property int $bar * @property int $bazz * @property int $s + * @property-deprecated int $depr */ class TestClass { @@ -69,6 +70,16 @@ class TestClass echo __METHOD__; return 'ERROR'; } + + + public function setDepr($value) + { + } + + + public function getDepr() + { + } } @@ -115,3 +126,15 @@ Assert::same('World', $obj->bar); Assert::exception(function () use ($obj) { $val = $obj->bazz; }, Nette\MemberAccessException::class, 'Cannot read a write-only property TestClass::$bazz.'); + + +// deprecated property +Assert::error(function () { + $obj = new TestClass; + $obj->depr = 10; +}, E_USER_DEPRECATED, 'Property TestClass::$depr is deprecated, use TestClass::setDepr() method in %a%SmartObject.property.phpt on line %d%.'); + +Assert::error(function () { + $obj = new TestClass; + $val = $obj->depr; +}, E_USER_DEPRECATED, 'Property TestClass::$depr is deprecated, use TestClass::getDepr() method in %a%SmartObject.property.phpt on line %d%.'); diff --git a/tests/Utils/SmartObject.undeclaredMethod.native.phpt b/tests/Utils/SmartObject.undeclaredMethod.native.phpt new file mode 100644 index 000000000..d5d62e120 --- /dev/null +++ b/tests/Utils/SmartObject.undeclaredMethod.native.phpt @@ -0,0 +1,149 @@ +privateMethod(); + } + + + public function callPrivateStatic() + { + static::privateStaticMethod(); + } + + + private function callPrivateParent() + { + } +} + + +class InterClass extends ParentClass +{ + public function callParents() + { + parent::callParents(); + } +} + + +class ChildClass extends InterClass +{ + public function callParents() + { + parent::callParents(); + } + + + public function callMissingParent() + { + parent::callMissingParent(); + } + + + public static function callMissingParentStatic() + { + parent::callMissingParentStatic(); + } + + + public function callPrivateParent() + { + parent::callPrivateParent(); + } + + + protected function protectedMethod() + { + } + + + protected static function protectedStaticMethod() + { + } + + + private function privateMethod() + { + } + + + private static function privateStaticMethod() + { + } +} + + + +Assert::exception(function () { + $obj = new ParentClass; + $obj->undef(); +}, Error::class, 'Call to undefined method ParentClass::undef()'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->undef(); +}, Error::class, 'Call to undefined method ChildClass::undef()'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->callParents(); +}, Error::class, 'Call to undefined method ParentClass::callParents()'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->callMissingParent(); +}, Error::class, 'Call to undefined method InterClass::callMissingParent()'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->callMissingParentStatic(); +}, Error::class, 'Call to undefined method InterClass::callMissingParentStatic()'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj::callMissingParentStatic(); +}, Error::class, 'Call to undefined method InterClass::callMissingParentStatic()'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->callPrivateParent(); +}, Error::class, 'Call to private method ParentClass::callPrivateParent() from scope ChildClass'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->protectedMethod(); +}, Error::class, 'Call to protected method ChildClass::protectedMethod() from global scope'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->protectedStaticMethod(); +}, Error::class, 'Call to protected method ChildClass::protectedStaticMethod() from global scope'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj::protectedStaticMethod(); +}, Error::class, 'Call to protected method ChildClass::protectedStaticMethod() from global scope'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->callPrivate(); +}, Error::class, 'Call to private method ChildClass::privateMethod() from scope ParentClass'); + +Assert::exception(function () { + $obj = new ChildClass; + $obj->callPrivateStatic(); +}, Error::class, 'Call to private method ChildClass::privateStaticMethod() from scope ParentClass'); diff --git a/tests/Utils/SmartObject.undeclaredMethod.phpt b/tests/Utils/SmartObject.undeclaredMethod.phpt index 60d255cb8..c11e52cd1 100644 --- a/tests/Utils/SmartObject.undeclaredMethod.phpt +++ b/tests/Utils/SmartObject.undeclaredMethod.phpt @@ -120,18 +120,12 @@ Assert::exception(function () { $obj::callMissingParentStatic(); }, Nette\MemberAccessException::class, 'Call to undefined static method InterClass::callMissingParentStatic().'); -Assert::exception( - function () { - $obj = new ChildClass; - $obj->callPrivateParent(); - }, - Nette\MemberAccessException::class, - PHP_VERSION_ID < 70400 - ? 'Call to private method InterClass::callPrivateParent() from scope ChildClass.' - : (PHP_VERSION_ID < 80100 - ? 'Call to undefined static method InterClass::callPrivateParent().' // differs from native error message - : 'Call to undefined method InterClass::callPrivateParent().') -); +Assert::exception(function () { + $obj = new ChildClass; + $obj->callPrivateParent(); +}, Nette\MemberAccessException::class, PHP_VERSION_ID < 80100 + ? 'Call to undefined static method InterClass::callPrivateParent().' // differs from native error message + : 'Call to undefined method InterClass::callPrivateParent().'); Assert::exception(function () { $obj = new ChildClass; diff --git a/tests/Utils/Strings.match().phpt b/tests/Utils/Strings.match().phpt index 5814b5753..34799a885 100644 --- a/tests/Utils/Strings.match().phpt +++ b/tests/Utils/Strings.match().phpt @@ -19,9 +19,22 @@ Assert::same(['hell', 'l'], Strings::match('hello world!', '#([e-l])+#')); Assert::same(['hell'], Strings::match('hello world!', '#[e-l]+#')); -Assert::same([['hell', 0]], Strings::match('hello world!', '#[e-l]+#', PREG_OFFSET_CAPTURE)); +Assert::same([['l', 2]], Strings::match('žluťoučký kůň', '#[e-l]+#u', PREG_OFFSET_CAPTURE)); +Assert::same([['l', 2]], Strings::match('žluťoučký kůň', '#[e-l]+#u', captureOffset: true)); -Assert::same(['ll'], Strings::match('hello world!', '#[e-l]+#', 0, 2)); +Assert::same([['l', 1]], Strings::match('žluťoučký kůň', '#[e-l]+#u', captureOffset: true, utf8: true)); +Assert::same(['e', null], Strings::match('hello world!', '#e(x)*#', unmatchedAsNull: true)); +Assert::same(['e', null], Strings::match('hello world!', '#e(x)*#', 0, 0, unmatchedAsNull: true)); // $flags = 0 -Assert::null(Strings::match('hello world!', '', 0, 50)); -Assert::null(Strings::match('', '', 0, 1)); +Assert::same(['ll'], Strings::match('hello world!', '#[e-l]+#', offset: 2)); + +Assert::same(['l'], Strings::match('žluťoučký kůň', '#[e-l]+#u', offset: 2)); + +Assert::same(['k'], Strings::match('žluťoučký kůň', '#[e-l]+#u', utf8: true, offset: 2)); + +Assert::same(['žluťoučký'], Strings::match('žluťoučký kůň', '#\w+#', utf8: true)); // without modifier + +Assert::same([['k', 7]], Strings::match('žluťoučký kůň', '#[e-l]+#u', captureOffset: true, utf8: true, offset: 2)); + +Assert::null(Strings::match('hello world!', '', offset: 50)); +Assert::null(Strings::match('', '', offset: 1)); diff --git a/tests/Utils/Strings.matchAll().phpt b/tests/Utils/Strings.matchAll().phpt index fd461cf1b..002b43599 100644 --- a/tests/Utils/Strings.matchAll().phpt +++ b/tests/Utils/Strings.matchAll().phpt @@ -26,14 +26,54 @@ Assert::same([ ], Strings::matchAll('hello world!', '#[e-l]+#')); Assert::same([ - [ - ['hell', 0], - ], - [ - ['l', 9], - ], -], Strings::matchAll('hello world!', '#[e-l]+#', PREG_OFFSET_CAPTURE)); + [['lu', 2], ['l', 2], ['u', 3]], + [['ou', 6], ['o', 6], ['u', 7]], + [['k', 10], ['k', 10], ['', 11]], + [['k', 14], ['k', 14], ['', 15]], +], Strings::matchAll('žluťoučký kůň!', '#([a-z])([a-z]*)#u', PREG_OFFSET_CAPTURE)); + +Assert::same([ + [['lu', 2], ['l', 2], ['u', 3]], + [['ou', 6], ['o', 6], ['u', 7]], + [['k', 10], ['k', 10], ['', 11]], + [['k', 14], ['k', 14], ['', 15]], +], Strings::matchAll('žluťoučký kůň!', '#([a-z])([a-z]*)#u', captureOffset: true)); + +Assert::same([ + [['lu', 2], ['ou', 6], ['k', 10], ['k', 14]], + [['l', 2], ['o', 6], ['k', 10], ['k', 14]], + [['u', 3], ['u', 7], ['', 11], ['', 15]], +], Strings::matchAll('žluťoučký kůň!', '#([a-z])([a-z]*)#u', PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER)); + +Assert::same([ + [['lu', 1], ['l', 1], ['u', 2]], + [['ou', 4], ['o', 4], ['u', 5]], + [['k', 7], ['k', 7], ['', 8]], + [['k', 10], ['k', 10], ['', 11]], +], Strings::matchAll('žluťoučký kůň!', '#([a-z])([a-z]*)#u', captureOffset: true, utf8: true)); + +Assert::same([ + [['lu', 2], ['ou', 6], ['k', 10], ['k', 14]], + [['l', 2], ['o', 6], ['k', 10], ['k', 14]], + [['u', 3], ['u', 7], ['', 11], ['', 15]], +], Strings::matchAll('žluťoučký kůň!', '#([a-z])([a-z]*)#u', captureOffset: true, patternOrder: true)); + +Assert::same([ + [['lu', 1], ['ou', 4], ['k', 7], ['k', 10]], + [['l', 1], ['o', 4], ['k', 7], ['k', 10]], + [['u', 2], ['u', 5], ['', 8], ['', 11]], +], Strings::matchAll('žluťoučký kůň!', '#([a-z])([a-z]*)#u', captureOffset: true, patternOrder: true, utf8: true)); + +Assert::same([['l'], ['k'], ['k']], Strings::matchAll('žluťoučký kůň', '#[e-l]+#u', offset: 2)); + +Assert::same([['k'], ['k']], Strings::matchAll('žluťoučký kůň', '#[e-l]+#u', offset: 2, utf8: true)); + +Assert::same([['žluťoučký'], ['kůň']], Strings::matchAll('žluťoučký kůň', '#\w+#', utf8: true)); // without modifier Assert::same([['ll', 'l']], Strings::matchAll('hello world!', '#[e-l]+#', PREG_PATTERN_ORDER, 2)); +Assert::same([['ll', 'l']], Strings::matchAll('hello world!', '#[e-l]+#', offset: 2, patternOrder: true)); + +Assert::same([['e', null]], Strings::matchAll('hello world!', '#e(x)*#', unmatchedAsNull: true)); +Assert::same([['e', null]], Strings::matchAll('hello world!', '#e(x)*#', 0, 0, unmatchedAsNull: true)); // $flags = 0 -Assert::same([], Strings::matchAll('hello world!', '', 0, 50)); +Assert::same([], Strings::matchAll('hello world!', '', offset: 50)); diff --git a/tests/Utils/Strings.ord().phpt b/tests/Utils/Strings.ord().phpt new file mode 100644 index 000000000..09f6ca2fd --- /dev/null +++ b/tests/Utils/Strings.ord().phpt @@ -0,0 +1,38 @@ + '@')); Assert::same('@o wor@d!', Strings::replace('hello world!', '#[e-l]+#', Closure::fromCallable('Test::cb'))); Assert::same('@o wor@d!', Strings::replace('hello world!', ['#[e-l]+#'], Closure::fromCallable('Test::cb'))); Assert::same('@o wor@d!', Strings::replace('hello world!', '#[e-l]+#', ['Test', 'cb'])); @@ -34,3 +34,13 @@ Assert::same('#@ @@@#d!', Strings::replace('hello world!', [ ])); Assert::same(' !', Strings::replace('hello world!', '#\w#')); Assert::same(' !', Strings::replace('hello world!', ['#\w#'])); + +// flags & callback +Assert::same('hell0o worl9d!', Strings::replace('hello world!', '#[e-l]+#', fn($m) => implode($m[0]), captureOffset: true)); +Assert::same('žl1uťoučk7ý k10ůň!', Strings::replace('žluťoučký kůň!', '#[e-l]+#u', fn($m) => implode($m[0]), captureOffset: true, utf8: true)); +Strings::replace('hello world!', '#e(x)*#', fn($m) => Assert::null($m[1]), unmatchedAsNull: true); + +// utf-8 without modifier +Assert::same('* *', Strings::replace('žluťoučký kůň', '#\w+#', fn() => '*', utf8: true)); +Assert::same('* *', Strings::replace('žluťoučký kůň', '#\w+#', '*', utf8: true)); +Assert::same('* *', Strings::replace('žluťoučký kůň', ['#\w+#'], '*', utf8: true)); diff --git a/tests/Utils/Strings.split().phpt b/tests/Utils/Strings.split().phpt index b291e9613..d888ffca9 100644 --- a/tests/Utils/Strings.split().phpt +++ b/tests/Utils/Strings.split().phpt @@ -29,6 +29,14 @@ Assert::same([ 'c', ], Strings::split('a, b, c', '#(,)\s*#', PREG_SPLIT_NO_EMPTY)); +Assert::same([ + 'a', + ',', + 'b', + ',', + 'c', +], Strings::split('a, b, c', '#(,)\s*#', skipEmpty: true)); + Assert::same([ ['a', 0], [',', 1], @@ -36,3 +44,31 @@ Assert::same([ [',', 4], ['c', 6], ], Strings::split('a, b, c', '#(,)\s*#', PREG_SPLIT_OFFSET_CAPTURE)); + +Assert::same([ + ['ž', 0], + ['lu', 2], + ['ť', 4], + ['ou', 6], + ['č', 8], + ['k', 10], + ['ý ', 11], + ['k', 14], + ['ůň', 15], +], Strings::split('žluťoučký kůň', '#([a-z]+)\s*#u', captureOffset: true)); + +Assert::same([ + ['ž', 0], + ['lu', 1], + ['ť', 3], + ['ou', 4], + ['č', 6], + ['k', 7], + ['ý ', 8], + ['k', 10], + ['ůň', 11], +], Strings::split('žluťoučký kůň', '#([a-z]+)\s*#u', captureOffset: true, utf8: true)); + +Assert::same(['', ' ', ''], Strings::split('žluťoučký kůň', '#\w+#', utf8: true)); // without modifier + +Assert::same(['a', ',', 'b, c'], Strings::split('a, b, c', '#(,)\s*#', limit: 2)); diff --git a/tests/Utils/fixtures.reflection/defaultValue.php b/tests/Utils/fixtures.reflection/defaultValue.php index 196ab13de..f2e646ad4 100644 --- a/tests/Utils/fixtures.reflection/defaultValue.php +++ b/tests/Utils/fixtures.reflection/defaultValue.php @@ -41,7 +41,7 @@ public function method( $m = DEFINED, $n = UNDEFINED, $o = NS_DEFINED, - $p = parent::PUBLIC_DEFINED + $p = parent::PUBLIC_DEFINED, ) { } }