From 57e2b0d4718f94abf1f9c45df05569b9f7e1c802 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sat, 13 Sep 2025 18:10:35 +0300 Subject: [PATCH 1/2] todo --- src/Typed.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Typed.php b/src/Typed.php index e5b6ff1..2c8a031 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -408,6 +408,8 @@ protected static function &resolveKey(&$source, $key, bool &$isResolved = false) ) { $isResolved = true; + // todo: if the property is accessed via __get(), then PHP will generate a Notice: + // "Indirect modification of overloaded property ... has no effect" // @phpstan-ignore-next-line $value = &$source->{$key}; } From 2edd1fe1ac510cb480ee708e893f090644850f68 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sat, 13 Sep 2025 19:29:44 +0300 Subject: [PATCH 2/2] safer work with magic properties (__get() based) --- src/Typed.php | 140 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 17 deletions(-) diff --git a/src/Typed.php b/src/Typed.php index 2c8a031..9848b86 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -26,7 +26,7 @@ final class Typed */ public static function any($source, $keys = null, $default = null) { - return self::anyAsReference($source, $keys, $default); + return self::resolveAny($source, $keys, $default); } /** @@ -346,7 +346,7 @@ public static function setItem(&$target, $keys, $value): bool return false; } - $parentItemReference = &self::anyAsReference($target, $keys); + $parentItemReference = &self::resolveAnyAsReference($target, $keys); if (is_array($parentItemReference)) { $parentItemReference[$itemKey] = $value; @@ -354,20 +354,65 @@ public static function setItem(&$target, $keys, $value): bool return true; } + $stringItemKey = (string) $itemKey; + if (is_object($parentItemReference)) { - try { - // @phpstan-ignore-next-line - $parentItemReference->{$itemKey} = $value; - } catch (Throwable $e) { - return false; - } + return self::setObjectProperty($parentItemReference, $stringItemKey, $value); + } - return true; + // fallback: if the parent element wasn't resolved as a reference, + // e.g. somewhere in the key chain there is __get(), + // we try to resolve it as a plain value, + // and if the resolved plain value is an object, then we assign, since any object is a link itself + if (is_null($parentItemReference)) { + $parentItem = self::resolveAny($target, $keys); + + if (is_object($parentItem)) { + return self::setObjectProperty($parentItem, $stringItemKey, $value); + } } return false; } + /** + * @param mixed $value + */ + protected static function setObjectProperty(object $object, string $property, $value): bool + { + try { + // @phpstan-ignore-next-line + $object->{$property} = $value; + } catch (Throwable $e) { + return false; + } + + return true; + } + + /** + * @param mixed $source + * @param int|string|array|null $keys + * @param mixed $default + * + * @return mixed + */ + protected static function &resolveAnyAsReference(&$source, $keys = null, $default = null) + { + if (null === $keys) { + return $source; + } + + if ( + is_string($keys) || + is_numeric($keys) + ) { + $keys = explode('.', (string) $keys); + } + + return self::resolveKeysAsReference($source, $keys, $default); + } + /** * @param mixed $source * @param int|string|array|null $keys @@ -375,7 +420,7 @@ public static function setItem(&$target, $keys, $value): bool * * @return mixed */ - protected static function &anyAsReference(&$source, $keys = null, $default = null) + protected static function resolveAny($source, $keys = null, $default = null) { if (null === $keys) { return $source; @@ -397,21 +442,55 @@ protected static function &anyAsReference(&$source, $keys = null, $default = nul * * @return mixed */ - protected static function &resolveKey(&$source, $key, bool &$isResolved = false) + protected static function &resolveKeyAsReference(&$source, $key, bool &$isResolved = false) + { + $value = null; + $stringKey = (string) $key; + + if ( + is_object($source) && + // for taking a reference property must exist, case with __get() based won't work: + // "Indirect modification of overloaded property ... has no effect" + property_exists($source, $stringKey) + ) { + $isResolved = true; + + // @phpstan-ignore-next-line + $value = &$source->{$stringKey}; + } + + if ( + is_array($source) && + key_exists($key, $source) + ) { + $isResolved = true; + + $value = &$source[$key]; + } + + return $value; + } + + /** + * @param mixed $source + * @param int|string $key + * + * @return mixed + */ + protected static function resolveKey($source, $key, bool &$isResolved = false) { $value = null; if ( is_object($source) && + // using isset() we support __get() based properties // @phpstan-ignore-next-line isset($source->{$key}) ) { $isResolved = true; - // todo: if the property is accessed via __get(), then PHP will generate a Notice: - // "Indirect modification of overloaded property ... has no effect" // @phpstan-ignore-next-line - $value = &$source->{$key}; + $value = $source->{$key}; } if ( @@ -420,7 +499,7 @@ protected static function &resolveKey(&$source, $key, bool &$isResolved = false) ) { $isResolved = true; - $value = &$source[$key]; + $value = $source[$key]; } return $value; @@ -433,14 +512,14 @@ protected static function &resolveKey(&$source, $key, bool &$isResolved = false) * * @return mixed */ - protected static function &resolveKeys(&$source, array $keys, $default) + protected static function &resolveKeysAsReference(&$source, array $keys, $default) { $origin = &$source; foreach ($keys as $key) { $isResolved = false; - $value = &self::resolveKey($origin, $key, $isResolved); + $value = &self::resolveKeyAsReference($origin, $key, $isResolved); if ($isResolved) { $origin = &$value; @@ -452,4 +531,31 @@ protected static function &resolveKeys(&$source, array $keys, $default) return $origin; } + + /** + * @param mixed $source + * @param array $keys + * @param mixed $default + * + * @return mixed + */ + protected static function resolveKeys($source, array $keys, $default) + { + $origin = $source; + + foreach ($keys as $key) { + $isResolved = false; + + $value = self::resolveKey($origin, $key, $isResolved); + + if ($isResolved) { + $origin = $value; + continue; + } + + return $default; + } + + return $origin; + } }