Subversion-Projekte lars-tiefland.laravel_shop

Revision

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

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7
 
8
declare(strict_types=1);
9
 
10
namespace Nette\Utils;
11
 
12
use Nette;
13
 
14
 
15
/**
16
 * Validation utilities.
17
 */
18
class Validators
19
{
20
	use Nette\StaticClass;
21
 
150 lars 22
	private const BuiltinTypes = [
23
		'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1,
24
		'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1,
25
		'never' => 1, 'true' => 1,
26
	];
27
 
148 lars 28
	/** @var array<string,?callable> */
29
	protected static $validators = [
30
		// PHP types
31
		'array' => 'is_array',
32
		'bool' => 'is_bool',
33
		'boolean' => 'is_bool',
34
		'float' => 'is_float',
35
		'int' => 'is_int',
36
		'integer' => 'is_int',
37
		'null' => 'is_null',
38
		'object' => 'is_object',
39
		'resource' => 'is_resource',
40
		'scalar' => 'is_scalar',
41
		'string' => 'is_string',
42
 
43
		// pseudo-types
44
		'callable' => [self::class, 'isCallable'],
45
		'iterable' => 'is_iterable',
46
		'list' => [Arrays::class, 'isList'],
47
		'mixed' => [self::class, 'isMixed'],
48
		'none' => [self::class, 'isNone'],
49
		'number' => [self::class, 'isNumber'],
50
		'numeric' => [self::class, 'isNumeric'],
51
		'numericint' => [self::class, 'isNumericInt'],
52
 
53
		// string patterns
54
		'alnum' => 'ctype_alnum',
55
		'alpha' => 'ctype_alpha',
56
		'digit' => 'ctype_digit',
57
		'lower' => 'ctype_lower',
58
		'pattern' => null,
59
		'space' => 'ctype_space',
60
		'unicode' => [self::class, 'isUnicode'],
61
		'upper' => 'ctype_upper',
62
		'xdigit' => 'ctype_xdigit',
63
 
64
		// syntax validation
65
		'email' => [self::class, 'isEmail'],
66
		'identifier' => [self::class, 'isPhpIdentifier'],
67
		'uri' => [self::class, 'isUri'],
68
		'url' => [self::class, 'isUrl'],
69
 
70
		// environment validation
71
		'class' => 'class_exists',
72
		'interface' => 'interface_exists',
73
		'directory' => 'is_dir',
74
		'file' => 'is_file',
75
		'type' => [self::class, 'isType'],
76
	];
77
 
78
	/** @var array<string,callable> */
79
	protected static $counters = [
80
		'string' => 'strlen',
81
		'unicode' => [Strings::class, 'length'],
82
		'array' => 'count',
83
		'list' => 'count',
84
		'alnum' => 'strlen',
85
		'alpha' => 'strlen',
86
		'digit' => 'strlen',
87
		'lower' => 'strlen',
88
		'space' => 'strlen',
89
		'upper' => 'strlen',
90
		'xdigit' => 'strlen',
91
	];
92
 
93
 
94
	/**
95
	 * Verifies that the value is of expected types separated by pipe.
96
	 * @throws AssertionException
97
	 */
399 lars 98
	public static function assert(mixed $value, string $expected, string $label = 'variable'): void
148 lars 99
	{
100
		if (!static::is($value, $expected)) {
101
			$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
102
			$translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null'];
103
			$type = $translate[gettype($value)] ?? gettype($value);
104
			if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) {
105
				$type .= ' ' . var_export($value, true);
106
			} elseif (is_object($value)) {
399 lars 107
				$type .= ' ' . $value::class;
148 lars 108
			}
109
 
110
			throw new AssertionException("The $label expects to be $expected, $type given.");
111
		}
112
	}
113
 
114
 
115
	/**
116
	 * Verifies that element $key in array is of expected types separated by pipe.
117
	 * @param  mixed[]  $array
118
	 * @throws AssertionException
119
	 */
120
	public static function assertField(
121
		array $array,
122
		$key,
123
		?string $expected = null,
399 lars 124
		string $label = "item '%' in array",
150 lars 125
	): void
126
	{
148 lars 127
		if (!array_key_exists($key, $array)) {
128
			throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.');
129
 
130
		} elseif ($expected) {
131
			static::assert($array[$key], $expected, str_replace('%', $key, $label));
132
		}
133
	}
134
 
135
 
136
	/**
137
	 * Verifies that the value is of expected types separated by pipe.
138
	 */
399 lars 139
	public static function is(mixed $value, string $expected): bool
148 lars 140
	{
141
		foreach (explode('|', $expected) as $item) {
399 lars 142
			if (str_ends_with($item, '[]')) {
148 lars 143
				if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) {
144
					return true;
145
				}
146
 
147
				continue;
399 lars 148
			} elseif (str_starts_with($item, '?')) {
148 lars 149
				$item = substr($item, 1);
150
				if ($value === null) {
151
					return true;
152
				}
153
			}
154
 
155
			[$type] = $item = explode(':', $item, 2);
156
			if (isset(static::$validators[$type])) {
157
				try {
158
					if (!static::$validators[$type]($value)) {
159
						continue;
160
					}
161
				} catch (\TypeError $e) {
162
					continue;
163
				}
164
			} elseif ($type === 'pattern') {
165
				if (Strings::match($value, '|^' . ($item[1] ?? '') . '$|D')) {
166
					return true;
167
				}
168
 
169
				continue;
170
			} elseif (!$value instanceof $type) {
171
				continue;
172
			}
173
 
174
			if (isset($item[1])) {
175
				$length = $value;
176
				if (isset(static::$counters[$type])) {
177
					$length = static::$counters[$type]($value);
178
				}
179
 
180
				$range = explode('..', $item[1]);
181
				if (!isset($range[1])) {
182
					$range[1] = $range[0];
183
				}
184
 
185
				if (($range[0] !== '' && $length < $range[0]) || ($range[1] !== '' && $length > $range[1])) {
186
					continue;
187
				}
188
			}
189
 
190
			return true;
191
		}
192
 
193
		return false;
194
	}
195
 
196
 
197
	/**
198
	 * Finds whether all values are of expected types separated by pipe.
199
	 * @param  mixed[]  $values
200
	 */
201
	public static function everyIs(iterable $values, string $expected): bool
202
	{
203
		foreach ($values as $value) {
204
			if (!static::is($value, $expected)) {
205
				return false;
206
			}
207
		}
208
 
209
		return true;
210
	}
211
 
212
 
213
	/**
214
	 * Checks if the value is an integer or a float.
215
	 */
399 lars 216
	public static function isNumber(mixed $value): bool
148 lars 217
	{
218
		return is_int($value) || is_float($value);
219
	}
220
 
221
 
222
	/**
223
	 * Checks if the value is an integer or a integer written in a string.
224
	 */
399 lars 225
	public static function isNumericInt(mixed $value): bool
148 lars 226
	{
227
		return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value));
228
	}
229
 
230
 
231
	/**
232
	 * Checks if the value is a number or a number written in a string.
233
	 */
399 lars 234
	public static function isNumeric(mixed $value): bool
148 lars 235
	{
236
		return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value));
237
	}
238
 
239
 
240
	/**
241
	 * Checks if the value is a syntactically correct callback.
242
	 */
399 lars 243
	public static function isCallable(mixed $value): bool
148 lars 244
	{
245
		return $value && is_callable($value, true);
246
	}
247
 
248
 
249
	/**
250
	 * Checks if the value is a valid UTF-8 string.
251
	 */
399 lars 252
	public static function isUnicode(mixed $value): bool
148 lars 253
	{
254
		return is_string($value) && preg_match('##u', $value);
255
	}
256
 
257
 
258
	/**
259
	 * Checks if the value is 0, '', false or null.
260
	 */
399 lars 261
	public static function isNone(mixed $value): bool
148 lars 262
	{
263
		return $value == null; // intentionally ==
264
	}
265
 
266
 
267
	/** @internal */
268
	public static function isMixed(): bool
269
	{
270
		return true;
271
	}
272
 
273
 
274
	/**
275
	 * Checks if a variable is a zero-based integer indexed array.
276
	 * @deprecated  use Nette\Utils\Arrays::isList
277
	 */
399 lars 278
	public static function isList(mixed $value): bool
148 lars 279
	{
280
		return Arrays::isList($value);
281
	}
282
 
283
 
284
	/**
285
	 * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null).
286
	 * Numbers, strings and DateTime objects can be compared.
287
	 */
399 lars 288
	public static function isInRange(mixed $value, array $range): bool
148 lars 289
	{
290
		if ($value === null || !(isset($range[0]) || isset($range[1]))) {
291
			return false;
292
		}
293
 
294
		$limit = $range[0] ?? $range[1];
295
		if (is_string($limit)) {
296
			$value = (string) $value;
297
		} elseif ($limit instanceof \DateTimeInterface) {
298
			if (!$value instanceof \DateTimeInterface) {
299
				return false;
300
			}
301
		} elseif (is_numeric($value)) {
302
			$value *= 1;
303
		} else {
304
			return false;
305
		}
306
 
307
		return (!isset($range[0]) || ($value >= $range[0])) && (!isset($range[1]) || ($value <= $range[1]));
308
	}
309
 
310
 
311
	/**
312
	 * Checks if the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified.
313
	 */
314
	public static function isEmail(string $value): bool
315
	{
316
		$atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part
317
		$alpha = "a-z\x80-\xFF"; // superset of IDN
318
		return (bool) preg_match(<<<XX
399 lars 319
			(^(?n)
320
				("([ !#-[\\]-~]*|\\\\[ -~])+"|$atom+(\\.$atom+)*)  # quoted or unquoted
321
				@
322
				([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+  # domain - RFC 1034
323
				[$alpha]([-0-9$alpha]{0,17}[$alpha])?              # top domain
324
			$)Dix
325
			XX, $value);
148 lars 326
	}
327
 
328
 
329
	/**
330
	 * Checks if the value is a valid URL address.
331
	 */
332
	public static function isUrl(string $value): bool
333
	{
334
		$alpha = "a-z\x80-\xFF";
335
		return (bool) preg_match(<<<XX
399 lars 336
			(^(?n)
337
				https?://(
338
					(([-_0-9$alpha]+\\.)*                       # subdomain
339
						[0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)?  # domain
340
						[$alpha]([-0-9$alpha]{0,17}[$alpha])?   # top domain
341
					|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}  # IPv4
342
					|\\[[0-9a-f:]{3,39}\\]                      # IPv6
343
				)(:\\d{1,5})?                                   # port
344
				(/\\S*)?                                        # path
345
				(\\?\\S*)?                                      # query
346
				(\\#\\S*)?                                      # fragment
347
			$)Dix
348
			XX, $value);
148 lars 349
	}
350
 
351
 
352
	/**
353
	 * Checks if the value is a valid URI address, that is, actually a string beginning with a syntactically valid schema.
354
	 */
355
	public static function isUri(string $value): bool
356
	{
357
		return (bool) preg_match('#^[a-z\d+\.-]+:\S+$#Di', $value);
358
	}
359
 
360
 
361
	/**
362
	 * Checks whether the input is a class, interface or trait.
399 lars 363
	 * @deprecated
148 lars 364
	 */
365
	public static function isType(string $type): bool
366
	{
367
		return class_exists($type) || interface_exists($type) || trait_exists($type);
368
	}
369
 
370
 
371
	/**
372
	 * Checks whether the input is a valid PHP identifier.
373
	 */
374
	public static function isPhpIdentifier(string $value): bool
375
	{
376
		return preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value) === 1;
377
	}
150 lars 378
 
379
 
380
	/**
381
	 * Determines if type is PHP built-in type. Otherwise, it is the class name.
382
	 */
383
	public static function isBuiltinType(string $type): bool
384
	{
385
		return isset(self::BuiltinTypes[strtolower($type)]);
386
	}
387
 
388
 
389
	/**
390
	 * Determines if type is special class name self/parent/static.
391
	 */
392
	public static function isClassKeyword(string $name): bool
393
	{
394
		return (bool) preg_match('#^(self|parent|static)$#Di', $name);
395
	}
396
 
397
 
398
	/**
399
	 * Checks whether the given type declaration is syntactically valid.
400
	 */
401
	public static function isTypeDeclaration(string $type): bool
402
	{
403
		return (bool) preg_match(<<<'XX'
399 lars 404
			~((?n)
405
				\?? (?<type> \\? (?<name> [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) |
406
				(?<intersection> (?&type) (& (?&type))+ ) |
407
				(?<upart> (?&type) | \( (?&intersection) \) )  (\| (?&upart))+
408
			)$~xAD
409
			XX, $type);
150 lars 410
	}
148 lars 411
}