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