Livewire 4.x: Synth orchestration extracted into a dedicated `HandleSynths` mechanism
Contributed by joshhanley
Part of the livewire v4.4.7 release
Livewire 4.x continues to untangle its internals, and pull request #10276 is a prime example: the property synthesizer ("synth") pipeline — the machinery that serialises and re-hydrates complex public property types like Carbon dates and collections — has been extracted from HandleComponents into its own dedicated mechanism, Livewire\Mechanisms\HandleSynths\HandleSynths. If you're upgrading from v3.8.10 and stick to the documented APIs, nothing about your code changes. The win is architectural: features that need synths no longer have to depend on — or reach into — the component lifecycle mechanism.
Why the synths engine needed its own home
In v3, HandleComponents wore two hats. It orchestrated the component lifecycle — mount, snapshot handling, update, render — and it also owned the entire synth pipeline: the registry of synthesizers, resolution by key or target, and the recursive dehydrate()/hydrate() walks over your public properties. That coupling meant anything synth-adjacent — like the upcoming #[Session] attribute work from #10252, and the fix for #10250 it enables — had to reference HandleComponents to register or resolve synthesizers. Pulling the pipeline out gives those features a small, focused class to depend on, and makes the serialisation engine independently testable.
What's new: the HandleSynths mechanism
The new mechanism is registered in the service provider ahead of HandleComponents and now owns:
- The synth registry — the same built-ins as before:
CarbonSynth,CollectionSynth,StringableSynth,EnumSynth,StdClassSynth,ArraySynth,IntSynth, andFloatSynth. registerSynth(), which still unshifts userland synths to the front of the registry, so your custom synths win over built-ins.- The
dehydrate(),hydrate(),hydratePropertyUpdate(), andhydrateForUpdate()methods that used to be protected onHandleComponents. find()andresolve()for looking up a synth by key or by target value, plusisRemoval()for the__rm__sentinel.- The type-match cache that keeps synth resolution fast.
HandleComponents now constructor-injects HandleSynths and delegates wherever it previously did this work inline. Every subtle behaviour is preserved: the negative-zero (-0.0 → 0) normalisation that prevents checksum mismatches, the __rm__ handling for removed nested properties, and the SecurityPolicy::validateClass() denylist check that fires before any synthesizer can instantiate a class.
Your code doesn't need to change
The public API is deliberately untouched. Livewire::propertySynthesizer() and Livewire::findSynth() are the same functions as before — internally they now delegate to the new mechanism:
// Exactly as documented — unchanged behaviour:
Livewire::propertySynthesizer(\App\Synthesizers\MoneySynth::class);
$synth = Livewire::findSynth('money', $component);
Custom synths are equally unaffected, because this PR deliberately does not move the synth classes. Synth and all the built-ins keep their namespace — the docs show userland synths extending Synth, so relocating them would be a breaking change:
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
class MoneySynth extends Synth
{
public static $key = 'money';
public static function match($target)
{
return $target instanceof Money;
}
public function dehydrate($target) { /* ... */ }
public function hydrate($value, $meta) { /* ... */ }
}
Upgrade impact: what to actually check
For the vast majority of apps: nothing. You only need to act if you were reaching into HandleComponents' internals directly:
// Before — these no longer exist on HandleComponents
app(\Livewire\Mechanisms\HandleComponents\HandleComponents::class)
->registerPropertySynthesizer(FooSynth::class);
// After
app(\Livewire\Mechanisms\HandleSynths\HandleSynths::class)
->registerSynth(FooSynth::class);
HandleComponents::findSynth() is gone too — use app(HandleSynths::class)->find(...) or the Livewire::findSynth() helper. A handful of protected methods were also removed from HandleComponents (dehydrate(), hydrate(), hydratePropertyUpdate(), hydrateForUpdate(), propertySynth(), getMetaForPath(), isRemoval(), and the three getSynthesizerBy*() variants), which only matters if you subclassed the mechanism and overrode them. If so, extend or call HandleSynths instead.
Takeaways
- The synth serialisation pipeline now lives in
Livewire\Mechanisms\HandleSynths\HandleSynths;HandleComponentskeeps only the component lifecycle. - Public API unchanged:
Livewire::propertySynthesizer()andLivewire::findSynth()behave exactly as before. Synthand the built-in synths keep their namespace — custom synths need zero changes.- Only internal callers of
registerPropertySynthesizer()/findSynth()onHandleComponents, or subclasses overriding the removed protected methods, need to update. - The new mechanism ships with dedicated unit tests, including coverage that denylisted classes are still blocked during hydration.
Changelog category: Technical report (12)