Skip to content

zero-to-prod/data-model-helper

Repository files navigation

Zerotoprod\DataModelHelper

Repo GitHub Actions Workflow Status Packagist Downloads php Packagist Version License

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