|
4 | 4 |
|
5 | 5 | use Lkrms\Concept\Utility; |
6 | 6 | use Lkrms\Contract\Arrayable; |
| 7 | +use Lkrms\Contract\IServiceShared; |
| 8 | +use Lkrms\Contract\IServiceSingleton; |
| 9 | +use Lkrms\Exception\UncloneableObjectException; |
| 10 | +use Lkrms\Utility\Catalog\CopyFlag; |
| 11 | +use Psr\Container\ContainerInterface; |
| 12 | +use DateTimeInterface; |
| 13 | +use DateTimeZone; |
7 | 14 | use ReflectionClass; |
| 15 | +use ReflectionObject; |
| 16 | +use UnitEnum; |
8 | 17 |
|
9 | 18 | /** |
10 | 19 | * Get values from other values |
@@ -82,6 +91,7 @@ public static function type($value): string |
82 | 91 | ? 'class@anonymous' |
83 | 92 | : get_class($value); |
84 | 93 | } |
| 94 | + |
85 | 95 | if (is_resource($value)) { |
86 | 96 | return sprintf('resource (%s)', get_resource_type($value)); |
87 | 97 | } |
@@ -109,15 +119,155 @@ public static function type($value): string |
109 | 119 | public static function eol(string $string): ?string |
110 | 120 | { |
111 | 121 | $lfPos = strpos($string, "\n"); |
| 122 | + |
112 | 123 | if ($lfPos === false) { |
113 | 124 | return strpos($string, "\r") === false |
114 | 125 | ? null |
115 | 126 | : "\r"; |
116 | 127 | } |
| 128 | + |
117 | 129 | if ($lfPos && $string[$lfPos - 1] === "\r") { |
118 | 130 | return "\r\n"; |
119 | 131 | } |
120 | 132 |
|
121 | 133 | return "\n"; |
122 | 134 | } |
| 135 | + |
| 136 | + /** |
| 137 | + * Get a deep copy of an object |
| 138 | + * |
| 139 | + * @template T of object |
| 140 | + * |
| 141 | + * @param T $object |
| 142 | + * @param class-string[] $skip |
| 143 | + * @param int-mask-of<CopyFlag::*> $flags |
| 144 | + * @return T |
| 145 | + */ |
| 146 | + public static function copy( |
| 147 | + object $object, |
| 148 | + array $skip = [], |
| 149 | + int $flags = CopyFlag::SKIP_UNCLONEABLE | CopyFlag::ASSIGN_PROPERTIES_BY_REFERENCE |
| 150 | + ): object { |
| 151 | + return self::doCopy($object, $skip, $flags); |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * @template T of resource|mixed[]|object|int|float|string|bool|null |
| 156 | + * |
| 157 | + * @param T $var |
| 158 | + * @param class-string[] $skip |
| 159 | + * @param int-mask-of<CopyFlag::*> $flags |
| 160 | + * @param array<int,object> $map |
| 161 | + * @return T |
| 162 | + */ |
| 163 | + private static function doCopy( |
| 164 | + $var, |
| 165 | + array $skip, |
| 166 | + int $flags, |
| 167 | + array &$map = [] |
| 168 | + ) { |
| 169 | + if (is_resource($var)) { |
| 170 | + return $var; |
| 171 | + } |
| 172 | + |
| 173 | + if (is_array($var)) { |
| 174 | + /** @var array<resource|mixed[]|object|int|float|string|bool|null> $var */ |
| 175 | + foreach ($var as $key => $value) { |
| 176 | + $array[$key] = self::doCopy($value, $skip, $flags, $map); |
| 177 | + } |
| 178 | + return $array ?? []; |
| 179 | + } |
| 180 | + |
| 181 | + if (!is_object($var) || $var instanceof UnitEnum) { |
| 182 | + return $var; |
| 183 | + } |
| 184 | + |
| 185 | + $id = spl_object_id($var); |
| 186 | + if (isset($map[$id])) { |
| 187 | + return $map[$id]; |
| 188 | + } |
| 189 | + |
| 190 | + if (( |
| 191 | + !($flags & CopyFlag::COPY_CONTAINERS) && |
| 192 | + $var instanceof ContainerInterface |
| 193 | + ) || ( |
| 194 | + !($flags & CopyFlag::COPY_SINGLETONS) && ( |
| 195 | + $var instanceof IServiceSingleton || |
| 196 | + $var instanceof IServiceShared |
| 197 | + ) |
| 198 | + )) { |
| 199 | + $map[$id] = $var; |
| 200 | + return $var; |
| 201 | + } |
| 202 | + |
| 203 | + foreach ($skip as $class) { |
| 204 | + if (is_a($var, $class)) { |
| 205 | + $map[$id] = $var; |
| 206 | + return $var; |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + $_var = new ReflectionObject($var); |
| 211 | + |
| 212 | + if (!$_var->isCloneable()) { |
| 213 | + if ($flags & CopyFlag::SKIP_UNCLONEABLE) { |
| 214 | + $map[$id] = $var; |
| 215 | + return $var; |
| 216 | + } |
| 217 | + |
| 218 | + throw new UncloneableObjectException( |
| 219 | + sprintf('%s cannot be copied', $_var->getName()) |
| 220 | + ); |
| 221 | + } |
| 222 | + |
| 223 | + $clone = clone $var; |
| 224 | + $map[$id] = $clone; |
| 225 | + $id = spl_object_id($clone); |
| 226 | + $map[$id] = $clone; |
| 227 | + |
| 228 | + if ( |
| 229 | + $flags & CopyFlag::TRUST_CLONE_METHODS && |
| 230 | + $_var->hasMethod('__clone') |
| 231 | + ) { |
| 232 | + return $clone; |
| 233 | + } |
| 234 | + |
| 235 | + if ( |
| 236 | + $clone instanceof DateTimeInterface || |
| 237 | + $clone instanceof DateTimeZone |
| 238 | + ) { |
| 239 | + return $clone; |
| 240 | + } |
| 241 | + |
| 242 | + $byRef = (bool) ($flags & CopyFlag::ASSIGN_PROPERTIES_BY_REFERENCE) && |
| 243 | + !$_var->isInternal(); |
| 244 | + foreach (Reflect::getAllProperties($_var) as $property) { |
| 245 | + if ($property->isStatic()) { |
| 246 | + continue; |
| 247 | + } |
| 248 | + |
| 249 | + $property->setAccessible(true); |
| 250 | + |
| 251 | + if (!$property->isInitialized($clone)) { |
| 252 | + continue; |
| 253 | + } |
| 254 | + |
| 255 | + $name = $property->getName(); |
| 256 | + /** @var resource|mixed[]|object|int|float|string|bool|null */ |
| 257 | + $value = $property->getValue($clone); |
| 258 | + $value = self::doCopy($value, $skip, $flags, $map); |
| 259 | + |
| 260 | + if (!$byRef) { |
| 261 | + $property->setValue($clone, $value); |
| 262 | + continue; |
| 263 | + } |
| 264 | + |
| 265 | + (function () use ($name, $value): void { |
| 266 | + // @phpstan-ignore-next-line |
| 267 | + $this->$name = &$value; |
| 268 | + })->bindTo($clone, $property->getDeclaringClass()->getName())(); |
| 269 | + } |
| 270 | + |
| 271 | + return $clone; |
| 272 | + } |
123 | 273 | } |
0 commit comments