Resolvers
Magewire specific (since: 3.0.0)
A Component Resolver is the bridge between a Magento AbstractBlock and a Magewire Component. Given a block, the resolver decides whether it should become a component, builds the component on the initial page render, and rebuilds it on every subsequent /magewire/update XHR.
For the default Layout-XML flow (a block with a <argument name="magewire" xsi:type="object">…</argument>), the built-in LayoutResolver handles everything. You only need a custom resolver when the component's binding shape isn't a plain layout argument — Magento Widgets, dynamically-generated blocks, data-driven component construction, or anything that doesn't fit the standard layout model.
Mental model
Think of a resolver as a three-state machine with a stable identity:
(block arrives) → complies? → construct() → snapshot (accessor stored in memo)
│
XHR update ▼
(block gone) ← reconstruct(request)
complies()— a cheap yes/no question the manager asks every registered resolver until one says yes.construct()— runs once per initial render. Hands back the block with aComponentbound to it.reconstruct()— runs on every update roundtrip. Must rebuild the block/component from only the incoming snapshot (no layout state survives the HTTP hop).accessor()— a unique short name (layout,flake,layout_admin). Stored in the snapshot memo so the same resolver is picked on reconstruction. Must match the DI item name. (getAccessor()is a deprecated alias.)
Responsibilities
A resolver must implement three abstract methods (construct, reconstruct, arguments). The remaining methods ship with a default implementation on the base ComponentResolver and are overridden only when needed:
| Method | Abstract? | Purpose |
|---|---|---|
construct(AbstractBlock $block): AbstractBlock |
yes | Attach a Component to the block (via $block->setData('magewire', $component)) and return the block. |
reconstruct(ComponentRequestContext $request): AbstractBlock |
yes | Rebuild the block + component from the XHR snapshot. |
arguments(): MagewireArguments |
yes | Provide the typed arguments object that collects magewire.* / magewire:* data keys from the block. |
complies(AbstractBlock $block, mixed $magewire = null): bool |
no | Cheap check. Returns true if this resolver should handle the block. The base implementation already matches when the block's magewire:resolver data key equals this resolver's accessor. |
assemble(AbstractBlock $block, Component $component): AbstractBlock |
no | Final step after construct/reconstruct — binds name, id and alias onto the component and resolves a default template. |
remember(): bool |
no | Cache the resolver/block pairing. Defaults to true. Return false for a fluent resolver that re-evaluates conditions per request. |
canPropagate(): bool |
no | Whether a dynamically-loaded child block with no resolver of its own may reuse this resolver. Defaults to false. |
How the manager picks a resolver
ComponentResolverManager::resolve($block) runs in this order:
- Snapshot cache. If the block was resolved before, the previous accessor is pulled from cache — skips
complies()entirely. - Manual selection.
$block->setData('magewire:resolver', 'accessor')forces a specific resolver by name or class string — useful for testing or edge cases. complies()scan. Every registered resolver is tested in registration order; the firsttruewins.- Not found. Throws
ComponentResolverNotFoundException.
Sort order in DI determines step 3's evaluation order. Lower sort orders run first — which is why FlakeResolver (98900) is tested before LayoutResolver (99900) in core: the more specific resolver gets first refusal.
The built-in LayoutResolver
Worth reading before writing your own — every custom resolver for block-based components should extend it.
// Magewirephp\Magewire\Mechanisms\ResolveComponents\ComponentResolver\LayoutResolver
public function construct(AbstractBlock $block): AbstractBlock
{
$magewire = $block->getData('magewire') ?? null;
if (! $magewire) {
throw new ComponentNotFoundException(sprintf('No component object found for "%s"', $block->getNameInLayout()));
}
// Supports both <argument name="magewire" xsi:type="object">...</argument>
// and <argument name="magewire" xsi:type="array"><item name="type" xsi:type="object">...</item></argument>
$component = is_array($magewire) ? $magewire['type'] : $magewire;
$block->setData('magewire', $component);
// On dehydrate, stash the active layout handles (and alias, if any) into the
// snapshot memo so reconstruct() can rebuild the layout on the next XHR.
on('dehydrate', function (Component $component, ComponentContext $context) {
if ($component->magewireResolver()->accessor() === $this->accessor()) {
if ($this->canMemorizeLayoutHandles()) {
$context->addMemo('handles', $this->determineLayoutHandles($component, $context));
}
if ($alias = $component->magewireBlock()->getData('magewire:alias')) {
$context->addMemo('alias', $alias);
}
}
});
return $block;
}
public function reconstruct(ComponentRequestContext $request): AbstractBlock
{
$snapshot = $request->getSnapshot();
$handles = $snapshot->getMemoValue('handles') ?? [];
// Replay layout with the stored handles, fetch the block by name, reuse construct().
$block = $this->generateBlocks($handles)[$snapshot->getMemoValue('name')];
return $this->construct($block);
}
The key insight: reconstruction replays the layout handles, then re-enters the same construct() path. This is why layout XML is the whole source of truth — snapshots only carry enough memo to replay the layout.
Default template convention
LayoutResolver::assemble() also resolves a template when the block doesn't define one. It falls back to the convention Vendor_Module::magewire/dashed-class-name.phtml, derived from the component's class name (e.g. Vendor\Module\Magewire\ProductList → Vendor_Module::magewire/product-list.phtml). Define a template on the block to override it.
Writing a custom resolver
Creating a custom resolver is intended for advanced Magewire developers who have a solid understanding of the Magewire lifecycle and how the snapshot / memo flow connects the pieces.
Extend LayoutResolver when your component is still block-based but needs a different matching rule or extra construction logic. Extend the abstract ComponentResolver only when the block-and-argument model genuinely doesn't fit.
Example 1: Convention-based resolver
A resolver that auto-wires a component onto any block whose class name ends in ReactiveBlock — useful for module authors who want to drop a marker interface instead of writing layout arguments:
<?php
declare(strict_types=1);
namespace Vendor\Module\Mechanisms\ResolveComponents\ComponentResolver;
use Magento\Framework\View\Element\AbstractBlock;
use Magewirephp\Magewire\Mechanisms\ResolveComponents\ComponentResolver\LayoutResolver;
use Vendor\Module\Component\ReactiveBlockComponentFactory;
class ReactiveBlockResolver extends LayoutResolver
{
protected string $accessor = 'reactive_block';
public function __construct(
private readonly ReactiveBlockComponentFactory $factory,
// …pass through parent deps
) {
// parent::__construct(...);
}
public function complies(mixed $block, mixed $magewire = null): bool
{
if (! $block instanceof AbstractBlock) {
return false;
}
return str_ends_with($block::class, 'ReactiveBlock');
}
public function construct(AbstractBlock $block): AbstractBlock
{
// No magewire argument in layout XML — synthesize one from the block class.
if (! $block->hasData('magewire')) {
$block->setData('magewire', $this->factory->createFor($block));
}
return parent::construct($block);
}
}
Register with a sort order lower than layout (99900) so complies() runs first:
<type name="Magewirephp\Magewire\Mechanisms\ResolveComponents\Management\ComponentResolverManager">
<arguments>
<argument name="resolvers" xsi:type="array">
<item name="reactive_block" xsi:type="object" sortOrder="90000">
Vendor\Module\Mechanisms\ResolveComponents\ComponentResolver\ReactiveBlockResolver
</item>
</argument>
</arguments>
</type>
DI item name matches the accessor
The name="reactive_block" attribute in DI must match $accessor on the resolver class. The manager uses this mapping during reconstruction to instantiate the correct class from the snapshot memo.
Example 2: Fixed-handle reconstruction (the real FlakeResolver)
The core FlakeResolver shows a resolver that does not rely on the complies() scan at all. Its complies() returns false outright — it is never selected by the auto-scan. Flakes are instead bound through the Flakes feature (and resolver propagation), then this resolver takes over construction and, crucially, reconstruction:
class FlakeResolver extends LayoutResolver
{
protected string $accessor = 'flake';
public function complies(mixed $block, mixed $magewire = null): bool
{
// Never matched by the auto-scan. The Flakes feature binds this resolver.
return false;
}
public function construct(AbstractBlock $block): AbstractBlock
{
// Block has an alias but no component? Synthesize an empty Flake so it still "has powers".
if ($block->hasData('magewire:alias') && ! $block->hasData('magewire')) {
$block->setData('magewire', $this->flakeFactory->create());
}
return parent::construct($block);
}
protected function canMemorizeLayoutHandles(): bool
{
return false; // Don't store the page's active handles in the memo…
}
protected function recoverLayoutHandles(Snapshot $snapshot): array
{
return ['magewire_flakes']; // …always replay this fixed handle instead.
}
}
Takeaways worth copying:
complies()is not the only entry point. A resolver can bow out of the scan (return false) and be bound explicitly (magewire:resolver) or by propagation from a parent (canPropagate()).- Override
canMemorizeLayoutHandles()/recoverLayoutHandles()to swap the default "store the page's active handles" strategy for a fixed handle set — useful when the same handle always rebuilds the block, regardless of which page the XHR originated from.
The conditions() builder
complies() doesn't have to be hand-rolled boolean logic. The base resolver exposes a $this->conditions() builder: ->if() (alias ->validate()) adds an AND clause, ->or() adds an alternative, and ->evaluate(...$args) runs them. The default ComponentResolver::complies() uses it to match the magewire:resolver data key, and LayoutResolver adds an is_block AND clause on top.
Example 3: Widget resolver (non-layout binding)
Magento Widgets instantiate blocks through the widget system, not layout XML — so there is no opportunity to add a magewire argument. A widget resolver can construct a Magewire component from the widget's parameters instead:
class WidgetResolver extends LayoutResolver
{
protected string $accessor = 'widget';
public function complies(mixed $block, mixed $magewire = null): bool
{
if (! $block instanceof AbstractBlock) {
return false;
}
// Widgets carry their template path through the 'template' data key
// and typically live under a known namespace.
return $block->hasData('widget_type')
&& str_starts_with((string) $block->getData('widget_type'), 'Vendor\\Module\\Widget\\');
}
public function construct(AbstractBlock $block): AbstractBlock
{
$widgetType = $block->getData('widget_type');
$parameters = $block->getData() ?? [];
$component = $this->widgetComponentFactory->createFor($widgetType, $parameters);
$block->setData('magewire', $component);
return parent::construct($block);
}
}
The component's mount() receives the widget parameters (pass them through your factory); state then survives the XHR like any other component.
Forcing a resolver manually
When the automatic complies() scan is too broad or too narrow, bind a resolver explicitly via the magewire:resolver data key. The value is either a registered accessor name or a fully-qualified class string:
<block name="product.custom"
template="Vendor_Module::magewire/custom.phtml">
<arguments>
<argument name="magewire:resolver" xsi:type="string">reactive_block</argument>
<argument name="magewire" xsi:type="object">Vendor\Module\Magewire\Custom</argument>
</arguments>
</block>
Works in both layout XML and programmatic block construction. The manager skips complies() entirely when this key is present.
Propagating a resolver to child blocks
A block dynamically loaded as a child of an already-resolved component (e.g. introduced through a parent's XHR re-render) may have no resolver binding of its own. Rather than re-running the full complies() scan, the child can inherit its parent's resolver — but only if that resolver opts in via canPropagate():
public function canPropagate(): bool
{
return true; // Children of my components reuse me by default.
}
The default is false. Enable it for resolvers whose construction logic is generic enough to handle children the same way as their parent — and leave it off when each block must be matched on its own merits.
The remember() cache
ComponentResolverManager caches {block cache key → resolver accessor} pairs after a successful resolution — the next page load (or XHR) skips complies() for that block. This is controlled per resolver by remember():
Return false when the resolver's match conditions depend on runtime state (customer group, session flags, A/B experiment assignment, etc.) that a cached pairing would stale through.
Cost of remember() === false: every page render pays the full complies() scan. Only switch off when the dynamic behaviour is worth it.
Registration summary
<type name="Magewirephp\Magewire\Mechanisms\ResolveComponents\Management\ComponentResolverManager">
<arguments>
<argument name="resolvers" xsi:type="array">
<!-- Sort order is required; lower runs first. -->
<!-- 98900 — flake (more specific) -->
<!-- 99900 — layout (default fallback) -->
<item name="my_custom" xsi:type="object" sortOrder="95000">
Vendor\Module\Mechanisms\ResolveComponents\ComponentResolver\MyCustomResolver
</item>
</argument>
</arguments>
</type>
Rules of thumb:
- Accessor name matches the DI
nameattribute exactly. - Sort order — put more-specific resolvers below 99900 so they are evaluated before the default layout resolver.
- Area scope — register in
etc/frontend/di.xmlfor storefront,etc/adminhtml/di.xmlfor admin. The admin package'sLayoutAdminResolveris the canonical adminhtml example. - Never register in global
etc/di.xml— Magewire's service provider only reads area-scoped DI.
When to reach for a custom resolver
Good fit:
- Components bound through a mechanism other than layout XML (widgets, CMS blocks, dynamically generated blocks).
- Convention-based wiring (e.g. any block in a namespace, any block with a marker interface).
- Components whose construction needs runtime data that can't be expressed as a layout argument.
Overkill:
- A component that differs from a standard one only by the data you pass to
mount()— just usemagewire:mount:*arguments with the defaultLayoutResolver. - A component that needs different behaviour per page — that's what
boot()/ lifecycle hooks are for. - Per-theme tweaks — use layout overrides, not a new resolver.
Related
- Mechanisms — the broader pipeline resolvers plug into.
- Component Hooks — extend lifecycle without a new resolver.
- Admin → How it works —
LayoutAdminResolverwalkthrough. - Magewire Flakes — real-world secondary resolver in core.