Skip to content

Content Resolution

Content resolution is the process of transforming raw JSON from the database into structured, typed objects that frontend components can consume.

graph LR
    A[Database JSON] -->|BlockCaster| B[BlockCollection]
    B -->|first| C[BlockData]
    B -->|all| D["BlockData[]"]
    C -->|get| E[Field Value]
    C -->|getSourceMap| F[CmsSourceMap]
    C -->|getFieldLabels| G["Labels[]"]

In the database, a section’s content column stores flat JSON:

[
{
"type": "hero",
"data": {
"label": "Welcome",
"headline": "Build Amazing Sites",
"primary_cta_text": "Get Started",
"primary_cta_url": "/contact"
}
},
{
"type": "paragraph",
"data": {
"content": "We create digital experiences..."
}
}
]

Important: Data uses flat string keys, NOT nested arrays.

JFA\FilamentCMSCore\CMS\BlockCaster is an Eloquent cast that automatically transforms JSON to BlockCollection:

// ContentModel (base class for Section) has:
protected function casts(): array
{
return [
'content' => BlockCaster::class,
];
}
// Reading the content attribute automatically returns a BlockCollection:
$section = Section::find(1);
$collection = $section->content; // BlockCollection (via BlockCaster)

The caster:

  1. Decodes the JSON content array
  2. Creates BlockData DTOs for each block using BlockRegistry
  3. Returns a BlockCollection with all blocks in original order
  4. Handles serialization back to JSON on save

For source-mapped content (used by frontend rendering):

use JFA\FilamentCMSCore\Models\Section;
$section = Section::find(1);
$collection = $section->resolve(pageId: 5);
// Returns: BlockCollection with source maps attached

The resolver:

  1. Reads raw attributes (bypasses BlockCaster to avoid double-casting)
  2. Creates BlockData DTOs with BlockRegistry
  3. Attaches source map context (section_id, page_id, block_type, block_index)
  4. Returns a BlockCollection instance

JFA\FilamentCMSCore\CMS\BlockCollection is the main container:

$collection = $section->resolve();
// Get first block (any type)
$firstBlock = $collection->first();
// Get first block of a specific type
$hero = $collection->first('hero');
// Get all blocks of a type
$allParagraphs = $collection->all('paragraph');
// Check if a type exists
if ($collection->has('hero')) {
// ...
}
// Count blocks
$count = $collection->count();
// Iterate all blocks
foreach ($collection->all() as $block) {
echo $block->get('headline');
}
// Convert to array (for serialization)
$array = $collection->toArray();

JFA\FilamentCMSCore\CMS\BlockData is an abstract base class wrapping a single block:

$hero = $collection->first('hero');
// Field access via get()
$label = $hero->get('label');
$headline = $hero->get('headline');
// Convert to array
$array = $hero->toArray();
// Get source map (for visual editing)
$sourceMap = $hero->getSourceMap();
// Get field labels (auto-extracted from ContentBlock schema)
$labels = $hero->getFieldLabels();

The default concrete implementation of BlockData:

use JFA\FilamentCMSCore\CMS\GenericBlockData;
// GenericBlockData provides get() for raw field access
$block = $collection->first('hero');
$value = $block->get('headline'); // 'Build Amazing Sites'

JFA\FilamentCMSCore\Forms\Blocks\BlockSchemaExtractor auto-extracts field labels from ContentBlock schemas:

use JFA\FilamentCMSCore\Forms\Blocks\BlockSchemaExtractor;
// Get labels for a block type
$labels = BlockSchemaExtractor::getLabels('hero');
// Returns: ['label' => 'Label', 'headline' => 'Headline', ...]
// Labels are automatically attached to BlockData via BlockCollection
$hero = $collection->first('hero');
$hero->getFieldLabels(); // ['label' => 'Label', ...]

This eliminates the need for HasFieldLabels interface — labels are extracted from the ->label() calls on your Filament form components.

During resolution via Section::resolve(), source maps are attached to each BlockData:

// Section::resolve() injects:
$blockData = $blockData->withSourceMap([
'sectionId' => $section->id,
'sectionSlug' => $section->slug,
'blockType' => $block['type'],
'blockIndex' => $index,
'pageId' => $pageId,
]);

This provenance metadata is what enables visual editing — it tells the system exactly which database record each rendered HTML element came from.

namespace App\Livewire;
use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent
{
use InteractsWithVisualEditing;
public string $label = '';
public string $headline = '';
public string $primaryCtaText = '';
public string $primaryCtaUrl = '';
public array $cmsSourceMap = [];
// No hydrateFromContent() needed — autoHydrate() handles it!
// No fieldLabels() needed — BlockSchemaExtractor handles it!
protected function initializeVisualEditing(): void
{
$block = $this->block?->first();
if ($block !== null) {
$this->cmsSourceMap = $block->getSourceMap();
}
}
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.components.hero');
}
}

The abstract Page class handles resolution automatically:

// In JFA\FilamentCMSLivewire\Livewire\Page::render()
$page = \JFA\FilamentCMSCore\Models\Page::bySlug($this->slug());
$sections = $page->sectionsForUI();
// sectionsForUI() returns an array of:
// [
// 'component' => 'App\Livewire\Hero',
// 'block' => BlockCollection,
// ]

The page blade passes BlockCollection to each section component:

@foreach ($components as $key => $component)
@livewire($component['class'], ['block' => $component['block']], key($key))
@endforeach

BlockCollection is a complex object that can’t be serialized by Livewire directly. The base SectionComponent handles this:

// public array $blockData stores raw data (Livewire-serializable)
public array $blockData = [];
// hydrate() reconstructs BlockCollection after Livewire restore
public function hydrate(): void
{
if (! empty($this->blockData)) {
$this->block = new BlockCollection(
$this->blockData,
registry: app(BlockRegistry::class),
);
}
}

Critical: protected properties are not serialized by Livewire. Always use public for properties that need to survive re-renders.

  • BlockCaster caching — Eloquent cast handles serialization efficiently
  • BlockSchemaExtractor caching — Labels are cached per block type (static cache)
  • Page slug cachingPage::bySlug() caches lookups
  • Eager loading — The Page model eager-loads sections and their pivot data
  • Reflection cachingautoHydrate() caches property mappings per class