diff --git a/README.md b/README.md index 9b0a428..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+`. @@ -16,21 +16,34 @@ 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 isset($userData['meta']['age']) && + 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 = is_string($mixedUserId) || + is_numeric($mixedUserId) + ? (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** @@ -39,24 +52,32 @@ 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); +} + +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 -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?" -> +> 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: @@ -65,7 +86,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; @@ -83,39 +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','default value'); -// alternatively: -$string = Typed::string($array, 'first.second','default value'); +$string = string($array, 'first.second'); +// 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. Supported types +### 2.2) Setting item usage: -Functions for the following types are present: +```php +use function WPLake\Typed\setItem; +use WPLake\Typed\Typed; -* `string` -* `int` -* `float` -* `bool` -* `array` -* `object` -* `dateTime` -* `any` (allows to use short dot-keys usage for unknowns) +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; +} -Additionally: +$array = [ + 'first' => [ + // ... + 'second' => [ + + ], + ], +]; -* `boolExtended` (`true`,`1`,`"1"`, `"on"` are treated as true, `false`,`0`,`"0"`, `"off"` as false) -* `stringExtended` (supports objects with `__toString`) +myFunction($array); -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 Retrieval Functions Work The logic of all casting methods follows this simple principle: @@ -135,42 +169,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, 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 Surprisingly, PHP allows functions to use the same names as variable types. @@ -179,12 +252,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. @@ -223,12 +306,12 @@ 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: -$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. @@ -242,11 +325,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? 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..e5b6ff1 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,15 +26,7 @@ final class Typed */ public static function any($source, $keys = null, $default = null) { - if (null === $keys) { - return $source; - } - - if (false === is_array($keys)) { - $keys = explode('.', (string)$keys); - } - - return self::resolveKeys($source, $keys, $default); + return self::anyAsReference($source, $keys, $default); } /** @@ -44,9 +37,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 +52,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 +76,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 +91,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 +115,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 +128,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 +141,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 +154,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 +167,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 +180,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 +200,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 +225,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 +247,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 +262,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 +279,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 +292,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 +309,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,35 +322,106 @@ public static function dateTimeOrNull($source, $keys = null): ?DateTime { $value = self::any($source, $keys); - return true === ($value instanceof DateTime) ? + return $value instanceof DateTime ? $value : null; } + /** + * @param mixed $target + * @param int|string|array $keys + * @param mixed $value + */ + 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_string($keys) || + is_numeric($keys) + ) { + $keys = explode('.', (string) $keys); + } + + return self::resolveKeys($source, $keys, $default); + } + /** * @param mixed $source * @param int|string $key - * @param mixed $value + * + * @return mixed */ - protected static function resolveKey($source, $key, &$value): bool + protected static function &resolveKey(&$source, $key, bool &$isResolved = false) { + $value = null; + if ( - true === is_object($source) && - true === isset($source->{$key}) + is_object($source) && + // @phpstan-ignore-next-line + isset($source->{$key}) ) { - $value = $source->{$key}; - return true; + $isResolved = true; + + // @phpstan-ignore-next-line + $value = &$source->{$key}; } if ( - true === is_array($source) && - true === isset($source[$key]) + is_array($source) && + key_exists($key, $source) ) { - $value = $source[$key]; - return true; + $isResolved = true; + + $value = &$source[$key]; } - return false; + return $value; } /** @@ -367,19 +431,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 (true === 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/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/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/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/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/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([]); +}); 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); - } -}