The Aggregator API turns CMS source data into new, prepared data structures – for example, for delivery to frontends, search indexes, or other consumers.
The API is clearly layered. Each role has a well-defined responsibility:
| Role | Purpose |
|---|---|
| Resolver | Reads raw source data (Source) |
| Assembler | Builds typed domain objects from a Resolver |
| Aggregator | Composes the target structure into an OutputNode |
| OutputNode | Hierarchical target data structure (Target) |
| Visitor | Traverses an OutputNode (serialization, analyses) |
flowchart LR
Source[("Source data<br/>(CMS, Map, JSON, ...)")]
Resolver["Resolver<br/><i>read-only view</i>"]
Assembler["Assembler<br/><i>optional</i>"]
Domain[["Domain objects<br/>(Link, Text, ...)"]]
Aggregator["Aggregator"]
Output[("OutputNode<br/><i>target structure</i>")]
Visitor["Visitor"]
Target[["PHP / JSON / Map /<br/>TranslatableTexts ..."]]
Source --> Resolver
Resolver --> Assembler
Assembler --> Domain
Resolver --> Aggregator
Domain --> Aggregator
Aggregator --> Output
Output --> Visitor
Visitor --> Target
| Question | Answer |
|---|---|
| How do I get individual values from a source? | Resolver |
How do I build a typed value object (e.g. Link)? |
Assembler |
| How do I write results into the target structure? | Aggregator + OutputNode |
| How do I structure the data hierarchy to be generated? | OutputNode (optionally with sub-aggregators) |
| How do I serialize the result (PHP, JSON, Map) or evaluate it? | Visitor |
| How do I deliver the same structure in multiple languages? | Translations |
In a typical aggregation, all roles work together:
public class LinkListAggregator implements Aggregator {
private final LinkListAssembler linkListAssembler;
@Override
public void aggregate(Resolver source, OutputNode output) {
// ^^^^^^^^ ^^^^^^^^^^
// RESOLVER OUTPUTNODE
// (Source) (Target)
linkListAssembler.assemble(LinkListRequest.of(source, options), null)
// ^^^^^^^^^
// ASSEMBLER returns a typed domain object
.ifPresent(linkList -> output.put("linkList", linkList));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// AGGREGATOR writes into the OutputNode
}
}Assemblers are optional: for simple fields, an Aggregator can read directly from the Resolver and write to the OutputNode. Only when constructing a value involves multiple steps (several fields, derived values, business rules, external resolutions) is a dedicated Assembler worthwhile.
The documentation is organized by genre:
| Genre | Directory | Content |
|---|---|---|
| Reference | docs/reference/ |
Role reference: Resolver, Assembler, Aggregator, OutputNode, Visitor |
| How-To | docs/how-to/ |
Task-oriented: Aggregator plugin project, Assembler customization, Translations pipeline |
| Concept | docs/concepts/ |
Background & design decisions: Translatable values: immutability & identity |