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
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
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
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 |
MediaEntryUri → MediaEntry |
| 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
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
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:
$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.