From 812803b67c697322658e74116b179b3d60a9ae1d Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sat, 21 Dec 2024 00:22:11 +0200 Subject: [PATCH 1/9] readme --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9b0a428..3b75062 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,10 @@ function getTypedStringFromMixedVariable($mixedData): string The code like `string($array, 'key')` resembles `(string)$array['key']` while being safe and smart — it even handles nested keys. -> In case now you're thinking: "Hold on guys, but this code won't work! Are your using type names as function names?" -> +> In case now you're thinking: "Hold on guys, but this code won't work! Are your using type names as function names?" +> > Our answer is: "Yes! And actually it isn't prohibited." -> +> > See the explanation in the special section - [5. Note about the function names](#5-note-about-the-function-names) Backing to the package. Want to provide a default value when the key is missing? Here you go: @@ -179,12 +179,22 @@ Think it’s prohibited? Not quite! While certain names are restricted for class are not: > “These names cannot be used to name a **class, interface, or -> trait**” - [PHP Manual: Reserved Other Reserved Words](https://www.php.net/manual/en/reserved.other-reserved-words.php) +> trait +**” - [PHP Manual: Reserved Other Reserved Words](https://www.php.net/manual/en/reserved.other-reserved-words.php) This means you we can have things like `string($array, 'key')`, which resembles `(string)$array['key']` while being safer and smarter — it even handles nested keys. +By the way, importing these functions does not interfere with native type casting in PHP. So, while practically +unnecessary, the following construction will still work: + +```php +use function WPLake\Typed\string; + +echo (string)string('hello'); +``` + Note: Unlike all the other types, the `array` keyword falls under a [different category](https://www.php.net/manual/en/reserved.keywords.php), which also prohibits its usage for function names. That's why in this case we used the `arr` name instead. From 94f661a43ae0fc71105077a6f5c52061b5f9cc19 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sat, 21 Dec 2024 00:50:42 +0200 Subject: [PATCH 2/9] readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b75062..146f2a4 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,9 @@ Functions for the following types are present: * `int` * `float` * `bool` -* `array` * `object` * `dateTime` +* `arr` (stands for `array`, because it's a keyword) * `any` (allows to use short dot-keys usage for unknowns) Additionally: @@ -238,7 +238,7 @@ $number = true === is_numeric($number)? 10; // Typed: -$number = Typed::int($data, 'meta.number', 10); +$number = int($data, 'meta.number', 10); ``` Additionally, with Null Coalescing Operator and a custom default value, you have to repeat yourself. From d20cd67bffd20a1782711f4b45df4b315311e2f2 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sat, 21 Dec 2024 16:05:45 +0200 Subject: [PATCH 3/9] readme --- README.md | 115 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 146f2a4..a2cad76 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,19 @@ allowing you to fetch and cast values with concise, readable code. **Example: Plain PHP** ```php -function getTypedIntFromArray(array $array): int +function getUserAge(array $userData): int { - return true === isset($array['meta']['number']) && - true === is_numeric($array['meta']['number']) - ? (int)$array['meta']['number'] - : 0; + return true === isset($userData['meta']['age']) && + true === is_numeric($userData['meta']['age']) + ? (int)$userData['meta']['age'] + : 0; } -function getTypedStringFromMixedVariable($mixed): string +function upgradeUserById($mixedUserId): void { - return true === is_string($mixed) || - true === is_numeric($mixed) - ? (string)$mixed + $userId = true === is_string($mixedUserId) || + true === is_numeric($mixedUserId) + ? (string)$mixedUserId : ''; } ``` @@ -39,19 +39,19 @@ function getTypedStringFromMixedVariable($mixed): string use function WPLake\Typed\int; use function WPLake\Typed\string; -function getTypedIntFromArray(array $data): int +function getUserAge(array $userData): int { - return int($data, 'meta.number'); + return int($userData, 'meta.age'); } -function getTypedStringFromMixedVariable($mixedData): string +function upgradeUserById($mixedUserId): void { - return string($mixedData); + $userId = string($mixedUserId); } ``` The code like `string($array, 'key')` resembles `(string)$array['key']` while being -safe and smart — it even handles nested keys. +safe and smart — it even handles nested keys and default values. > In case now you're thinking: "Hold on guys, but this code won't work! Are your using type names as function names?" > @@ -65,7 +65,7 @@ Backing to the package. Want to provide a default value when the key is missing? string($data, 'some.key', 'Default Value'); ``` -Don't like functions? The same functions set is available as static methods of the `Typed` class: +Can't stand functions? The same functions set is available as static methods of the `Typed` class: ```php use WPLake\Typed\Typed; @@ -89,33 +89,14 @@ Usage: use function WPLake\Typed\string; use WPLake\Typed\Typed; -$string = string($array, 'first.second','default value'); +$string = string($array, 'first.second'); // alternatively: -$string = Typed::string($array, 'first.second','default value'); +$string = Typed::string($array, 'first.second'); +// custom fallback: +$string = string($array, 'first.second', 'custom default'); ``` -## 3. Supported types - -Functions for the following types are present: - -* `string` -* `int` -* `float` -* `bool` -* `object` -* `dateTime` -* `arr` (stands for `array`, because it's a keyword) -* `any` (allows to use short dot-keys usage for unknowns) - -Additionally: - -* `boolExtended` (`true`,`1`,`"1"`, `"on"` are treated as true, `false`,`0`,`"0"`, `"off"` as false) -* `stringExtended` (supports objects with `__toString`) - -For optional cases, each item has an `OrNull` method option (e.g. `stringOrNull`, `intOrNull`, and so on), -which returns `null` if the key doesn’t exist. - -## 4. How It Works +## 3. How It Works The logic of all casting methods follows this simple principle: @@ -135,42 +116,81 @@ function string($source, $keys = null, string $default = ''): string; Usage Scenarios: -1. Extract a string from a mixed variable (returning the default if absent or of an incompatible type) +1. Extract a string from a mixed variable + +By default, returning an empty string if the variable can't be converted to a string: ```php $userName = string($unknownVar); +// you can customize the fallback: +$userName = string($unknownVar, null, 'custom fallback value'); ``` -2. Retrieve a string from an array, including nested structures (with dot notation or as an array). +2. Retrieve a string from an array + +Including nested structures (with dot notation or as an array): ```php $userName = string($array, 'user.name'); // alternatively: $userName = string($array, ['user','name',]); +// custom fallback: +$userName = string($array, 'user.name', 'Guest'); ``` -3. Access a string from an object. It also supports the nested properties. +3. Access a string from an object + +Including nested properties: ```php $userName = string($companyObject, 'user.name'); // alternatively: $userName = string($companyObject, ['user', 'name',]); +// custom fallback: +$userName = string($companyObject, 'user.name', 'Guest'); ``` -4. Work with mixed structures (e.g., `object->arrayProperty['key']->anotherProperty or ['key' => $object]`). +4. Work with mixed structures + +(e.g., `object->arrayProperty['key']->anotherProperty` or `['key' => $object]`) ```php $userName = string($companyObject,'users.john.name'); // alternatively: $userName = string($companyObject,['users','john','name',]); +// custom fallback: +$userName = string($companyObject, 'users.john.name', 'Guest'); ``` -In all the cases, you can pass a default value as the third argument, e.g.: +In all the cases, the fallback value is the 'empty' value for the specific type (e.g. `0`, `false`, `""`, and so on), +but you +can pass a custom default value as the third argument: ```php $userName = string($companyObject,'users.john.name', 'Guest'); ``` +## 4. Supported types + +Functions for the following types are present: + +* `string` +* `int` +* `float` +* `bool` +* `object` +* `dateTime` +* `arr` (stands for `array`, because it's a keyword) +* `any` (allows to use short dot-keys usage for unknowns) + +Additionally: + +* `boolExtended` (`true`,`1`,`"1"`, `"on"` are treated as true, `false`,`0`,`"0"`, `"off"` as false) +* `stringExtended` (supports objects with `__toString`) + +For optional cases, each item has an `OrNull` method option (e.g. `stringOrNull`, `intOrNull`, and so on), +which returns `null` if the key doesn’t exist. + ## 5. Note about the function names Surprisingly, PHP allows functions to use the same names as variable types. @@ -252,11 +272,10 @@ This package simplifies handling such scenarios. Any seasoned PHP developer knows the pain of type-casting when working with environments outside of frameworks, e.g. in WordPress. -### 6.4) Is the dot syntax in keys inspired by Laravel Collections? +### 6.4) Is the dot syntax in keys inspired by Laravel Helpers? -Yes, the dot syntax is inspired by [Laravel Collections](https://laravel.com/docs/11.x/collections) and similar -solutions. It provides an intuitive way to access -nested data structures. +Yes, the dot syntax is inspired by [Laravel’s Arr::get](https://laravel.com/docs/11.x/helpers) and similar +solutions. It provides an intuitive way to access nested data structures. ### 6.5) Why not just use Laravel Collections? From f6f8c0aba7379b5aa1629762635be34c0b590f9b Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sat, 21 Dec 2024 16:08:18 +0200 Subject: [PATCH 4/9] readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a2cad76..d86f64e 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ function string($source, $keys = null, string $default = ''): string; Usage Scenarios: -1. Extract a string from a mixed variable +**1. Extract a string from a mixed variable** By default, returning an empty string if the variable can't be converted to a string: @@ -126,7 +126,7 @@ $userName = string($unknownVar); $userName = string($unknownVar, null, 'custom fallback value'); ``` -2. Retrieve a string from an array +**2. Retrieve a string from an array** Including nested structures (with dot notation or as an array): @@ -138,7 +138,7 @@ $userName = string($array, ['user','name',]); $userName = string($array, 'user.name', 'Guest'); ``` -3. Access a string from an object +**3. Access a string from an object** Including nested properties: @@ -150,7 +150,7 @@ $userName = string($companyObject, ['user', 'name',]); $userName = string($companyObject, 'user.name', 'Guest'); ``` -4. Work with mixed structures +**4. Work with mixed structures** (e.g., `object->arrayProperty['key']->anotherProperty` or `['key' => $object]`) @@ -188,8 +188,8 @@ Additionally: * `boolExtended` (`true`,`1`,`"1"`, `"on"` are treated as true, `false`,`0`,`"0"`, `"off"` as false) * `stringExtended` (supports objects with `__toString`) -For optional cases, each item has an `OrNull` method option (e.g. `stringOrNull`, `intOrNull`, and so on), -which returns `null` if the key doesn’t exist. +For optional cases, when you need to apply the logic only when the item is present, each function has an `OrNull` +variation (e.g. `stringOrNull`, `intOrNull`, and so on), which returns `null` if the key doesn’t exist. ## 5. Note about the function names From cccabf1dc42c01673f5175750cbdb64b70288051 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Sun, 22 Dec 2024 10:32:14 +0200 Subject: [PATCH 5/9] phpstan rules --- README.md | 16 ++++----- phpcs.xml | 12 +++++++ phpstan.neon | 2 ++ src/Typed.php | 97 +++++++++++++++++++++++++++------------------------ 4 files changed, 74 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d86f64e..915cdeb 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@ allowing you to fetch and cast values with concise, readable code. ```php function getUserAge(array $userData): int { - return true === isset($userData['meta']['age']) && - true === is_numeric($userData['meta']['age']) - ? (int)$userData['meta']['age'] + return isset($userData['meta']['age']) && + is_numeric($userData['meta']['age']) + ? (int) $userData['meta']['age'] : 0; } function upgradeUserById($mixedUserId): void { - $userId = true === is_string($mixedUserId) || - true === is_numeric($mixedUserId) - ? (string)$mixedUserId + $userId = is_string($mixedUserId) || + is_numeric($mixedUserId) + ? (string) $mixedUserId : ''; } ``` @@ -253,8 +253,8 @@ While the Null Coalescing Operator (`??`) is useful, it doesn’t address type c ```php // Plain PHP: $number = $data['meta']['number']?? 10; -$number = true === is_numeric($number)? -(int)$number: +$number = is_numeric($number)? +(int) $number: 10; // Typed: diff --git a/phpcs.xml b/phpcs.xml index 1ea07c8..1d51c12 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -4,6 +4,18 @@ + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon index 22bf6a4..d60a117 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,5 @@ parameters: level: 9 paths: - ./src +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon \ No newline at end of file diff --git a/src/Typed.php b/src/Typed.php index 7d5115f..bfb82b3 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -29,8 +29,11 @@ public static function any($source, $keys = null, $default = null) return $source; } - if (false === is_array($keys)) { - $keys = explode('.', (string)$keys); + if ( + is_string($keys) || + is_numeric($keys) + ) { + $keys = explode('.', (string) $keys); } return self::resolveKeys($source, $keys, $default); @@ -44,9 +47,9 @@ public static function string($source, $keys = null, string $default = ''): stri { $value = self::any($source, $keys, $default); - return true === is_string($value) || - true === is_numeric($value) ? - (string)$value : + return is_string($value) || + is_numeric($value) ? + (string) $value : $default; } @@ -59,17 +62,17 @@ public static function stringExtended($source, $keys = null, string $default = ' $value = self::any($source, $keys, $default); if ( - true === is_string($value) || - true === is_numeric($value) + is_string($value) || + is_numeric($value) ) { - return (string)$value; + return (string) $value; } if ( - true === is_object($value) && - true === method_exists($value, '__toString') + is_object($value) && + method_exists($value, '__toString') ) { - return (string)$value; + return (string) $value; } return $default; @@ -83,9 +86,9 @@ public static function stringOrNull($source, $keys = null): ?string { $value = self::any($source, $keys); - return true === is_string($value) || - true === is_numeric($value) ? - (string)$value : + return is_string($value) || + is_numeric($value) ? + (string) $value : null; } @@ -98,17 +101,17 @@ public static function stringExtendedOrNull($source, $keys = null): ?string $value = self::any($source, $keys); if ( - true === is_string($value) || - true === is_numeric($value) + is_string($value) || + is_numeric($value) ) { - return (string)$value; + return (string) $value; } if ( - true === is_object($value) && - true === method_exists($value, '__toString') + is_object($value) && + method_exists($value, '__toString') ) { - return (string)$value; + return (string) $value; } return null; @@ -122,8 +125,8 @@ public static function int($source, $keys = null, int $default = 0): int { $value = self::any($source, $keys, $default); - return true === is_numeric($value) ? - (int)$value : + return is_numeric($value) ? + (int) $value : $default; } @@ -135,8 +138,8 @@ public static function intOrNull($source, $keys = null): ?int { $value = self::any($source, $keys); - return true === is_numeric($value) ? - (int)$value : + return is_numeric($value) ? + (int) $value : null; } @@ -148,8 +151,8 @@ public static function float($source, $keys = null, float $default = 0.0): float { $value = self::any($source, $keys, $default); - return true === is_numeric($value) ? - (float)$value : + return is_numeric($value) ? + (float) $value : $default; } @@ -161,8 +164,8 @@ public static function floatOrNull($source, $keys = null): ?float { $value = self::any($source, $keys); - return true === is_numeric($value) ? - (float)$value : + return is_numeric($value) ? + (float) $value : null; } @@ -174,7 +177,7 @@ public static function bool($source, $keys = null, bool $default = false): bool { $value = self::any($source, $keys, $default); - return true === is_bool($value) ? + return is_bool($value) ? $value : $default; } @@ -187,7 +190,7 @@ public static function boolOrNull($source, $keys = null): ?bool { $value = self::any($source, $keys); - return true === is_bool($value) ? + return is_bool($value) ? $value : null; } @@ -207,11 +210,11 @@ public static function boolExtended( ): bool { $value = self::any($source, $keys, $default); - if (true === in_array($value, $positive, true)) { + if (in_array($value, $positive, true)) { return true; } - if (true === in_array($value, $negative, true)) { + if (in_array($value, $negative, true)) { return false; } @@ -232,11 +235,11 @@ public static function boolExtendedOrNull( ): ?bool { $value = self::any($source, $keys); - if (true === in_array($value, $positive, true)) { + if (in_array($value, $positive, true)) { return true; } - if (true === in_array($value, $negative, true)) { + if (in_array($value, $negative, true)) { return false; } @@ -254,7 +257,7 @@ public static function array($source, $keys = null, array $default = []): array { $value = self::any($source, $keys, $default); - return true === is_array($value) ? + return is_array($value) ? $value : $default; } @@ -269,7 +272,7 @@ public static function arrayOrNull($source, $keys = null): ?array { $value = self::any($source, $keys); - return true === is_array($value) ? + return is_array($value) ? $value : null; } @@ -286,7 +289,7 @@ public static function object($source, $keys = null, ?object $default = null): o $value = self::any($source, $keys, $default); - return true === is_object($value) ? + return is_object($value) ? $value : $default; } @@ -299,7 +302,7 @@ public static function objectOrNull($source, $keys = null): ?object { $value = self::any($source, $keys); - return true === is_object($value) ? + return is_object($value) ? $value : null; } @@ -316,7 +319,7 @@ public static function dateTime($source, $keys = null, ?DateTime $default = null $value = self::object($source, $keys, $default); - return true === ($value instanceof DateTime) ? + return $value instanceof DateTime ? $value : $default; } @@ -329,7 +332,7 @@ public static function dateTimeOrNull($source, $keys = null): ?DateTime { $value = self::any($source, $keys); - return true === ($value instanceof DateTime) ? + return $value instanceof DateTime ? $value : null; } @@ -342,18 +345,22 @@ public static function dateTimeOrNull($source, $keys = null): ?DateTime protected static function resolveKey($source, $key, &$value): bool { if ( - true === is_object($source) && - true === isset($source->{$key}) + is_object($source) && + // @phpstan-ignore-next-line + isset($source->{$key}) ) { + // @phpstan-ignore-next-line $value = $source->{$key}; + return true; } if ( - true === is_array($source) && - true === isset($source[$key]) + is_array($source) && + key_exists($key, $source) ) { $value = $source[$key]; + return true; } @@ -372,7 +379,7 @@ protected static function resolveKeys($source, array $keys, $default) foreach ($keys as $key) { $value = null; - if (true === self::resolveKey($source, $key, $value)) { + if (self::resolveKey($source, $key, $value)) { $source = $value; continue; } From 8205b42c8ca156672a240d781de7f6d1e9b8cd5c Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Wed, 16 Apr 2025 16:55:01 +0300 Subject: [PATCH 6/9] feature: setItem function --- src/Typed.php | 123 ++++-- src/functions.php | 10 + tests/Array/GetFromArrayTest.php | 49 +++ tests/Array/SetToArrayTest.php | 76 ++++ tests/BorderCaseTest.php | 45 +++ .../GetFromDynamicObjectPropertyTest.php | 62 +++ tests/FunctionTest.php | 13 + .../GetFromMixedStructureTest.php | 61 +++ .../GetFromTypedObjectPropertyTest.php | 119 ++++++ tests/Unit/TypedTest.php | 382 ------------------ 10 files changed, 528 insertions(+), 412 deletions(-) create mode 100644 tests/Array/GetFromArrayTest.php create mode 100644 tests/Array/SetToArrayTest.php create mode 100644 tests/BorderCaseTest.php create mode 100644 tests/DynamicObject/GetFromDynamicObjectPropertyTest.php create mode 100644 tests/FunctionTest.php create mode 100644 tests/MixedStructure/GetFromMixedStructureTest.php create mode 100644 tests/TypedObject/GetFromTypedObjectPropertyTest.php delete mode 100644 tests/Unit/TypedTest.php diff --git a/src/Typed.php b/src/Typed.php index bfb82b3..0c49a7b 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -6,6 +6,7 @@ use DateTime; use stdClass; +use Throwable; /** * This class is marked as final to prevent extension. @@ -25,18 +26,7 @@ final class Typed */ public static function any($source, $keys = null, $default = null) { - if (null === $keys) { - return $source; - } - - if ( - is_string($keys) || - is_numeric($keys) - ) { - $keys = explode('.', (string) $keys); - } - - return self::resolveKeys($source, $keys, $default); + return self::anyAsReference($source, $keys, $default); } /** @@ -338,33 +328,102 @@ public static function dateTimeOrNull($source, $keys = null): ?DateTime } /** - * @param mixed $source - * @param int|string $key + * @param mixed $target + * @param int|string|array $keys * @param mixed $value */ - protected static function resolveKey($source, $key, &$value): bool + public static function setItem(&$target, $keys, $value): bool + { + $keys = is_numeric($keys) || + is_string($keys) ? + explode('.', (string) $keys) : + $keys; + + $itemKey = array_pop($keys); + + // at least one key must be defined. + if (null === $itemKey) { + return false; + } + + $parentItemReference = &self::anyAsReference($target, $keys); + + if (is_array($parentItemReference)) { + $parentItemReference[$itemKey] = $value; + + return true; + } + + if (is_object($parentItemReference)) { + try { + // @phpstan-ignore-next-line + $parentItemReference->{$itemKey} = $value; + } catch (Throwable $e) { + return false; + } + + + return true; + } + + return false; + } + + /** + * @param mixed $source + * @param int|string|array|null $keys + * @param mixed $default + * + * @return mixed + */ + protected static function &anyAsReference(&$source, $keys = null, $default = null) { + if (null === $keys) { + return $source; + } + if ( - is_object($source) && - // @phpstan-ignore-next-line - isset($source->{$key}) + is_string($keys) || + is_numeric($keys) ) { + $keys = explode('.', (string) $keys); + } + + return self::resolveKeys($source, $keys, $default); + } + + /** + * @param mixed $source + * @param int|string $key + * + * @return mixed + */ + protected static function &resolveKey(&$source, $key, bool &$isResolved = false) + { + $value = null; + + if ( + is_object($source) && // @phpstan-ignore-next-line - $value = $source->{$key}; + isset($source->{$key}) + ) { + $isResolved = true; - return true; + // @phpstan-ignore-next-line + $value = &$source->{$key}; } if ( - is_array($source) && - key_exists($key, $source) + is_array($source) && + key_exists($key, $source) ) { - $value = $source[$key]; + $isResolved = true; - return true; + // @phpstan-ignore-next-line + $value = &$source[$key]; } - return false; + return $value; } /** @@ -374,19 +433,23 @@ protected static function resolveKey($source, $key, &$value): bool * * @return mixed */ - protected static function resolveKeys($source, array $keys, $default) + protected static function &resolveKeys(&$source, array $keys, $default) { + $origin = &$source; + foreach ($keys as $key) { - $value = null; + $isResolved = false; + + $value = &self::resolveKey($origin, $key, $isResolved); - if (self::resolveKey($source, $key, $value)) { - $source = $value; + if ($isResolved) { + $origin = &$value; continue; } return $default; } - return $source; + return $origin; } } diff --git a/src/functions.php b/src/functions.php index d4a4020..53e60ab 100644 --- a/src/functions.php +++ b/src/functions.php @@ -208,3 +208,13 @@ function dateTimeOrNull($source, $keys = null): ?DateTime { return Typed::dateTimeOrNull($source, $keys); } + +/** + * @param mixed $target + * @param int|string|array $keys + * @param mixed $value + */ +function setItem(&$target, $keys, $value): bool +{ + return Typed::setItem($target, $keys, $value); +} diff --git a/tests/Array/GetFromArrayTest.php b/tests/Array/GetFromArrayTest.php new file mode 100644 index 0000000..1b98a64 --- /dev/null +++ b/tests/Array/GetFromArrayTest.php @@ -0,0 +1,49 @@ + 'value'], 'key', 'default'); + + expect($result)->toBe('value'); +}); + +it('returns the default value for a missing key in an array', function () { + $data = ['key' => 'value']; + + $result = Typed::any($data, 'missing', 'default'); + + expect($result)->toBe('default'); +}); + +it('works with an array and inner keys when passed as a string', function () { + $data = ['level1' => ['level2' => ['key' => 'value']]]; + + $result = Typed::any($data, 'level1.level2.key', 'default'); + + expect($result)->toBe('value'); +}); + +it('works with an array and inner keys when passed as an array', function () { + $data = ['level1' => ['level2' => ['key' => 'value']]]; + + $result = Typed::any($data, ['level1', 'level2', 'key'], 'default'); + + expect($result)->toBe('value'); +}); + +it('returns the default value for a missing inner key in an array when passed as a string', function () { + $data = ['level1' => ['level2' => []]]; + + $result = Typed::any($data, 'level1.level2.missing', 'default'); + + expect($result)->toBe('default'); +}); + +it('returns the default value for a missing inner key in an array when passed as an array', function () { + $data = ['level1' => ['level2' => []]]; + + $result = Typed::any($data, ['level1', 'level2', 'missing'], 'default'); + + expect($result)->toBe('default'); +}); diff --git a/tests/Array/SetToArrayTest.php b/tests/Array/SetToArrayTest.php new file mode 100644 index 0000000..a534c85 --- /dev/null +++ b/tests/Array/SetToArrayTest.php @@ -0,0 +1,76 @@ + [ + 'second' => [], + ]]; + + $isSet = setItem($target, ['first', 'second', 'third'], 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toBe([ + 'first' => [ + 'second' => [ + 'third' => 'value' + ] + ] + ]); +}); + +it('sets to array when string keys are valid', function () { + $target = ['first' => [ + 'second' => [], + ]]; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toBe([ + 'first' => [ + 'second' => [ + 'third' => 'value' + ] + ] + ]); +}); + +it('skips set to array when target is not an array', function () { + $target = 'string'; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toBe('string'); +}); + +it('skips set to array when array keys are invalid', function () { + $target = ['first' => [ + 'second' => [], + ]]; + + $isSet = setItem($target, ['first', 'another', 'third'], 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toBe([ + 'first' => [ + 'second' => [], + ] + ]); +}); + +it('skips set to array when string keys are invalid', function () { + $target = ['first' => [ + 'second' => [], + ]]; + + $isSet = setItem($target, 'first.another.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toBe([ + 'first' => [ + 'second' => [] + ] + ]); +}); diff --git a/tests/BorderCaseTest.php b/tests/BorderCaseTest.php new file mode 100644 index 0000000..70a2f38 --- /dev/null +++ b/tests/BorderCaseTest.php @@ -0,0 +1,45 @@ +toBe('origin'); +}); + +it('returns null when source is not iterable', function () { + $result = Typed::any('origin', 'key'); + + expect($result)->toBeNull(); +}); + +it('returns default when source is not iterable', function () { + $result = Typed::any('origin', 'key', 'default'); + + expect($result)->toBe('default'); +}); + +it('works with mixed keys passed as string', function () { + $result = Typed::any([ + 0 => [ + 'key' => [ + '0' => 'value', + ], + ], + ], '0.key.0'); + + expect($result)->toBe('value'); +}); + +it('works with mixed keys passed as array', function () { + $result = Typed::any([ + 0 => [ + 'key' => [ + '0' => 'value', + ], + ], + ], [0, 'key', '0']); + + expect($result)->toBe('value'); +}); diff --git a/tests/DynamicObject/GetFromDynamicObjectPropertyTest.php b/tests/DynamicObject/GetFromDynamicObjectPropertyTest.php new file mode 100644 index 0000000..728ac36 --- /dev/null +++ b/tests/DynamicObject/GetFromDynamicObjectPropertyTest.php @@ -0,0 +1,62 @@ +key = 'value'; + + $result = Typed::any($object, 'key', 'default'); + + expect($result)->toBe('value'); +}); + +it('returns default for missing key in object with dynamic properties', function (): void { + $object = new stdClass(); + + $result = Typed::any($object, 'missing', 'default'); + + expect($result)->toBe('default'); +}); + +it('works with object with dynamic inner properties when passed as string', function (): void { + $object = new stdClass(); + $object->level1 = new stdClass(); + $object->level1->level2 = new stdClass(); + $object->level1->level2->key = 'value'; + + $result = Typed::any($object, 'level1.level2.key', 'default'); + + expect($result)->toBe('value'); +}); + +it('works with object with dynamic inner properties when passed as array', function (): void { + $object = new stdClass(); + $object->level1 = new stdClass(); + $object->level1->level2 = new stdClass(); + $object->level1->level2->key = 'value'; + + $result = Typed::any($object, ['level1', 'level2', 'key'], 'default'); + + expect($result)->toBe('value'); +}); + +it('returns default for missing dynamic inner properties in object when passed as string', function (): void { + $object = new stdClass(); + $object->level1 = new stdClass(); + $object->level1->level2 = new stdClass(); + + $result = Typed::any($object, 'level1.level2.missing', 'default'); + + expect($result)->toBe('default'); +}); + +it('returns default for missing dynamic inner properties in object when passed as array', function (): void { + $object = new stdClass(); + $object->level1 = new stdClass(); + $object->level1->level2 = new stdClass(); + + $result = Typed::any($object, ['level1', 'level2', 'missing'], 'default'); + + expect($result)->toBe('default'); +}); diff --git a/tests/FunctionTest.php b/tests/FunctionTest.php new file mode 100644 index 0000000..8bdeb42 --- /dev/null +++ b/tests/FunctionTest.php @@ -0,0 +1,13 @@ + 'value']; + $result = string($array, 'key'); + + expect($result)->toBe('value'); +}); diff --git a/tests/MixedStructure/GetFromMixedStructureTest.php b/tests/MixedStructure/GetFromMixedStructureTest.php new file mode 100644 index 0000000..2455119 --- /dev/null +++ b/tests/MixedStructure/GetFromMixedStructureTest.php @@ -0,0 +1,61 @@ +property = new stdClass(); + $object->property->values = [ + 'arrayKey' => [ + 'innerObject' => new stdClass() + ] + ]; + $object->property->values['arrayKey']['innerObject']->property = 'value'; + + $result = Typed::any($object, 'property.values.arrayKey.innerObject.property', 'default'); + + expect($result)->toBe('value'); +}); + +it('works with mixed structures when passed as array', function (): void { + $object = new stdClass(); + $object->property = new stdClass(); + $object->property->values = [ + 'arrayKey' => [ + 'innerObject' => new stdClass() + ] + ]; + $object->property->values['arrayKey']['innerObject']->property = 'value'; + + $result = Typed::any($object, ['property','values','arrayKey','innerObject','property'], 'default'); + + expect($result)->toBe('value'); +}); + +it('returns default for missing key in mixed structures when passed as string', function (): void { + $object = new stdClass(); + $object->property = new stdClass(); + $object->property->values = [ + 'arrayKey' => [ + 'innerObject' => new stdClass() + ] + ]; + + $result = Typed::any($object, 'property.values.arrayKey.innerObject.missing', 'default'); + + expect($result)->toBe('default'); +}); + +it('returns default for missing key in mixed structures when passed as array', function (): void { + $object = new stdClass(); + $object->property = new stdClass(); + $object->property->values = [ + 'arrayKey' => [ + 'innerObject' => new stdClass() + ] + ]; + + $result = Typed::any($object, ['property','values','arrayKey','innerObject','missing'], 'default'); + + expect($result)->toBe('default'); +}); diff --git a/tests/TypedObject/GetFromTypedObjectPropertyTest.php b/tests/TypedObject/GetFromTypedObjectPropertyTest.php new file mode 100644 index 0000000..cf9e9dc --- /dev/null +++ b/tests/TypedObject/GetFromTypedObjectPropertyTest.php @@ -0,0 +1,119 @@ +toBe('value'); +}); + +it('returns default for missing key in object with typed properties', function () { + $object = new class { + public ?string $key = null; + }; + + $result = Typed::any($object, 'key', 'default'); + + expect($result)->toBe('default'); +}); + +it('works with object with typed inner properties when passed as string', function () { + $object = new class { + public object $level1; + + public function __construct() + { + $this->level1 = new class { + public object $level2; + + public function __construct() + { + $this->level2 = new class { + public string $key = 'value'; + }; + } + }; + } + }; + + $result = Typed::any($object, 'level1.level2.key', 'default'); + + expect($result)->toBe('value'); +}); + +it('works with object with typed inner properties when passed as array', function () { + $object = new class { + public object $level1; + + public function __construct() + { + $this->level1 = new class { + public object $level2; + + public function __construct() + { + $this->level2 = new class { + public string $key = 'value'; + }; + } + }; + } + }; + + $result = Typed::any($object, ['level1', 'level2', 'key'], 'default'); + + expect($result)->toBe('value'); +}); + +it('returns default for missing typed inner properties in object when passed as string', function () { + $object = new class { + public object $level1; + + public function __construct() + { + $this->level1 = new class { + public object $level2; + + public function __construct() + { + $this->level2 = new class { + public ?string $key = null; + }; + } + }; + } + }; + + $result = Typed::any($object, 'level1.level2.key', 'default'); + + expect($result)->toBe('default'); +}); + +it('returns default for missing typed inner properties in object when passed as array', function () { + $object = new class { + public object $level1; + + public function __construct() + { + $this->level1 = new class { + public object $level2; + + public function __construct() + { + $this->level2 = new class { + public ?string $key = null; + }; + } + }; + } + }; + + $result = Typed::any($object, ['level1', 'level2', 'key'], 'default'); + + expect($result)->toBe('default'); +}); diff --git a/tests/Unit/TypedTest.php b/tests/Unit/TypedTest.php deleted file mode 100644 index c346aa7..0000000 --- a/tests/Unit/TypedTest.php +++ /dev/null @@ -1,382 +0,0 @@ -assertSame('origin', $result); - } - - public function testAnyReturnsNullWhenSourceIsNotIterable(): void - { - $result = Typed::any('origin', 'key'); - - $this->assertNull($result); - } - - public function testAnyReturnsDefaultWhenSourceIsNotIterable(): void - { - $result = Typed::any('origin', 'key', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyWorksWithMixedKeysPassedAsString(): void - { - $result = Typed::any([ - 0 => [ - 'key' => [ - '0' => 'value' - ] - ], - ], '0.key.0'); - - $this->assertSame('value', $result); - } - - public function testAnyWorksWithMixedKeysPassedAsArray(): void - { - $result = Typed::any([ - 0 => [ - 'key' => [ - '0' => 'value' - ] - ], - ], [0,'key','0',]); - - $this->assertSame('value', $result); - } - - // arrays - - public function testAnyMethodWorksWithArray(): void - { - $result = Typed::any(['key' => 'value'], 'key', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingKeyInArray(): void - { - $data = ['key' => 'value']; - - $result = Typed::any($data, 'missing', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodWorksWithArrayAndInnerKeysWhenPassedAsString(): void - { - $data = ['level1' => ['level2' => ['key' => 'value']]]; - - $result = Typed::any($data, 'level1.level2.key', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodWorksWithArrayAndInnerKeysWhenPassedAsArray(): void - { - $data = ['level1' => ['level2' => ['key' => 'value']]]; - - $result = Typed::any($data, ['level1','level2','key'], 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingInnerKeyInArrayWhenPassesAsString(): void - { - $data = ['level1' => ['level2' => []]]; - - $result = Typed::any($data, 'level1.level2.missing', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodReturnsDefaultForMissingInnerKeyInArrayWhenPassesAsArray(): void - { - $data = ['level1' => ['level2' => []]]; - - $result = Typed::any($data, ['level1','level2','missing'], 'default'); - - $this->assertSame('default', $result); - } - - // objects with typed properties - - public function testAnyMethodWorksWithObjectWithTypedProperties(): void - { - $object = new class { - public string $key = 'value'; - }; - - $result = Typed::any($object, 'key', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingKeyInObjectWithTypedProperties(): void - { - $object = new class { - public ?string $key = null; - }; - - $result = Typed::any($object, 'key', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodWorksWithObjectWithTypedInnerPropertiesWhenPassedAsString(): void - { - $object = new class { - public object $level1; - - public function __construct() - { - $this->level1 = new class { - public object $level2; - - public function __construct() - { - $this->level2 = new class { - public string $key = 'value'; - }; - } - }; - } - }; - - $result = Typed::any($object, 'level1.level2.key', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodWorksWithObjectWithTypedInnerPropertiesWhenPassedAsArray(): void - { - $object = new class { - public object $level1; - - public function __construct() - { - $this->level1 = new class { - public object $level2; - - public function __construct() - { - $this->level2 = new class { - public string $key = 'value'; - }; - } - }; - } - }; - - $result = Typed::any($object, ['level1','level2','key'], 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingTypedInnerPropertiesInObjectWhenPassedAsString(): void - { - $object = new class { - public object $level1; - - public function __construct() - { - $this->level1 = new class { - public object $level2; - - public function __construct() - { - $this->level2 = new class { - public ?string $key = null; - }; - } - }; - } - }; - - $result = Typed::any($object, 'level1.level2.key', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodReturnsDefaultForMissingTypedInnerPropertiesInObjectWhenPassedAsArray(): void - { - $object = new class { - public object $level1; - - public function __construct() - { - $this->level1 = new class { - public object $level2; - - public function __construct() - { - $this->level2 = new class { - public ?string $key = null; - }; - } - }; - } - }; - - $result = Typed::any($object, ['level1','level2','key'], 'default'); - - $this->assertSame('default', $result); - } - - // objects with dynamic properties - - public function testAnyMethodWorksWithObjectWithDynamicProperties(): void - { - $object = new stdClass(); - $object->key = 'value'; - - $result = Typed::any($object, 'key', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingKeyInObjectWithDynamicProperties(): void - { - $object = new stdClass(); - - $result = Typed::any($object, 'missing', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodWorksWithObjectWithDynamicInnerPropertiesWhenPassedAsString(): void - { - $object = new stdClass(); - $object->level1 = new stdClass(); - $object->level1->level2 = new stdClass(); - $object->level1->level2->key = 'value'; - - $result = Typed::any($object, 'level1.level2.key', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodWorksWithObjectWithDynamicInnerPropertiesWhenPassedAsArray(): void - { - $object = new stdClass(); - $object->level1 = new stdClass(); - $object->level1->level2 = new stdClass(); - $object->level1->level2->key = 'value'; - - $result = Typed::any($object, ['level1','level2','key'], 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingDynamicInnerPropertiesInObjectWhenPassedAsString(): void - { - $object = new stdClass(); - $object->level1 = new stdClass(); - $object->level1->level2 = new stdClass(); - - $result = Typed::any($object, 'level1.level2.missing', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodReturnsDefaultForMissingDynamicInnerPropertiesInObjectWhenPassedAsArray(): void - { - $object = new stdClass(); - $object->level1 = new stdClass(); - $object->level1->level2 = new stdClass(); - - $result = Typed::any($object, ['level1','level2','missing'], 'default'); - - $this->assertSame('default', $result); - } - - // mixed cases - - public function testAnyMethodWorksWithMixedStructuresWhenPassedAsString(): void - { - $object = new stdClass(); - $object->property = new stdClass(); - $object->property->values = [ - 'arrayKey' => [ - 'innerObject' => new stdClass() - ] - ]; - $object->property->values['arrayKey']['innerObject']->property = 'value'; - - $result = Typed::any($object, 'property.values.arrayKey.innerObject.property', 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodWorksWithMixedStructuresWhenPassedAsArray(): void - { - $object = new stdClass(); - $object->property = new stdClass(); - $object->property->values = [ - 'arrayKey' => [ - 'innerObject' => new stdClass() - ] - ]; - $object->property->values['arrayKey']['innerObject']->property = 'value'; - - $result = Typed::any($object, ['property','values','arrayKey','innerObject','property'], 'default'); - - $this->assertSame('value', $result); - } - - public function testAnyMethodReturnsDefaultForMissingKeyInMixedStructuresWhenPassedAsString(): void - { - $object = new stdClass(); - $object->property = new stdClass(); - $object->property->values = [ - 'arrayKey' => [ - 'innerObject' => new stdClass() - ] - ]; - - $result = Typed::any($object, 'property.values.arrayKey.innerObject.missing', 'default'); - - $this->assertSame('default', $result); - } - - public function testAnyMethodReturnsDefaultForMissingKeyInMixedStructuresWhenPassedAsArray(): void - { - $object = new stdClass(); - $object->property = new stdClass(); - $object->property->values = [ - 'arrayKey' => [ - 'innerObject' => new stdClass() - ] - ]; - - $result = Typed::any($object, ['property','values','arrayKey','innerObject','missing'], 'default'); - - $this->assertSame('default', $result); - } - - // functions.php - - public function testFunctionsAreAvailable(): void - { - $array = ['key' => 'value']; - $result = string($array, 'key'); - - $this->assertSame('value', $result); - } -} From f16c054a612015b6fb0c084bf14cceb7d816d5b2 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Thu, 17 Apr 2025 16:25:47 +0300 Subject: [PATCH 7/9] feature: setItem function --- src/Typed.php | 1 - .../SetToDynamicObjectPropertyTest.php | 90 ++++++++++++++ .../SetToMixedStructureTest.php | 94 +++++++++++++++ .../SetToTypedObjectPropertyTest.php | 112 ++++++++++++++++++ 4 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 tests/DynamicObject/SetToDynamicObjectPropertyTest.php create mode 100644 tests/MixedStructure/SetToMixedStructureTest.php create mode 100644 tests/TypedObject/SetToTypedObjectPropertyTest.php diff --git a/src/Typed.php b/src/Typed.php index 0c49a7b..2d3f3c4 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -362,7 +362,6 @@ public static function setItem(&$target, $keys, $value): bool return false; } - return true; } diff --git a/tests/DynamicObject/SetToDynamicObjectPropertyTest.php b/tests/DynamicObject/SetToDynamicObjectPropertyTest.php new file mode 100644 index 0000000..1b577eb --- /dev/null +++ b/tests/DynamicObject/SetToDynamicObjectPropertyTest.php @@ -0,0 +1,90 @@ +first = new stdClass(); + $target->first->second = new stdClass(); + + $isSet = setItem($target, ['first', 'second', 'third'], 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([ + 'third' + ]); + expect($target->first->second->third)->toBe( + 'value' + ); +}); + +it('sets to dynamic object property when string keys are valid', function () { + $target = new stdClass(); + $target->first = new stdClass(); + $target->first->second = new stdClass(); + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([ + 'third' + ]); + expect($target->first->second->third)->toBe( + 'value' + ); +}); + +it('skips set to dynamic object property when target is not an object', function () { + $target = 'string'; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toBe('string'); +}); + +it('skips set to dynamic object property when array keys are invalid', function () { + $target = new stdClass(); + $target->first = new stdClass(); + $target->first->second = new stdClass(); + + $isSet = setItem($target, ['first', 'another', 'third'], 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([]); +}); + +it('skips set to dynamic object property when string keys are invalid', function () { + $target = new stdClass(); + $target->first = new stdClass(); + $target->first->second = new stdClass(); + + $isSet = setItem($target, 'first.another.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([]); +}); diff --git a/tests/MixedStructure/SetToMixedStructureTest.php b/tests/MixedStructure/SetToMixedStructureTest.php new file mode 100644 index 0000000..c052998 --- /dev/null +++ b/tests/MixedStructure/SetToMixedStructureTest.php @@ -0,0 +1,94 @@ +first = [ + 'second' => new stdClass(), + ]; + + $isSet = setItem($target, ['first', 'second', 'third'], 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveKeys([ + 'second' + ]); + expect($target->first['second'])->toHaveProperties([ + 'third' + ]); + expect($target->first['second']->third)->toBe( + 'value' + ); +}); + +it('sets to mixed structure when string keys are valid', function () { + $target = new stdClass(); + $target->first = [ + 'second' => new stdClass(), + ]; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveKeys([ + 'second' + ]); + expect($target->first['second'])->toHaveProperties([ + 'third' + ]); + expect($target->first['second']->third)->toBe( + 'value' + ); +}); + +it('skips set mixed structure when target is neither an array nor an object', function () { + $target = 'string'; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toBe('string'); +}); + +it('skips set to mixed structure when array keys are invalid', function () { + $target = new stdClass(); + $target->first = [ + 'second' => new stdClass(), + ]; + + $isSet = setItem($target, ['first', 'another', 'third'], 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveKeys([ + 'second' + ]); + expect($target->first['second'])->toHaveProperties([]); +}); + +it('skips set to mixed structure when string keys are invalid', function () { + $target = new stdClass(); + $target->first = [ + 'second' => new stdClass(), + ]; + + $isSet = setItem($target, 'first.another.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveKeys([ + 'second' + ]); + expect($target->first['second'])->toHaveProperties([]); +}); diff --git a/tests/TypedObject/SetToTypedObjectPropertyTest.php b/tests/TypedObject/SetToTypedObjectPropertyTest.php new file mode 100644 index 0000000..bdd3f75 --- /dev/null +++ b/tests/TypedObject/SetToTypedObjectPropertyTest.php @@ -0,0 +1,112 @@ +first = new class { + public object $second; + }; + $target->first->second = new class { + public string $third; + }; + + $isSet = setItem($target, ['first', 'second', 'third'], 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([ + 'third' + ]); + expect($target->first->second->third)->toBe( + 'value' + ); +}); + +it('sets to typed object property when string keys are valid', function () { + $target = new class { + public object $first; + }; + $target->first = new class { + public object $second; + }; + $target->first->second = new class { + public string $third; + }; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeTrue(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([ + 'third' + ]); + expect($target->first->second->third)->toBe( + 'value' + ); +}); + +it('skips set to typed object property when target is not an object', function () { + $target = 'string'; + + $isSet = setItem($target, 'first.second.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toBe('string'); +}); + +it('skips set to typed object property when array keys are invalid', function () { + $target = new class { + public object $first; + }; + $target->first = new class { + public object $second; + }; + $target->first->second = new class { + }; + + $isSet = setItem($target, ['first', 'another', 'third'], 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([]); +}); + +it('skips set to typed object property when string keys are invalid', function () { + $target = new class { + public object $first; + }; + $target->first = new class { + public object $second; + }; + $target->first->second = new class { + }; + + $isSet = setItem($target, 'first.another.third', 'value'); + + expect($isSet)->toBeFalse(); + expect($target)->toHaveProperties([ + 'first' + ]); + expect($target->first)->toHaveProperties([ + 'second' + ]); + expect($target->first->second)->toHaveProperties([]); +}); From 751701bd49b8a343075a7da19ddb4d9ce9796a23 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Thu, 17 Apr 2025 16:50:35 +0300 Subject: [PATCH 8/9] feature: setItem function --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 915cdeb..936f665 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PHP Typed -> `Typed` is a lightweight PHP utility for seamless type-casting and data retrieval from dynamic variables, arrays, and -> objects. +> `Typed` is a lightweight PHP utility for seamless type-casting and item manipulations, perfect for dynamic variables, +> arrays, and objects. This package provides a single `Typed` class with static methods and offers compatibility with PHP versions `7.4+` and `8.0+`. @@ -31,6 +31,19 @@ function upgradeUserById($mixedUserId): void ? (string) $mixedUserId : ''; } + +function setUserEducation(array $user, string $education): array +{ + // such long chain is the only safe way passing PHPStan checks. + if(key_exists('data', $userData) && + is_array($userData['data']) && + key_exists('bio', $userData['data']) && + is_array($userData['data']['bio'])) { + $userData['data']['bio']['education'] = $education; + } + + return $userData; +} ``` **The same with the `Typed` utility** @@ -48,6 +61,14 @@ function upgradeUserById($mixedUserId): void { $userId = string($mixedUserId); } + +function setUserEducation(array $userData, string $education): array +{ + // will set only if 'data' and 'bio' keys are present. + $isSet = setItem($userData, 'data.bio.education', $education); + + return $userData; +} ``` The code like `string($array, 'key')` resembles `(string)$array['key']` while being @@ -83,20 +104,52 @@ After installation, ensure that your application includes the Composer autoloade `require __DIR__ . '/vendor/autoload.php';` -Usage: +### 2.1) Retrieval usage: ```php use function WPLake\Typed\string; use WPLake\Typed\Typed; $string = string($array, 'first.second'); -// alternatively: +// alternatively, array of keys: +$string = string($array, ['first', 'second',]); +// alternatively, static method: $string = Typed::string($array, 'first.second'); // custom fallback: $string = string($array, 'first.second', 'custom default'); ``` -## 3. How It Works +### 2.2) Setting item usage: + +```php +use function WPLake\Typed\setItem; +use WPLake\Typed\Typed; + +function myFunction(array $unknownKeys): void { + // will set only if 'first' and 'second' keys exist. + $isSet = setItem($unknownKeys, 'first.second.third', 'value'); + // alternatively, array of keys + $isSet = Typed::setItem($unknownKeys, ['first', 'second', 'third',], 'value'); + // alternatively, static method + $isSet = Typed::setItem($unknownKeys, 'first.second.third', 'value'); + + return $array; +} + +$array = [ + 'first' => [ + // ... + 'second' => [ + + ], + ], +]; + +myFunction($array); + +``` + +## 3. How Retrieval Functions Work The logic of all casting methods follows this simple principle: From a70874b773b2132963cee6e6d4e84945770025a2 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Thu, 17 Apr 2025 16:55:14 +0300 Subject: [PATCH 9/9] feature: setItem function --- src/Typed.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Typed.php b/src/Typed.php index 2d3f3c4..e5b6ff1 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -418,7 +418,6 @@ protected static function &resolveKey(&$source, $key, bool &$isResolved = false) ) { $isResolved = true; - // @phpstan-ignore-next-line $value = &$source[$key]; }