Block DTOs
Block DTOs (Data Transfer Objects) are typed wrappers that replace the old ContentResolver → SectionContent → BlockData chain with a simpler, more explicit system.
Overview
Section titled “Overview”graph TD
subgraph "Database"
JSON["JSON: [{type:'hero', data:{...}}]"]
end
subgraph "Casting"
BC["BlockCaster (Eloquent Cast)"]
BR["BlockRegistry"]
end
subgraph "DTOs"
BCol["BlockCollection"]
BD["BlockData (abstract)"]
GBD["GenericBlockData"]
end
subgraph "Frontend"
SC["SectionComponent"]
AH["autoHydrate()"]
end
JSON -->|read attribute| BC
BC -->|decode + wrap| BCol
BC -->|uses| BR
BCol -->|contains| BD
BD -.->|concrete| GBD
BCol -->|passed to| SC
SC -->|maps properties| AH
BlockData
Section titled “BlockData”JFA\FilamentCMSCore\CMS\BlockData is an abstract base class:
abstract class BlockData{ public function __construct( public readonly ?array $sourceMap = null, ) {}
abstract public static function fromArray(array $data, ?array $sourceMap = null): self; abstract public function toArray(): array; abstract public function get(string $key): mixed; abstract public function getSourceMap(): ?array; abstract public function getFieldLabels(): array;}GenericBlockData
Section titled “GenericBlockData”The default concrete implementation:
use JFA\FilamentCMSCore\CMS\GenericBlockData;
$block = new GenericBlockData( data: ['headline' => 'Hello', 'label' => 'Welcome'], sourceMap: ['sectionId' => 1, 'blockType' => 'hero'], fieldLabels: ['headline' => 'Headline', 'label' => 'Label'],);
$block->get('headline'); // 'Hello'$block->toArray(); // ['headline' => 'Hello', 'label' => 'Welcome']$block->getSourceMap(); // ['sectionId' => 1, 'blockType' => 'hero']$block->getFieldLabels(); // ['headline' => 'Headline', 'label' => 'Label']BlockCollection
Section titled “BlockCollection”JFA\FilamentCMSCore\CMS\BlockCollection is an iterable container:
use JFA\FilamentCMSCore\CMS\BlockCollection;
$collection = new BlockCollection($dataArray, registry: app(BlockRegistry::class));Access Methods
Section titled “Access Methods”// Get first block (any type)$first = $collection->first();
// Get first block of a type$hero = $collection->first('hero');
// Get all blocks of a type$paragraphs = $collection->all('paragraph');
// Check existence$collection->has('hero'); // true/false
// Count$collection->count(); // int
// Check if empty$collection->isEmpty(); // true/false
// Iterateforeach ($collection->all() as $block) { echo $block->get('headline');}
// Convert to array$array = $collection->toArray();BlockRegistry
Section titled “BlockRegistry”JFA\FilamentCMSCore\CMS\BlockRegistry maps block types to DTO classes:
use JFA\FilamentCMSCore\CMS\BlockRegistry;
$registry = app(BlockRegistry::class);
// Register a DTO class for a block type$registry->register('hero', HeroBlock::class);
// Resolve a block type$blockClass = $registry->resolve('hero'); // HeroBlock::classFor most use cases, GenericBlockData is used automatically. Custom DTO classes are optional — they provide typed properties but aren’t required.
BlockCaster
Section titled “BlockCaster”JFA\FilamentCMSCore\CMS\BlockCaster is an Eloquent cast:
// In ContentModel (base class for Section):protected function casts(): array{ return [ 'content' => BlockCaster::class, ];}How It Works
Section titled “How It Works”$section = Section::find(1);
// Reading: JSON → BlockCollection$collection = $section->content; // BlockCollection
// Writing: BlockCollection → JSON$section->content = $newCollection;$section->save(); // Saves as JSONBlockSchemaExtractor
Section titled “BlockSchemaExtractor”JFA\FilamentCMSCore\Forms\Blocks\BlockSchemaExtractor auto-extracts labels from ContentBlock schemas:
graph LR
A[ContentBlock Class] -->|make| B[Filament Block]
B -->|getChildSchema| C[Form Components]
C -->|getLabel| D["Labels Map"]
D -->|attached to| E[BlockData]
use JFA\FilamentCMSCore\Forms\Blocks\BlockSchemaExtractor;
// Extract labels for a block type$labels = BlockSchemaExtractor::getLabels('hero');// Returns: ['label' => 'Label', 'headline' => 'Headline', ...]
// Labels are cached per block type (static cache)// Clear cache in tests:BlockSchemaExtractor::clearCache();How Labels Flow
Section titled “How Labels Flow”- You define
->label('Headline')on form components in yourContentBlock BlockSchemaExtractorreads the schema at resolution time- Labels are attached to each
BlockDataviagetFieldLabels() renderField()uses these labels for visual editing tooltips
No manual fieldLabels() method needed!
autoHydrate()
Section titled “autoHydrate()”SectionComponent::autoHydrate() maps block data to public properties:
graph TD
A[BlockData] -->|toArray| B["Array: headline → 'Hello'"]
B -->|snake_case → camelCase| C["Property: $headline = 'Hello'"]
C -->|reflection cache| D[Component Instance]
How It Works
Section titled “How It Works”// In SectionComponentprotected function autoHydrate(): void{ $data = $this->block?->first()?->toArray() ?? [];
foreach ($data as $key => $value) { $property = $this->camelCase($key); // primary_cta_text → primaryCtaText
if ($this->isNativeType($property)) { $this->{$property} = $value; } }}Type Filtering
Section titled “Type Filtering”autoHydrate() only sets properties with native PHP types:
| Type | Auto-Hydrated | Example |
|---|---|---|
string | Yes | $label, $headline |
int | Yes | $count |
float | Yes | $price |
bool | Yes | $isActive |
array | Yes | $members, $features |
mixed | Yes | $data |
Collection | No | $serviceModels |
| Custom objects | No | $customData |
For non-native types, use initializeVisualEditing():
protected function initializeVisualEditing(): void{ $this->serviceModels = Service::whereIn('id', $ids)->get();}Reflection Caching
Section titled “Reflection Caching”autoHydrate() caches the property map per class:
// First call: builds reflection map// Subsequent calls: uses cached map// Cache key: static::classLivewire Serialization
Section titled “Livewire Serialization”BlockCollection can’t be serialized by Livewire directly. The base class handles this:
// SectionComponent has:public array $blockData = []; // Livewire-serializable raw data
// mount() syncs both:public function mount($block = []): void{ $this->block = $block instanceof BlockCollection ? $block : ...; $this->blockData = $this->block?->toArray() ?? []; $this->autoHydrate(); $this->initializeVisualEditing();}
// hydrate() reconstructs 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.
Custom BlockData DTOs (Optional)
Section titled “Custom BlockData DTOs (Optional)”For full type safety, create custom DTO classes:
namespace App\CMS\DTOs;
use JFA\FilamentCMSCore\CMS\BlockData;
final class HeroBlock extends BlockData{ public function __construct( public readonly string $label = '', public readonly string $headline = '', public readonly string $primaryCtaText = '', public readonly string $primaryCtaUrl = '', ?array $sourceMap = null, array $fieldLabels = [], ) { parent::__construct($sourceMap); }
public static function fromArray(array $data, ?array $sourceMap = null): self { return new self( label: $data['label'] ?? '', headline: $data['headline'] ?? '', primaryCtaText: $data['primary_cta_text'] ?? '', primaryCtaUrl: $data['primary_cta_url'] ?? '', sourceMap: $sourceMap, ); }
public function toArray(): array { return [ 'label' => $this->label, 'headline' => $this->headline, 'primary_cta_text' => $this->primaryCtaText, 'primary_cta_url' => $this->primaryCtaUrl, ]; }
public function get(string $key): mixed { return match ($key) { 'label' => $this->label, 'headline' => $this->headline, 'primary_cta_text' => $this->primaryCtaText, 'primary_cta_url' => $this->primaryCtaUrl, default => null, }; }}Register it:
// In a service provider or configapp(BlockRegistry::class)->register('hero', HeroBlock::class);Now $collection->first('hero') returns a HeroBlock with typed properties:
$hero = $collection->first('hero');$hero->label; // string — IDE autocomplete works!$hero->headline; // stringComparison with Old System
Section titled “Comparison with Old System”| Aspect | Old System | New System |
|---|---|---|
| Resolution | ContentResolver class | BlockCaster Eloquent cast |
| Container | SectionContent | BlockCollection |
| Block access | __get magic | $block->get() |
| Null handling | NullBlockData | null from first() |
| Hydration | Manual hydrateFromContent() | Automatic autoHydrate() |
| Labels | Manual fieldLabels() | Auto-extracted BlockSchemaExtractor |
| Ordering | Grouped by type | Original array order preserved |