Awesome
Zerotoprod\DataModelHelper
Utilities for casting values using the DataModel package.
Installation
Install the package via Composer:
composer require zero-to-prod/data-model-helper
Usage
Including the Trait
Include the DataModelHelper
trait in your class to access helper methods:
class DataModelHelper
{
use \Zerotoprod\DataModelHelper\DataModelHelper;
}
Helper Methods
- when: Create a map of any type by using
- mapOf: Create a map of any type by using
- pregReplace: Perform a regular expression search and replace.
- pregMatch: Perform a regular expression match.
- isUrl: Validates a url.
- isEmail: Validates an email.
- isMultiple: Validate a value is a multiple of another.
when
Use when
to call a function based on a condition.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
#[Describe([
'cast' => [self::class, 'when'],
'eval' => <<<'PHP' // Provides (mixed $value, array $context, ?ReflectionAttribute $Attribute, ?ReflectionProperty $Property)
$value >= $context["value_2"] // The expression to evaluate.
PHP,
'true' => [MyAction::class, 'passed'], // Optional. Invoked when condition is true.
'false' => [MyAction::class, 'failed'], // Optional. Invoked when condition is true.
'required', // Throws PropertyRequiredException when value not present.
])]
public string $value;
}
mapOf
Create a map of any type by using the DataModelHelper::mapOf()
method.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Collection<int, Alias> $Aliases */
#[Describe([
'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;
}
Usage
In this case the mapOf()
method returns an array of Alias
instances.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Alias[] $Aliases */
#[Describe([
'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;
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $name;
}
$User = User::from([
'Aliases' => [
['name' => 'John Doe'],
['name' => 'John Smith'],
]
]);
echo $User->Aliases[0]->name; // Outputs: John Doe
echo $User->Aliases[1]->name; // Outputs: John Smith
Laravel Collection Example
The mapOf
helper is designed to work will with the \Illuminate\Support\Collection
class.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Collection<int, Alias> $Aliases */
#[Describe([
'cast' => [self::class, 'mapOf'],
'type' => Alias::class,
'required', // Throws PropertyRequiredException when value not present
])]
public \Illuminate\Support\Collection $Aliases;
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $name;
}
$User = User::from([
'Aliases' => [
['name' => 'John Doe'],
['name' => 'John Smith'],
]
]);
echo $User->Aliases->first()->name; // Outputs: John Doe
Coercing
Sometimes, an attribute may contain either a single element or an array of elements. By setting 'coerce' => true
, you can ensure that single
elements
are coerced into an array.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Alias[] $Aliases */
#[Describe([
'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;
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $name;
}
$User = User::from([
'Aliases' => ['name' => 'John Doe'], // Single element instead of an array
]);
echo $User->Aliases[0]->name; // Outputs: John Doe
Using a Custom Mapping Function
Specify your mapping function by setting the using
option.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Collection $Aliases */
#[Describe([
'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;
public static function map(array $values): Collection
{
// Map each value to an Alias instance
$items = array_map(fn($value) => Alias::from($value), $values);
// Return as a Collection
return new Collection($items);
}
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $name;
}
class Collection
{
public array $items;
public function __construct(array $items = [])
{
$this->items = $items;
}
}
$User = User::from([
'Aliases' => [
['name' => 'John Doe'],
],
]);
echo $User->Aliases->items[0]->name; // Outputs: John Doe
Specifying a Custom Mapping Method
By default, the map method is used to map over elements. You can specify a different method using the map_via
option.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Collection $Aliases */
#[Describe([
'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;
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $name;
}
class Collection
{
public array $items;
public function __construct(array $values)
{
$this->items = $values;
}
public function mapper(callable $callable): Collection
{
$this->items = array_map($callable, $this->items);
return $this;
}
}
$User = User::from([
'Aliases' => [
['name' => 'John Doe'],
],
]);
echo $User->Aliases->items[0]->name; // Outputs: John Doe
Deep Mapping
You can set the level for mapping deep arrays.
use Zerotoprod\DataModel\Describe;
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Alias[] $Aliases */
#[Describe([
'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;
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $name;
}
$User = User::from([
'Aliases' => [
[
['name' => 'John Doe'],
['name' => 'John Smith'],
]
]
]);
echo $User->Aliases[0][0]->name; // Outputs: John Doe
echo $User->Aliases[0][1]->name; // Outputs: John Smith
KeyBy
Key an array by an element value by using the key_by
argument.
This also supports deep mapping.
Note: this only applies to arrays.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Alias[] $Aliases */
#[Describe([
'cast' => [self::class, 'mapOf'],
'type' => Alias::class,
'key_by' => 'id',
'required', // Throws PropertyRequiredException when value not present
])]
public array $Aliases;
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $id;
public string $name;
}
$User = User::from([
'Aliases' => [
[
'id' => 'jd1',
'name' => 'John Doe',
],
[
'id' => 'js1',
'name' => 'John Smith'
],
]
]);
echo $User->Aliases['jd1']->name; // 'John Doe'
echo $User->Aliases['js1']->name); // 'John Smith'
Map
Call a function for that value.
Note: This does not work with arrays.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
/** @var Alias[] $Aliases */
#[Describe([
'cast' => [self::class, 'mapOf'],
'type' => Alias::class,
'map' => [self::class, 'keyBy'],
'required', // Throws PropertyRequiredException when value not present
])]
public Collection $Aliases;
public static function keyBy(Collection $values): Collection
{
return $values->keyBy('id');
}
}
class Alias
{
use \Zerotoprod\DataModel\DataModel;
public string $id;
public string $name;
}
$User = User::from([
'Aliases' => [
[
'id' => 'jd1',
'name' => 'John Doe',
]
]
]);
echo $User->Aliases->get('jd1')->name; // 'John Doe'
pregReplace
Use pregReplace
to perform a regular expression search and replace.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
public const ascii_only = '/[^\x00-\x7F]/';
#[Describe([
'cast' => [self::class, 'pregReplace'],
'pattern' => ascii_only,
'replacement' => '!' // defaults to '' when not specified
'required', // Throws PropertyRequiredException when value not present
])]
public string $name;
}
$User = User::from([
'name' => 'Trophy🏆',
]);
echo $User->name; // Outputs: 'Trophy!'
pregMatch
Use pregMatch
to perform a regular expression match.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
#[Describe([
'cast' => [self::class, 'pregMatch'],
'pattern' => '/s/', // Required
'match_on' => 0 // Index of the $matches to return
'flags' => PREG_UNMATCHED_AS_NULL
'offset' => 0,
'required', // Throws PropertyRequiredException when value not present
])]
public string $name;
}
$User = User::from([
'name' => 'sarah',
]);
echo $User->name; // Outputs: 's'
isUrl
Use isUrl
to validate an url.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
#[Describe([
'cast' => [self::class, 'isUrl'],
'protocols' => ['http', 'udp'], // Optional. Defaults to all.
'on_fail' => [MyAction::class, 'method'], // Optional. Invoked when validation fails.
'exception' => MyCustomException::class, // Optional. Throws an exception when not url.
'required' // Optional. Throws \Zerotoprod\DataModel\PropertyRequiredException::class
])]
public string $url;
}
isEmail
Use isEmail
to validate an email.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
#[Describe([
'cast' => [self::class, 'isEmail'],
'on_fail' => [MyAction::class, 'method'], // Optional. Invoked when validation fails.
'exception' => MyCustomException::class, // Optional. Throws an exception when not url.
'required' // Optional. Throws \Zerotoprod\DataModel\PropertyRequiredException::class
])]
public string $url;
}
isMultiple
Use isMultiple
to validate a value is a multiple of another.
class User
{
use \Zerotoprod\DataModel\DataModel;
use \Zerotoprod\DataModelHelper\DataModelHelper;
#[Describe([
'cast' => [self::class, 'isMultiple'],
'of' => 2 // The number the value is a multiple of
'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 string $url;
}