file_cache_준비

This commit is contained in:
2020-06-26 21:24:24 +09:00
parent 05904d31fd
commit 40968b301a
60 changed files with 8621 additions and 109 deletions
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace PHPSTORM_META;
override(\Nette\Utils\Arrays::get(0), elementType(0));
override(\Nette\Utils\Arrays::getRef(0), elementType(0));
override(\Nette\Utils\Arrays::grep(0), type(0));
override(\Nette\Utils\Arrays::toObject(0), type(1));
expectedArguments(\Nette\Utils\Arrays::grep(), 2, PREG_GREP_INVERT);
expectedArguments(\Nette\Utils\Image::resize(), 2, \Nette\Utils\Image::SHRINK_ONLY, \Nette\Utils\Image::STRETCH, \Nette\Utils\Image::FIT, \Nette\Utils\Image::FILL, \Nette\Utils\Image::EXACT);
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);
+47
View File
@@ -0,0 +1,47 @@
{
"name": "nette/utils",
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"keywords": ["nette", "images", "json", "password", "validation", "utility", "string", "array", "core", "slugify", "utf-8", "unicode", "paginator", "datetime"],
"homepage": "https://nette.org",
"license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"require": {
"php": ">=7.1"
},
"require-dev": {
"nette/tester": "~2.0",
"tracy/tracy": "^2.3",
"phpstan/phpstan": "^0.12"
},
"suggest": {
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-xml": "to use Strings::length() etc. when mbstring is not available",
"ext-gd": "to use Image",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"autoload": {
"classmap": ["src/"]
},
"minimum-stability": "dev",
"scripts": {
"phpstan": "phpstan analyse --level 5 --configuration tests/phpstan.neon src",
"tester": "tester tests -s"
},
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
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:
+60
View File
@@ -0,0 +1,60 @@
Licenses
========
Good news! You may use Nette Framework under the terms of either
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
The BSD License is recommended for most projects. It is easy to understand and it
places almost no restrictions on what you can do with the framework. If the GPL
fits better to your project, you can use the framework under this license.
You don't have to notify anyone which license you are using. You can freely
use Nette Framework in commercial projects as long as the copyright header
remains intact.
Please be advised that the name "Nette Framework" is a protected trademark and its
usage has some limitations. So please do not use word "Nette" in the name of your
project or top-level domain, and choose a name that stands on its own merits.
If your stuff is good, it will not take long to establish a reputation for yourselves.
New BSD License
---------------
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of "Nette Framework" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as is" and
any express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed. In no event shall the copyright owner or contributors be liable for
any direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
GNU General Public License
--------------------------
GPL licenses are very very long, so instead of including them here we offer
you URLs with full text:
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
+42
View File
@@ -0,0 +1,42 @@
Nette Utility Classes
=====================
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/utils.svg)](https://packagist.org/packages/nette/utils)
[![Build Status](https://travis-ci.org/nette/utils.svg?branch=master)](https://travis-ci.org/nette/utils)
[![Coverage Status](https://coveralls.io/repos/github/nette/utils/badge.svg?branch=master)](https://coveralls.io/github/nette/utils?branch=master)
[![Latest Stable Version](https://poser.pugx.org/nette/utils/v/stable)](https://github.com/nette/utils/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/utils/blob/master/license.md)
Introduction
------------
In package nette/utils you will find a set of useful classes for everyday use:
- [Arrays](https://doc.nette.org/arrays) - manipulate arrays
- [Callback](https://doc.nette.org/callback) - PHP callbacks
- [Date and Time](https://doc.nette.org/datetime) - modify times and dates
- [Filesystem](https://doc.nette.org/filesystem) - copying, renaming, …
- [HTML elements](https://doc.nette.org/html-elements) - generate HTML
- [Images](https://doc.nette.org/images) - crop, resize, rotate images
- [JSON](https://doc.nette.org/json) - encoding and decoding
- [Generating Random Strings](https://doc.nette.org/random)
- [Pagination](https://doc.nette.org/pagination) - comfort pagination
- [Strings](https://doc.nette.org/strings) - useful text transpilers
- [SmartObject](https://doc.nette.org/smartobject) - PHP Object Enhancements
- [Validation](https://doc.nette.org/validators) - validate inputs
Documentation can be found on the [website](https://doc.nette.org/utils).
Installation
------------
The recommended way to install is via Composer:
```
composer require nette/utils
```
- Nette Utils 3.0 is compatible with PHP 7.1 to 7.4
- Nette Utils 2.5 is compatible with PHP 5.6 to 7.4
+166
View File
@@ -0,0 +1,166 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Iterators;
use Nette;
/**
* Smarter caching iterator.
*
* @property-read bool $first
* @property-read bool $last
* @property-read bool $empty
* @property-read bool $odd
* @property-read bool $even
* @property-read int $counter
* @property-read mixed $nextKey
* @property-read mixed $nextValue
*/
class CachingIterator extends \CachingIterator implements \Countable
{
use Nette\SmartObject;
/** @var int */
private $counter = 0;
public function __construct($iterator)
{
if (is_array($iterator) || $iterator instanceof \stdClass) {
$iterator = new \ArrayIterator($iterator);
} elseif ($iterator instanceof \IteratorAggregate) {
do {
$iterator = $iterator->getIterator();
} while ($iterator instanceof \IteratorAggregate);
assert($iterator instanceof \Iterator);
} elseif ($iterator instanceof \Iterator) {
} elseif ($iterator instanceof \Traversable) {
$iterator = new \IteratorIterator($iterator);
} else {
throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', __CLASS__, is_object($iterator) ? get_class($iterator) : gettype($iterator)));
}
parent::__construct($iterator, 0);
}
/**
* Is the current element the first one?
*/
public function isFirst(int $gridWidth = null): bool
{
return $this->counter === 1 || ($gridWidth && $this->counter !== 0 && (($this->counter - 1) % $gridWidth) === 0);
}
/**
* Is the current element the last one?
*/
public function isLast(int $gridWidth = null): bool
{
return !$this->hasNext() || ($gridWidth && ($this->counter % $gridWidth) === 0);
}
/**
* Is the iterator empty?
*/
public function isEmpty(): bool
{
return $this->counter === 0;
}
/**
* Is the counter odd?
*/
public function isOdd(): bool
{
return $this->counter % 2 === 1;
}
/**
* Is the counter even?
*/
public function isEven(): bool
{
return $this->counter % 2 === 0;
}
/**
* Returns the counter.
*/
public function getCounter(): int
{
return $this->counter;
}
/**
* Returns the count of elements.
*/
public function count(): int
{
$inner = $this->getInnerIterator();
if ($inner instanceof \Countable) {
return $inner->count();
} else {
throw new Nette\NotSupportedException('Iterator is not countable.');
}
}
/**
* Forwards to the next element.
*/
public function next(): void
{
parent::next();
if (parent::valid()) {
$this->counter++;
}
}
/**
* Rewinds the Iterator.
*/
public function rewind(): void
{
parent::rewind();
$this->counter = parent::valid() ? 1 : 0;
}
/**
* Returns the next key.
* @return mixed
*/
public function getNextKey()
{
return $this->getInnerIterator()->key();
}
/**
* Returns the next element.
* @return mixed
*/
public function getNextValue()
{
return $this->getInnerIterator()->current();
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Iterators;
/**
* Applies the callback to the elements of the inner iterator.
*/
class Mapper extends \IteratorIterator
{
/** @var callable */
private $callback;
public function __construct(\Traversable $iterator, callable $callback)
{
parent::__construct($iterator);
$this->callback = $callback;
}
public function current()
{
return ($this->callback)(parent::current(), parent::key());
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Provides objects to work as array.
*/
class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate
{
/** @return static */
public static function from(array $arr, bool $recursive = true)
{
$obj = new static;
foreach ($arr as $key => $value) {
if ($recursive && is_array($value)) {
$obj->$key = static::from($value, true);
} else {
$obj->$key = $value;
}
}
return $obj;
}
/**
* Returns an iterator over all items.
*/
public function getIterator(): \RecursiveArrayIterator
{
return new \RecursiveArrayIterator((array) $this);
}
/**
* Returns items count.
*/
public function count(): int
{
return count((array) $this);
}
/**
* Replaces or appends a item.
* @param string|int $key
* @param mixed $value
*/
public function offsetSet($key, $value): void
{
if (!is_scalar($key)) { // prevents null
throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', gettype($key)));
}
$this->$key = $value;
}
/**
* Returns a item.
* @param string|int $key
* @return mixed
*/
public function offsetGet($key)
{
return $this->$key;
}
/**
* Determines whether a item exists.
* @param string|int $key
*/
public function offsetExists($key): bool
{
return isset($this->$key);
}
/**
* Removes the element from this list.
* @param string|int $key
*/
public function offsetUnset($key): void
{
unset($this->$key);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Provides the base class for a generic list (items can be accessed by index).
*/
class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
{
use Nette\SmartObject;
/** @var mixed[] */
private $list = [];
/**
* Returns an iterator over all items.
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->list);
}
/**
* Returns items count.
*/
public function count(): int
{
return count($this->list);
}
/**
* Replaces or appends a item.
* @param int|null $index
* @param mixed $value
* @throws Nette\OutOfRangeException
*/
public function offsetSet($index, $value): void
{
if ($index === null) {
$this->list[] = $value;
} elseif (!is_int($index) || $index < 0 || $index >= count($this->list)) {
throw new Nette\OutOfRangeException('Offset invalid or out of range');
} else {
$this->list[$index] = $value;
}
}
/**
* Returns a item.
* @param int $index
* @return mixed
* @throws Nette\OutOfRangeException
*/
public function offsetGet($index)
{
if (!is_int($index) || $index < 0 || $index >= count($this->list)) {
throw new Nette\OutOfRangeException('Offset invalid or out of range');
}
return $this->list[$index];
}
/**
* Determines whether a item exists.
* @param int $index
*/
public function offsetExists($index): bool
{
return is_int($index) && $index >= 0 && $index < count($this->list);
}
/**
* Removes the element at the specified position in this list.
* @param int $index
* @throws Nette\OutOfRangeException
*/
public function offsetUnset($index): void
{
if (!is_int($index) || $index < 0 || $index >= count($this->list)) {
throw new Nette\OutOfRangeException('Offset invalid or out of range');
}
array_splice($this->list, $index, 1);
}
/**
* Prepends a item.
* @param mixed $value
*/
public function prepend($value): void
{
$first = array_slice($this->list, 0, 1);
$this->offsetSet(0, $value);
array_splice($this->list, 1, 0, $first);
}
}
+310
View File
@@ -0,0 +1,310 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
use function is_array, is_int, is_object, count;
/**
* Array tools library.
*/
class Arrays
{
use Nette\StaticClass;
/**
* Returns item from array or $default if item is not set.
* @param string|int|array $key one or more keys
* @param mixed $default
* @return mixed
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
*/
public static function get(array $arr, $key, $default = null)
{
foreach (is_array($key) ? $key : [$key] as $k) {
if (is_array($arr) && array_key_exists($k, $arr)) {
$arr = $arr[$k];
} else {
if (func_num_args() < 3) {
throw new Nette\InvalidArgumentException("Missing item '$k'.");
}
return $default;
}
}
return $arr;
}
/**
* Returns reference to array item.
* @param string|int|array $key one or more keys
* @return mixed
* @throws Nette\InvalidArgumentException if traversed item is not an array
*/
public static function &getRef(array &$arr, $key)
{
foreach (is_array($key) ? $key : [$key] as $k) {
if (is_array($arr) || $arr === null) {
$arr = &$arr[$k];
} else {
throw new Nette\InvalidArgumentException('Traversed item is not an array.');
}
}
return $arr;
}
/**
* Recursively appends elements of remaining keys from the second array to the first.
*/
public static function mergeTree(array $arr1, array $arr2): array
{
$res = $arr1 + $arr2;
foreach (array_intersect_key($arr1, $arr2) as $k => $v) {
if (is_array($v) && is_array($arr2[$k])) {
$res[$k] = self::mergeTree($v, $arr2[$k]);
}
}
return $res;
}
/**
* Searches the array for a given key and returns the offset if successful.
* @param string|int $key
* @return int|null offset if it is found, null otherwise
*/
public static function searchKey(array $arr, $key): ?int
{
$foo = [$key => null];
return Helpers::falseToNull(array_search(key($foo), array_keys($arr), true));
}
/**
* Inserts new array before item specified by key.
* @param string|int $key
*/
public static function insertBefore(array &$arr, $key, array $inserted): void
{
$offset = (int) self::searchKey($arr, $key);
$arr = array_slice($arr, 0, $offset, true) + $inserted + array_slice($arr, $offset, count($arr), true);
}
/**
* Inserts new array after item specified by key.
* @param string|int $key
*/
public static function insertAfter(array &$arr, $key, array $inserted): void
{
$offset = self::searchKey($arr, $key);
$offset = $offset === null ? count($arr) : $offset + 1;
$arr = array_slice($arr, 0, $offset, true) + $inserted + array_slice($arr, $offset, count($arr), true);
}
/**
* Renames key in array.
* @param string|int $oldKey
* @param string|int $newKey
*/
public static function renameKey(array &$arr, $oldKey, $newKey): void
{
$offset = self::searchKey($arr, $oldKey);
if ($offset !== null) {
$keys = array_keys($arr);
$keys[$offset] = $newKey;
$arr = array_combine($keys, $arr);
}
}
/**
* Returns array entries that match the pattern.
*/
public static function grep(array $arr, string $pattern, int $flags = 0): array
{
return Strings::pcre('preg_grep', [$pattern, $arr, $flags]);
}
/**
* Returns flattened array.
*/
public static function flatten(array $arr, bool $preserveKeys = false): array
{
$res = [];
$cb = $preserveKeys
? function ($v, $k) use (&$res): void { $res[$k] = $v; }
: function ($v) use (&$res): void { $res[] = $v; };
array_walk_recursive($arr, $cb);
return $res;
}
/**
* Finds whether a variable is a zero-based integer indexed array.
* @param mixed $value
*/
public static function isList($value): bool
{
return is_array($value) && (!$value || array_keys($value) === range(0, count($value) - 1));
}
/**
* 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 $arr, $path)
{
$parts = is_array($path)
? $path
: preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') {
throw new Nette\InvalidArgumentException("Invalid path '$path'.");
}
$res = $parts[0] === '->' ? new \stdClass : [];
foreach ($arr as $rowOrig) {
$row = (array) $rowOrig;
$x = &$res;
for ($i = 0; $i < count($parts); $i++) {
$part = $parts[$i];
if ($part === '[]') {
$x = &$x[];
} elseif ($part === '=') {
if (isset($parts[++$i])) {
$x = $row[$parts[$i]];
$row = null;
}
} elseif ($part === '->') {
if (isset($parts[++$i])) {
if ($x === null) {
$x = new \stdClass;
}
$x = &$x->{$row[$parts[$i]]};
} else {
$row = is_object($rowOrig) ? $rowOrig : (object) $row;
}
} elseif ($part !== '|') {
$x = &$x[(string) $row[$part]];
}
}
if ($x === null) {
$x = $row;
}
}
return $res;
}
/**
* Normalizes to associative array.
* @param mixed $filling
*/
public static function normalize(array $arr, $filling = null): array
{
$res = [];
foreach ($arr as $k => $v) {
$res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v;
}
return $res;
}
/**
* Picks element from the array by key and return its value.
* @param string|int $key
* @param mixed $default
* @return mixed
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
*/
public static function pick(array &$arr, $key, $default = null)
{
if (array_key_exists($key, $arr)) {
$value = $arr[$key];
unset($arr[$key]);
return $value;
} elseif (func_num_args() < 3) {
throw new Nette\InvalidArgumentException("Missing item '$key'.");
} else {
return $default;
}
}
/**
* Tests whether some element in the array passes the callback test.
*/
public static function some(array $arr, callable $callback): bool
{
foreach ($arr as $k => $v) {
if ($callback($v, $k, $arr)) {
return true;
}
}
return false;
}
/**
* Tests whether all elements in the array pass the callback test.
*/
public static function every(array $arr, callable $callback): bool
{
foreach ($arr as $k => $v) {
if (!$callback($v, $k, $arr)) {
return false;
}
}
return true;
}
/**
* Applies the callback to the elements of the array.
*/
public static function map(array $arr, callable $callback): array
{
$res = [];
foreach ($arr as $k => $v) {
$res[$k] = $callback($v, $k, $arr);
}
return $res;
}
/**
* Converts array to object
* @param object $obj
* @return object
*/
public static function toObject(array $arr, $obj)
{
foreach ($arr as $k => $v) {
$obj->$k = $v;
}
return $obj;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
use function is_array, is_object, is_string;
/**
* PHP callable tools.
*/
final class Callback
{
use Nette\StaticClass;
/**
* @param string|object|callable $callable class, object, callable
* @deprecated use Closure::fromCallable()
*/
public static function closure($callable, string $method = null): \Closure
{
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)
{
$prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
if ($file === __FILE__) {
$msg = ini_get('html_errors') ? Html::htmlToText($message) : $message;
$msg = preg_replace("#^$function\(.*?\): #", '', $msg);
if ($onError($msg, $severity) !== false) {
return null;
}
}
return $prev ? $prev(...func_get_args()) : false;
});
try {
return $function(...$args);
} finally {
restore_error_handler();
}
}
/**
* @param mixed $callable
* @return callable
*/
public static function check($callable, bool $syntax = false)
{
if (!is_callable($callable, $syntax)) {
throw new Nette\InvalidArgumentException($syntax
? 'Given value is not a callable type.'
: sprintf("Callback '%s' is not callable.", self::toString($callable))
);
}
return $callable;
}
/**
* @param mixed $callable may be syntactically correct but not callable
*/
public static function toString($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;
}
}
/**
* @param callable $callable is escalated to ReflectionException
* @return \ReflectionMethod|\ReflectionFunction
*/
public static function toReflection($callable): \ReflectionFunctionAbstract
{
if ($callable instanceof \Closure) {
$callable = self::unwrap($callable);
}
if (is_string($callable) && strpos($callable, '::')) {
return new \ReflectionMethod($callable);
} elseif (is_array($callable)) {
return new \ReflectionMethod($callable[0], $callable[1]);
} elseif (is_object($callable) && !$callable instanceof \Closure) {
return new \ReflectionMethod($callable, '__invoke');
} else {
return new \ReflectionFunction($callable);
}
}
public static function isStatic(callable $callable): bool
{
return is_array($callable) ? is_string($callable[0]) : is_string($callable);
}
/**
* Unwraps closure created by Closure::fromCallable()
* @internal
*/
public static function unwrap(\Closure $closure): callable
{
$r = new \ReflectionFunction($closure);
if (substr($r->name, -1) === '}') {
return $closure;
} elseif ($obj = $r->getClosureThis()) {
return [$obj, $r->name];
} elseif ($class = $r->getClosureScopeClass()) {
return [$class->name, $r->name];
} else {
return $r->name;
}
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* DateTime.
*/
class DateTime extends \DateTime implements \JsonSerializable
{
use Nette\SmartObject;
/** minute in seconds */
public const MINUTE = 60;
/** hour in seconds */
public const HOUR = 60 * self::MINUTE;
/** day in seconds */
public const DAY = 24 * self::HOUR;
/** week in seconds */
public const WEEK = 7 * self::DAY;
/** average month in seconds */
public const MONTH = 2629800;
/** average year in seconds */
public const YEAR = 31557600;
/**
* DateTime object factory.
* @param string|int|\DateTimeInterface $time
* @return static
*/
public static function from($time)
{
if ($time instanceof \DateTimeInterface) {
return new static($time->format('Y-m-d H:i:s.u'), $time->getTimezone());
} elseif (is_numeric($time)) {
if ($time <= self::YEAR) {
$time += time();
}
return (new static('@' . $time))->setTimezone(new \DateTimeZone(date_default_timezone_get()));
} else { // textual or null
return new static((string) $time);
}
}
/**
* Creates DateTime object.
* @return static
*/
public static function fromParts(int $year, int $month, int $day, int $hour = 0, int $minute = 0, float $second = 0.0)
{
$s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5f', $year, $month, $day, $hour, $minute, $second);
if (!checkdate($month, $day, $year) || $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59 || $second < 0 || $second >= 60) {
throw new Nette\InvalidArgumentException("Invalid date '$s'");
}
return new static($s);
}
/**
* 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
*/
public static function createFromFormat($format, $time, $timezone = null)
{
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);
return $date ? static::from($date) : false;
}
/**
* Returns JSON representation in ISO 8601 (used by JavaScript).
*/
public function jsonSerialize(): string
{
return $this->format('c');
}
public function __toString(): string
{
return $this->format('Y-m-d H:i:s');
}
/** @return static */
public function modifyClone(string $modify = '')
{
$dolly = clone $this;
return $modify ? $dolly->modify($modify) : $dolly;
}
}
+182
View File
@@ -0,0 +1,182 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* File system tool.
*/
final class FileSystem
{
use Nette\StaticClass;
/**
* Creates a directory.
* @throws Nette\IOException
*/
public static function createDir(string $dir, int $mode = 0777): void
{
if (!is_dir($dir) && !@mkdir($dir, $mode, true) && !is_dir($dir)) { // @ - dir may already exist
throw new Nette\IOException("Unable to create directory '$dir'. " . Helpers::getLastError());
}
}
/**
* Copies a file or directory.
* @throws Nette\IOException
*/
public static function copy(string $source, string $dest, bool $overwrite = true): void
{
if (stream_is_local($source) && !file_exists($source)) {
throw new Nette\IOException("File or directory '$source' not found.");
} elseif (!$overwrite && file_exists($dest)) {
throw new Nette\InvalidStateException("File or directory '$dest' already exists.");
} elseif (is_dir($source)) {
static::createDir($dest);
foreach (new \FilesystemIterator($dest) as $item) {
static::delete($item->getPathname());
}
foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
if ($item->isDir()) {
static::createDir($dest . '/' . $iterator->getSubPathName());
} else {
static::copy($item->getPathname(), $dest . '/' . $iterator->getSubPathName());
}
}
} else {
static::createDir(dirname($dest));
if (($s = @fopen($source, 'rb')) && ($d = @fopen($dest, 'wb')) && @stream_copy_to_stream($s, $d) === false) { // @ is escalated to exception
throw new Nette\IOException("Unable to copy file '$source' to '$dest'. " . Helpers::getLastError());
}
}
}
/**
* Deletes a file or directory.
* @throws Nette\IOException
*/
public static function delete(string $path): void
{
if (is_file($path) || is_link($path)) {
$func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink';
if (!@$func($path)) { // @ is escalated to exception
throw new Nette\IOException("Unable to delete '$path'. " . Helpers::getLastError());
}
} elseif (is_dir($path)) {
foreach (new \FilesystemIterator($path) as $item) {
static::delete($item->getPathname());
}
if (!@rmdir($path)) { // @ is escalated to exception
throw new Nette\IOException("Unable to delete directory '$path'. " . Helpers::getLastError());
}
}
}
/**
* Renames a file or directory.
* @throws Nette\IOException
* @throws Nette\InvalidStateException if the target file or directory already exist
*/
public static function rename(string $name, string $newName, bool $overwrite = true): void
{
if (!$overwrite && file_exists($newName)) {
throw new Nette\InvalidStateException("File or directory '$newName' already exists.");
} elseif (!file_exists($name)) {
throw new Nette\IOException("File or directory '$name' not found.");
} else {
static::createDir(dirname($newName));
if (realpath($name) !== realpath($newName)) {
static::delete($newName);
}
if (!@rename($name, $newName)) { // @ is escalated to exception
throw new Nette\IOException("Unable to rename file or directory '$name' to '$newName'. " . Helpers::getLastError());
}
}
}
/**
* Reads file content.
* @throws Nette\IOException
*/
public static function read(string $file): string
{
$content = @file_get_contents($file); // @ is escalated to exception
if ($content === false) {
throw new Nette\IOException("Unable to read file '$file'. " . Helpers::getLastError());
}
return $content;
}
/**
* Writes a string to a file.
* @throws Nette\IOException
*/
public static function write(string $file, string $content, ?int $mode = 0666): void
{
static::createDir(dirname($file));
if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
throw new Nette\IOException("Unable to write file '$file'. " . Helpers::getLastError());
}
if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception
throw new Nette\IOException("Unable to chmod file '$file' to mode " . decoct($mode) . '. ' . Helpers::getLastError());
}
}
/**
* Is path absolute?
*/
public static function isAbsolute(string $path): bool
{
return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
}
/**
* Normalizes ../. and directory separators in path.
*/
public static function normalizePath(string $path): string
{
$parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path);
$res = [];
foreach ($parts as $part) {
if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
array_pop($res);
} elseif ($part !== '.') {
$res[] = $part;
}
}
return $res === ['']
? DIRECTORY_SEPARATOR
: implode(DIRECTORY_SEPARATOR, $res);
}
/**
* Joins all given path segments then normalizes the resulting path.
*/
public static function joinPaths(string ...$paths): string
{
return self::normalizePath(implode('/', $paths));
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
class Helpers
{
/**
* Captures PHP output into a string.
*/
public static function capture(callable $func): string
{
ob_start(function () {});
try {
$func();
return ob_get_clean();
} catch (\Throwable $e) {
ob_end_clean();
throw $e;
}
}
/**
* Returns the last PHP error as plain string.
*/
public static function getLastError(): string
{
$message = error_get_last()['message'] ?? '';
$message = ini_get('html_errors') ? Html::htmlToText($message) : $message;
$message = preg_replace('#^\w+\(.*?\): #', '', $message);
return $message;
}
/**
* Converts false to null.
* @param mixed $val
* @return mixed
*/
public static function falseToNull($val)
{
return $val === false ? null : $val;
}
/**
* Finds the best suggestion (for 8-bit encoding).
* @param string[] $possibilities
*/
public static function getSuggestion(array $possibilities, string $value): ?string
{
$best = null;
$min = (strlen($value) / 4 + 1) * 10 + .1;
foreach (array_unique($possibilities) as $item) {
if ($item !== $value && ($len = levenshtein($item, $value, 10, 11, 10)) < $min) {
$min = $len;
$best = $item;
}
}
return $best;
}
}
+870
View File
@@ -0,0 +1,870 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
use function is_array, is_float, is_object, is_string;
/**
* HTML helper.
*
* @property string|null $accept
* @property string|null $accesskey
* @property string|null $action
* @property string|null $align
* @property string|null $allow
* @property string|null $alt
* @property bool|null $async
* @property string|null $autocapitalize
* @property string|null $autocomplete
* @property bool|null $autofocus
* @property bool|null $autoplay
* @property string|null $charset
* @property bool|null $checked
* @property string|null $cite
* @property string|null $class
* @property int|null $cols
* @property int|null $colspan
* @property string|null $content
* @property bool|null $contenteditable
* @property bool|null $controls
* @property string|null $coords
* @property string|null $crossorigin
* @property string|null $data
* @property string|null $datetime
* @property string|null $decoding
* @property bool|null $default
* @property bool|null $defer
* @property string|null $dir
* @property string|null $dirname
* @property bool|null $disabled
* @property bool|null $download
* @property string|null $draggable
* @property string|null $dropzone
* @property string|null $enctype
* @property string|null $for
* @property string|null $form
* @property string|null $formaction
* @property string|null $formenctype
* @property string|null $formmethod
* @property bool|null $formnovalidate
* @property string|null $formtarget
* @property string|null $headers
* @property int|null $height
* @property bool|null $hidden
* @property float|null $high
* @property string|null $href
* @property string|null $hreflang
* @property string|null $id
* @property string|null $integrity
* @property string|null $inputmode
* @property bool|null $ismap
* @property string|null $itemprop
* @property string|null $kind
* @property string|null $label
* @property string|null $lang
* @property string|null $list
* @property bool|null $loop
* @property float|null $low
* @property float|null $max
* @property int|null $maxlength
* @property int|null $minlength
* @property string|null $media
* @property string|null $method
* @property float|null $min
* @property bool|null $multiple
* @property bool|null $muted
* @property string|null $name
* @property bool|null $novalidate
* @property bool|null $open
* @property float|null $optimum
* @property string|null $pattern
* @property string|null $ping
* @property string|null $placeholder
* @property string|null $poster
* @property string|null $preload
* @property string|null $radiogroup
* @property bool|null $readonly
* @property string|null $rel
* @property bool|null $required
* @property bool|null $reversed
* @property int|null $rows
* @property int|null $rowspan
* @property string|null $sandbox
* @property string|null $scope
* @property bool|null $selected
* @property string|null $shape
* @property int|null $size
* @property string|null $sizes
* @property string|null $slot
* @property int|null $span
* @property string|null $spellcheck
* @property string|null $src
* @property string|null $srcdoc
* @property string|null $srclang
* @property string|null $srcset
* @property int|null $start
* @property float|null $step
* @property string|null $style
* @property int|null $tabindex
* @property string|null $target
* @property string|null $title
* @property string|null $translate
* @property string|null $type
* @property string|null $usemap
* @property string|null $value
* @property int|null $width
* @property string|null $wrap
*
* @method self accept(?string $val)
* @method self accesskey(?string $val, bool $state = null)
* @method self action(?string $val)
* @method self align(?string $val)
* @method self allow(?string $val, bool $state = null)
* @method self alt(?string $val)
* @method self async(?bool $val)
* @method self autocapitalize(?string $val)
* @method self autocomplete(?string $val)
* @method self autofocus(?bool $val)
* @method self autoplay(?bool $val)
* @method self charset(?string $val)
* @method self checked(?bool $val)
* @method self cite(?string $val)
* @method self class(?string $val, bool $state = null)
* @method self cols(?int $val)
* @method self colspan(?int $val)
* @method self content(?string $val)
* @method self contenteditable(?bool $val)
* @method self controls(?bool $val)
* @method self coords(?string $val)
* @method self crossorigin(?string $val)
* @method self datetime(?string $val)
* @method self decoding(?string $val)
* @method self default(?bool $val)
* @method self defer(?bool $val)
* @method self dir(?string $val)
* @method self dirname(?string $val)
* @method self disabled(?bool $val)
* @method self download(?bool $val)
* @method self draggable(?string $val)
* @method self dropzone(?string $val)
* @method self enctype(?string $val)
* @method self for(?string $val)
* @method self form(?string $val)
* @method self formaction(?string $val)
* @method self formenctype(?string $val)
* @method self formmethod(?string $val)
* @method self formnovalidate(?bool $val)
* @method self formtarget(?string $val)
* @method self headers(?string $val, bool $state = null)
* @method self height(?int $val)
* @method self hidden(?bool $val)
* @method self high(?float $val)
* @method self hreflang(?string $val)
* @method self id(?string $val)
* @method self integrity(?string $val)
* @method self inputmode(?string $val)
* @method self ismap(?bool $val)
* @method self itemprop(?string $val)
* @method self kind(?string $val)
* @method self label(?string $val)
* @method self lang(?string $val)
* @method self list(?string $val)
* @method self loop(?bool $val)
* @method self low(?float $val)
* @method self max(?float $val)
* @method self maxlength(?int $val)
* @method self minlength(?int $val)
* @method self media(?string $val)
* @method self method(?string $val)
* @method self min(?float $val)
* @method self multiple(?bool $val)
* @method self muted(?bool $val)
* @method self name(?string $val)
* @method self novalidate(?bool $val)
* @method self open(?bool $val)
* @method self optimum(?float $val)
* @method self pattern(?string $val)
* @method self ping(?string $val, bool $state = null)
* @method self placeholder(?string $val)
* @method self poster(?string $val)
* @method self preload(?string $val)
* @method self radiogroup(?string $val)
* @method self readonly(?bool $val)
* @method self rel(?string $val)
* @method self required(?bool $val)
* @method self reversed(?bool $val)
* @method self rows(?int $val)
* @method self rowspan(?int $val)
* @method self sandbox(?string $val, bool $state = null)
* @method self scope(?string $val)
* @method self selected(?bool $val)
* @method self shape(?string $val)
* @method self size(?int $val)
* @method self sizes(?string $val)
* @method self slot(?string $val)
* @method self span(?int $val)
* @method self spellcheck(?string $val)
* @method self src(?string $val)
* @method self srcdoc(?string $val)
* @method self srclang(?string $val)
* @method self srcset(?string $val)
* @method self start(?int $val)
* @method self step(?float $val)
* @method self style(?string $property, string $val = null)
* @method self tabindex(?int $val)
* @method self target(?string $val)
* @method self title(?string $val)
* @method self translate(?string $val)
* @method self type(?string $val)
* @method self usemap(?string $val)
* @method self value(?string $val)
* @method self width(?int $val)
* @method self wrap(?string $val)
*/
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, IHtmlString
{
use Nette\SmartObject;
/** @var array<string, mixed> element's attributes */
public $attrs = [];
/** @var bool use XHTML syntax? */
public static $xhtml = false;
/** @var array<string, int> 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,
'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
];
/** @var array<int, Html|string> nodes */
protected $children = [];
/** @var string element's name */
private $name;
/** @var bool is element empty? */
private $isEmpty;
/**
* 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)
{
$el = new static;
$parts = explode(' ', (string) $name, 2);
$el->setName($parts[0]);
if (is_array($attrs)) {
$el->attrs = $attrs;
} elseif ($attrs !== null) {
$el->setText($attrs);
}
if (isset($parts[1])) {
foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) {
$el->attrs[$m[1]] = $m[3] ?? true;
}
}
return $el;
}
/**
* Returns an object representing HTML text.
*/
public static function fromHtml(string $html): self
{
return (new static)->setHtml($html);
}
/**
* Returns an object representing plain text.
*/
public static function fromText(string $text): self
{
return (new static)->setText($text);
}
/**
* Converts to HTML.
*/
final public function toHtml(): string
{
return $this->render();
}
/**
* Converts to plain text.
*/
final public function toText(): string
{
return $this->getText();
}
/**
* Converts given HTML code to plain text.
*/
public static function htmlToText(string $html): string
{
return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Changes element's name.
* @return static
*/
final public function setName(string $name, bool $isEmpty = null)
{
$this->name = $name;
$this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
return $this;
}
/**
* Returns element's name.
*/
final public function getName(): string
{
return $this->name;
}
/**
* Is element empty?
*/
final public function isEmpty(): bool
{
return $this->isEmpty;
}
/**
* Sets multiple attributes.
* @return static
*/
public function addAttributes(array $attrs)
{
$this->attrs = array_merge($this->attrs, $attrs);
return $this;
}
/**
* Appends value to element's attribute.
* @param mixed $value
* @param mixed $option
* @return static
*/
public function appendAttribute(string $name, $value, $option = true)
{
if (is_array($value)) {
$prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
$this->attrs[$name] = $value + $prev;
} elseif ((string) $value === '') {
$tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists
} elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array
$this->attrs[$name][$value] = $option;
} else {
$this->attrs[$name] = [$this->attrs[$name] => true, $value => $option];
}
return $this;
}
/**
* Sets element's attribute.
* @param mixed $value
* @return static
*/
public function setAttribute(string $name, $value)
{
$this->attrs[$name] = $value;
return $this;
}
/**
* Returns element's attribute.
* @return mixed
*/
public function getAttribute(string $name)
{
return $this->attrs[$name] ?? null;
}
/**
* Unsets element's attribute.
* @return static
*/
public function removeAttribute(string $name)
{
unset($this->attrs[$name]);
return $this;
}
/**
* Unsets element's attributes.
* @return static
*/
public function removeAttributes(array $attributes)
{
foreach ($attributes as $name) {
unset($this->attrs[$name]);
}
return $this;
}
/**
* Overloaded setter for element's attribute.
* @param mixed $value
*/
final public function __set(string $name, $value): void
{
$this->attrs[$name] = $value;
}
/**
* Overloaded getter for element's attribute.
* @return mixed
*/
final public function &__get(string $name)
{
return $this->attrs[$name];
}
/**
* Overloaded tester for element's attribute.
*/
final public function __isset(string $name): bool
{
return isset($this->attrs[$name]);
}
/**
* Overloaded unsetter for element's attribute.
*/
final public function __unset(string $name): void
{
unset($this->attrs[$name]);
}
/**
* Overloaded setter for element's attribute.
* @return mixed
*/
final public function __call(string $m, array $args)
{
$p = substr($m, 0, 3);
if ($p === 'get' || $p === 'set' || $p === 'add') {
$m = substr($m, 3);
$m[0] = $m[0] | "\x20";
if ($p === 'get') {
return $this->attrs[$m] ?? null;
} elseif ($p === 'add') {
$args[] = true;
}
}
if (count($args) === 0) { // invalid
} elseif (count($args) === 1) { // set
$this->attrs[$m] = $args[0];
} else { // add
$this->appendAttribute($m, $args[0], $args[1]);
}
return $this;
}
/**
* Special setter for element's attribute.
* @return static
*/
final public function href(string $path, array $query = null)
{
if ($query) {
$query = http_build_query($query, '', '&');
if ($query !== '') {
$path .= '?' . $query;
}
}
$this->attrs['href'] = $path;
return $this;
}
/**
* Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.
* @param mixed $value
* @return static
*/
public function data(string $name, $value = null)
{
if (func_num_args() === 1) {
$this->attrs['data'] = $name;
} else {
$this->attrs["data-$name"] = is_bool($value) ? json_encode($value) : $value;
}
return $this;
}
/**
* Sets element's HTML content.
* @param IHtmlString|string $html
* @return static
*/
final public function setHtml($html)
{
$this->children = [(string) $html];
return $this;
}
/**
* Returns element's HTML content.
*/
final public function getHtml(): string
{
return implode('', $this->children);
}
/**
* Sets element's textual content.
* @param IHtmlString|string|int|float $text
* @return static
*/
final public function setText($text)
{
if (!$text instanceof IHtmlString) {
$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
}
$this->children = [(string) $text];
return $this;
}
/**
* Returns element's textual content.
*/
final public function getText(): string
{
return self::htmlToText($this->getHtml());
}
/**
* Adds new element's child.
* @param IHtmlString|string $child Html node or raw HTML string
* @return static
*/
final public function addHtml($child)
{
return $this->insert(null, $child);
}
/**
* Appends plain-text string to element content.
* @param IHtmlString|string|int|float $text
* @return static
*/
public function addText($text)
{
if (!$text instanceof IHtmlString) {
$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
}
return $this->insert(null, $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)
{
$this->insert(null, $child = static::el($name, $attrs));
return $child;
}
/**
* Inserts child node.
* @param IHtmlString|string $child Html node or raw HTML string
* @return static
*/
public function insert(?int $index, $child, bool $replace = false)
{
$child = $child instanceof self ? $child : (string) $child;
if ($index === null) { // append
$this->children[] = $child;
} else { // insert or replace
array_splice($this->children, $index, $replace ? 1 : 0, [$child]);
}
return $this;
}
/**
* Inserts (replaces) child node (\ArrayAccess implementation).
* @param int|null $index position or null for appending
* @param Html|string $child Html node or raw HTML string
*/
final public function offsetSet($index, $child): void
{
$this->insert($index, $child, true);
}
/**
* Returns child node (\ArrayAccess implementation).
* @param int $index
* @return static|string
*/
final public function offsetGet($index)
{
return $this->children[$index];
}
/**
* Exists child node? (\ArrayAccess implementation).
* @param int $index
*/
final public function offsetExists($index): bool
{
return isset($this->children[$index]);
}
/**
* Removes child node (\ArrayAccess implementation).
* @param int $index
*/
public function offsetUnset($index): void
{
if (isset($this->children[$index])) {
array_splice($this->children, $index, 1);
}
}
/**
* Returns children count.
*/
final public function count(): int
{
return count($this->children);
}
/**
* Removes all children.
*/
public function removeChildren(): void
{
$this->children = [];
}
/**
* Iterates over elements.
*/
final public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->children);
}
/**
* Returns all children.
*/
final public function getChildren(): array
{
return $this->children;
}
/**
* Renders element's start tag, content and end tag.
*/
final public function render(int $indent = null): string
{
$s = $this->startTag();
if (!$this->isEmpty) {
// add content
if ($indent !== null) {
$indent++;
}
foreach ($this->children as $child) {
if ($child instanceof self) {
$s .= $child->render($indent);
} else {
$s .= $child;
}
}
// add end tag
$s .= $this->endTag();
}
if ($indent !== null) {
return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
}
return $s;
}
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 '';
}
}
/**
* Returns element's start tag.
*/
final public function startTag(): string
{
return $this->name
? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>')
: '';
}
/**
* Returns element's end tag.
*/
final public function endTag(): string
{
return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
}
/**
* Returns element's attributes.
* @internal
*/
final public function attributes(): string
{
if (!is_array($this->attrs)) {
return '';
}
$s = '';
$attrs = $this->attrs;
foreach ($attrs as $key => $value) {
if ($value === null || $value === false) {
continue;
} elseif ($value === true) {
if (static::$xhtml) {
$s .= ' ' . $key . '="' . $key . '"';
} else {
$s .= ' ' . $key;
}
continue;
} elseif (is_array($value)) {
if (strncmp($key, 'data-', 5) === 0) {
$value = Json::encode($value);
} else {
$tmp = null;
foreach ($value as $k => $v) {
if ($v != null) { // intentionally ==, skip nulls & empty string
// composite 'style' vs. 'others'
$tmp[] = $v === true ? $k : (is_string($k) ? $k . ':' . $v : $v);
}
}
if ($tmp === null) {
continue;
}
$value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
}
} elseif (is_float($value)) {
$value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
} else {
$value = (string) $value;
}
$q = strpos($value, '"') === false ? '"' : "'";
$s .= ' ' . $key . '=' . $q
. str_replace(
['&', $q, '<'],
['&amp;', $q === '"' ? '&quot;' : '&#39;', self::$xhtml ? '&lt;' : '<'],
$value
)
. (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '')
. $q;
}
$s = str_replace('@', '&#64;', $s);
return $s;
}
/**
* Clones all children too.
*/
public function __clone()
{
foreach ($this->children as $key => $value) {
if (is_object($value)) {
$this->children[$key] = clone $value;
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
interface IHtmlString
{
/**
* Returns string in HTML format
*/
function __toString(): string;
}
+24
View File
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Localization;
/**
* Translator adapter.
*/
interface ITranslator
{
/**
* Translates the given string.
* @param mixed $message
* @param mixed ...$parameters
*/
function translate($message, ...$parameters): string;
}
+666
View File
@@ -0,0 +1,666 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Basic manipulation with images.
*
* <code>
* $image = Image::fromFile('nette.jpg');
* $image->resize(150, 100);
* $image->sharpen();
* $image->send();
* </code>
*
* @method void alphaBlending(bool $on)
* @method void antialias(bool $on)
* @method void arc($x, $y, $w, $h, $start, $end, $color)
* @method void char(int $font, $x, $y, string $char, $color)
* @method void charUp(int $font, $x, $y, string $char, $color)
* @method int colorAllocate($red, $green, $blue)
* @method int colorAllocateAlpha($red, $green, $blue, $alpha)
* @method int colorAt($x, $y)
* @method int colorClosest($red, $green, $blue)
* @method int colorClosestAlpha($red, $green, $blue, $alpha)
* @method int colorClosestHWB($red, $green, $blue)
* @method void colorDeallocate($color)
* @method int colorExact($red, $green, $blue)
* @method int colorExactAlpha($red, $green, $blue, $alpha)
* @method void colorMatch(Image $image2)
* @method int colorResolve($red, $green, $blue)
* @method int colorResolveAlpha($red, $green, $blue, $alpha)
* @method void colorSet($index, $red, $green, $blue)
* @method array colorsForIndex($index)
* @method int colorsTotal()
* @method int colorTransparent($color = null)
* @method void convolution(array $matrix, float $div, float $offset)
* @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH)
* @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity)
* @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity)
* @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH)
* @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH)
* @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1)
* @method void dashedLine($x1, $y1, $x2, $y2, $color)
* @method void ellipse($cx, $cy, $w, $h, $color)
* @method void fill($x, $y, $color)
* @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style)
* @method void filledEllipse($cx, $cy, $w, $h, $color)
* @method void filledPolygon(array $points, $numPoints, $color)
* @method void filledRectangle($x1, $y1, $x2, $y2, $color)
* @method void fillToBorder($x, $y, $border, $color)
* @method void filter($filtertype)
* @method void flip(int $mode)
* @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null)
* @method void gammaCorrect(float $inputgamma, float $outputgamma)
* @method array getClip()
* @method int interlace($interlace = null)
* @method bool isTrueColor()
* @method void layerEffect($effect)
* @method void line($x1, $y1, $x2, $y2, $color)
* @method void openPolygon(array $points, int $num_points, int $color)
* @method void paletteCopy(Image $source)
* @method void paletteToTrueColor()
* @method void polygon(array $points, $numPoints, $color)
* @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null)
* @method void rectangle($x1, $y1, $x2, $y2, $col)
* @method mixed resolution(int $res_x = null, int $res_y = null)
* @method Image rotate(float $angle, $backgroundColor)
* @method void saveAlpha(bool $saveflag)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED)
* @method void setBrush(Image $brush)
* @method void setClip(int $x1, int $y1, int $x2, int $y2)
* @method void setPixel($x, $y, $color)
* @method void setStyle(array $style)
* @method void setThickness($thickness)
* @method void setTile(Image $tile)
* @method void string($font, $x, $y, string $s, $col)
* @method void stringUp($font, $x, $y, string $s, $col)
* @method void trueColorToPalette(bool $dither, $ncolors)
* @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text)
* @property-read int $width
* @property-read int $height
* @property-read resource $imageResource
*/
class Image
{
use Nette\SmartObject;
/** {@link resize()} only shrinks images */
public const SHRINK_ONLY = 0b0001;
/** {@link resize()} will ignore aspect ratio */
public const STRETCH = 0b0010;
/** {@link resize()} fits in given area so its dimensions are less than or equal to the required dimensions */
public const FIT = 0b0000;
/** {@link resize()} fills given area so its dimensions are greater than or equal to the required dimensions */
public const FILL = 0b0100;
/** {@link resize()} fills given area exactly */
public const EXACT = 0b1000;
/** image types */
public const
JPEG = IMAGETYPE_JPEG,
PNG = IMAGETYPE_PNG,
GIF = IMAGETYPE_GIF,
WEBP = IMAGETYPE_WEBP,
BMP = IMAGETYPE_BMP;
public const EMPTY_GIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
private const FORMATS = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::BMP => 'bmp'];
/** @var resource */
private $image;
/**
* Returns RGB color (0..255) and transparency (0..127).
*/
public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
{
return [
'red' => max(0, min(255, $red)),
'green' => max(0, min(255, $green)),
'blue' => max(0, min(255, $blue)),
'alpha' => max(0, min(127, $transparency)),
];
}
/**
* Opens image from file.
* @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 &$detectedFormat = null)
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
$detectedFormat = @getimagesize($file)[2]; // @ - files smaller than 12 bytes causes read error
if (!isset(self::FORMATS[$detectedFormat])) {
$detectedFormat = null;
throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
}
return new static(Callback::invokeSafe('imagecreatefrom' . image_type_to_extension($detectedFormat, false), [$file], function (string $message): void {
throw new ImageException($message);
}));
}
/**
* Create a new image from the image stream in the string.
* @return static
* @throws ImageException
*/
public static function fromString(string $s, int &$detectedFormat = null)
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
if (func_num_args() > 1) {
$tmp = @getimagesizefromstring($s)[2]; // @ - strings smaller than 12 bytes causes read error
$detectedFormat = isset(self::FORMATS[$tmp]) ? $tmp : null;
}
return new static(Callback::invokeSafe('imagecreatefromstring', [$s], function (string $message): void {
throw new ImageException($message);
}));
}
/**
* Creates blank image.
* @return static
*/
public static function fromBlank(int $width, int $height, array $color = null)
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
if ($width < 1 || $height < 1) {
throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
}
$image = imagecreatetruecolor($width, $height);
if ($color) {
$color += ['alpha' => 0];
$color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']);
imagealphablending($image, false);
imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
imagealphablending($image, true);
}
return new static($image);
}
public static function typeToExtension(int $type): string
{
if (!isset(self::FORMATS[$type])) {
throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
}
return self::FORMATS[$type];
}
public static function typeToMimeType(int $type): string
{
return 'image/' . self::typeToExtension($type);
}
/**
* Wraps GD image.
* @param resource $image
*/
public function __construct($image)
{
$this->setImageResource($image);
imagesavealpha($image, true);
}
/**
* Returns image width.
*/
public function getWidth(): int
{
return imagesx($this->image);
}
/**
* Returns image height.
*/
public function getHeight(): int
{
return imagesy($this->image);
}
/**
* Sets image resource.
* @param resource $image
* @return static
*/
protected function setImageResource($image)
{
if (!is_resource($image) || get_resource_type($image) !== 'gd') {
throw new Nette\InvalidArgumentException('Image is not valid.');
}
$this->image = $image;
return $this;
}
/**
* Returns image GD resource.
* @return resource
*/
public function getImageResource()
{
return $this->image;
}
/**
* Resizes image.
* @param int|string|null $width in pixels or percent
* @param int|string|null $height in pixels or percent
* @return static
*/
public function resize($width, $height, int $flags = self::FIT)
{
if ($flags & 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);
if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
$newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource();
imagecopyresampled(
$newImage, $this->image,
0, 0, 0, 0,
$newWidth, $newHeight, $this->getWidth(), $this->getHeight()
);
$this->image = $newImage;
}
if ($width < 0 || $height < 0) {
imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL);
}
return $this;
}
/**
* Calculates dimensions of resized image.
* @param int|string|null $newWidth in pixels or percent
* @param int|string|null $newHeight in pixels or percent
*/
public static function calculateSize(int $srcWidth, int $srcHeight, $newWidth, $newHeight, int $flags = self::FIT): array
{
if ($newWidth !== null && self::isPercent($newWidth)) {
$newWidth = (int) round($srcWidth / 100 * abs($newWidth));
$percents = true;
} else {
$newWidth = abs($newWidth);
}
if ($newHeight !== null && self::isPercent($newHeight)) {
$newHeight = (int) round($srcHeight / 100 * abs($newHeight));
$flags |= empty($percents) ? 0 : self::STRETCH;
} else {
$newHeight = abs($newHeight);
}
if ($flags & 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) {
$newWidth = (int) round($srcWidth * min(1, $newWidth / $srcWidth));
$newHeight = (int) round($srcHeight * min(1, $newHeight / $srcHeight));
}
} else { // proportional
if (!$newWidth && !$newHeight) {
throw new Nette\InvalidArgumentException('At least width or height must be specified.');
}
$scale = [];
if ($newWidth > 0) { // fit width
$scale[] = $newWidth / $srcWidth;
}
if ($newHeight > 0) { // fit height
$scale[] = $newHeight / $srcHeight;
}
if ($flags & self::FILL) {
$scale = [max($scale)];
}
if ($flags & self::SHRINK_ONLY) {
$scale[] = 1;
}
$scale = min($scale);
$newWidth = (int) round($srcWidth * $scale);
$newHeight = (int) round($srcHeight * $scale);
}
return [max($newWidth, 1), max($newHeight, 1)];
}
/**
* 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
*/
public function crop($left, $top, $width, $height)
{
[$r['x'], $r['y'], $r['width'], $r['height']]
= static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') {
$this->image = imagecrop($this->image, $r);
imagesavealpha($this->image, true);
} else {
$newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource();
imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
$this->image = $newImage;
}
return $this;
}
/**
* Calculates dimensions of cutout in image.
* @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
*/
public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $top, $newWidth, $newHeight): array
{
if (self::isPercent($newWidth)) {
$newWidth = (int) round($srcWidth / 100 * $newWidth);
}
if (self::isPercent($newHeight)) {
$newHeight = (int) round($srcHeight / 100 * $newHeight);
}
if (self::isPercent($left)) {
$left = (int) round(($srcWidth - $newWidth) / 100 * $left);
}
if (self::isPercent($top)) {
$top = (int) round(($srcHeight - $newHeight) / 100 * $top);
}
if ($left < 0) {
$newWidth += $left;
$left = 0;
}
if ($top < 0) {
$newHeight += $top;
$top = 0;
}
$newWidth = min($newWidth, $srcWidth - $left);
$newHeight = min($newHeight, $srcHeight - $top);
return [$left, $top, $newWidth, $newHeight];
}
/**
* Sharpen image.
* @return static
*/
public function sharpen()
{
imageconvolution($this->image, [ // my magic numbers ;)
[-1, -1, -1],
[-1, 24, -1],
[-1, -1, -1],
], 16, 0);
return $this;
}
/**
* Puts another image into this image.
* @param int|string $left in pixels or percent
* @param int|string $top in pixels or percent
* @param int $opacity 0..100
* @return static
*/
public function place(self $image, $left = 0, $top = 0, int $opacity = 100)
{
$opacity = max(0, min(100, $opacity));
if ($opacity === 0) {
return $this;
}
$width = $image->getWidth();
$height = $image->getHeight();
if (self::isPercent($left)) {
$left = (int) round(($this->getWidth() - $width) / 100 * $left);
}
if (self::isPercent($top)) {
$top = (int) round(($this->getHeight() - $height) / 100 * $top);
}
$output = $input = $image->image;
if ($opacity < 100) {
$tbl = [];
for ($i = 0; $i < 128; $i++) {
$tbl[$i] = round(127 - (127 - $i) * $opacity / 100);
}
$output = imagecreatetruecolor($width, $height);
imagealphablending($output, false);
if (!$image->isTrueColor()) {
$input = $output;
imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127));
imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
}
for ($x = 0; $x < $width; $x++) {
for ($y = 0; $y < $height; $y++) {
$c = \imagecolorat($input, $x, $y);
$c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24);
\imagesetpixel($output, $x, $y, $c);
}
}
imagealphablending($output, true);
}
imagecopy(
$this->image, $output,
$left, $top, 0, 0, $width, $height
);
return $this;
}
/**
* Saves image to the file. Quality is 0..100 for JPEG and WEBP, 0..9 for PNG.
* @throws ImageException
*/
public function save(string $file, int $quality = null, int $type = null): void
{
if ($type === null) {
$extensions = array_flip(self::FORMATS) + ['jpg' => self::JPEG];
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (!isset($extensions[$ext])) {
throw new Nette\InvalidArgumentException("Unsupported file extension '$ext'.");
}
$type = $extensions[$ext];
}
$this->output($type, $quality, $file);
}
/**
* Outputs image to string. Quality is 0..100 for JPEG and WEBP, 0..9 for PNG.
*/
public function toString(int $type = self::JPEG, int $quality = null): string
{
return Helpers::capture(function () use ($type, $quality) {
$this->output($type, $quality);
});
}
/**
* Outputs image to 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 '';
}
}
/**
* Outputs image to browser. Quality is 0..100 for JPEG and WEBP, 0..9 for PNG.
* @throws ImageException
*/
public function send(int $type = self::JPEG, int $quality = null): void
{
header('Content-Type: ' . self::typeToMimeType($type));
$this->output($type, $quality);
}
/**
* Outputs image to browser or file.
* @throws ImageException
*/
private function output(int $type, ?int $quality, string $file = null): void
{
switch ($type) {
case self::JPEG:
$quality = $quality === null ? 85 : max(0, min(100, $quality));
$success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception
break;
case self::PNG:
$quality = $quality === null ? 9 : max(0, min(9, $quality));
$success = @imagepng($this->image, $file, $quality); // @ is escalated to exception
break;
case self::GIF:
$success = @imagegif($this->image, $file); // @ is escalated to exception
break;
case self::WEBP:
$quality = $quality === null ? 80 : max(0, min(100, $quality));
$success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception
break;
case self::BMP:
$success = @imagebmp($this->image, $file); // @ is escalated to exception
break;
default:
throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
}
if (!$success) {
throw new ImageException(Helpers::getLastError() ?: 'Unknown error');
}
}
/**
* Call to undefined method.
* @return mixed
* @throws Nette\MemberAccessException
*/
public function __call(string $name, array $args)
{
$function = 'image' . $name;
if (!function_exists($function)) {
ObjectHelpers::strictCall(get_class($this), $name);
}
foreach ($args as $key => $value) {
if ($value instanceof self) {
$args[$key] = $value->getImageResource();
} elseif (is_array($value) && isset($value['red'])) { // rgb
$args[$key] = imagecolorallocatealpha(
$this->image,
$value['red'], $value['green'], $value['blue'], $value['alpha']
) ?: imagecolorresolvealpha(
$this->image,
$value['red'], $value['green'], $value['blue'], $value['alpha']
);
}
}
$res = $function($this->image, ...$args);
return is_resource($res) && get_resource_type($res) === 'gd' ? $this->setImageResource($res) : $res;
}
public function __clone()
{
ob_start(function () {});
imagegd2($this->image);
$this->setImageResource(imagecreatefromstring(ob_get_clean()));
}
/**
* @param int|string $num in pixels or percent
*/
private static function isPercent(&$num): bool
{
if (is_string($num) && substr($num, -1) === '%') {
$num = (float) substr($num, 0, -1);
return true;
} elseif (is_int($num) || $num === (string) (int) $num) {
$num = (int) $num;
return false;
}
throw new Nette\InvalidArgumentException("Expected dimension in int|string, '$num' given.");
}
/**
* Prevents serialization.
*/
public function __sleep(): array
{
throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.');
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* JSON encoder and decoder.
*/
final class Json
{
use Nette\StaticClass;
public const FORCE_ARRAY = 0b0001;
public const PRETTY = 0b0010;
public const ESCAPE_UNICODE = 0b0100;
/**
* Returns the JSON representation of a value. Accepts flag Json::PRETTY.
* @param mixed $value
* @throws JsonException
*/
public static function encode($value, int $flags = 0): string
{
$flags = ($flags & self::ESCAPE_UNICODE ? 0 : JSON_UNESCAPED_UNICODE)
| JSON_UNESCAPED_SLASHES
| ($flags & self::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);
if ($error = json_last_error()) {
throw new JsonException(json_last_error_msg(), $error);
}
return $json;
}
/**
* Decodes a JSON string. Accepts flag Json::FORCE_ARRAY.
* @return mixed
* @throws JsonException
*/
public static function decode(string $json, int $flags = 0)
{
$forceArray = (bool) ($flags & self::FORCE_ARRAY);
$value = json_decode($json, $forceArray, 512, JSON_BIGINT_AS_STRING);
if ($error = json_last_error()) {
throw new JsonException(json_last_error_msg(), $error);
}
return $value;
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
use Nette\MemberAccessException;
/**
* Nette\SmartObject helpers.
*/
final class ObjectHelpers
{
use Nette\StaticClass;
/** @throws MemberAccessException */
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')
), $name);
throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
}
/** @throws MemberAccessException */
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')
), $name);
throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
}
/** @throws MemberAccessException */
public static function strictCall(string $class, string $method, array $additionalMethods = []): void
{
$hint = self::getSuggestion(array_merge(
get_class_methods($class),
self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:\S+[ \t]+)??(\w+)\(~m'),
$additionalMethods
), $method);
if (method_exists($class, $method)) { // called parent::$method()
$class = 'parent';
}
throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
}
/** @throws MemberAccessException */
public static function strictStaticCall(string $class, string $method): void
{
$hint = self::getSuggestion(
array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), function ($m) { return $m->isStatic(); }),
$method
);
throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
}
/**
* Returns array of magic properties defined by annotation @property.
* @return array of [name => bit mask]
* @internal
*/
public static function getMagicProperties(string $class): array
{
static $cache;
$props = &$cache[$class];
if ($props !== null) {
return $props;
}
$rc = new \ReflectionClass($class);
preg_match_all(
'~^ [ \t*]* @property(|-read|-write) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx',
(string) $rc->getDocComment(), $matches, PREG_SET_ORDER
);
$props = [];
foreach ($matches as [, $type, $name]) {
$uname = ucfirst($name);
$write = $type !== '-read'
&& $rc->hasMethod($nm = 'set' . $uname)
&& ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic();
$read = $type !== '-write'
&& ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname))
&& ($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;
}
}
foreach ($rc->getTraits() as $trait) {
$props += self::getMagicProperties($trait->name);
}
if ($parent = get_parent_class($class)) {
$props += self::getMagicProperties($parent);
}
return $props;
}
/**
* Finds the best suggestion (for 8-bit encoding).
* @param (\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionClass|\ReflectionProperty|string)[] $possibilities
* @internal
*/
public static function getSuggestion(array $possibilities, string $value): ?string
{
$norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '+', $value);
$best = null;
$min = (strlen($value) / 4 + 1) * 10 + .1;
foreach (array_unique($possibilities, SORT_REGULAR) as $item) {
$item = $item instanceof \Reflector ? $item->name : $item;
if ($item !== $value && (
($len = levenshtein($item, $value, 10, 11, 10)) < $min
|| ($len = levenshtein(preg_replace($re, '*', $item), $norm, 10, 11, 10)) < $min
)) {
$min = $len;
$best = $item;
}
}
return $best;
}
private static function parseFullDoc(\ReflectionClass $rc, string $pattern): array
{
do {
$doc[] = $rc->getDocComment();
$traits = $rc->getTraits();
while ($trait = array_pop($traits)) {
$doc[] = $trait->getDocComment();
$traits += $trait->getTraits();
}
} while ($rc = $rc->getParentClass());
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
* @internal
*/
public static function hasProperty(string $class, string $name)
{
static $cache;
$prop = &$cache[$class][$name];
if ($prop === null) {
$prop = false;
try {
$rp = new \ReflectionProperty($class, $name);
if ($rp->isPublic() && !$rp->isStatic()) {
$prop = $name >= 'onA' && $name < 'on_' ? 'event' : true;
}
} catch (\ReflectionException $e) {
}
}
return $prop;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Nette\Object behaviour mixin.
* @deprecated
*/
final class ObjectMixin
{
use Nette\StaticClass;
/** @deprecated use ObjectHelpers::getSuggestion() */
public static function getSuggestion(array $possibilities, string $value): ?string
{
trigger_error(__METHOD__ . '() has been renamed to Nette\Utils\ObjectHelpers::getSuggestion()', E_USER_DEPRECATED);
return ObjectHelpers::getSuggestion($possibilities, $value);
}
public static function setExtensionMethod(): void
{
trigger_error('Class Nette\Utils\ObjectMixin is deprecated', E_USER_DEPRECATED);
}
public static function getExtensionMethod(): void
{
trigger_error('Class Nette\Utils\ObjectMixin is deprecated', E_USER_DEPRECATED);
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Paginating math.
*
* @property int $page
* @property-read int $firstPage
* @property-read int|null $lastPage
* @property int $base
* @property-read bool $first
* @property-read bool $last
* @property-read int|null $pageCount
* @property int $itemsPerPage
* @property int|null $itemCount
* @property-read int $offset
* @property-read int|null $countdownOffset
* @property-read int $length
*/
class Paginator
{
use Nette\SmartObject;
/** @var int */
private $base = 1;
/** @var int */
private $itemsPerPage = 1;
/** @var int */
private $page = 1;
/** @var int|null */
private $itemCount;
/**
* Sets current page number.
* @return static
*/
public function setPage(int $page)
{
$this->page = $page;
return $this;
}
/**
* Returns current page number.
*/
public function getPage(): int
{
return $this->base + $this->getPageIndex();
}
/**
* Returns first page number.
*/
public function getFirstPage(): int
{
return $this->base;
}
/**
* Returns last page number.
*/
public function getLastPage(): ?int
{
return $this->itemCount === null ? null : $this->base + max(0, $this->getPageCount() - 1);
}
/**
* Sets first page (base) number.
* @return static
*/
public function setBase(int $base)
{
$this->base = $base;
return $this;
}
/**
* Returns first page (base) number.
*/
public function getBase(): int
{
return $this->base;
}
/**
* Returns zero-based page number.
*/
protected function getPageIndex(): int
{
$index = max(0, $this->page - $this->base);
return $this->itemCount === null ? $index : min($index, max(0, $this->getPageCount() - 1));
}
/**
* Is the current page the first one?
*/
public function isFirst(): bool
{
return $this->getPageIndex() === 0;
}
/**
* Is the current page the last one?
*/
public function isLast(): bool
{
return $this->itemCount === null ? false : $this->getPageIndex() >= $this->getPageCount() - 1;
}
/**
* Returns the total number of pages.
*/
public function getPageCount(): ?int
{
return $this->itemCount === null ? null : (int) ceil($this->itemCount / $this->itemsPerPage);
}
/**
* Sets the number of items to display on a single page.
* @return static
*/
public function setItemsPerPage(int $itemsPerPage)
{
$this->itemsPerPage = max(1, $itemsPerPage);
return $this;
}
/**
* Returns the number of items to display on a single page.
*/
public function getItemsPerPage(): int
{
return $this->itemsPerPage;
}
/**
* Sets the total number of items.
* @return static
*/
public function setItemCount(int $itemCount = null)
{
$this->itemCount = $itemCount === null ? null : max(0, $itemCount);
return $this;
}
/**
* Returns the total number of items.
*/
public function getItemCount(): ?int
{
return $this->itemCount;
}
/**
* Returns the absolute index of the first item on current page.
*/
public function getOffset(): int
{
return $this->getPageIndex() * $this->itemsPerPage;
}
/**
* Returns the absolute index of the first item on current page in countdown paging.
*/
public function getCountdownOffset(): ?int
{
return $this->itemCount === null
? null
: max(0, $this->itemCount - ($this->getPageIndex() + 1) * $this->itemsPerPage);
}
/**
* Returns the number of items on current page.
*/
public function getLength(): int
{
return $this->itemCount === null
? $this->itemsPerPage
: min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage);
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Secure random string generator.
*/
final class Random
{
use Nette\StaticClass;
/**
* Generate random string.
*/
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);
$chLen = strlen($charlist);
if ($length < 1) {
throw new Nette\InvalidArgumentException('Length must be greater than zero.');
} elseif ($chLen < 2) {
throw new Nette\InvalidArgumentException('Character list must contain at least two chars.');
}
$res = '';
for ($i = 0; $i < $length; $i++) {
$res .= $charlist[random_int(0, $chLen - 1)];
}
return $res;
}
}
+340
View File
@@ -0,0 +1,340 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* PHP reflection helpers.
*/
final class Reflection
{
use Nette\StaticClass;
private const BUILTIN_TYPES = [
'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1,
'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1,
];
public static function isBuiltinType(string $type): bool
{
return isset(self::BUILTIN_TYPES[strtolower($type)]);
}
public static function getReturnType(\ReflectionFunctionAbstract $func): ?string
{
$type = $func->getReturnType();
return $type instanceof \ReflectionNamedType && $func instanceof \ReflectionMethod
? self::normalizeType($type->getName(), $func)
: null;
}
public static function getParameterType(\ReflectionParameter $param): ?string
{
$type = $param->getType();
return $type instanceof \ReflectionNamedType
? self::normalizeType($type->getName(), $param)
: null;
}
public static function getPropertyType(\ReflectionProperty $prop): ?string
{
$type = PHP_VERSION_ID >= 70400 ? $prop->getType() : null;
return $type instanceof \ReflectionNamedType
? self::normalizeType($type->getName(), $prop)
: null;
}
/**
* @param \ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflection
*/
private static function normalizeType(string $type, $reflection): string
{
$lower = strtolower($type);
if ($lower === 'self') {
return $reflection->getDeclaringClass()->name;
} elseif ($lower === 'parent' && $reflection->getDeclaringClass()->getParentClass()) {
return $reflection->getDeclaringClass()->getParentClass()->name;
} else {
return $type;
}
}
/**
* @return mixed
* @throws \ReflectionException when default value is not available or resolvable
*/
public static function getParameterDefaultValue(\ReflectionParameter $param)
{
if ($param->isDefaultValueConstant()) {
$const = $orig = $param->getDefaultValueConstantName();
$pair = explode('::', $const);
if (isset($pair[1])) {
$pair[0] = self::normalizeType($pair[0], $param);
try {
$rcc = new \ReflectionClassConstant($pair[0], $pair[1]);
} catch (\ReflectionException $e) {
$name = self::toString($param);
throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.", 0, $e);
}
return $rcc->getValue();
} elseif (!defined($const)) {
$const = substr((string) strrchr($const, '\\'), 1);
if (!defined($const)) {
$name = self::toString($param);
throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.");
}
}
return constant($const);
}
return $param->getDefaultValue();
}
/**
* Returns declaring class or trait.
*/
public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass
{
foreach ($prop->getDeclaringClass()->getTraits() as $trait) {
if ($trait->hasProperty($prop->name)
// doc-comment guessing as workaround for insufficient PHP reflection
&& $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment()
) {
return self::getPropertyDeclaringClass($trait->getProperty($prop->name));
}
}
return $prop->getDeclaringClass();
}
/**
* Returns declaring method in class or trait.
*/
public static function getMethodDeclaringMethod(\ReflectionMethod $method): \ReflectionMethod
{
// file & line guessing as workaround for insufficient PHP reflection
$decl = $method->getDeclaringClass();
if ($decl->getFileName() === $method->getFileName()
&& $decl->getStartLine() <= $method->getStartLine()
&& $decl->getEndLine() >= $method->getEndLine()
) {
return $method;
}
$hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()];
if (($alias = $decl->getTraitAliases()[$method->name] ?? null)
&& ($m = new \ReflectionMethod($alias))
&& $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]
) {
return self::getMethodDeclaringMethod($m);
}
foreach ($decl->getTraits() as $trait) {
if ($trait->hasMethod($method->name)
&& ($m = $trait->getMethod($method->name))
&& $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]
) {
return self::getMethodDeclaringMethod($m);
}
}
return $method;
}
/**
* Are documentation comments available?
*/
public static function areCommentsAvailable(): bool
{
static $res;
return $res === null
? $res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment()
: $res;
}
public static function toString(\Reflector $ref): string
{
if ($ref instanceof \ReflectionClass) {
return $ref->name;
} elseif ($ref instanceof \ReflectionMethod) {
return $ref->getDeclaringClass()->name . '::' . $ref->name;
} elseif ($ref instanceof \ReflectionFunction) {
return $ref->name;
} elseif ($ref instanceof \ReflectionProperty) {
return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name;
} elseif ($ref instanceof \ReflectionParameter) {
return '$' . $ref->name . ' in ' . self::toString($ref->getDeclaringFunction()) . '()';
} else {
throw new Nette\InvalidArgumentException;
}
}
/**
* Expands class name into full name.
* @throws Nette\InvalidArgumentException
*/
public static function expandClassName(string $name, \ReflectionClass $rc): string
{
$lower = strtolower($name);
if (empty($name)) {
throw new Nette\InvalidArgumentException('Class name must not be empty.');
} elseif (isset(self::BUILTIN_TYPES[$lower])) {
return $lower;
} elseif ($lower === 'self') {
return $rc->name;
} elseif ($name[0] === '\\') { // fully qualified name
return ltrim($name, '\\');
}
$uses = self::getUseStatements($rc);
$parts = explode('\\', $name, 2);
if (isset($uses[$parts[0]])) {
$parts[0] = $uses[$parts[0]];
return implode('\\', $parts);
} elseif ($rc->inNamespace()) {
return $rc->getNamespaceName() . '\\' . $name;
} else {
return $name;
}
}
/** @return array of [alias => class] */
public static function getUseStatements(\ReflectionClass $class): array
{
if ($class->isAnonymous()) {
throw new Nette\NotImplementedException('Anonymous classes are not supported.');
}
static $cache = [];
if (!isset($cache[$name = $class->name])) {
if ($class->isInternal()) {
$cache[$name] = [];
} else {
$code = file_get_contents($class->getFileName());
$cache = self::parseUseStatements($code, $name) + $cache;
}
}
return $cache[$name];
}
/**
* Parses PHP code to [class => [alias => class, ...]]
*/
private static function parseUseStatements(string $code, string $forClass = null): array
{
try {
$tokens = token_get_all($code, TOKEN_PARSE);
} catch (\ParseError $e) {
trigger_error($e->getMessage(), E_USER_NOTICE);
$tokens = [];
}
$namespace = $class = $classLevel = $level = null;
$res = $uses = [];
while ($token = current($tokens)) {
next($tokens);
switch (is_array($token) ? $token[0] : $token) {
case T_NAMESPACE:
$namespace = ltrim(self::fetch($tokens, [T_STRING, T_NS_SEPARATOR]) . '\\', '\\');
$uses = [];
break;
case T_CLASS:
case T_INTERFACE:
case T_TRAIT:
if ($name = self::fetch($tokens, T_STRING)) {
$class = $namespace . $name;
$classLevel = $level + 1;
$res[$class] = $uses;
if ($class === $forClass) {
return $res;
}
}
break;
case T_USE:
while (!$class && ($name = self::fetch($tokens, [T_STRING, T_NS_SEPARATOR]))) {
$name = ltrim($name, '\\');
if (self::fetch($tokens, '{')) {
while ($suffix = self::fetch($tokens, [T_STRING, T_NS_SEPARATOR])) {
if (self::fetch($tokens, T_AS)) {
$uses[self::fetch($tokens, T_STRING)] = $name . $suffix;
} else {
$tmp = explode('\\', $suffix);
$uses[end($tmp)] = $name . $suffix;
}
if (!self::fetch($tokens, ',')) {
break;
}
}
} elseif (self::fetch($tokens, T_AS)) {
$uses[self::fetch($tokens, T_STRING)] = $name;
} else {
$tmp = explode('\\', $name);
$uses[end($tmp)] = $name;
}
if (!self::fetch($tokens, ',')) {
break;
}
}
break;
case T_CURLY_OPEN:
case T_DOLLAR_OPEN_CURLY_BRACES:
case '{':
$level++;
break;
case '}':
if ($level === $classLevel) {
$class = $classLevel = null;
}
$level--;
}
}
return $res;
}
private static function fetch(array &$tokens, $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)) {
break;
}
next($tokens);
}
return $res;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;
use Nette\Utils\ObjectHelpers;
/**
* Strict class for better experience.
* - 'did you mean' hints
* - access to undeclared members throws exceptions
* - support for @property annotations
* - support for calling event handlers stored in $onEvent via onEvent()
*/
trait SmartObject
{
/**
* @return void
* @throws MemberAccessException
*/
public function __call(string $name, array $args)
{
$class = get_class($this);
if (ObjectHelpers::hasProperty($class, $name) === 'event') { // calling event handlers
if (is_iterable($this->$name)) {
foreach ($this->$name as $handler) {
$handler(...$args);
}
} elseif ($this->$name !== null) {
throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . gettype($this->$name) . ' given.');
}
} else {
ObjectHelpers::strictCall($class, $name);
}
}
/**
* @return void
* @throws MemberAccessException
*/
public static function __callStatic(string $name, array $args)
{
ObjectHelpers::strictStaticCall(static::class, $name);
}
/**
* @return mixed
* @throws MemberAccessException if the property is not defined.
*/
public function &__get(string $name)
{
$class = get_class($this);
if ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property getter
if (!($prop & 0b0001)) {
throw new MemberAccessException("Cannot read a write-only property $class::\$$name.");
}
$m = ($prop & 0b0010 ? 'get' : 'is') . $name;
if ($prop & 0b0100) { // return by reference
return $this->$m();
} else {
$val = $this->$m();
return $val;
}
} else {
ObjectHelpers::strictGet($class, $name);
}
}
/**
* @param mixed $value
* @return void
* @throws MemberAccessException if the property is not defined or is read-only
*/
public function __set(string $name, $value)
{
$class = get_class($this);
if (ObjectHelpers::hasProperty($class, $name)) { // unsetted property
$this->$name = $value;
} elseif ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property setter
if (!($prop & 0b1000)) {
throw new MemberAccessException("Cannot write to a read-only property $class::\$$name.");
}
$this->{'set' . $name}($value);
} else {
ObjectHelpers::strictSet($class, $name);
}
}
/**
* @return void
* @throws MemberAccessException
*/
public function __unset(string $name)
{
$class = get_class($this);
if (!ObjectHelpers::hasProperty($class, $name)) {
throw new MemberAccessException("Cannot unset the property $class::\$$name.");
}
}
public function __isset(string $name): bool
{
return isset(ObjectHelpers::getMagicProperties(get_class($this))[$name]);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;
/**
* Static class.
*/
trait StaticClass
{
/** @throws \Error */
final public function __construct()
{
throw new \Error('Class ' . get_class($this) . ' is static and cannot be instantiated.');
}
/**
* Call to undefined static method.
* @return void
* @throws MemberAccessException
*/
public static function __callStatic(string $name, array $args)
{
Utils\ObjectHelpers::strictStaticCall(static::class, $name);
}
}
+532
View File
@@ -0,0 +1,532 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
use function is_array, is_object, strlen;
/**
* String tools library.
*/
class Strings
{
use Nette\StaticClass;
public const TRIM_CHARACTERS = " \t\n\r\0\x0B\u{A0}";
/**
* Checks if the string is valid for UTF-8 encoding.
*/
public static function checkEncoding(string $s): bool
{
return $s === self::fixEncoding($s);
}
/**
* Removes invalid code unit sequences from UTF-8 string.
*/
public static function fixEncoding(string $s): string
{
// removes xD800-xDFFF, x110000 and higher
return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES);
}
/**
* Returns a specific character in UTF-8 from code point (0x0 to 0xD7FF or 0xE000 to 0x10FFFF).
* @throws Nette\InvalidArgumentException if code point is not in valid range
*/
public static function chr(int $code): string
{
if ($code < 0 || ($code >= 0xD800 && $code <= 0xDFFF) || $code > 0x10FFFF) {
throw new Nette\InvalidArgumentException('Code point must be in range 0x0 to 0xD7FF or 0xE000 to 0x10FFFF.');
} elseif (!extension_loaded('iconv')) {
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
return iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code));
}
/**
* Starts the $haystack string with the prefix $needle?
*/
public static function startsWith(string $haystack, string $needle): bool
{
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
/**
* Ends the $haystack string with the suffix $needle?
*/
public static function endsWith(string $haystack, string $needle): bool
{
return $needle === '' || substr($haystack, -strlen($needle)) === $needle;
}
/**
* Does $haystack contain $needle?
*/
public static function contains(string $haystack, string $needle): bool
{
return strpos($haystack, $needle) !== false;
}
/**
* Returns a part of UTF-8 string.
*/
public static function substring(string $s, int $start, int $length = null): string
{
if (function_exists('mb_substr')) {
return mb_substr($s, $start, $length, 'UTF-8'); // MB is much faster
} elseif (!extension_loaded('iconv')) {
throw new Nette\NotSupportedException(__METHOD__ . '() requires extension ICONV or MBSTRING, neither is loaded.');
} elseif ($length === null) {
$length = self::length($s);
} elseif ($start < 0 && $length < 0) {
$start += self::length($s); // unifies iconv_substr behavior with mb_substr
}
return iconv_substr($s, $start, $length, 'UTF-8');
}
/**
* Removes special controls characters and normalizes line endings, spaces and normal form to NFC in UTF-8 string.
*/
public static function normalize(string $s): string
{
// convert to compressed normal form (NFC)
if (class_exists('Normalizer', false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) {
$s = $n;
}
$s = self::normalizeNewLines($s);
// remove control characters; leave \t + \n
$s = self::pcre('preg_replace', ['#[\x00-\x08\x0B-\x1F\x7F-\x9F]+#u', '', $s]);
// right trim
$s = self::pcre('preg_replace', ['#[\t ]+$#m', '', $s]);
// leading and trailing blank lines
$s = trim($s, "\n");
return $s;
}
/**
* Standardize line endings to unix-like.
*/
public static function normalizeNewLines(string $s): string
{
return str_replace(["\r\n", "\r"], "\n", $s);
}
/**
* Converts UTF-8 string to ASCII.
*/
public static function toAscii(string $s): string
{
$iconv = defined('ICONV_IMPL') ? ICONV_IMPL : null;
static $transliterator = null;
if ($transliterator === null && class_exists('Transliterator', false)) {
$transliterator = \Transliterator::create('Any-Latin; Latin-ASCII');
}
// remove control characters and check UTF-8 validity
$s = self::pcre('preg_replace', ['#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s]);
// transliteration (by Transliterator and iconv) is not optimal, replace some characters directly
$s = strtr($s, ["\u{201E}" => '"', "\u{201C}" => '"', "\u{201D}" => '"', "\u{201A}" => "'", "\u{2018}" => "'", "\u{2019}" => "'", "\u{B0}" => '^', "\u{42F}" => 'Ya', "\u{44F}" => 'ya', "\u{42E}" => 'Yu', "\u{44E}" => 'yu']); // „ “ ” ‚ ‘ ’ ° Я я Ю ю
if ($iconv !== 'libiconv') {
$s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ ` € ™ ℮ ← ↑ → ↓ ↔
}
if ($transliterator) {
$s = $transliterator->transliterate($s);
// use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ
if ($iconv === 'glibc') {
$s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
$s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters
} elseif ($iconv === 'libiconv') {
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
} else { // null or 'unknown' (#216)
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
}
} elseif ($iconv === 'glibc' || $iconv === 'libiconv') {
// temporarily hide these characters to distinguish them from the garbage that iconv creates
$s = strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06");
if ($iconv === 'glibc') {
// glibc implementation is very limited. transliterate into Windows-1250 and then into ASCII, so most Eastern European characters are preserved
$s = iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s);
$s = strtr($s,
"\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96\xa0\x8b\x97\x9b\xa6\xad\xb7",
'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.');
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]);
} else {
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
}
// remove garbage that iconv creates during transliteration (eg Ý -> Y')
$s = str_replace(['`', "'", '"', '^', '~', '?'], '', $s);
// restore temporarily hidden characters
$s = strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?');
} else {
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
}
return $s;
}
/**
* Converts UTF-8 string to web safe characters [a-z0-9-] text.
*/
public static function webalize(string $s, string $charlist = null, bool $lower = true): string
{
$s = self::toAscii($s);
if ($lower) {
$s = strtolower($s);
}
$s = self::pcre('preg_replace', ['#[^a-z0-9' . ($charlist !== null ? preg_quote($charlist, '#') : '') . ']+#i', '-', $s]);
$s = trim($s, '-');
return $s;
}
/**
* Truncates UTF-8 string to maximal length.
*/
public static function truncate(string $s, int $maxLen, string $append = "\u{2026}"): string
{
if (self::length($s) > $maxLen) {
$maxLen -= self::length($append);
if ($maxLen < 1) {
return $append;
} elseif ($matches = self::match($s, '#^.{1,' . $maxLen . '}(?=[\s\x00-/:-@\[-`{-~])#us')) {
return $matches[0] . $append;
} else {
return self::substring($s, 0, $maxLen) . $append;
}
}
return $s;
}
/**
* Indents UTF-8 string from the left.
*/
public static function indent(string $s, int $level = 1, string $chars = "\t"): string
{
if ($level > 0) {
$s = self::replace($s, '#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level));
}
return $s;
}
/**
* Converts UTF-8 string to lower case.
*/
public static function lower(string $s): string
{
return mb_strtolower($s, 'UTF-8');
}
/**
* Converts first character to lower case.
*/
public static function firstLower(string $s): string
{
return self::lower(self::substring($s, 0, 1)) . self::substring($s, 1);
}
/**
* Converts UTF-8 string to upper case.
*/
public static function upper(string $s): string
{
return mb_strtoupper($s, 'UTF-8');
}
/**
* Converts first character to upper case.
*/
public static function firstUpper(string $s): string
{
return self::upper(self::substring($s, 0, 1)) . self::substring($s, 1);
}
/**
* Capitalizes UTF-8 string.
*/
public static function capitalize(string $s): string
{
return mb_convert_case($s, MB_CASE_TITLE, 'UTF-8');
}
/**
* Case-insensitive compares UTF-8 strings.
*/
public static function compare(string $left, string $right, int $len = null): bool
{
if (class_exists('Normalizer', false)) {
$left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster
$right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster
}
if ($len < 0) {
$left = self::substring($left, $len, -$len);
$right = self::substring($right, $len, -$len);
} elseif ($len !== null) {
$left = self::substring($left, 0, $len);
$right = self::substring($right, 0, $len);
}
return self::lower($left) === self::lower($right);
}
/**
* Finds the length of common prefix of strings.
* @param string[] $strings
*/
public static function findPrefix(array $strings): string
{
$first = array_shift($strings);
for ($i = 0; $i < strlen($first); $i++) {
foreach ($strings as $s) {
if (!isset($s[$i]) || $first[$i] !== $s[$i]) {
while ($i && $first[$i - 1] >= "\x80" && $first[$i] >= "\x80" && $first[$i] < "\xC0") {
$i--;
}
return substr($first, 0, $i);
}
}
}
return $first;
}
/**
* Returns number of characters (not bytes) in UTF-8 string.
* That is the number of Unicode code points which may differ from the number of graphemes.
*/
public static function length(string $s): int
{
return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen(utf8_decode($s));
}
/**
* Strips whitespace from UTF-8 string.
*/
public static function trim(string $s, string $charlist = self::TRIM_CHARACTERS): string
{
$charlist = preg_quote($charlist, '#');
return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', '');
}
/**
* Pad a UTF-8 string to a certain length with another string.
*/
public static function padLeft(string $s, int $length, string $pad = ' '): string
{
$length = max(0, $length - self::length($s));
$padLen = self::length($pad);
return str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s;
}
/**
* Pad a UTF-8 string to a certain length with another string.
*/
public static function padRight(string $s, int $length, string $pad = ' '): string
{
$length = max(0, $length - self::length($s));
$padLen = self::length($pad);
return $s . str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen);
}
/**
* Reverse string.
*/
public static function reverse(string $s): string
{
if (!extension_loaded('iconv')) {
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', $s)));
}
/**
* Returns part of $haystack before $nth occurence of $needle (negative value means searching from the end).
* @return string|null returns null if the needle was not found
*/
public static function before(string $haystack, string $needle, int $nth = 1): ?string
{
$pos = self::pos($haystack, $needle, $nth);
return $pos === null
? null
: substr($haystack, 0, $pos);
}
/**
* Returns part of $haystack after $nth occurence of $needle (negative value means searching from the end).
* @return string|null returns null if the needle was not found
*/
public static function after(string $haystack, string $needle, int $nth = 1): ?string
{
$pos = self::pos($haystack, $needle, $nth);
return $pos === null
? null
: substr($haystack, $pos + strlen($needle));
}
/**
* Returns position of $nth occurence of $needle in $haystack (negative value means searching from the end).
* @return int|null offset in characters or null if the needle was not found
*/
public static function indexOf(string $haystack, string $needle, int $nth = 1): ?int
{
$pos = self::pos($haystack, $needle, $nth);
return $pos === null
? null
: self::length(substr($haystack, 0, $pos));
}
/**
* Returns position of $nth occurence of $needle in $haystack.
* @return int|null offset in bytes or null if the needle was not found
*/
private static function pos(string $haystack, string $needle, int $nth = 1): ?int
{
if (!$nth) {
return null;
} elseif ($nth > 0) {
if ($needle === '') {
return 0;
}
$pos = 0;
while (($pos = strpos($haystack, $needle, $pos)) !== false && --$nth) {
$pos++;
}
} else {
$len = strlen($haystack);
if ($needle === '') {
return $len;
}
$pos = $len - 1;
while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false && ++$nth) {
$pos--;
}
}
return Helpers::falseToNull($pos);
}
/**
* Splits string by a regular expression.
*/
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]);
}
/**
* Performs a regular expression match. Accepts flag PREG_OFFSET_CAPTURE (returned in bytes).
*/
public static function match(string $subject, string $pattern, int $flags = 0, int $offset = 0): ?array
{
if ($offset > strlen($subject)) {
return null;
}
return self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])
? $m
: null;
}
/**
* Performs a global regular expression match. Accepts flag PREG_OFFSET_CAPTURE (returned in bytes), PREG_SET_ORDER is default.
*/
public static function matchAll(string $subject, string $pattern, int $flags = 0, int $offset = 0): array
{
if ($offset > strlen($subject)) {
return [];
}
self::pcre('preg_match_all', [
$pattern, $subject, &$m,
($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER),
$offset,
]);
return $m;
}
/**
* Perform a regular expression search and replace.
* @param string|array $pattern
* @param string|callable $replacement
*/
public static function replace(string $subject, $pattern, $replacement = null, int $limit = -1): 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]);
} elseif ($replacement === null && is_array($pattern)) {
$replacement = array_values($pattern);
$pattern = array_keys($pattern);
}
return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]);
}
/** @internal */
public static function pcre(string $func, array $args)
{
$res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void {
// compile-time error, not detectable by preg_last_error
throw new RegexpException($message . ' in pattern: ' . implode(' or ', (array) $args[0]));
});
if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars
&& ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true))
) {
throw new RegexpException((RegexpException::MESSAGES[$code] ?? 'Unknown error')
. ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code);
}
return $res;
}
}
+365
View File
@@ -0,0 +1,365 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Validation utilities.
*/
class Validators
{
use Nette\StaticClass;
/** @var array<string,?callable> */
protected static $validators = [
// PHP types
'array' => 'is_array',
'bool' => 'is_bool',
'boolean' => 'is_bool',
'float' => 'is_float',
'int' => 'is_int',
'integer' => 'is_int',
'null' => 'is_null',
'object' => 'is_object',
'resource' => 'is_resource',
'scalar' => 'is_scalar',
'string' => 'is_string',
// pseudo-types
'callable' => [__CLASS__, 'isCallable'],
'iterable' => 'is_iterable',
'list' => [Arrays::class, 'isList'],
'mixed' => [__CLASS__, 'isMixed'],
'none' => [__CLASS__, 'isNone'],
'number' => [__CLASS__, 'isNumber'],
'numeric' => [__CLASS__, 'isNumeric'],
'numericint' => [__CLASS__, 'isNumericInt'],
// string patterns
'alnum' => 'ctype_alnum',
'alpha' => 'ctype_alpha',
'digit' => 'ctype_digit',
'lower' => 'ctype_lower',
'pattern' => null,
'space' => 'ctype_space',
'unicode' => [__CLASS__, 'isUnicode'],
'upper' => 'ctype_upper',
'xdigit' => 'ctype_xdigit',
// syntax validation
'email' => [__CLASS__, 'isEmail'],
'identifier' => [__CLASS__, 'isPhpIdentifier'],
'uri' => [__CLASS__, 'isUri'],
'url' => [__CLASS__, 'isUrl'],
// environment validation
'class' => 'class_exists',
'interface' => 'interface_exists',
'directory' => 'is_dir',
'file' => 'is_file',
'type' => [__CLASS__, 'isType'],
];
/** @var array<string,callable> */
protected static $counters = [
'string' => 'strlen',
'unicode' => [Strings::class, 'length'],
'array' => 'count',
'list' => 'count',
'alnum' => 'strlen',
'alpha' => 'strlen',
'digit' => 'strlen',
'lower' => 'strlen',
'space' => 'strlen',
'upper' => 'strlen',
'xdigit' => 'strlen',
];
/**
* Throws exception if a variable is of unexpected type (separated by pipe).
* @param mixed $value
*/
public static function assert($value, string $expected, string $label = 'variable'): void
{
if (!static::is($value, $expected)) {
$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
static $translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null'];
$type = $translate[gettype($value)] ?? gettype($value);
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);
}
throw new AssertionException("The $label expects to be $expected, $type given.");
}
}
/**
* Throws exception if an array field is missing or of unexpected type (separated by pipe).
* @param mixed[] $arr
* @param int|string $field
*/
public static function assertField(array $arr, $field, string $expected = null, string $label = "item '%' in array"): void
{
if (!array_key_exists($field, $arr)) {
throw new AssertionException('Missing ' . str_replace('%', $field, $label) . '.');
} elseif ($expected) {
static::assert($arr[$field], $expected, str_replace('%', $field, $label));
}
}
/**
* Finds whether a variable is of expected type (separated by pipe).
* @param mixed $value
*/
public static function is($value, string $expected): bool
{
foreach (explode('|', $expected) as $item) {
if (substr($item, -2) === '[]') {
if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) {
return true;
}
continue;
} elseif (substr($item, 0, 1) === '?') {
$item = substr($item, 1);
if ($value === null) {
return true;
}
}
[$type] = $item = explode(':', $item, 2);
if (isset(static::$validators[$type])) {
try {
if (!static::$validators[$type]($value)) {
continue;
}
} catch (\TypeError $e) {
continue;
}
} elseif ($type === 'pattern') {
if (Strings::match($value, '|^' . ($item[1] ?? '') . '$|D')) {
return true;
}
continue;
} elseif (!$value instanceof $type) {
continue;
}
if (isset($item[1])) {
$length = $value;
if (isset(static::$counters[$type])) {
$length = static::$counters[$type]($value);
}
$range = explode('..', $item[1]);
if (!isset($range[1])) {
$range[1] = $range[0];
}
if (($range[0] !== '' && $length < $range[0]) || ($range[1] !== '' && $length > $range[1])) {
continue;
}
}
return true;
}
return false;
}
/**
* Finds whether all values are of expected type (separated by pipe).
* @param mixed[] $values
*/
public static function everyIs(iterable $values, string $expected): bool
{
foreach ($values as $value) {
if (!static::is($value, $expected)) {
return false;
}
}
return true;
}
/**
* Finds whether a value is an integer or a float.
* @param mixed $value
*/
public static function isNumber($value): bool
{
return is_int($value) || is_float($value);
}
/**
* Finds whether a value is an integer.
* @param mixed $value
*/
public static function isNumericInt($value): bool
{
return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value));
}
/**
* Finds whether a string is a floating point number in decimal base.
* @param mixed $value
*/
public static function isNumeric($value): bool
{
return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]*[.]?[0-9]+$#D', $value));
}
/**
* Finds whether a value is a syntactically correct callback.
* @param mixed $value
*/
public static function isCallable($value): bool
{
return $value && is_callable($value, true);
}
/**
* Finds whether a value is an UTF-8 encoded string.
* @param mixed $value
*/
public static function isUnicode($value): bool
{
return is_string($value) && preg_match('##u', $value);
}
/**
* Finds whether a value is "falsy".
* @param mixed $value
*/
public static function isNone($value): bool
{
return $value == null; // intentionally ==
}
/** @internal */
public static function isMixed(): bool
{
return true;
}
/**
* Finds whether a variable is a zero-based integer indexed array.
* @param mixed $value
*/
public static function isList($value): bool
{
return Arrays::isList($value);
}
/**
* Is a value in specified min and max value pair?
* @param mixed $value
*/
public static function isInRange($value, array $range): bool
{
if ($value === null || !(isset($range[0]) || isset($range[1]))) {
return false;
}
$limit = $range[0] ?? $range[1];
if (is_string($limit)) {
$value = (string) $value;
} elseif ($limit instanceof \DateTimeInterface) {
if (!$value instanceof \DateTimeInterface) {
return false;
}
} elseif (is_numeric($value)) {
$value *= 1;
} else {
return false;
}
return (!isset($range[0]) || ($value >= $range[0])) && (!isset($range[1]) || ($value <= $range[1]));
}
/**
* Finds whether a string is a valid email address.
*/
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(<<<XX
(^
("([ !#-[\\]-~]*|\\\\[ -~])+"|$atom+(\\.$atom+)*) # quoted or unquoted
@
([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+ # domain - RFC 1034
[$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain
$)Dix
XX
, $value);
}
/**
* Finds whether a string is a valid http(s) URL.
*/
public static function isUrl(string $value): bool
{
$alpha = "a-z\x80-\xFF";
return (bool) preg_match(<<<XX
(^
https?://(
(([-_0-9$alpha]+\\.)* # subdomain
[0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)? # domain
[$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain
|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} # IPv4
|\[[0-9a-f:]{3,39}\] # IPv6
)(:\\d{1,5})? # port
(/\\S*)? # path
(\?\\S*)? # query
(\#\\S*)? # fragment
$)Dix
XX
, $value);
}
/**
* Finds whether a string is a valid URI according to RFC 1738.
*/
public static function isUri(string $value): bool
{
return (bool) preg_match('#^[a-z\d+\.-]+:\S+$#Di', $value);
}
/**
* Checks whether the input is a class, interface or trait.
*/
public static function isType(string $type): bool
{
return class_exists($type) || interface_exists($type) || trait_exists($type);
}
/**
* Checks whether the input is a valid PHP identifier.
*/
public static function isPhpIdentifier(string $value): bool
{
return is_string($value) && preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;
/**
* The exception that is thrown when the value of an argument is
* outside the allowable range of values as defined by the invoked method.
*/
class ArgumentOutOfRangeException extends \InvalidArgumentException
{
}
/**
* The exception that is thrown when a method call is invalid for the object's
* current state, method has been invoked at an illegal or inappropriate time.
*/
class InvalidStateException extends \RuntimeException
{
}
/**
* The exception that is thrown when a requested method or operation is not implemented.
*/
class NotImplementedException extends \LogicException
{
}
/**
* The exception that is thrown when an invoked method is not supported. For scenarios where
* it is sometimes possible to perform the requested operation, see InvalidStateException.
*/
class NotSupportedException extends \LogicException
{
}
/**
* The exception that is thrown when a requested method or operation is deprecated.
*/
class DeprecatedException extends NotSupportedException
{
}
/**
* The exception that is thrown when accessing a class member (property or method) fails.
*/
class MemberAccessException extends \Error
{
}
/**
* The exception that is thrown when an I/O error occurs.
*/
class IOException extends \RuntimeException
{
}
/**
* The exception that is thrown when accessing a file that does not exist on disk.
*/
class FileNotFoundException extends IOException
{
}
/**
* The exception that is thrown when part of a file or directory cannot be found.
*/
class DirectoryNotFoundException extends IOException
{
}
/**
* The exception that is thrown when an argument does not match with the expected value.
*/
class InvalidArgumentException extends \InvalidArgumentException
{
}
/**
* The exception that is thrown when an illegal index was requested.
*/
class OutOfRangeException extends \OutOfRangeException
{
}
/**
* The exception that is thrown when a value (typically returned by function) does not match with the expected value.
*/
class UnexpectedValueException extends \UnexpectedValueException
{
}
namespace Nette\Utils;
/**
* The exception that is thrown when an image error occurs.
*/
class ImageException extends \Exception
{
}
/**
* The exception that indicates invalid image file.
*/
class UnknownImageFileException extends ImageException
{
}
/**
* The exception that indicates error of JSON encoding/decoding.
*/
class JsonException extends \Exception
{
}
/**
* The exception that indicates error of the last Regexp execution.
*/
class RegexpException extends \Exception
{
public const MESSAGES = [
PREG_INTERNAL_ERROR => 'Internal error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
PREG_BAD_UTF8_OFFSET_ERROR => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point',
6 => 'Failed due to limited JIT stack space', // PREG_JIT_STACKLIMIT_ERROR
];
}
/**
* The exception that indicates assertion error.
*/
class AssertionException extends \Exception
{
}