diezz/yaml-to-object-mapper

Yaml to Object mapper

0.1.1 2024-02-11 19:15 UTC

This package is auto-updated.

Last update: 2024-05-18 23:06:23 UTC


README

Scrutinizer Code Quality Build Status Code Coverage

Yaml to Object Mapper

Bridge the Gap Between YAML and PHP Objects

Effortlessly transform your YAML configurations into robust PHP objects with the Yaml to Object Mapper library. Embrace efficient mapping, powerful validation, and flexible variable processing, all while leveraging the convenience of PHP 8 attributes.

Key Features:

  • Seamless Mapping: Effortlessly map hierarchical YAML data to deeply nested PHP objects, saving you tedious manual configuration.
  • Comprehensive Validation: Ensure data integrity with built-in validation mechanisms like #[Required] attributes, type hints, and customizable rules.
  • Declarative DSL: Customize attribute-based configuration for fields and validation, reducing boilerplate code and promoting readability.
  • Variable Processing: Utilize dynamic variables like ${env:DB_PASSWORD} directly within your YAML files, streamlining configuration management.
  • Custom Argument Resolvers: Gain ultimate flexibility by crafting custom logic for processing variables, tailored to your specific needs.
  • Built-in Variables: Enjoy a range of pre-defined variables like now, self, and format for common tasks within your YAML configurations.
  • PHP 8 Integration: Leverage the power of PHP 8 attributes for cleaner, more concise code, enhancing your development experience.

Installation

composer require diezz/yaml-to-object-mapper

Basic usage

We have a simple config.yml file

name: object mapper
connection:
  host: localhost
  port: 3202
  username: test
  password: password

which we want to be mapped to the object of class:

class Config {
    public string $name;
    public ConnectionSettings $connection
}

class ConnectionSettings {
    public string $host;
    public string $port;
    public string $username;
    public string $password;
}

$config = Mapper::make()->mapFromFile(Config::class, 'config.yml');

Setting Values in Target Class Properties

Setting Values in Target Class Properties The mapper automatically assigns mapped values to corresponding properties in the target class. Here's what to keep in mind:

Property Access:

  • Public Properties: Direct assignment for seamless updates.
  • Non-Public Properties: Require corresponding setter methods.

Setter Naming:

  • Default Approach: Uses a setter named like the property with a set prefix (e.g., setName for name property).
  • Custom Naming: Override the default with the #[Setter('yourSetterName')] attribute on the property (e.g., #[Setter('updateName')]).

Extended usage

Dynamic Field Mapping with Default Value Resolvers

Tired of writing redundant property names and nested lists in your YAML? Default value resolvers let you automatically infer field names and structure, streamlining your configuration.

Example:

Instead of this verbose YAML:

tables:
  - name: users
    columns:
      username: varchar(255)
      email: varchar(255)
      password: varchar(255)

  - name: orders
    columns: 
      id: int
      user_id: int
      price: float
use Diezz\YamlToObjectMapper\Attributes\Collection;

class DatabaseSchema {
    #[Collection(class: Table::class)]
    public array $tables;
}

class Table {
    public string $name;
    public array $columns;
}

Imagine the conciseness of:

tables:
  users:
    username: varchar(255)
    email: varchar(255)
    password: varchar(255)

  orders:
    id: int
    user_id: int
    price: float
class Table {
    #[DefaultValueResolver(resolver: DefaultValueResolver::PARENT_KEY)]
    public string $name;
    
    #[DefaultValueResolver(resolver: DefaultValueResolver::NESTED_LIST)]
    public array $columns;
}

How it works:

  • [DefaultValueResolver(resolver: DefaultValueResolver::PARENT_KEY)]: Automatically sets the name field of each table object to its key in the YAML list.
  • [DefaultValueResolver(resolver: DefaultValueResolver::NESTED_LIST)]: Treats each YAML list item as a property of the object and creates an associative array based on the keys.

Validation

By default, the mapper checks that required fields are present in yml file. There are multiple ways how the mapper defines which field is required:

  1. The most obvious way is mark required field with #[Required] attribute.
  2. If class field isn't marked with required attribute the mapper checks type hint of the field. It could be a php 7 type hint or phpdoc comment. Nullable properties or properties initiated with default value are treated as not required or vice verse.

Maintain the consistency and accuracy of your configurations with the library's integrated validation mechanisms. It offers a flexible approach to define required fields and enforce data types.

How It Works:

  • Explicit Marking with #[Required]: Clearly indicate mandatory fields using the #[Required] attribute. This provides the most direct control over validation expectations.
  • Type Hints and Doc Comments:
    • PHP 7 Type Hints: Fields with explicit type hints (e.g., public string $name) are implicitly treated as required.
    • Doc Comments: Fields with type hints in their doc comments (e.g., @var string) are also considered required.
  • Default Values and Nullable Types:
    • Fields with Default Values: Fields assigned a default value in their declaration are not required, as they have a fallback value.
    • Nullable Types: Nullable fields (using a question mark or |null in the type declaration) are not required, as they can accept null as a valid value.
use Diezz\YamlToObjectMapper\Attributes\Required;

class Model {
    /**
     * Explicitly required field
     */
    #[Required]
    public $value0;

    /**
     * Required due to string type hint
     */
    public string $value1;

    /**
     * Required based on type hint in doc comment
     *
     * @var string
     */
    public $value2;

    /**
     * Not required due to default value
     */
    public string $value3 = 'value3';

    /**
     * Not required due to nullable type hint
     */
    public ?string $value4;

    /**
     * Not required due to nullable type hint in doc comment
     *
     * @var string|null
     */
    public $value5;
}

Dynamic Configuration with Variable Processing

Unleash the full potential of your YAML configurations with dynamic variable processing. The library supports various built-in variables and even allows you to create custom logic for advanced scenarios

Built-in variables:

  • self - Access values within the same configuration object, enabling self-referencing and cross-property dependencies. ${self::connection.host}
  • substring - Extract a portion of a string, providing string manipulation capabilities ${substring:${self:name}:7}
  • now - Retrieve the current date and time, enabling dynamic timestamps and time-based configurations ${now}
  • format - Format dates and times according to specified patterns, ensuring consistent date/time representations. ${format:${now}:Y-M-D}
  • env - Read environment variables, securely integrating configuration with environment-specific settings ${env:DB_PASSWORD}

Syntax:

${varName:firstArgument:secondArgument}

Another variable could be passed as an argument of another one, for example:

${varName1:${varName2:argument1}:argument2}

Custom Argument Resolvers

For even more flexibility, create custom argument resolvers to handle unique variable processing needs.

Steps:

  • Create a Custom Resolver Class: Extend the CustomArgumentResolver class and implement the doResolve method to define the variable's logic.
  • Register the Resolver: Use the Mapper::registerCustomArgumentResolver method to associate the resolver with a variable name.
  • Utilize the Variable in YAML: Use the registered variable within your YAML configuration, passing arguments as needed.

In top of variable processing there is an ArgumentResolver. You can create your own ArgumentResolvers which gives an ability of processing custom variables in your yaml files. To do so create a class extending CustomArgumentResolver. Constructor of your CustomArgumentResolver implementation should accept ArgumentResolver which is resolver for argument of the variable. Count of params in the constuctor should be equals to count of argument supporting by the variable.

class SumArgumentResolver extends CustomArgumentResolver
{
    private array $arguments;

    public function __construct(...$arguments)
    {
        $this->arguments = $arguments;
    }

    protected function doResolve($context = null): int
    {
        $sum = 0;
        foreach ($this->arguments as $iValue) {
            $sum += $iValue->resolve($context);
        }

        return $sum;
    }
}

$mapper = Mapper::make();
//Register the resolver
$mapper->registerCustomArgumentResolver('sum', SumArgumentResolver::class);
$result = $mapper->map($file, Output::class);

and now you can use inside yaml file, for example

price:
  item1: 100
  item2: 200
total: ${sum:${self:price.item1}:${self:price.item2}}