Platform
← Patterns

PATTERNS FEATURE

The composition model: parts

Components compose primitives through declared parts: #[UiPart] binds a primitive, #[ProvidesUiPart] computes its props, and props resolve in a fixed order.

Live demo
1 · Defaults Part defaults

The #[UiPart] declaration's baseline props for the bound primitive.

2 · Provider #[ProvidesUiPart]

A method computes structural props from the component's state — overrides defaults.

3 · Bind The bound value

bind: 'value' threads the live value into the part — overrides the provider.

4 · Caller Caller overrides win

Anything the caller passes to ui_part() wins last — deterministic, always.

#[UiPart] / #[ProvidesUiPart] Deterministic prop resolution Primitives to components

The composition model — parts

A component is not bespoke markup — it is a declared composition of primitives. The field and form components don't hand-assemble inputs and labels; they declare parts and let the model resolve them. That is what keeps composition predictable instead of ad-hoc template assembly.

Parts, providers, and a fixed resolution order

  • #[UiPart(name, uses: Primitive, bind?, defaults?)] declares a named part backed by a primitive (e.g. field.input uses the input primitive).
  • #[ProvidesUiPart(part)] marks a method that computes that part's structural props from the component's state.
  • ui_part() / ui_part_props() resolve and render (or just resolve) a part atomically in Twig.

Props resolve in one fixed order, each step overriding the previous:

  1. Part defaults — the declaration's baseline.
  2. Provider — the #[ProvidesUiPart] method.
  3. Bind — the bound value (bind: 'value').
  4. Caller overrides — anything passed to ui_part() wins last.

So field.input is the input primitive, bound to the value, carrying the field's computed props — and a caller can still override any of it.

A declared, deterministically-resolved part model is what makes components consistent and composable instead of a pile of one-off templates. The same model underlies every component on this site. You read a component's parts to know its structure, and you extend behaviour by adding declarations — the Page Contract principle, one level down, at the component scale.

© Harold Abelson:"Programs must be written for people to read, and only incidentally for machines to execute."

ComponentParts bound to primitives
#[AsComponent(name: 'platform.field')]#[UiPart(name: 'input', uses: InputPrimitive::class, bind: 'value')]final class FieldComponent{    // computes the structural props for the 'input' part    #[ProvidesUiPart(part: 'input')]    public function inputPart(array $props): array    {        return ['size' => $props['size'] ?? 'md', 'state' => $props['invalid'] ?? false ? 'invalid' : 'default'];    }}

How it works

#[UiPart(name, uses: Primitive, bind?, defaults?)] declares a named part backed by a primitive; #[ProvidesUiPart(part)] marks a method that computes that part's structural props. At render, props resolve in a fixed order — part defaults → the provider method → the bound value → caller overrides — so behaviour is deterministic. The Twig helpers ui_part() / ui_part_props() resolve and render a part atomically. So field.input IS the input primitive, bound to value, carrying the field's computed props, and a caller can still override any of it.

Why it matters

A declared, deterministically-resolved part model is what makes components consistent and composable instead of a pile of one-off templates. The same model underlies every component on this site. You read a component's parts to know its structure, and you extend behaviour by adding declarations — the Page Contract principle, one level down, at the component scale.