1: <?php declare(strict_types=1);
2:
3: namespace Salient\Utility;
4:
5: use Psr\Container\ContainerInterface as PsrContainerInterface;
6: use Salient\Contract\Container\SingletonInterface;
7: use Salient\Contract\Core\Arrayable;
8: use Salient\Utility\Exception\InvalidArgumentTypeException;
9: use Salient\Utility\Exception\UncloneableObjectException;
10: use Closure;
11: use Countable;
12: use DateTimeInterface;
13: use DateTimeZone;
14: use InvalidArgumentException;
15: use LogicException;
16: use ReflectionClass;
17: use ReflectionObject;
18: use Stringable;
19: use UnitEnum;
20:
21: /**
22: * Get values from other values
23: *
24: * @api
25: */
26: final class Get extends AbstractUtility
27: {
28: /**
29: * Do not throw an exception if an uncloneable object is encountered
30: */
31: public const COPY_SKIP_UNCLONEABLE = 1;
32:
33: /**
34: * Assign values to properties by reference
35: *
36: * Required if an object graph contains nodes with properties passed or
37: * assigned by reference.
38: */
39: public const COPY_BY_REFERENCE = 2;
40:
41: /**
42: * Take a shallow copy of objects with a __clone method
43: */
44: public const COPY_TRUST_CLONE = 4;
45:
46: /**
47: * Copy service containers
48: */
49: public const COPY_CONTAINERS = 8;
50:
51: /**
52: * Copy singletons
53: */
54: public const COPY_SINGLETONS = 16;
55:
56: /**
57: * Cast a value to boolean, converting boolean strings and preserving null
58: *
59: * @see Test::isBoolean()
60: *
61: * @param mixed $value
62: * @return ($value is null ? null : bool)
63: */
64: public static function boolean($value): ?bool
65: {
66: if ($value === null || is_bool($value)) {
67: return $value;
68: }
69:
70: if (is_string($value) && Regex::match(
71: '/^' . Regex::BOOLEAN_STRING . '$/',
72: $value,
73: $matches,
74: \PREG_UNMATCHED_AS_NULL
75: )) {
76: return $matches['true'] !== null;
77: }
78:
79: return (bool) $value;
80: }
81:
82: /**
83: * Cast a value to integer, preserving null
84: *
85: * @param int|float|string|bool|null $value
86: * @return ($value is null ? null : int)
87: */
88: public static function integer($value): ?int
89: {
90: if ($value === null) {
91: return null;
92: }
93:
94: return (int) $value;
95: }
96:
97: /**
98: * Cast a value to the array-key it appears to be, preserving null
99: *
100: * @param int|string|null $value
101: * @return ($value is null ? null : ($value is int ? int : int|string))
102: */
103: public static function arrayKey($value)
104: {
105: if ($value === null || is_int($value)) {
106: return $value;
107: }
108:
109: if (!is_string($value)) {
110: throw new InvalidArgumentTypeException(1, 'value', 'int|string|null', $value);
111: }
112:
113: if (Regex::match('/^' . Regex::INTEGER_STRING . '$/', $value)) {
114: return (int) $value;
115: }
116:
117: return $value;
118: }
119:
120: /**
121: * Convert a callable to a closure
122: *
123: * @return ($callable is null ? null : Closure)
124: */
125: public static function closure(?callable $callable): ?Closure
126: {
127: return $callable === null
128: ? null
129: : ($callable instanceof Closure
130: ? $callable
131: : Closure::fromCallable($callable));
132: }
133:
134: /**
135: * Resolve a closure to its return value
136: *
137: * @template T
138: *
139: * @param (Closure(mixed...): T)|T $value
140: * @param mixed ...$args Passed to `$value` if it is a closure.
141: * @return T
142: */
143: public static function value($value, ...$args)
144: {
145: if ($value instanceof Closure) {
146: return $value(...$args);
147: }
148: return $value;
149: }
150:
151: /**
152: * Convert "key[=value]" pairs to an associative array
153: *
154: * @param string[] $values
155: * @return mixed[]
156: */
157: public static function filter(array $values, bool $discardInvalid = false): array
158: {
159: $valid = Regex::grep('/^[^ .=]++/', $values);
160: if (!$discardInvalid && $valid !== $values) {
161: $invalid = array_diff($values, $valid);
162: throw new InvalidArgumentException(Inflect::format(
163: $invalid,
164: "Invalid key-value {{#:pair}}: '%s'",
165: implode("', '", $invalid),
166: ));
167: }
168:
169: /** @var int|null */
170: static $maxInputVars;
171:
172: $maxInputVars ??= (int) ini_get('max_input_vars');
173: if (count($valid) > $maxInputVars) {
174: throw new InvalidArgumentException(sprintf(
175: 'Key-value pairs exceed max_input_vars (%d)',
176: $maxInputVars,
177: ));
178: }
179:
180: $values = Regex::replaceCallback(
181: '/^([^=]++)(?:=(.++))?/s',
182: fn($matches) =>
183: rawurlencode((string) $matches[1])
184: . ($matches[2] === null
185: ? ''
186: : '=' . rawurlencode($matches[2])),
187: $valid,
188: -1,
189: $count,
190: \PREG_UNMATCHED_AS_NULL,
191: );
192:
193: $query = [];
194: parse_str(implode('&', $values), $query);
195: return $query;
196: }
197:
198: /**
199: * Get the first value that is not null, or return the last value
200: *
201: * @template T
202: *
203: * @param T|null ...$values
204: * @return T|null
205: */
206: public static function coalesce(...$values)
207: {
208: $value = null;
209: foreach ($values as $value) {
210: if ($value === null) {
211: continue;
212: }
213: return $value;
214: }
215: return $value;
216: }
217:
218: /**
219: * Resolve a value to an array
220: *
221: * @template TKey of array-key
222: * @template TValue
223: *
224: * @param Arrayable<TKey,TValue>|iterable<TKey,TValue> $value
225: * @return array<TKey,TValue>
226: */
227: public static function array($value): array
228: {
229: if (is_array($value)) {
230: return $value;
231: }
232: if ($value instanceof Arrayable) {
233: return $value->toArray();
234: }
235: return iterator_to_array($value);
236: }
237:
238: /**
239: * Resolve a value to a list
240: *
241: * @template TValue
242: *
243: * @param Arrayable<array-key,TValue>|iterable<TValue> $value
244: * @return list<TValue>
245: */
246: public static function list($value): array
247: {
248: if (is_array($value)) {
249: return array_values($value);
250: }
251: if ($value instanceof Arrayable) {
252: return array_values($value->toArray());
253: }
254: return iterator_to_array($value, false);
255: }
256:
257: /**
258: * Resolve a value to an item count
259: *
260: * @param Arrayable<array-key,mixed>|iterable<array-key,mixed>|Countable|int $value
261: */
262: public static function count($value): int
263: {
264: if (is_int($value)) {
265: return $value;
266: }
267: if (is_array($value) || $value instanceof Countable) {
268: return count($value);
269: }
270: if ($value instanceof Arrayable) {
271: return count($value->toArray());
272: }
273: return iterator_count($value);
274: }
275:
276: /**
277: * Get the unqualified name of a class, optionally removing a suffix
278: *
279: * Only the first matching `$suffix` is removed, so longer suffixes should
280: * be given first.
281: */
282: public static function basename(string $class, string ...$suffix): string
283: {
284: /** @var string */
285: $class = strrchr('\\' . $class, '\\');
286: $class = substr($class, 1);
287:
288: if (!$suffix) {
289: return $class;
290: }
291:
292: foreach ($suffix as $suffix) {
293: if ($suffix === $class) {
294: continue;
295: }
296: $length = strlen($suffix);
297: if (substr($class, -$length) === $suffix) {
298: return substr($class, 0, -$length);
299: }
300: }
301:
302: return $class;
303: }
304:
305: /**
306: * Get the namespace of a class
307: */
308: public static function namespace(string $class): string
309: {
310: $length = strrpos('\\' . $class, '\\') - 1;
311:
312: return $length < 1
313: ? ''
314: : trim(substr($class, 0, $length), '\\');
315: }
316:
317: /**
318: * Normalise a class name for comparison
319: *
320: * @template T of object
321: *
322: * @param class-string<T> $class
323: * @return class-string<T>
324: */
325: public static function fqcn(string $class): string
326: {
327: /** @var class-string<T> */
328: return Str::lower(ltrim($class, '\\'));
329: }
330:
331: /**
332: * Get a UUID in raw binary form
333: *
334: * If `$uuid` is not given, an \[RFC4122]-compliant UUID is generated.
335: *
336: * @throws InvalidArgumentException if an invalid UUID is given.
337: */
338: public static function binaryUuid(?string $uuid = null): string
339: {
340: return $uuid === null
341: ? self::getUuid(true)
342: : self::normaliseUuid($uuid, true);
343: }
344:
345: /**
346: * Get a UUID in hexadecimal form
347: *
348: * If `$uuid` is not given, an \[RFC4122]-compliant UUID is generated.
349: *
350: * @throws InvalidArgumentException if an invalid UUID is given.
351: */
352: public static function uuid(?string $uuid = null): string
353: {
354: return $uuid === null
355: ? self::getUuid(false)
356: : self::normaliseUuid($uuid, false);
357: }
358:
359: private static function getUuid(bool $binary): string
360: {
361: $uuid = [
362: random_bytes(4),
363: random_bytes(2),
364: // Version 4 (most significant 4 bits = 0b0100)
365: chr(random_int(0, 0x0F) | 0x40) . random_bytes(1),
366: // Variant 1 (most significant 2 bits = 0b10)
367: chr(random_int(0, 0x3F) | 0x80) . random_bytes(1),
368: random_bytes(6),
369: ];
370:
371: if ($binary) {
372: return implode('', $uuid);
373: }
374:
375: foreach ($uuid as $bin) {
376: $hex[] = bin2hex($bin);
377: }
378:
379: return implode('-', $hex);
380: }
381:
382: private static function normaliseUuid(string $uuid, bool $binary): string
383: {
384: $length = strlen($uuid);
385:
386: if ($length !== 16) {
387: $uuid = str_replace('-', '', $uuid);
388:
389: if (!Regex::match('/^[0-9a-f]{32}$/i', $uuid)) {
390: throw new InvalidArgumentException(sprintf(
391: 'Invalid UUID: %s',
392: $uuid,
393: ));
394: }
395:
396: if ($binary) {
397: /** @var string */
398: return hex2bin($uuid);
399: }
400:
401: $uuid = Str::lower($uuid);
402:
403: return implode('-', [
404: substr($uuid, 0, 8),
405: substr($uuid, 8, 4),
406: substr($uuid, 12, 4),
407: substr($uuid, 16, 4),
408: substr($uuid, 20, 12),
409: ]);
410: }
411:
412: if ($binary) {
413: return $uuid;
414: }
415:
416: $uuid = [
417: substr($uuid, 0, 4),
418: substr($uuid, 4, 2),
419: substr($uuid, 6, 2),
420: substr($uuid, 8, 2),
421: substr($uuid, 10, 6),
422: ];
423:
424: foreach ($uuid as $bin) {
425: $hex[] = bin2hex($bin);
426: }
427:
428: return implode('-', $hex);
429: }
430:
431: /**
432: * Get a sequence of random characters
433: */
434: public static function randomText(int $length, string $chars = Str::ALPHANUMERIC): string
435: {
436: if ($chars === '') {
437: throw new InvalidArgumentException('Argument #1 ($chars) must be a non-empty string');
438: }
439: $max = strlen($chars) - 1;
440: $text = '';
441: for ($i = 0; $i < $length; $i++) {
442: $text .= $chars[random_int(0, $max)];
443: }
444: return $text;
445: }
446:
447: /**
448: * Get the hash of a value in raw binary form
449: *
450: * @param int|float|string|bool|Stringable|null $value
451: */
452: public static function binaryHash($value): string
453: {
454: // xxHash isn't supported until PHP 8.1, so MD5 is the best fit
455: return hash('md5', (string) $value, true);
456: }
457:
458: /**
459: * Get the hash of a value in hexadecimal form
460: *
461: * @param int|float|string|bool|Stringable|null $value
462: */
463: public static function hash($value): string
464: {
465: return hash('md5', (string) $value);
466: }
467:
468: /**
469: * Get the type of a variable
470: *
471: * @param mixed $value
472: */
473: public static function type($value): string
474: {
475: if (is_object($value)) {
476: return (new ReflectionClass($value))->isAnonymous()
477: ? 'class@anonymous'
478: : get_class($value);
479: }
480:
481: if (is_resource($value)) {
482: return sprintf('resource (%s)', get_resource_type($value));
483: }
484:
485: $type = gettype($value);
486: return [
487: 'boolean' => 'bool',
488: 'integer' => 'int',
489: 'double' => 'float',
490: 'NULL' => 'null',
491: ][$type] ?? $type;
492: }
493:
494: /**
495: * Get php.ini values like "128M" in bytes
496: *
497: * From the PHP FAQ: "The available options are K (for Kilobytes), M (for
498: * Megabytes) and G (for Gigabytes), and are all case-insensitive. Anything
499: * else assumes bytes. 1M equals one Megabyte or 1048576 bytes. 1K equals
500: * one Kilobyte or 1024 bytes."
501: */
502: public static function bytes(string $size): int
503: {
504: // PHP is very forgiving with the syntax of these values
505: $size = rtrim($size);
506: $exp = [
507: 'K' => 1, 'k' => 1, 'M' => 2, 'm' => 2, 'G' => 3, 'g' => 3
508: ][$size[-1] ?? ''] ?? 0;
509: return (int) $size * 1024 ** $exp;
510: }
511:
512: /**
513: * Convert a value to PHP code
514: *
515: * Similar to {@see var_export()}, but with more economical output.
516: *
517: * @param mixed $value
518: * @param string[] $classes Strings found in this array are output as
519: * `<string>::class` instead of `'<string>'`.
520: * @param array<non-empty-string,string> $constants An array that maps
521: * strings to constant identifiers, e.g. `[\PHP_EOL => '\PHP_EOL']`.
522: */
523: public static function code(
524: $value,
525: string $delimiter = ', ',
526: string $arrow = ' => ',
527: ?string $escapeCharacters = null,
528: string $tab = ' ',
529: array $classes = [],
530: array $constants = []
531: ): string {
532: $eol = (string) self::eol($delimiter);
533: $multiline = (bool) $eol;
534: $escapeRegex = null;
535: $search = [];
536: $replace = [];
537: if ($escapeCharacters !== null && $escapeCharacters !== '') {
538: $escapeRegex = Regex::quoteCharacterClass($escapeCharacters, '/');
539: foreach (str_split($escapeCharacters) as $character) {
540: $search[] = sprintf(
541: '/((?<!\\\\)(?:\\\\\\\\)*)%s/',
542: preg_quote(addcslashes($character, $character), '/'),
543: );
544: $replace[] = sprintf('$1\x%02x', ord($character));
545: }
546: }
547: $classes = Arr::toIndex($classes);
548: $constRegex = [];
549: foreach (array_keys($constants) as $string) {
550: $constRegex[] = preg_quote($string, '/');
551: }
552: switch (count($constRegex)) {
553: case 0:
554: $constRegex = null;
555: break;
556: case 1:
557: $constRegex = '/' . $constRegex[0] . '/';
558: break;
559: default:
560: $constRegex = '/(?:' . implode('|', $constRegex) . ')/';
561: break;
562: }
563: return self::doCode(
564: $value,
565: $delimiter,
566: $arrow,
567: $escapeCharacters,
568: $escapeRegex,
569: $search,
570: $replace,
571: $tab,
572: $classes,
573: $constants,
574: $constRegex,
575: $multiline,
576: $eol,
577: );
578: }
579:
580: /**
581: * @param mixed $value
582: * @param string[] $search
583: * @param string[] $replace
584: * @param array<string,true> $classes
585: * @param array<non-empty-string,string> $constants
586: */
587: private static function doCode(
588: $value,
589: string $delimiter,
590: string $arrow,
591: ?string $escapeCharacters,
592: ?string $escapeRegex,
593: array $search,
594: array $replace,
595: string $tab,
596: array $classes,
597: array $constants,
598: ?string $regex,
599: bool $multiline,
600: string $eol,
601: string $indent = ''
602: ): string {
603: if ($value === null) {
604: return 'null';
605: }
606:
607: if (is_string($value)) {
608: if ($classes && isset($classes[$value])) {
609: return $value . '::class';
610: }
611:
612: if ($regex !== null) {
613: $parts = [];
614: while (Regex::match($regex, $value, $matches, \PREG_OFFSET_CAPTURE)) {
615: if ($matches[0][1] > 0) {
616: $parts[] = substr($value, 0, $matches[0][1]);
617: }
618: $parts[] = $matches[0][0];
619: $value = substr($value, $matches[0][1] + strlen($matches[0][0]));
620: }
621: if ($parts) {
622: if ($value !== '') {
623: $parts[] = $value;
624: }
625: foreach ($parts as &$part) {
626: $part = $constants[$part]
627: ?? self::doCode($part, $delimiter, $arrow, $escapeCharacters, $escapeRegex, $search, $replace, $tab, $classes, [], null, $multiline, $eol, $indent);
628: }
629: return implode(' . ', $parts);
630: }
631: }
632:
633: if ($multiline) {
634: $escape = '';
635: $match = '';
636: } else {
637: $escape = "\n\r";
638: $match = '\n\r';
639: }
640:
641: // Don't escape UTF-8 leading bytes (\xc2 -> \xf4) or continuation
642: // bytes (\x80 -> \xbf)
643: if (mb_check_encoding($value, 'UTF-8')) {
644: $escape .= "\x7f\xc0\xc1\xf5..\xff";
645: $match .= '\x7f\xc0\xc1\xf5-\xff';
646: $utf8 = true;
647: } else {
648: $escape .= "\x7f..\xff";
649: $match .= '\x7f-\xff';
650: $utf8 = false;
651: }
652:
653: // Escape strings that contain characters in `$escape` or
654: // `$escapeCharacters`
655: if (Regex::match("/[\\x00-\\x09\\x0b\\x0c\\x0e-\\x1f{$match}{$escapeRegex}]/", $value)) {
656: // \0..\t\v\f\x0e..\x1f = \0..\x1f without \n and \r
657: $escaped = addcslashes(
658: $value,
659: "\0..\t\v\f\x0e..\x1f\"\$\\" . $escape . $escapeCharacters
660: );
661:
662: // Convert blank/ignorable code points to "\u{xxxx}" unless they
663: // belong to a recognised Unicode sequence
664: if ($utf8) {
665: $escaped = Regex::replaceCallback(
666: '/(?![\x00-\x7f])\X/u',
667: fn(array $matches): string =>
668: Regex::match('/^' . Regex::INVISIBLE_CHAR . '$/u', $matches[0])
669: ? sprintf('\u{%04X}', mb_ord($matches[0]))
670: : $matches[0],
671: $escaped,
672: );
673: }
674:
675: // Replace characters in `$escapeCharacters` with the equivalent
676: // hexadecimal escape
677: if ($search) {
678: $escaped = Regex::replace($search, $replace, $escaped);
679: }
680:
681: // Convert octal notation to hex (e.g. "\177" to "\x7f") and
682: // correct for differences between C and PHP escape sequences:
683: // - recognised by PHP: \0 \e \f \n \r \t \v
684: // - applied by addcslashes: \000 \033 \a \b \f \n \r \t \v
685: $escaped = Regex::replaceCallback(
686: '/((?<!\\\\)(?:\\\\\\\\)*)\\\\(?:(?<NUL>000(?![0-7]))|(?<octal>[0-7]{3})|(?<cslash>[ab]))/',
687: fn(array $matches): string =>
688: $matches[1]
689: . ($matches['NUL'] !== null
690: ? '\0'
691: : ($matches['octal'] !== null
692: ? (($dec = octdec($matches['octal'])) === 27
693: ? '\e'
694: : sprintf('\x%02x', $dec))
695: : sprintf('\x%02x', ['a' => 7, 'b' => 8][$matches['cslash']]))),
696: $escaped,
697: -1,
698: $count,
699: \PREG_UNMATCHED_AS_NULL,
700: );
701:
702: // Remove unnecessary backslashes
703: $escaped = Regex::replace(
704: '/(?<!\\\\)\\\\\\\\(?![nrtvef\\\\$"]|[0-7]|x[0-9a-fA-F]|u\{[0-9a-fA-F]+\}|$)/',
705: '\\',
706: $escaped
707: );
708:
709: return '"' . $escaped . '"';
710: }
711: }
712:
713: if (!is_array($value)) {
714: $result = var_export($value, true);
715: if (is_float($value)) {
716: return Str::lower($result);
717: }
718: return $result;
719: }
720:
721: if (!$value) {
722: return '[]';
723: }
724:
725: $prefix = '[';
726: $suffix = ']';
727: $glue = $delimiter;
728:
729: if ($multiline) {
730: $suffix = $delimiter . $indent . $suffix;
731: $indent .= $tab;
732: $prefix .= $eol . $indent;
733: $glue .= $indent;
734: }
735:
736: $isList = Arr::isList($value);
737: if (!$isList) {
738: $isMixedList = false;
739: $keys = 0;
740: foreach (array_keys($value) as $key) {
741: if (!is_int($key)) {
742: continue;
743: }
744: if ($keys++ !== $key) {
745: $isMixedList = false;
746: break;
747: }
748: $isMixedList = true;
749: }
750: }
751: foreach ($value as $key => $value) {
752: $value = self::doCode($value, $delimiter, $arrow, $escapeCharacters, $escapeRegex, $search, $replace, $tab, $classes, $constants, $regex, $multiline, $eol, $indent);
753: if ($isList || ($isMixedList && is_int($key))) {
754: $values[] = $value;
755: continue;
756: }
757: $key = self::doCode($key, $delimiter, $arrow, $escapeCharacters, $escapeRegex, $search, $replace, $tab, $classes, $constants, $regex, $multiline, $eol, $indent);
758: $values[] = $key . $arrow . $value;
759: }
760:
761: return $prefix . implode($glue, $values) . $suffix;
762: }
763:
764: /**
765: * Get the end-of-line sequence used in a string
766: *
767: * Recognised line endings are LF (`"\n"`), CRLF (`"\r\n"`) and CR (`"\r"`).
768: *
769: * @return non-empty-string|null `null` if there are no recognised line
770: * breaks in `$string`.
771: *
772: * @see Filesystem::getEol()
773: * @see Str::setEol()
774: */
775: public static function eol(string $string): ?string
776: {
777: $lfPos = strpos($string, "\n");
778:
779: if ($lfPos === false) {
780: return strpos($string, "\r") === false
781: ? null
782: : "\r";
783: }
784:
785: if ($lfPos && $string[$lfPos - 1] === "\r") {
786: return "\r\n";
787: }
788:
789: return "\n";
790: }
791:
792: /**
793: * Get a deep copy of a value
794: *
795: * @template T
796: *
797: * @param T $value
798: * @param class-string[]|(Closure(object): (object|bool)) $skip A list of
799: * classes to skip, or a closure that returns:
800: * - `true` if the object should be skipped
801: * - `false` if the object should be copied normally, or
802: * - a copy of the object
803: * @param int-mask-of<Get::COPY_*> $flags
804: * @return T
805: */
806: public static function copy(
807: $value,
808: $skip = [],
809: int $flags = Get::COPY_SKIP_UNCLONEABLE | Get::COPY_BY_REFERENCE
810: ) {
811: return self::doCopy($value, $skip, $flags);
812: }
813:
814: /**
815: * @template T
816: *
817: * @param T $var
818: * @param class-string[]|(Closure(object): (object|bool)) $skip
819: * @param int-mask-of<Get::COPY_*> $flags
820: * @param array<int,object> $map
821: * @return T
822: */
823: private static function doCopy(
824: $var,
825: $skip,
826: int $flags,
827: array &$map = []
828: ) {
829: if (is_resource($var)) {
830: return $var;
831: }
832:
833: if (is_array($var)) {
834: foreach ($var as $key => $value) {
835: $array[$key] = self::doCopy($value, $skip, $flags, $map);
836: }
837: /** @var T */
838: return $array ?? [];
839: }
840:
841: if (!is_object($var) || $var instanceof UnitEnum) {
842: return $var;
843: }
844:
845: $id = spl_object_id($var);
846: if (isset($map[$id])) {
847: /** @var T */
848: return $map[$id];
849: }
850:
851: if ((
852: !($flags & self::COPY_CONTAINERS)
853: && $var instanceof PsrContainerInterface
854: ) || (
855: !($flags & self::COPY_SINGLETONS)
856: && $var instanceof SingletonInterface
857: )) {
858: $map[$id] = $var;
859: return $var;
860: }
861:
862: if ($skip instanceof Closure) {
863: if (($result = $skip($var)) !== false) {
864: if ($result === true) {
865: $map[$id] = $var;
866: return $var;
867: }
868: if (
869: !is_object($result)
870: || get_class($result) !== get_class($var)
871: ) {
872: throw new LogicException(sprintf(
873: '$skip returned %s (%s|bool expected)',
874: self::type($result),
875: get_class($var),
876: ));
877: }
878: $map[$id] = $result;
879: $id = spl_object_id($result);
880: $map[$id] = $result;
881: /** @var T */
882: return $result;
883: }
884: } else {
885: foreach ($skip as $class) {
886: if (is_a($var, $class)) {
887: $map[$id] = $var;
888: return $var;
889: }
890: }
891: }
892:
893: $_var = new ReflectionObject($var);
894:
895: if (!$_var->isCloneable()) {
896: if ($flags & self::COPY_SKIP_UNCLONEABLE) {
897: $map[$id] = $var;
898: return $var;
899: }
900:
901: throw new UncloneableObjectException(
902: sprintf('%s cannot be copied', $_var->getName())
903: );
904: }
905:
906: $clone = clone $var;
907: $map[$id] = $clone;
908: $id = spl_object_id($clone);
909: $map[$id] = $clone;
910:
911: if (
912: $flags & self::COPY_TRUST_CLONE
913: && $_var->hasMethod('__clone')
914: ) {
915: return $clone;
916: }
917:
918: if (
919: $clone instanceof DateTimeInterface
920: || $clone instanceof DateTimeZone
921: ) {
922: return $clone;
923: }
924:
925: $byRef = (bool) ($flags & self::COPY_BY_REFERENCE)
926: && !$_var->isInternal();
927: foreach (Reflect::getAllProperties($_var) as $property) {
928: if ($property->isStatic()) {
929: continue;
930: }
931:
932: $property->setAccessible(true);
933:
934: if (!$property->isInitialized($clone)) {
935: continue;
936: }
937:
938: $name = $property->getName();
939: $value = $property->getValue($clone);
940: $value = self::doCopy($value, $skip, $flags, $map);
941:
942: if (
943: !$byRef
944: || ($declaring = $property->getDeclaringClass())->isInternal()
945: ) {
946: $property->setValue($clone, $value);
947: continue;
948: }
949:
950: (function () use ($name, $value): void {
951: // @phpstan-ignore-next-line
952: $this->$name = &$value;
953: })->bindTo($clone, $declaring->getName())();
954: }
955:
956: return $clone;
957: }
958: }
959: