Skip to content

Data

This page covers five data processing utilities: condition providers, data enrichers, data normalizers, data transformers, and data reducers. Each is a framework-level extension point with a dedicated DI tag that modules use to register implementations.


Condition providers

The condition system represents rule-based matching as a structured data tree. A Condition holds one or more ConditionGroup objects, each group holds ConditionItem entries and has a match type (any, all, or none). Items can have nested groups for sub-conditions.

Data structures

ConditionInterface
  └── ConditionGroupInterface[]  (getGroups)
        ├── matchType: 'any' | 'all' | 'none'     (ConditionGroupInterface::MATCH_TYPE_*)
        └── ConditionItemInterface[]  (getConditions)
              ├── key: string
              ├── operator: string
              ├── value: mixed
              └── children: ConditionGroupInterface[]   (nested sub-conditions)

Condition key providers

A condition key provider describes what fields are available for building conditions against a given subject. The UI reads these to populate the condition builder; the resolver reads them to evaluate conditions.

Implement ConditionKeyProviderInterface:

use Amarant\Framework\Contract\Condition\Provider\ConditionKeyInterface;
use Amarant\Framework\Contract\Condition\Provider\ConditionKeyProviderInterface;
use Amarant\Framework\Data\Condition\Provider\ConditionKey;
use Amarant\Framework\Search\Builder\Filter;
use Override;

final class OrderConditionProvider implements ConditionKeyProviderInterface
{
    public const string CODE = 'order_condition_provider';

    public function getCode(): string
    {
        return self::CODE;
    }

    public function getLabel(): string
    {
        return 'Order';
    }

    public function getKeys(): array
    {
        return [
            new ConditionKey(
                providerCode: self::CODE,
                key: 'order.total',
                label: 'Order total',
                dataType: ConditionKeyInterface::DATA_TYPE_PRICE,
                allowedOperators: [
                    Filter::OPERATOR_EQ  => Filter::OPERATOR_LABEL_EQ,
                    Filter::OPERATOR_GT  => Filter::OPERATOR_LABEL_GT,
                    Filter::OPERATOR_GTE => Filter::OPERATOR_LABEL_GTE,
                    Filter::OPERATOR_LT  => Filter::OPERATOR_LABEL_LT,
                    Filter::OPERATOR_LTE => Filter::OPERATOR_LABEL_LTE,
                ]
            ),
            new ConditionKey(
                providerCode: self::CODE,
                key: 'order.status',
                label: 'Order status',
                dataType: ConditionKeyInterface::DATA_TYPE_OPTIONS,
                allowedOperators: [
                    Filter::OPERATOR_IN  => Filter::OPERATOR_LABEL_IN,
                    Filter::OPERATOR_NIN => Filter::OPERATOR_LABEL_NIN,
                ],
                options: ['pending' => 'Pending', 'complete' => 'Complete', 'cancelled' => 'Cancelled']
            ),
        ];
    }

    public function hasKey(string $key): bool
    {
        foreach ($this->getKeys() as $conditionKey) {
            if ($conditionKey->getKey() === $key) {
                return true;
            }
        }
        return false;
    }

    public function getKey(string $key): ?ConditionKeyInterface
    {
        foreach ($this->getKeys() as $conditionKey) {
            if ($conditionKey->getKey() === $key) {
                return $conditionKey;
            }
        }
        return null;
    }

    public function supports(mixed $subject): bool
    {
        return $subject === Order::class || is_subclass_of($subject, Order::class);
    }
}

ConditionKey constructor

Parameter Type Description
providerCode string Code of the owning provider (ConditionKeyProviderInterface::getCode()).
key string Dot-notation key used in condition items (e.g. 'order.total').
label string Human-readable label shown in the condition builder.
dataType string One of the ConditionKeyInterface::DATA_TYPE_* constants (see below).
allowedOperators array<string, string> Map of Filter::OPERATOR_* constant → Filter::OPERATOR_LABEL_* constant.
options array<string, string> Value → label pairs for DATA_TYPE_OPTIONS fields.
multiple bool Whether multiple values can be selected (for option-type fields).
explodeable bool Whether comma-separated string values should be split before matching.

Available data types (constants on ConditionKeyInterface):

Constant Meaning
DATA_TYPE_BOOL Boolean — operators: OPERATOR_TRUE, OPERATOR_FALSE
DATA_TYPE_TEXT String — operators: OPERATOR_EQ, OPERATOR_NEQ, OPERATOR_LIKE, etc.
DATA_TYPE_NUMBER Numeric — operators: OPERATOR_EQ, OPERATOR_GT, OPERATOR_GTE, etc.
DATA_TYPE_PRICE Monetary amount (integer cents) — same operators as number
DATA_TYPE_DATE Date string — operators: OPERATOR_GT, OPERATOR_GTE, OPERATOR_RANGE, etc.
DATA_TYPE_DATETIME Datetime string — same operators as date
DATA_TYPE_OPTIONS Enum-like — operators: OPERATOR_IN, OPERATOR_NIN

Registering a provider

Vendor/ModuleName/Configuration/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\Condition\Provider\OrderConditionProvider;

$container->tag(
    [OrderConditionProvider::class],
    [DiEnum::CONDITION_KEY_PROVIDER]
);

Priority is optional. When multiple providers are tagged, they are all returned by ConditionKeyProviderPool::getProviders(). Use getSupportedProviders($subject) to filter to those whose supports() returns true for the current subject.

Attribute-based condition keys

AttributeConditionKeyProviderInterface is a special provider that generates condition keys directly from entity attribute definitions. Inject it and call getKeysForEntity():

use Amarant\Framework\Contract\Condition\Provider\AttributeConditionKeyProviderInterface;

$attributeKeys = $this->attributeConditionKeyProvider->getKeysForEntity(
    entityName: 'product',
    conditionPrefix: 'attributes.',
    labelPrefix: '',
    labelSuffix: ' (attribute)'
);

The framework registers this automatically. Keys are prefixed with conditionPrefix (default 'attributes.') so they do not clash with static keys from other providers.

Condition resolvers

A condition resolver evaluates a ConditionInterface tree against a subject and returns a result.

Implement ConditionResolverInterface:

use Amarant\Framework\Contract\Condition\Resolver\ConditionResolverInterface;
use Amarant\Framework\Contract\Condition\Resolver\Result\ConditionResolverResultInterface;

final class OrderConditionResolver implements ConditionResolverInterface
{
    public static function supports(mixed $subject): bool
    {
        return $subject instanceof Order;
    }

    public function resolve(mixed $subject): ConditionResolverResultInterface
    {
        // evaluate $subject against the condition tree, return a result
    }
}

There is no framework DI tag for resolvers — wire them directly where needed or tag them in a module-level pool if the project adds a resolver collection.

Result interfaces:

Interface Method Use case
ConditionResolverIdResultInterface getIds(): iterable<int\|string> Return IDs of entities that matched
ConditionResolverDataResultInterface getData(): iterable<mixed> Return the matched objects directly

Data enrichers

A data enricher mutates an object in place. It adds computed or related data to a subject without returning a new value.

Interface

namespace Amarant\Framework\Contract\DataEnricher;

interface DataEnricherInterface
{
    public static function supports(mixed $subject, array $context = []): bool;
    public function enrich(mixed $subject, array $context = []): void;
}

supports() is static — it is called on the class before the instance is resolved from the container.

Implementing an enricher

use Amarant\Framework\Contract\DataEnricher\DataEnricherInterface;
use Override;

final class ProductStockEnricher implements DataEnricherInterface
{
    public function __construct(
        private readonly StockRepositoryInterface $stockRepository
    ) {
    }

    #[Override] public static function supports(mixed $subject, array $context = []): bool
    {
        return $subject instanceof ProductModel;
    }

    #[Override] public function enrich(mixed $subject, array $context = []): void
    {
        /** @var ProductModel $subject */
        $subject->stock = $this->stockRepository->getStockByProductId($subject->id);
    }
}

Registering an enricher

Vendor/ModuleName/Configuration/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\DataEnricher\ProductStockEnricher;

$container->tag(
    [ProductStockEnricher::class],
    [DiEnum::DATA_ENRICHER]
);

The framework wires all DiEnum::DATA_ENRICHER-tagged classes into ChainedDataEnricher. Inject DataEnricherInterface anywhere you need the full chain:

use Amarant\Framework\Contract\DataEnricher\DataEnricherInterface;

public function __construct(
    private readonly DataEnricherInterface $dataEnricher
) {
}

// ...
$this->dataEnricher->enrich($productModel);

Data normalizers

A data normalizer converts a value to a different representation and returns the result. Unlike an enricher it does not mutate — it produces a new value from the input.

Interface

namespace Amarant\Framework\Contract\DataNormalizer;

interface DataNormalizerInterface
{
    public static function supports(mixed $data, array $context = []): bool;
    public function normalize(mixed $data, array $context = []): mixed;
}

supports() is static — it is called on the class before resolving the instance.

DataNormalizerPool

The pool is injected wherever normalization is needed. It provides:

Method Description
normalize(mixed $data, array $context = []) Finds the first supported normalizer and returns the normalized value. Returns $data unchanged if no normalizer matches.
getSupported(mixed $data, array $context = []) Returns the first matching normalizer instance, or null.
get(string $classname) Retrieves a normalizer directly by its class name.

The context passed to normalize() is automatically enriched with context['pool'] = $this, so normalizers can recursively normalize nested values via the pool.

Implementing a normalizer

use Amarant\Framework\Contract\DataNormalizer\DataNormalizerInterface;
use Override;

final class PriceValueNormalizer implements DataNormalizerInterface
{
    #[Override] public static function supports(mixed $data, array $context = []): bool
    {
        return $data instanceof PriceValue;
    }

    #[Override] public function normalize(mixed $data, array $context = []): mixed
    {
        /** @var PriceValue $data */
        return [
            'amount'   => $data->getAmount(),
            'currency' => $data->getCurrency(),
        ];
    }
}

Registering a normalizer

Vendor/ModuleName/Configuration/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\DataNormalizer\PriceValueNormalizer;

$container->tag(
    [300 => PriceValueNormalizer::class],
    [DiEnum::DATA_NORMALIZER]
);

Normalizers are keyed by class name in the pool and checked in descending priority order. Higher numeric key = checked first. Built-in normalizer priorities for reference:

Priority Normalizer Converts
550 MediaEntryUriNormalizer MediaEntryUriMediaEntry
500 AttributeAwareDataModelArrayNormalizer Attribute-aware DataModel → PHP array
450 AttributeValueMediaUriNormalizer Attribute value with media URI → model
400 UploadedFileNormalizer UploadedFile (or array) → content or media URI

Register custom normalizers below 400 unless they must take precedence over the built-ins.


Data transformers

A data transformer converts an object of one type to another type. The target type is specified explicitly as a class-name string. This is the primary mechanism for mapping DataModel objects to their output models (DTOs).

Interface

namespace Amarant\Framework\Contract\DataTransformer;

interface DataTransformerInterface
{
    public function supports(mixed $subject, string $to, array $context = []): bool;
    public function transform(mixed $subject, string $to, array $context = []): mixed;
}

Unlike enrichers and normalizers, supports() is an instance method (not static). The $to parameter is the fully-qualified target class name.

Context constants

Constant Description
DataTransformerInterface::CONTEXT_DATA_TRANSFORMER The chained transformer instance is injected under this key so individual transformers can delegate recursively.
DataTransformerInterface::CONTEXT_DATA_TRANSFORMER_GROUP When set, only transformers tagged to that group are iterated (see Groups).
DataTransformerInterface::CONTEXT_NORMALIZATION_GROUPS Serialization groups forwarded from the API layer.
DataTransformerInterface::CONTEXT_RESOURCE_OPERATION The current API resource operation, when called from the API layer.
DataTransformerInterface::OUTPUT_INSTANCE Pre-created output object. When set, a transformer may populate it instead of constructing a new one.

Implementing a transformer

use Amarant\Framework\Contract\DataTransformer\DataTransformerInterface;
use Override;

final class ProductToProductModelDataTransformer implements DataTransformerInterface
{
    #[Override] public function supports(mixed $subject, string $to, array $context = []): bool
    {
        return $subject instanceof Product && $to === ProductModel::class;
    }

    #[Override] public function transform(mixed $subject, string $to, array $context = []): mixed
    {
        /** @var Product $subject */
        $model = new ProductModel();
        $model->id   = (int) $subject->getIdValue();
        $model->name = (string) $subject->getName();
        $model->sku  = (string) $subject->getSku();
        return $model;
    }
}

Registering a transformer

Vendor/ModuleName/Configuration/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\DataTransformer\DataModel\ProductToProductModelDataTransformer;

$container->tag(
    [50 => ProductToProductModelDataTransformer::class],
    [DiEnum::DATA_TRANSFORMER]
);

Transformers are checked in descending priority order. The first whose supports() returns true handles the call. Framework built-in transformers use priorities 100–450; register module transformers in the same range or below, depending on whether they should run before or after the built-ins.

Using the transformer

Inject DataTransformerInterface — the DI container resolves it to the ChainedDataTransformer:

use Amarant\Framework\Contract\DataTransformer\DataTransformerInterface;

public function __construct(
    private readonly DataTransformerInterface $dataTransformer
) {
}

// ...
$model = $this->dataTransformer->transform($product, ProductModel::class);

Groups

The transformer supports group-scoped iteration. When CONTEXT_DATA_TRANSFORMER_GROUP is set in the context, the chained transformer only iterates transformers that are tagged under that group name. This allows modules to register transformers that are active only in specific contexts (e.g. backend-only model variants) without interfering with the global chain.

use Amarant\Framework\Contract\DataTransformer\DataTransformerInterface;

$model = $this->dataTransformer->transform(
    $product,
    ProductModel::class,
    [DataTransformerInterface::CONTEXT_DATA_TRANSFORMER_GROUP => 'backend']
);

Tag the transformer under the group name when registering:

$container->tag(
    ['backend' => BackendProductToProductModelDataTransformer::class],
    [DiEnum::DATA_TRANSFORMER]
);

Data reducers

A data reducer strips or modifies data on a model before it is serialized. Where enrichers add data and transformers convert between types, reducers remove or override fields that should not be part of the serialized output — for example, removing invisible attributes or replacing stored prices with request-time contextual prices.

Reducers run automatically in the frontend area via ReducerSerializerEventSubscriber, which fires on EventEnum::SERIALIZATION_SERIALIZE_BEFORE at priority −999. Every model that goes through the serializer will have reduce() called on it before serialization begins. The ReducerInterface binding and the subscriber are only wired in the frontend area DI.

Interface

namespace Amarant\Framework\Contract\Search\Normalization;

interface ReducerInterface
{
    public static function supports(mixed $data): bool;
    public function reduce(mixed $data): void;
}

supports() is static — called on the class before resolving the instance. reduce() mutates the subject in place; there is no return value.

Implementing a reducer

use Amarant\Framework\Contract\Search\Normalization\ReducerInterface;
use Override;

final class ProductVisibilityReducer implements ReducerInterface
{
    public function __construct(
        private readonly VisibilityResolverInterface $visibilityResolver
    ) {
    }

    #[Override] public static function supports(mixed $data): bool
    {
        return $data instanceof ProductModel;
    }

    #[Override] public function reduce(mixed $data): void
    {
        /** @var ProductModel $data */
        if (!$this->visibilityResolver->isVisible($data)) {
            $data->visible = false;
        }
    }
}

Registering a reducer

Vendor/ModuleName/Configuration/Frontend/Di.php
use Amarant\Framework\Contract\Di\ConfigurableContainerInterface;
use Amarant\Framework\Enum\DiEnum;
use Vendor\ModuleName\Search\Normalization\ProductVisibilityReducer;

$container->tag(
    [400 => ProductVisibilityReducer::class],
    [DiEnum::DATA_REDUCER]
);

Reducers are checked in descending priority order. The built-in AttributeAwareModelReducer runs at priority 500. Register module reducers below 500 unless they need to run before the attribute reducer.

Frontend area only

ReducerInterface and the reducer chain are only bound in the frontend area. Register reducers in Configuration/Frontend/Di.php, not in Configuration/Di.php.

Attribute reducers

AttributeAwareModelReducer (built-in, priority 500) is a ReducerInterface implementation that iterates the attribute values of any AttributeAwareModelInterface and removes those for which an attribute reducer returns true from shouldReduce().

Implement AttributeReducerInterface to control per-attribute visibility:

use Amarant\Framework\Contract\Search\Normalization\Attribute\AttributeReducerInterface;
use Amarant\Framework\DataModel\Attribute;
use Override;

final class SearchableAttributeReducer implements AttributeReducerInterface
{
    #[Override] public static function supports(string $entityName, Attribute $attribute, mixed $value): bool
    {
        return $entityName === 'product';
    }

    #[Override] public function shouldReduce(string $entityName, Attribute $attribute, mixed $value): bool
    {
        return !$attribute->isFlagged(AttributeFlagEnum::FLAG_SEARCHABLE);
    }
}

Register with DiEnum::DATA_ATTRIBUTE_REDUCER:

Vendor/ModuleName/Configuration/Frontend/Di.php
$container->tag(
    [400 => SearchableAttributeReducer::class],
    [DiEnum::DATA_ATTRIBUTE_REDUCER]
);

The built-in VisibleAttributeReducer (priority 500) removes any attribute not flagged with AttributeFlagEnum::FLAG_VISIBLE. It runs before any module-registered attribute reducers at the default priority.