php-delta-orchestrator is a library for orchestrating partial updates by comparing incoming input against the current state, producing a
Delta and executing Action instances only when appropriate.
// Patch + current state
$startDate = Field::from(
// incoming patch vs current value
patch : $payload->startDate,
current: $availability->startDate,
);
$endDate = Field::from(
patch : $payload->endDate,
current: $availability->endDate,
);
$orchestrator = new Orchestrator();
$orchestrator->register(new Action(
fields: [$startDate, $endDate],
// runs only if the action applies, contract is satisfied, and there is an effective delta
apply: function (Field $startDate, Field $endDate): void {
// use $field->delta(), $field->value(), $field->current()
},
));
$orchestrator->execute();Delta Orchestrator requires PHP 8.2 or higher and can be installed via Composer:
composer require vaened/php-delta-orchestratorWhen handling partial updates, code tends to quickly degrade into scattered conditional logic:
- checking whether a field is present in the input,
- comparing it with the current value,
- deciding whether to execute business logic,
- avoiding unnecessary operations when nothing has changed.
This usually leads to nested conditionals, duplicated comparison logic, and implicit rules spread across the application layer.
The core issue is that this approach mixes in the same place:
- input handling,
- change detection,
- action execution.
An explicit flow is introduced where each responsibility is clearly separated:
PatchValuemodels input presence and normalization,Fieldevaluates changes against the current state,Deltarepresents an effective transition,Actiondefines when and how to execute logic.
The library organizes the flow of a partial update into explicit steps:
flowchart LR
A["Patch + current state"] --> B["Field<br/>(presence + comparison)"]
B --> C{"Does the action apply?<br/>(when)"}
C -- "No" --> X["Skip"]
C -- "Yes" --> D{"Is contract satisfied?<br/>(behaviors)"}
D -- "No" --> Z["Throw exception"]
D -- "Yes" --> E{"Is there an effective delta?"}
E -- "No" --> X
E -- "Yes" --> G["apply()"]
The following section shows how to apply the flow defined in the conceptual model.
You can represent partial input in two ways.
final readonly class UpdateAvailabilityCommand
{
public function __construct(
public DateTimeImmutablePatchValue $startDate,
public DateTimeImmutablePatchValue $endDate,
) {}
}Option B: From array using PatchInput
$input = new PatchInput(
input : $request->all(),
expectedKeys: ['start_date', 'end_date'],
);
$startDate = $input->dateTimeImmutable('start_date');
$endDate = $input->dateTimeImmutable('end_date');PatchValue represents:
- presence (
isPresent()) - incoming value (
value()), potentially normalized
You connect the patch with the current state using Field::from(). Each patch represents a PatchValue, not the final
value, so the incoming value may differ in type from the current state.
$startDate = Field::from(
patch : $payload->startDate,
current: $availability->startDate,
);You can optionally define a comparator:
$endDate = Field::from(
patch : $payload->endDate,
current: $availability->endDate,
)->using(comparator: DateTimeComparator::create());You can also transform the incoming patch value before comparison and action execution:
$name = Field::from(
patch : $payload->name,
current: $current->name,
)
->transform(static fn(string $value): string => strtolower(trim($value)))
->using(comparator: StrictComparator::create());Each Field exposes:
isPresent()→ whether the field was provided in the patchvalue()→ incoming valuecurrent()→ current valuedelta()→ returns the transition (previous → next) if a change exists, ornullotherwise
You define what should happen when a combination of fields applies through an Action.
$orchestrator->register(new Action(
fields : [$startDate, $endDate],
apply : function (Field $startDate, Field $endDate): void {
// call to application/domain service
},
when: static fn(Field ...$fields) => any($fields),
description: 'Update availability period',
));Behaviors define the execution contract through Required and Optional:
fields: [
$startDate->required(),
$endDate->optional(),
]required()→ the field must provide a usable valueoptional()→ the field may be absent
when determines whether the action participates in the current patch.
By default, an action applies if at least one field is present.
You can define custom rules:
when: static fn(Field ...$fields) => all($fields)$orchestrator->execute();The Orchestrator performs:
- Evaluates
when(presence-based activation) - Validates the contract (
behaviors) - Checks for an effective delta
- Executes
apply()if applicable
The library does not automatically build a projected state.
If you need to combine patch values with the current state, you must do it explicitly:
$start = $startDate->isPresent() ? $startDate->value() : $startDate->current();This allows you to control type handling, normalization, and domain rules.
Rules allow you to declaratively define activation conditions (when) through helpers in
src/Rules/functions.php.
Checks whether a field is present in the patch.
present($startDate)Allow composing conditions:
use function Vaened\DeltaOrchestrator\Rules\all;
use function Vaened\DeltaOrchestrator\Rules\any;
all([$startDate, $endDate]);
any([$startDate, $endDate]);You can also nest rules:
all([
$startDate,
any([$endDate, $publishedAt]),
]);Advanced details on how to define custom activation rules.
$action = new Action(
fields: [$startDate, $endDate],
when : static fn(Field ...$fields) => all($fields),
apply : static function (Field $startDate, Field $endDate): void {
// ...
},
);when determines whether the action participates in the current patch.
Each Field compares the incoming value against the current value using a comparator.
If none is defined, StrictComparator is used.
- compares strictly by type and value,
- compares dates by exact temporal value,
- throws
ComparisonTypeMismatchif types are not compatible.
For numeric values and numeric strings.
$quantity = Field::from(
patch : $payload->quantity,
current: $current->quantity,
)->using(comparator: NumericComparator::create());For date comparisons with explicit semantics.
$startDate = Field::from(
patch : $payload->startDate,
current: $current->startDate,
)->using(comparator: DateTimeComparator::create());For cases where intentional loose comparison (==) is desired.
$value = Field::from(
patch : $payload->value,
current: $current->value,
)->using(comparator: LooseComparator::create());For recursive array comparisons, with support for injecting an item comparator.
$settings = Field::from(
patch : $payload->settings,
current: $current->settings,
)->using(comparator: ArrayComparator::create());You can also provide a custom comparator for leaf values:
$settings = Field::from(
patch : $payload->settings,
current: $current->settings,
)->using(comparator: ArrayComparator::create(LooseComparator::create()));Concrete PatchValue implementations can accept flexible inputs and return normalized values.
new IntPatchValue(true, '20')->value();
new BoolPatchValue(true, 'true')->value();
new DateTimeImmutablePatchValue(true, '2026-04-26 10:20:30')->value();This keeps normalization at the input boundary, preventing raw values from leaking into the domain.
The repository includes an executable usage scenario located at playground/playground.php.
Unlike the snippets in the README, this example brings multiple cases together in a single flow:
- multiple
Actioninstances over the same patch, - combination of
required()andoptional(), - use of
whento control activation by presence, - cases with and without effective
delta, - use of current values as fallback,
- a contract failure case (
requiredwithnull).
The scenario is not intended to be minimal. It deliberately groups more logic than usual to expose different behaviors in a single execution.
make playgroundYou can find more details in the source code as well as in the tests located in tests/.
The tests cover different usage scenarios and can serve as additional reference for understanding the library’s behavior.