diff --git a/README.md b/README.md index 7e3d419..9e296f6 100644 --- a/README.md +++ b/README.md @@ -54,14 +54,15 @@ class User /** @var Collection $Aliases */ #[Describe([ - 'cast' => [self::class, 'mapOf'], // Casting method to use - 'type' => Alias::class, // Target type for each item - 'coerce' => true, // Coerce single elements into an array - 'using' => [self::class, 'map'], // Custom mapping function - 'map_via' => 'mapper', // Custom mapping method (defaults to 'map') - 'map' => [self::class, 'keyBy'], // Run a function for that value. - 'level' => 1, // The dimension of the array. Defaults to 1. - 'key_by' => 'key', // Key an associative array by a field. + 'cast' => [self::class, 'mapOf'], // Casting method to use + 'type' => Alias::class, // Target type for each item + 'required', // Throws PropertyRequiredException when value not present + 'coerce' => true, // Coerce single elements into an array + 'using' => [self::class, 'map'], // Custom mapping function + 'map_via' => 'mapper', // Custom mapping method (defaults to 'map') + 'map' => [self::class, 'keyBy'], // Run a function for that value. + 'level' => 1, // The dimension of the array. Defaults to 1. + 'key_by' => 'key', // Key an associative array by a field. ])] public Collection $Aliases; } @@ -81,8 +82,9 @@ class User /** @var Alias[] $Aliases */ #[Describe([ - 'cast' => [self::class, 'mapOf'], // Use the mapOf helper method - 'type' => Alias::class, // Target type for each item + 'cast' => [self::class, 'mapOf'], // Use the mapOf helper method + 'type' => Alias::class, // Target type for each item + 'required', // Throws PropertyRequiredException when value not present ])] public array $Aliases; } @@ -121,6 +123,7 @@ class User #[Describe([ 'cast' => [self::class, 'mapOf'], 'type' => Alias::class, + 'required', // Throws PropertyRequiredException when value not present ])] public \Illuminate\Support\Collection $Aliases; } @@ -161,6 +164,7 @@ class User 'cast' => [self::class, 'mapOf'], 'type' => Alias::class, 'coerce' => true, // Coerce single elements into an array + 'required', // Throws PropertyRequiredException when value not present ])] public array $Aliases; } @@ -196,6 +200,7 @@ class User 'cast' => [self::class, 'mapOf'], 'type' => Alias::class, 'using' => [self::class, 'map'], // Use custom mapping function + 'required', // Throws PropertyRequiredException when value not present ])] public Collection $Aliases; @@ -252,6 +257,7 @@ class User 'cast' => [self::class, 'mapOf'], 'type' => Alias::class, 'map_via' => 'mapper', // Use custom mapping method for the `Collection` class. + 'required', // Throws PropertyRequiredException when value not present ])] public Collection $Aliases; } @@ -305,6 +311,7 @@ class User 'cast' => [self::class, 'mapOf'], // Use the mapOf helper method 'type' => Alias::class, // Target type for each item 'level' => 2, // The dimension of the array. Defaults to 1. + 'required', // Throws PropertyRequiredException when value not present ])] public array $Aliases; } @@ -346,6 +353,7 @@ class User 'cast' => [self::class, 'mapOf'], 'type' => Alias::class, 'key_by' => 'id', + 'required', // Throws PropertyRequiredException when value not present ])] public array $Aliases; } @@ -390,6 +398,7 @@ class User 'cast' => [self::class, 'mapOf'], 'type' => Alias::class, 'map' => [self::class, 'keyBy'], + 'required', // Throws PropertyRequiredException when value not present ])] public Collection $Aliases; @@ -435,6 +444,7 @@ class User 'cast' => [self::class, 'pregReplace'], 'pattern' => ascii_only, 'replacement' => '!' // defaults to '' when not specified + 'required', // Throws PropertyRequiredException when value not present ])] public string $name; } @@ -461,7 +471,8 @@ class User 'pattern' => '/s/', // Required 'match_on' => 0 // Index of the $matches to return 'flags' => PREG_UNMATCHED_AS_NULL - 'offset' => 0 + 'offset' => 0, + 'required', // Throws PropertyRequiredException when value not present ])] public string $name; } diff --git a/src/DataModelHelper.php b/src/DataModelHelper.php index d824a9e..7bdccd4 100644 --- a/src/DataModelHelper.php +++ b/src/DataModelHelper.php @@ -31,13 +31,15 @@ trait DataModelHelper * use \Zerotoprod\DataModelHelper\DataModelHelper; * * #[Describe([ - * 'cast' => [DataModelHelper::class, 'mapOf'], // Casting method to use - * 'type' => Alias::class, // Target type for each item - * 'coerce' => true, // Coerce single elements into an array - * 'using' => [User::class, 'map'], // Custom mapping function - * 'map_via' => 'mapper', // Custom mapping method (defaults to 'map') - * 'level' => 1, // The dimension of the array. Defaults to 1. - * 'key_by' => 'key', // Key an associative array by a field. + * 'cast' => [self::class, 'mapOf'], // Casting method to use + * 'type' => Alias::class, // Target type for each item + * 'required', // Throws PropertyRequiredException when value not present + * 'coerce' => true, // Coerce single elements into an array + * 'using' => [self::class, 'map'], // Custom mapping function + * 'map_via' => 'mapper', // Custom mapping method (defaults to 'map') + * 'map' => [self::class, 'keyBy'], // Run a function for that value. + * 'level' => 1, // The dimension of the array. Defaults to 1. + * 'key_by' => 'key', // Key an associative array by a field. * ])] * public Collection $Aliases; * } @@ -53,11 +55,17 @@ trait DataModelHelper */ public static function mapOf(mixed $value, array $context, ?ReflectionAttribute $Attribute, ReflectionProperty $Property) { + $args = $Attribute?->getArguments()[0]; + if ((!empty($args['required']) || in_array('required', $args, true)) + && !isset($context[$Property->getName()]) + ) { + throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); + } + if (!$value && $Property->getType()?->allowsNull()) { return null; } - $args = $Attribute?->getArguments()[0]; $value = isset($args['coerce']) && !isset($value[0]) ? [$value] : $value; if (isset($args['using'])) { @@ -99,21 +107,27 @@ public static function mapOf(mixed $value, array $context, ?ReflectionAttribute * ``` * #[Describe([ * 'cast' => [self::class, 'pregReplace'], - * 'pattern' => '/s/', // any regular expression - * 'replacement' => '' // default + * 'pattern' => '/s/', // any regular expression + * 'replacement' => '', // default + * 'required', // Throws PropertyRequiredException when value not present * ])] * ``` */ public static function pregReplace(mixed $value, array $context, ?ReflectionAttribute $Attribute, ReflectionProperty $Property): array|string|null { + $args = $Attribute?->getArguments()[0]; + if ((!empty($args['required']) || in_array('required', $args, true)) + && !isset($context[$Property->getName()]) + ) { + throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); + } + if (!$value) { return $Property->getType()?->allowsNull() ? null : ''; } - $args = $Attribute?->getArguments()[0]; - return preg_replace($args['pattern'], $args['replacement'] ?? '', $value); } @@ -125,15 +139,23 @@ public static function pregReplace(mixed $value, array $context, ?ReflectionAttr * ``` * #[Describe([ * 'cast' => [self::class, 'pregMatch'], - * 'pattern' => '/s/', // Required - * 'match_on' => 0 // Index of the $matches to return + * 'pattern' => '/s/', // Required + * 'match_on' => 0 // Index of the $matches to return * 'flags' => PREG_UNMATCHED_AS_NULL - * 'offset' => 0 + * 'offset' => 0, + * 'required', // Throws PropertyRequiredException when value not present * ])] * ``` */ public static function pregMatch(mixed $value, array $context, ?ReflectionAttribute $Attribute, ReflectionProperty $Property) { + $args = $Attribute?->getArguments()[0]; + if ((!empty($args['required']) || in_array('required', $args, true)) + && !isset($context[$Property->getName()]) + ) { + throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); + } + if (!$value && $Property->getType()?->allowsNull()) { return null; } @@ -142,12 +164,12 @@ public static function pregMatch(mixed $value, array $context, ?ReflectionAttrib return $value; } - $args = $Attribute?->getArguments()[0]; preg_match($args['pattern'], $value, $matches, $args['flags'] ?? 0, $args['offset'] ?? 0); - if(isset($args['match_on']) && !isset($matches[$args['match_on']])) { + if (isset($args['match_on']) && !isset($matches[$args['match_on']])) { return; } + return isset($args['match_on']) ? $matches[$args['match_on']] : $matches; @@ -158,21 +180,24 @@ public static function pregMatch(mixed $value, array $context, ?ReflectionAttrib * ``` * #[Describe([ * 'cast' => [self::class, 'isUrl'], - * 'protocols' => ['http', 'udp'], // Optional. Defaults to all. - * 'on_fail' => [MyAction::class, 'method'], // Optional. Invoked when validation fails. - * 'exception' => MyException::class, // Optional. Throws an exception when not url. + * 'protocols' => ['http', 'udp'], // Optional. Defaults to all. + * 'on_fail' => [MyAction::class, 'method'], // Optional. Invoked when validation fails. + * 'exception' => MyException::class, // Optional. Throws an exception when not url. + * 'required', // Throws PropertyRequiredException when value not present * ])] * ``` */ public static function isUrl(mixed $value, array $context, ?ReflectionAttribute $Attribute, ReflectionProperty $Property): ?string { $args = $Attribute?->getArguments()[0]; - if (!$value && $Property->getType()?->allowsNull()) { - return null; + if ((!empty($args['required']) || in_array('required', $args, true)) + && !isset($context[$Property->getName()]) + ) { + throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); } - if (!$value && in_array('required', $args, true)) { - throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); + if (!$value && $Property->getType()?->allowsNull()) { + return null; } if (!is_string($value)) { @@ -202,20 +227,23 @@ public static function isUrl(mixed $value, array $context, ?ReflectionAttribute * ``` * #[Describe([ * 'cast' => [self::class, 'isEmail'], - * 'on_fail' => [MyAction::class, 'method'], // Optional. Invoked when validation fails. - * 'exception' => MyException::class, // Optional. Throws an exception when not a valid email. + * 'on_fail' => [MyAction::class, 'method'], // Optional. Invoked when validation fails. + * 'exception' => MyException::class, // Optional. Throws an exception when not a valid email. + * 'required', // Throws PropertyRequiredException when value not present * ])] * ``` */ public static function isEmail(mixed $value, array $context, ?ReflectionAttribute $Attribute, ReflectionProperty $Property): ?string { $args = $Attribute?->getArguments()[0]; - if (!$value && $Property->getType()?->allowsNull()) { - return null; + if ((!empty($args['required']) || in_array('required', $args, true)) + && !isset($context[$Property->getName()]) + ) { + throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); } - if (!$value && in_array('required', $args, true)) { - throw new PropertyRequiredException("Property `\${$Property->getName()}` is required."); + if (!$value && $Property->getType()?->allowsNull()) { + return null; } if (!is_string($value)) { diff --git a/test.sh b/test.sh old mode 100644 new mode 100755 diff --git a/tests/Unit/MapOf/Required/Alias.php b/tests/Unit/MapOf/Required/Alias.php new file mode 100644 index 0000000..5c211f3 --- /dev/null +++ b/tests/Unit/MapOf/Required/Alias.php @@ -0,0 +1,13 @@ + [[ + 'id' => 'id' + ]] + ]); + + self::assertEquals('id', $UserFalse->Aliases[0]->id); + } +} \ No newline at end of file diff --git a/tests/Unit/MapOf/Required/RequireTest.php b/tests/Unit/MapOf/Required/RequireTest.php new file mode 100644 index 0000000..470d0e5 --- /dev/null +++ b/tests/Unit/MapOf/Required/RequireTest.php @@ -0,0 +1,16 @@ +expectException(PropertyRequiredException::class); + User::from(); + } +} \ No newline at end of file diff --git a/tests/Unit/MapOf/Required/RequireTrueTest.php b/tests/Unit/MapOf/Required/RequireTrueTest.php new file mode 100644 index 0000000..02f00ba --- /dev/null +++ b/tests/Unit/MapOf/Required/RequireTrueTest.php @@ -0,0 +1,16 @@ +expectException(PropertyRequiredException::class); + UserTrue::from(); + } +} \ No newline at end of file diff --git a/tests/Unit/MapOf/Required/User.php b/tests/Unit/MapOf/Required/User.php new file mode 100644 index 0000000..7b78a10 --- /dev/null +++ b/tests/Unit/MapOf/Required/User.php @@ -0,0 +1,21 @@ + [self::class, 'mapOf'], + 'type' => Alias::class, + 'required' + ])] + public ?array $Aliases; +} \ No newline at end of file diff --git a/tests/Unit/MapOf/Required/UserFalse.php b/tests/Unit/MapOf/Required/UserFalse.php new file mode 100644 index 0000000..693df9f --- /dev/null +++ b/tests/Unit/MapOf/Required/UserFalse.php @@ -0,0 +1,21 @@ + [self::class, 'mapOf'], + 'type' => Alias::class, + 'required' => false, + ])] + public ?array $Aliases; +} \ No newline at end of file diff --git a/tests/Unit/MapOf/Required/UserTrue.php b/tests/Unit/MapOf/Required/UserTrue.php new file mode 100644 index 0000000..54a277b --- /dev/null +++ b/tests/Unit/MapOf/Required/UserTrue.php @@ -0,0 +1,21 @@ + [self::class, 'mapOf'], + 'type' => Alias::class, + 'required' => true, + ])] + public ?array $Aliases; +} \ No newline at end of file diff --git a/tests/Unit/PregMatch/UserRequired.php b/tests/Unit/PregMatch/UserRequired.php new file mode 100644 index 0000000..2db4a09 --- /dev/null +++ b/tests/Unit/PregMatch/UserRequired.php @@ -0,0 +1,22 @@ + [self::class, 'pregMatch'], + 'pattern' => '/s/', + 'required' + ])] + public array $name; +} \ No newline at end of file diff --git a/tests/Unit/PregMatch/UserRequiredTest.php b/tests/Unit/PregMatch/UserRequiredTest.php new file mode 100644 index 0000000..f71f4cb --- /dev/null +++ b/tests/Unit/PregMatch/UserRequiredTest.php @@ -0,0 +1,17 @@ +expectException(PropertyRequiredException::class); + + UserRequired::from(); + } +} \ No newline at end of file diff --git a/tests/Unit/PregReplace/PregReplaceRequiredTest.php b/tests/Unit/PregReplace/PregReplaceRequiredTest.php new file mode 100644 index 0000000..8425f8c --- /dev/null +++ b/tests/Unit/PregReplace/PregReplaceRequiredTest.php @@ -0,0 +1,17 @@ +expectException(PropertyRequiredException::class); + + UserRequired::from(); + } +} \ No newline at end of file diff --git a/tests/Unit/PregReplace/UserRequired.php b/tests/Unit/PregReplace/UserRequired.php new file mode 100644 index 0000000..4ced07d --- /dev/null +++ b/tests/Unit/PregReplace/UserRequired.php @@ -0,0 +1,22 @@ + [self::class, 'pregReplace'], + 'pattern' => '/[^\x00-\x7F]/', + 'required' + ])] + public string $name; +} \ No newline at end of file