Skip to content

feat: feed the record and arbitrary data through to Brick::toHtml() - #57

Merged
awcodes merged 4 commits into
3.xfrom
feat/brick-render-data
Sep 10, 2026
Merged

feat: feed the record and arbitrary data through to Brick::toHtml()#57
awcodes merged 4 commits into
3.xfrom
feat/brick-render-data

Conversation

@awcodes

@awcodes awcodes commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Closes the gap raised in #56.

Brick::toHtml() has always taken a second ?array $data parameter, and nothing has ever passed anything to it — MasonRenderer hardcoded data: [], both iframe renderers omitted the argument. The hook was in the signature, the generator stub and the docs, but unreachable. This wires it up.

What works now

Front end — a third argument on the helper, or data() on the renderer (both accept a closure on the renderer side):

@mason($post->content, BrickCollection::make(), ['record' => $post])
{!! mason($post->content, BrickCollection::make(), ['record' => $post])->toHtml() !!}

The @mason directive forwards its whole expression to the helper, so it picked the argument up for free, named arguments included.

InfolistsMasonEntry defaults to ['record' => $this->getRecord()], so a brick reading $data['record'] works with no wiring. data() overrides it.

public static function toHtml(array $config, ?array $data = null): ?string
{
    $record = $data['record'] ?? null;
    // ...
}

The entry crosses a request boundary

MasonEntry does not render in the Livewire request — it renders in an iframe fed by a POST to MasonController, so its data has to survive the trip. DataPayload reduces Eloquent models to a class-and-key marker, encrypts the JSON with Crypt, and re-resolves the model on arrival:

  • model attributes never go over the wire;
  • a payload we did not sign is treated as absent rather than fatal, so a forged one renders no data instead of a chosen record (there's a test for exactly this);
  • a record deleted between render and request arrives as null.

Values that cannot survive the round trip are rejected at encode time rather than silently dropped — json_encode() turns a closure into {} instead of failing, so anything that is an object but neither JsonSerializable nor Arrayable throws, as does a resource.

The editor preview is deliberately untouched

A record-dependent brick renders its null branch in the editor for now, and the docs tell brick authors to write that branch on purpose.

Plumbing is not the blocker — the preview route could reuse DataPayload in about five lines. What it should show is the open question:

  1. Which record? The last-saved row would contradict unsaved sibling fields in the same form. The infolist can't have this problem; the editor can, constantly.
  2. There is no record on CreategetRecord() is null until the first save, so it would only ever be truthful on Edit.
  3. Cost. The preview re-renders on every state change with no debounce, so each one would re-resolve the model plus any relations a brick touches.

Left for a follow-up, pending the answer in #56.

Workbench

PageMeta renders from the record rather than its own config, so the three call sites can be compared in a running app. It's seeded into PageFactory, so composer serve shows the contrast with no setup — open a page on the frontend, then edit the same page and watch that brick fall back to its placeholder.

Notes for review

  • The PHPStan baseline gains one entry for the new workbench file. larastan can't resolve view names in this setup, so every view() call in the repo is already baselined. Confirmed file-specific rather than a real defect by swapping view names between Hero and PageMeta — the error follows the file, not the string. Appended by hand, because the baseline is included from phpstan.neon.dist and --generate-baseline would drop every existing entry.
  • The README gains the @mason directive, which it has never documented at all. Since that section now describes three ways to render the same content, the helper and renderer got headings alongside it, matching docs/bricks/rendering.md.
  • resources/dist and workbench/resources/dist are rebuilt; verified reproducible against a fresh npm run build.
  • testbench.yaml now lists MasonServiceProvider explicitly. This is a no-op at runtime — the provider already loads through package discovery, and testbench route:list is identical with or without it — but it keeps the workbench's provider set readable in one place.

composer test is green — 201 passed, Rector/Pint/PHPStan clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BbLDdHUbgfXQ12eWn7zC68

awcodes and others added 4 commits September 10, 2026 11:20
Brick::toHtml() has always taken a second `?array $data` parameter and nothing
has ever passed anything to it: MasonRenderer hardcoded `data: []`, and both
iframe renderers omitted the argument entirely. The hook existed in the
signature, in the generator stub and in the docs, but was unreachable.

MasonRenderer gains a data() setter (array or closure) forwarded to every
brick, exposed as a third argument on the mason() helper. The @mason directive
already forwards its whole expression to the helper, so it picks the argument
up for free, named arguments included.

MasonEntry defaults its data to ['record' => $this->getRecord()], so a brick
that reads $data['record'] works in an infolist with no wiring, and data()
overrides it. getRecord() is called with withContainerRecord tied to
isset($this->container): it walks up to the schema container for its record,
and that container is unset on a component not yet attached to one, which
throws rather than returning null.

The entry does not render in the Livewire request. It renders in an iframe fed
by a POST to MasonController, so its data has to cross a request boundary.
DataPayload reduces Eloquent models to a class-and-key marker, encrypts the
JSON with Crypt, and re-resolves the model on arrival. Attributes never go over
the wire, a payload we did not sign is treated as absent rather than fatal, and
a record deleted between render and request arrives as null.

Values that cannot survive the round trip are rejected at encode time rather
than silently dropped, because json_encode() turns a closure into {} instead of
failing: anything that is an object but neither JsonSerializable nor Arrayable
throws, as does a resource.

The editor preview is deliberately untouched. It POSTs to the same controller
from mason.js with no record in scope, and what it should show is an open
question — the last-saved row would contradict unsaved sibling fields, there is
no record at all before the first save, and the preview re-renders on every
state change with no debounce, so each one would re-resolve the model and any
relations a brick touches. A record-dependent brick renders its null branch
there for now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbLDdHUbgfXQ12eWn7zC68
Covers the second toHtml() argument from both ends: how a brick reads it, and
how the helper, the directive, the renderer and MasonEntry supply it.

Two notes that are easy to trip over are called out explicitly. The editor
preview supplies no data, so a record-dependent brick needs a null branch
written on purpose rather than one that renders nothing. And MasonEntry's data
crosses a request boundary, so it must be JSON-serializable or an Eloquent
model, and the record can come back null if it was deleted meanwhile.

The README gains the @mason directive, which it has never documented at all —
only the helper. Both forms take the same three arguments and both accept named
arguments. Since the section now describes three ways to render the same
content, the helper and the renderer get headings alongside the new directive
one, matching how docs/bricks/rendering.md is already laid out.

Anchors are deliberately avoided in the README cross-references. It has no
internal links today, and readme-to-docs splits these sections into separate
files, which would leave any #anchor pointing at the wrong document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbLDdHUbgfXQ12eWn7zC68
PageMeta renders from the record rather than from its own config — title, URL
and last-updated — so the three call sites can be compared side by side in a
running app: the front end passes the record explicitly, MasonEntry supplies
its own, and the editor preview has none and falls through to the placeholder
branch.

Seeded into PageFactory so every generated page already contains one, which
makes `composer serve` show the contrast without any setup: open a page on the
frontend, then edit the same page and watch that brick change.

The baseline gains a PHPStan entry for the new file. larastan cannot resolve
view names in this setup, so every view() call in the repo is already baselined
— Section, Hero, CardGrid, Divider, both iframe renderers and routes/web.php.
Confirmed file-specific rather than a real defect by swapping view names
between Hero and PageMeta: the error follows the file, not the string. Appended
by hand rather than regenerated, because the baseline is included from
phpstan.neon.dist and --generate-baseline would drop every existing entry.

workbench/resources/dist is rebuilt for the classes the new view introduces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbLDdHUbgfXQ12eWn7zC68
The provider already loads through package discovery — composer.json declares
it under extra.laravel.providers, and `testbench route:list` registers Mason's
routes identically with or without this line. Listing it alongside
AdminPanelProvider makes the workbench's provider set readable in one place
instead of split between testbench.yaml and discovery, and keeps it working if
the discovery cache is ever absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbLDdHUbgfXQ12eWn7zC68
@awcodes
awcodes merged commit 6c2225c into 3.x Sep 10, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant