Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 150 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

<?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;

        private const BuiltinTypes = [
                'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1,
                'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1,
                'never' => 1, 'true' => 1,
        ];

        /** @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' => [self::class, 'isCallable'],
                'iterable' => 'is_iterable',
                'list' => [Arrays::class, 'isList'],
                'mixed' => [self::class, 'isMixed'],
                'none' => [self::class, 'isNone'],
                'number' => [self::class, 'isNumber'],
                'numeric' => [self::class, 'isNumeric'],
                'numericint' => [self::class, 'isNumericInt'],

                // string patterns
                'alnum' => 'ctype_alnum',
                'alpha' => 'ctype_alpha',
                'digit' => 'ctype_digit',
                'lower' => 'ctype_lower',
                'pattern' => null,
                'space' => 'ctype_space',
                'unicode' => [self::class, 'isUnicode'],
                'upper' => 'ctype_upper',
                'xdigit' => 'ctype_xdigit',

                // syntax validation
                'email' => [self::class, 'isEmail'],
                'identifier' => [self::class, 'isPhpIdentifier'],
                'uri' => [self::class, 'isUri'],
                'url' => [self::class, 'isUrl'],

                // environment validation
                'class' => 'class_exists',
                'interface' => 'interface_exists',
                'directory' => 'is_dir',
                'file' => 'is_file',
                'type' => [self::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',
        ];


        /**
         * Verifies that the value is of expected types separated by pipe.
         * @throws AssertionException
         */
        public static function assert(mixed $value, string $expected, string $label = 'variable'): void
        {
                if (!static::is($value, $expected)) {
                        $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
                        $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 .= ' ' . $value::class;
                        }

                        throw new AssertionException("The $label expects to be $expected, $type given.");
                }
        }


        /**
         * Verifies that element $key in array is of expected types separated by pipe.
         * @param  mixed[]  $array
         * @throws AssertionException
         */
        public static function assertField(
                array $array,
                $key,
                ?string $expected = null,
                string $label = "item '%' in array",
        ): void
        {
                if (!array_key_exists($key, $array)) {
                        throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.');

                } elseif ($expected) {
                        static::assert($array[$key], $expected, str_replace('%', $key, $label));
                }
        }


        /**
         * Verifies that the value is of expected types separated by pipe.
         */
        public static function is(mixed $value, string $expected): bool
        {
                foreach (explode('|', $expected) as $item) {
                        if (str_ends_with($item, '[]')) {
                                if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) {
                                        return true;
                                }

                                continue;
                        } elseif (str_starts_with($item, '?')) {
                                $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 types 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;
        }


        /**
         * Checks if the value is an integer or a float.
         */
        public static function isNumber(mixed $value): bool
        {
                return is_int($value) || is_float($value);
        }


        /**
         * Checks if the value is an integer or a integer written in a string.
         */
        public static function isNumericInt(mixed $value): bool
        {
                return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value));
        }


        /**
         * Checks if the value is a number or a number written in a string.
         */
        public static function isNumeric(mixed $value): bool
        {
                return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value));
        }


        /**
         * Checks if the value is a syntactically correct callback.
         */
        public static function isCallable(mixed $value): bool
        {
                return $value && is_callable($value, true);
        }


        /**
         * Checks if the value is a valid UTF-8 string.
         */
        public static function isUnicode(mixed $value): bool
        {
                return is_string($value) && preg_match('##u', $value);
        }


        /**
         * Checks if the value is 0, '', false or null.
         */
        public static function isNone(mixed $value): bool
        {
                return $value == null; // intentionally ==
        }


        /** @internal */
        public static function isMixed(): bool
        {
                return true;
        }


        /**
         * Checks if a variable is a zero-based integer indexed array.
         * @deprecated  use Nette\Utils\Arrays::isList
         */
        public static function isList(mixed $value): bool
        {
                return Arrays::isList($value);
        }


        /**
         * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null).
         * Numbers, strings and DateTime objects can be compared.
         */
        public static function isInRange(mixed $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]));
        }


        /**
         * Checks if the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified.
         */
        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
                        (^(?n)
                                ("([ !#-[\\]-~]*|\\\\[ -~])+"|$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);
        }


        /**
         * Checks if the value is a valid URL address.
         */
        public static function isUrl(string $value): bool
        {
                $alpha = "a-z\x80-\xFF";
                return (bool) preg_match(<<<XX
                        (^(?n)
                                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);
        }


        /**
         * Checks if the value is a valid URI address, that is, actually a string beginning with a syntactically valid schema.
         */
        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.
         * @deprecated
         */
        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 preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value) === 1;
        }


        /**
         * Determines if type is PHP built-in type. Otherwise, it is the class name.
         */
        public static function isBuiltinType(string $type): bool
        {
                return isset(self::BuiltinTypes[strtolower($type)]);
        }


        /**
         * Determines if type is special class name self/parent/static.
         */
        public static function isClassKeyword(string $name): bool
        {
                return (bool) preg_match('#^(self|parent|static)$#Di', $name);
        }


        /**
         * Checks whether the given type declaration is syntactically valid.
         */
        public static function isTypeDeclaration(string $type): bool
        {
                return (bool) preg_match(<<<'XX'
                        ~((?n)
                                \?? (?<type> \\? (?<name> [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) |
                                (?<intersection> (?&type) (& (?&type))+ ) |
                                (?<upart> (?&type) | \( (?&intersection) \) )  (\| (?&upart))+
                        )$~xAD
                        XX, $type);
        }
}