diff --git a/README.md b/README.md index 93bd92f..58c88a4 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,14 @@ This package provides a single `Typed` class with static methods and offers comp ## 1. Why Use Typed? -Handling type casting in PHP often leads to verbose and repetitive constructions. `Typed` streamlines this process, +Handling type casting in PHP often leads to verbose and repetitive constructions, especially in array-related cases. + +`Typed` streamlines this process, allowing you to fetch and cast values with concise, readable code. **Example: Plain PHP** ```php -function getTypedStringFromMixedVariable($mixed): string -{ - return true === is_string($mixed) || true === is_numeric($mixed) - ? (string)$mixed - : ''; -} - function getTypedIntFromArray(array $array): int { return true === isset($array['meta']['number']) && @@ -28,12 +23,21 @@ function getTypedIntFromArray(array $array): int ? (int)$array['meta']['number'] : 0; } + +function getTypedStringFromMixedVariable($mixed): string +{ + return true === is_string($mixed) || + true === is_numeric($mixed) + ? (string)$mixed + : ''; +} ``` **The same with `Typed` utility** ```php use WPLake\Typed\Typed; +use WPLake\Typed\Typed; function getTypedStringFromMixedVariable($mixedData): string { @@ -76,22 +80,22 @@ $string = Typed::string($array, 'first.second','default value'); Static methods for the following types are present: -* `string` -* `int` -* `float` -* `bool` -* `array` -* `object` -* `dateTime` -* `any` (allows to use short dot-keys usage for unknowns) +* `Typed::string` +* `Typed::int` +* `Typed::float` +* `Typed::bool` +* `Typed::array` +* `Typed::object` +* `Typed::dateTime` +* `Typed::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`) +* `Typed::boolExtended` (`true`,`1`,`"1"`, `"on"` are treated as true, `false`,`0`,`"0"`, `"off"` as false) +* `Typed::stringExtended` (supports objects with `__toString`) -> Plus, for optional cases, each item has an `OrNull` method option (e.g. `stringOrNull`, `intOrNull`, and so on), which returns `null` if the key -doesn’t exist. +For optional cases, each item has an `OrNull` method option (e.g. `Typed::stringOrNull`, `Typed::intOrNull`, and so on), +which returns `null` if the key doesn’t exist. ## 4. How It Works @@ -105,8 +109,13 @@ For example, let's review the `string` method declaration: namespace WPLake\Typed; class Typed { -public static function string($source, $key = null, string $default = ''): string; -// ... + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + public static function string($source, $keys = null, string $default = ''): string; + + // ... } ``` @@ -118,22 +127,28 @@ Usage Scenarios: $userName = Typed::string($unknownVar); ``` -2. Retrieve a string from an array, including nested structures using dot notation (e.g., `some.key.subkey`). +2. Retrieve a string from an array, including nested structures (with dot notation or as an array). ```php $userName = Typed::string($array, 'user.name'); +// alternatively: +$userName = Typed::string($array, ['user','name',]); ``` -3. Access a string from an object, supporting nested properties with dot notation (e.g., `some.key.subkey`). +3. Access a string from an object. It also supports the nested properties. ```php $userName = Typed::string($companyObject, 'user.name'); +// alternatively: +$userName = Typed::string($companyObject, ['user', 'name',]); ``` 4. Work with mixed structures (e.g., `object->arrayProperty['key']->anotherProperty or ['key' => $object]`). ```php $userName = Typed::string($companyObject,'users.john.name'); +// alternatively: +$userName = Typed::string($companyObject,['users','john','name',]); ``` In all the cases, you can pass a default value as the third argument, e.g.: @@ -142,9 +157,67 @@ In all the cases, you can pass a default value as the third argument, e.g.: $userName = Typed::string($companyObject,'users.john.name', 'Guest'); ``` -## 5. FAQ +## 5. Global Helper Functions + +Surprisingly, PHP allows global functions to share the same names as variable types. + +Think it’s prohibited? Not quite! While certain names are restricted for classes, interfaces, and traits, function names +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) + +This means you can have something like `string($array, 'key')`, which resembles `(string)$array['key']` while being +safer +and smarter — it even handles nested keys. + +However, since these functions must be declared in the global namespace (you can’t use `WPLake\Typed\string`), their +usage is optional. + +**How to Enable** + +To enable these global helper functions, define the following constant before including the Composer autoloader: + +```php +define('WPLAKE_TYPED_FUNCTIONS', true); + +require __DIR__ . '/vendor/autoload.php'; +``` + +Once enabled, you can enjoy clean and intuitive syntax for the all types, with the added safety and flexibility. + +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` instead. + +## 6. FAQ + +### 6.1) Why not just straight type casting? + +Straight type casting in PHP can be unsafe and unpredictable in certain scenarios. + +For example, the following code will throw an error if the `$mixed` variable is an object of a class that doesn’t +explicitly implement `__toString`: + +```php +class Example { +// ... +} +$mixed = new Example(); +// ... +function getName($mixed):void{ + return (string)$mixed; +} +``` + +Additionally, attempting to cast an array to a string, like `(string)$myArray` will: + +1. Produce a PHP Notice: Array to string conversion. +2. Return the string "Array", which is rarely the intended behavior. + +This unpredictability can lead to unexpected bugs and unreliable code. -### 5.1) Why not just Null Coalescing Operator? +### 6.2) Why not just Null Coalescing Operator? While the Null Coalescing Operator (`??`) is useful, it doesn’t address type checking or casting requirements. @@ -161,7 +234,7 @@ $number = Typed::int($data, 'meta.number', 10); Additionally, with Null Coalescing Operator and a custom default value, you have to repeat yourself. -### 5.2) Shouldn't we use typed objects instead? +### 6.3) Shouldn't we use typed objects instead? OOP is indeed powerful, and you should always prioritize using objects whenever possible. However, the reality is that our code often interacts with external dependencies beyond our control. @@ -170,13 +243,13 @@ This package simplifies handling such scenarios. Any seasoned PHP developer knows the pain of type-casting when working with environments outside of frameworks like WordPress. -### 5.3) Is the dot syntax in keys inspired by Laravel Collections? +### 6.4) Is the dot syntax in keys inspired by Laravel Collections? 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. -### 5.4) Why not just use Laravel Collections? +### 6.5) Why not just use Laravel Collections? Laravel Collections and similar libraries don’t offer type-specific methods like this package does. @@ -191,7 +264,7 @@ standalone package because: In addition, when we only need to extract a single variable, requiring the entire array to be wrapped in a collection would be excessive. -## 6. Contribution +## 7. Contribution We would be excited if you decide to contribute 🤝 @@ -199,16 +272,16 @@ Please open Pull Requests against the `main` branch. ### Code Style Agreements: -#### 6.1) PSR-12 Compliance +#### 7.1) PSR-12 Compliance Use the [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) tool in your IDE with the provided `phpcs.xml`, or run `composer phpcbf` to format your code. -#### 6.2) Static Analysis with PHPStan +#### 7.2) Static Analysis with PHPStan Set up [PHPStan](https://phpstan.org/) in your IDE with the provided `phpstan.neon`, or run `composer phpstan` to validate your code. -#### 6.3) Unit Tests +#### 7.3) Unit Tests [Pest](https://pestphp.com/) is setup for Unit tests. Run them using `composer pest`. \ No newline at end of file diff --git a/composer.json b/composer.json index a4fe76d..a065499 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,10 @@ "autoload": { "psr-4": { "WPLake\\Typed\\": "./src" - } + }, + "files": [ + "./src/functions.php" + ] }, "require-dev": { "pestphp/pest": "^3.7", diff --git a/src/Typed.php b/src/Typed.php index 5f98e09..7d5115f 100644 --- a/src/Typed.php +++ b/src/Typed.php @@ -18,39 +18,31 @@ final class Typed { /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys * @param mixed $default * * @return mixed */ - public static function any($source, $key = null, $default = null) + public static function any($source, $keys = null, $default = null) { - if (null === $key) { + if (null === $keys) { return $source; } - $stringKey = (string)$key; - $isInnerKey = false !== strpos($stringKey, '.'); - - if (false === $isInnerKey) { - $value = null; - return true === self::resolveKey($source, $key, $value) ? - $value : - $default; + if (false === is_array($keys)) { + $keys = explode('.', (string)$keys); } - $keys = explode('.', $stringKey); - return self::resolveKeys($source, $keys, $default); } /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function string($source, $key = null, string $default = ''): string + public static function string($source, $keys = null, string $default = ''): string { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); return true === is_string($value) || true === is_numeric($value) ? @@ -60,11 +52,11 @@ public static function string($source, $key = null, string $default = ''): strin /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function stringExtended($source, $key = null, string $default = ''): string + public static function stringExtended($source, $keys = null, string $default = ''): string { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); if ( true === is_string($value) || @@ -85,11 +77,11 @@ public static function stringExtended($source, $key = null, string $default = '' /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function stringOrNull($source, $key = null): ?string + public static function stringOrNull($source, $keys = null): ?string { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === is_string($value) || true === is_numeric($value) ? @@ -99,11 +91,11 @@ public static function stringOrNull($source, $key = null): ?string /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function stringExtendedOrNull($source, $key = null): ?string + public static function stringExtendedOrNull($source, $keys = null): ?string { - $value = self::any($source, $key); + $value = self::any($source, $keys); if ( true === is_string($value) || @@ -124,11 +116,11 @@ public static function stringExtendedOrNull($source, $key = null): ?string /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function int($source, $key = null, int $default = 0): int + public static function int($source, $keys = null, int $default = 0): int { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); return true === is_numeric($value) ? (int)$value : @@ -137,11 +129,11 @@ public static function int($source, $key = null, int $default = 0): int /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function intOrNull($source, $key = null): ?int + public static function intOrNull($source, $keys = null): ?int { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === is_numeric($value) ? (int)$value : @@ -150,11 +142,11 @@ public static function intOrNull($source, $key = null): ?int /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function float($source, $key = null, float $default = 0.0): float + public static function float($source, $keys = null, float $default = 0.0): float { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); return true === is_numeric($value) ? (float)$value : @@ -163,11 +155,11 @@ public static function float($source, $key = null, float $default = 0.0): float /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function floatOrNull($source, $key = null): ?float + public static function floatOrNull($source, $keys = null): ?float { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === is_numeric($value) ? (float)$value : @@ -176,11 +168,11 @@ public static function floatOrNull($source, $key = null): ?float /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function bool($source, $key = null, bool $default = false): bool + public static function bool($source, $keys = null, bool $default = false): bool { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); return true === is_bool($value) ? $value : @@ -189,11 +181,11 @@ public static function bool($source, $key = null, bool $default = false): bool /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function boolOrNull($source, $key = null): ?bool + public static function boolOrNull($source, $keys = null): ?bool { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === is_bool($value) ? $value : @@ -202,18 +194,18 @@ public static function boolOrNull($source, $key = null): ?bool /** * @param mixed $source - * @param int|string|null $key - * @param array $positive - * @param array $negative + * @param int|string|array|null $keys + * @param array $positive + * @param array $negative */ public static function boolExtended( $source, - $key = null, + $keys = null, bool $default = false, array $positive = [true, 1, '1', 'on',], array $negative = [false, 0, '0', 'off',] ): bool { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); if (true === in_array($value, $positive, true)) { return true; @@ -228,17 +220,17 @@ public static function boolExtended( /** * @param mixed $source - * @param int|string|null $key - * @param array $positive - * @param array $negative + * @param int|string|array|null $keys + * @param array $positive + * @param array $negative */ public static function boolExtendedOrNull( $source, - $key = null, + $keys = null, array $positive = [true, 1, '1', 'on',], array $negative = [false, 0, '0', 'off',] ): ?bool { - $value = self::any($source, $key); + $value = self::any($source, $keys); if (true === in_array($value, $positive, true)) { return true; @@ -253,14 +245,14 @@ public static function boolExtendedOrNull( /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys * @param array $default * * @return array */ - public static function array($source, $key = null, array $default = []): array + public static function array($source, $keys = null, array $default = []): array { - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); return true === is_array($value) ? $value : @@ -269,13 +261,13 @@ public static function array($source, $key = null, array $default = []): array /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys * * @return array|null */ - public static function arrayOrNull($source, $key = null): ?array + public static function arrayOrNull($source, $keys = null): ?array { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === is_array($value) ? $value : @@ -284,15 +276,15 @@ public static function arrayOrNull($source, $key = null): ?array /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function object($source, $key = null, ?object $default = null): object + public static function object($source, $keys = null, ?object $default = null): object { $default = null === $default ? new stdClass() : $default; - $value = self::any($source, $key, $default); + $value = self::any($source, $keys, $default); return true === is_object($value) ? $value : @@ -301,11 +293,11 @@ public static function object($source, $key = null, ?object $default = null): ob /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function objectOrNull($source, $key = null): ?object + public static function objectOrNull($source, $keys = null): ?object { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === is_object($value) ? $value : @@ -314,15 +306,15 @@ public static function objectOrNull($source, $key = null): ?object /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function dateTime($source, $key = null, ?DateTime $default = null): DateTime + public static function dateTime($source, $keys = null, ?DateTime $default = null): DateTime { $default = null === $default ? new DateTime() : $default; - $value = self::object($source, $key, $default); + $value = self::object($source, $keys, $default); return true === ($value instanceof DateTime) ? $value : @@ -331,11 +323,11 @@ public static function dateTime($source, $key = null, ?DateTime $default = null) /** * @param mixed $source - * @param int|string|null $key + * @param int|string|array|null $keys */ - public static function dateTimeOrNull($source, $key = null): ?DateTime + public static function dateTimeOrNull($source, $keys = null): ?DateTime { - $value = self::any($source, $key); + $value = self::any($source, $keys); return true === ($value instanceof DateTime) ? $value : diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..7edc34f --- /dev/null +++ b/src/functions.php @@ -0,0 +1,256 @@ +|null $keys + * @param mixed $default + * + * @return mixed + */ + function any($source, $keys = null, $default = null) + { + return Typed::any($source, $keys, $default); + } + } + + if (false === function_exists('string')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function string($source, $keys = null, string $default = ''): string + { + return Typed::string($source, $keys, $default); + } + } + + if (false === function_exists('stringExtended')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function stringExtended($source, $keys = null, string $default = ''): string + { + return Typed::stringExtended($source, $keys, $default); + } + } + + if (false === function_exists('stringOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function stringOrNull($source, $keys = null): ?string + { + return Typed::stringOrNull($source, $keys); + } + } + + if (false === function_exists('stringExtendedOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function stringExtendedOrNull($source, $keys = null): ?string + { + return Typed::stringExtendedOrNull($source, $keys); + } + } + + if (false === function_exists('int')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function int($source, $keys = null, int $default = 0): int + { + return Typed::int($source, $keys, $default); + } + } + + if (false === function_exists('intOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function intOrNull($source, $keys = null): ?int + { + return Typed::intOrNull($source, $keys); + } + } + + if (false === function_exists('float')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function float($source, $keys = null, float $default = 0.0): float + { + return Typed::float($source, $keys, $default); + } + } + + if (false === function_exists('floatOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function floatOrNull($source, $keys = null): ?float + { + return Typed::floatOrNull($source, $keys); + } + } + + if (false === function_exists('bool')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function bool($source, $keys = null, bool $default = false): bool + { + return Typed::bool($source, $keys, $default); + } + } + + if (false === function_exists('boolOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function boolOrNull($source, $keys = null): ?bool + { + return Typed::boolOrNull($source, $keys); + } + } + + if (false === function_exists('boolExtended')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + * @param array $positive + * @param array $negative + */ + function boolExtended( + $source, + $keys = null, + bool $default = false, + array $positive = [true, 1, '1', 'on',], + array $negative = [false, 0, '0', 'off',] + ): bool { + return Typed::boolExtended($source, $keys, $default, $positive, $negative); + } + } + + if (false === function_exists('boolExtendedOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + * @param array $positive + * @param array $negative + */ + function boolExtendedOrNull( + $source, + $keys = null, + array $positive = [true, 1, '1', 'on',], + array $negative = [false, 0, '0', 'off',] + ): ?bool { + return Typed::boolExtendedOrNull($source, $keys, $positive, $negative); + } + } + + if (false === function_exists('arr')) { + /** + * Unlike other types, the 'array' keyword falls under a different category, + * which also prohibits its usage for function names – https://www.php.net/manual/en/reserved.keywords.php + * That's why we'll stick to using 'arr' instead. + * + * @param mixed $source + * @param int|string|array|null $keys + * @param array $default + * + * @return array + */ + function arr($source, $keys = null, array $default = []): array + { + return Typed::array($source, $keys, $default); + } + } + + if (false === function_exists('arrayOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + * + * @return array|null + */ + function arrayOrNull($source, $keys = null): ?array + { + return Typed::arrayOrNull($source, $keys); + } + } + + if (false === function_exists('object')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function object($source, $keys = null, ?object $default = null): object + { + return Typed::object($source, $keys, $default); + } + } + + if (false === function_exists('objectOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function objectOrNull($source, $keys = null): ?object + { + return Typed::objectOrNull($source, $keys); + } + } + + if (false === function_exists('dateTime')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function dateTime($source, $keys = null, ?DateTime $default = null): DateTime + { + return Typed::dateTime($source, $keys, $default); + } + } + + if (false === function_exists('dateTimeOrNull')) { + /** + * @param mixed $source + * @param int|string|array|null $keys + */ + function dateTimeOrNull($source, $keys = null): ?DateTime + { + return Typed::dateTimeOrNull($source, $keys); + } + } +} diff --git a/tests/Unit/TypedTest.php b/tests/Unit/TypedTest.php index 46873db..356bd48 100644 --- a/tests/Unit/TypedTest.php +++ b/tests/Unit/TypedTest.php @@ -8,7 +8,7 @@ class TypedTest extends TestCase { - // Note: All the class methods use the 'any()' method, so we can focus solely on it. + // Note: All the methods are decorators for the 'any()' method, so we can focus solely on it. // border cases @@ -33,7 +33,7 @@ public function testAnyReturnsDefaultWhenSourceIsNotIterable(): void $this->assertSame('default', $result); } - public function testAnyWorksWithMixedKeys(): void + public function testAnyWorksWithMixedKeysPassedAsString(): void { $result = Typed::any([ 0 => [ @@ -46,6 +46,19 @@ public function testAnyWorksWithMixedKeys(): void $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 @@ -64,7 +77,7 @@ public function testAnyMethodReturnsDefaultForMissingKeyInArray(): void $this->assertSame('default', $result); } - public function testAnyMethodWorksWithArrayAndInnerKeys(): void + public function testAnyMethodWorksWithArrayAndInnerKeysWhenPassedAsString(): void { $data = ['level1' => ['level2' => ['key' => 'value']]]; @@ -73,7 +86,16 @@ public function testAnyMethodWorksWithArrayAndInnerKeys(): void $this->assertSame('value', $result); } - public function testAnyMethodReturnsDefaultForMissingInnerKeyInArray(): void + 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' => []]]; @@ -82,6 +104,15 @@ public function testAnyMethodReturnsDefaultForMissingInnerKeyInArray(): void $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 @@ -106,7 +137,7 @@ public function testAnyMethodReturnsDefaultForMissingKeyInObjectWithTypedPropert $this->assertSame('default', $result); } - public function testAnyMethodWorksWithObjectWithTypedInnerProperties(): void + public function testAnyMethodWorksWithObjectWithTypedInnerPropertiesWhenPassedAsString(): void { $object = new class { public object $level1; @@ -131,7 +162,32 @@ public function __construct() $this->assertSame('value', $result); } - public function testAnyMethodReturnsDefaultForMissingTypedInnerPropertiesInObject(): void + 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; @@ -156,6 +212,31 @@ public function __construct() $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 @@ -177,7 +258,7 @@ public function testAnyMethodReturnsDefaultForMissingKeyInObjectWithDynamicPrope $this->assertSame('default', $result); } - public function testAnyMethodWorksWithObjectWithDynamicInnerProperties(): void + public function testAnyMethodWorksWithObjectWithDynamicInnerPropertiesWhenPassedAsString(): void { $object = new stdClass(); $object->level1 = new stdClass(); @@ -189,7 +270,19 @@ public function testAnyMethodWorksWithObjectWithDynamicInnerProperties(): void $this->assertSame('value', $result); } - public function testAnyMethodReturnsDefaultForMissingDynamicInnerPropertiesInObject(): void + 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(); @@ -200,9 +293,20 @@ public function testAnyMethodReturnsDefaultForMissingDynamicInnerPropertiesInObj $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 testAnyMethodWorksWithMixedStructures(): void + public function testAnyMethodWorksWithMixedStructuresWhenPassedAsString(): void { $object = new stdClass(); $object->property = new stdClass(); @@ -218,7 +322,23 @@ public function testAnyMethodWorksWithMixedStructures(): void $this->assertSame('value', $result); } - public function testAnyMethodReturnsDefaultForMissingKeyInMixedStructures(): void + 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(); @@ -232,4 +352,37 @@ public function testAnyMethodReturnsDefaultForMissingKeyInMixedStructures(): voi $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 testFunctionsFileSkipsDeclarationsByDefault() + { + include __DIR__ . '/../../src/functions.php'; + + $this->assertFalse(function_exists('string')); + } + + public function testFunctionsFileDeclaretsFunctionsWithConstant() + { + define('WPLAKE_TYPED_FUNCTIONS', true); + + include __DIR__ . '/../../src/functions.php'; + + $this->assertTrue(function_exists('string')); + } }