Skip to content

Commit

Permalink
Add 'required' to features. (#20)
Browse files Browse the repository at this point in the history
Co-authored-by: david_smith <[email protected]>
  • Loading branch information
zero-to-prod and david_smith authored Nov 18, 2024
1 parent 3713df8 commit a3b2898
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 41 deletions.
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ class User

/** @var Collection<int, Alias> $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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
88 changes: 58 additions & 30 deletions src/DataModelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
* }
Expand All @@ -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'])) {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down
Empty file modified test.sh
100644 → 100755
Empty file.
13 changes: 13 additions & 0 deletions tests/Unit/MapOf/Required/Alias.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Tests\Unit\MapOf\Required;

use Zerotoprod\DataModel\DataModel;

class Alias
{
use DataModel;

public string $id;
public string $name;
}
21 changes: 21 additions & 0 deletions tests/Unit/MapOf/Required/RequireFalseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Tests\Unit\MapOf\Required;

use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
use Zerotoprod\DataModel\PropertyRequiredException;

class RequireFalseTest extends TestCase
{
#[Test] public function from(): void
{
$UserFalse = UserFalse::from([
'Aliases' => [[
'id' => 'id'
]]
]);

self::assertEquals('id', $UserFalse->Aliases[0]->id);
}
}
16 changes: 16 additions & 0 deletions tests/Unit/MapOf/Required/RequireTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Tests\Unit\MapOf\Required;

use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
use Zerotoprod\DataModel\PropertyRequiredException;

class RequireTest extends TestCase
{
#[Test] public function from(): void
{
$this->expectException(PropertyRequiredException::class);
User::from();
}
}
16 changes: 16 additions & 0 deletions tests/Unit/MapOf/Required/RequireTrueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Tests\Unit\MapOf\Required;

use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
use Zerotoprod\DataModel\PropertyRequiredException;

class RequireTrueTest extends TestCase
{
#[Test] public function from(): void
{
$this->expectException(PropertyRequiredException::class);
UserTrue::from();
}
}
21 changes: 21 additions & 0 deletions tests/Unit/MapOf/Required/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Tests\Unit\MapOf\Required;

use Zerotoprod\DataModel\DataModel;
use Zerotoprod\DataModel\Describe;
use Zerotoprod\DataModelHelper\DataModelHelper;

class User
{
use DataModel;
use DataModelHelper;

/** @var Alias[] $Aliases */
#[Describe([
'cast' => [self::class, 'mapOf'],
'type' => Alias::class,
'required'
])]
public ?array $Aliases;
}
Loading

0 comments on commit a3b2898

Please sign in to comment.