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);
- }
-}