ContentBlocks ContentBlocks 2.x Developer Parser and Rendering Building Content in PHP
ContentBlocks exposes element classes to provide a fluid PHP approach (modmore\ContentBlocks\Elements\Canvas and friends) to coding a page in PHP. That builds an in-memory tree that can be persisted to the database (using xPDO models cbCanvas, cbCanvasLayout, cbCanvasBlock) or rendered with the RenderService.
Instead of manually writing JSON, it’s recommended to use this fluid API.
Use toArray() / fromArray($cbCanvas, $data) on Elements classes, or cbCanvas::fromElementsCanvas() / toElementsCanvas() to move between PHP objects and the database.
Table of Contents
Class map
| Layer | Namespace / location | Purpose |
|---|---|---|
| Elements | modmore\ContentBlocks\Elements\{Canvas,Layout,Column,Block} |
Programmatic tree building and rendering |
| Interfaces | Elements\{Canvas,Layout,Column,Block}Interface |
Contracts for Elements classes |
| xPDO models | cbCanvas, cbCanvasLayout, cbCanvasBlock |
Database persistence |
| Manager client | assets/components/contentblocks/js/core/Elements/ |
DOM, events, editor UX |
Canvas
└── Layout (many)
└── Column (many, keyed by reference)
└── Block (many)
On the client, collections (LayoutCollection, ColumnCollection, BlockCollection) sit between the element classes and manage ordering, modals, and drag-and-drop. The PHP Elements layer has no collection classes; parents create children through addLayout(), addColumn(), and addBlock().
Every Elements tree is bound to the database:
Canvasrequires acbCanvasrecordLayoutwraps acbCanvasLayoutand points at its parent canvasColumnhas no table; it points at its parent layoutBlockwraps acbCanvasBlockand points at its parent column
New children get unsaved xPDO objects immediately. You can still render without calling save().
Building content for rendering
Use the Elements classes when you want HTML output — imports, migrations, CLI tools, or one-off generation.
use modmore\ContentBlocks\Elements\Canvas;
/** @var \ContentBlocks $contentBlocks */
/** @var cbCanvas $cbCanvas */
$cbCanvas = $contentBlocks->getCanvas('modResource', $resourceId);
$renderService = $contentBlocks->getRenderService();
$canvas = new Canvas($cbCanvas);
$canvas->addLayout('full-width')
->setTitle('Hero section')
->addColumn('main')
->addBlock('richtext', ['text' => ['value' => '<p>Hello from PHP</p>']]);
$html = $canvas->generateOutput($renderService);
addLayout(), addColumn(), and addBlock() return the created child. Each element implements generateOutput(RenderService $renderService) so you can render at any level (single block, column, layout, or full canvas).
Block data shape
Block data must match what input types produce on the client. Scalar inputs typically nest under their input key:
// text input
['text' => ['value' => 'Hello']]
// richtext input
['body' => ['value' => '<p>Rich HTML</p>']]
// image input (simplified)
['image' => [
'url' => '/assets/image.jpg',
'relativeUrl' => 'assets/image.jpg',
'width' => 800,
'height' => 600,
]]
Check the block definition JSON and input type docs for the exact structure per input. Rendering does not validate data against definitions; missing keys surface as empty template variables.
Update block data after construction with setData():
$block = $column->addBlock('text');
$block->setData(['text' => ['value' => 'Updated']]);
Publishing fields
Layouts and blocks support the same publishing properties as the manager:
| Property | PHP setter | Default |
|---|---|---|
active |
setActive(bool) |
true |
activate_on |
setActivateOn(int) |
0 (Unix timestamp) |
deactivate_on |
setDeactivateOn(int) |
0 |
RenderService skips elements where isActive() is false. It does not evaluate activate_on / deactivate_on against the current time at render time. Scheduled visibility is handled by PublishingService, which updates the stored active flag in the database. When building Elements in PHP for immediate render, set active to the effective value yourself.
Content array format
Elements classes serialize to the same structure the manager uses for layouts and blocks, including instance IDs when set.
{
"id": 12,
"layouts": [
{
"id": 45,
"layout": "full-width",
"title": "Hero section",
"data": {},
"active": true,
"activate_on": 0,
"deactivate_on": 0,
"columns": {
"main": {
"blocks": [
{
"id": 101,
"block": "text",
"data": { "text": { "value": "Hello" } },
"active": true,
"activate_on": 0,
"deactivate_on": 0
}
]
}
}
}
]
}
| Field | PHP API | Serialized key |
|---|---|---|
| Canvas ID | getId() from the cbCanvas record |
id (optional on canvas root) |
| Layout/block instance ID | getId() / hasId() |
id (0 when unset, same as client) |
| Layout definition key | setKey() / getKey() |
layout |
| Block definition key | setKey() / getKey() |
block |
| Column structure | $layout->addColumn('main') |
object keyed by column key |
| Layout title | setTitle() / getTitle() |
title |
| Layout modal data | setData() / getData() |
data |
| Block input data | setData() / getData() |
data |
Column serialize output matches the client: { "blocks": [...] } without a reference field. The column key lives on the parent layout’s columns object. When deserializing a column payload on its own, pass the parent layout and the reference:
Column::deserialize($layout, 'main', $columnJson);
Column::fromArray($layout, 'main', $columnData);
$canvas = Canvas::fromArray($cbCanvas, $payload);
$payload = $canvas->toArray();
$json = $canvas->serialize(); // json_encode(toArray())
$canvas = Canvas::deserialize($cbCanvas, $json); // fromArray(json_decode())
Legacy payloads using key instead of layout/block, or column lists with reference, are still accepted by fromArray().
Editing a loaded canvas
Load the canvas, change an element you already hold, and call save() on that element. That writes only that row (and, for blocks, that block’s repeater children). It does not delete siblings.
/** @var cbCanvas $cbCanvas */
$cbCanvas = $contentBlocks->getCanvas('modResource', $resourceId);
$canvas = $cbCanvas->toElementsCanvas();
$block = $canvas->getLayouts()[0]['layout']->getColumns()[0]['blocks'][0];
$block->setData(['text' => ['value' => 'Updated copy']]);
$block->save();
Repeater row IDs live inside the parent block’s data.rows arrays (same as the client).
To replace the entire canvas the way the manager does (create missing rows and delete anything absent from the payload), use fromElementsCanvas() / updateContentFromJSON(). Always send the complete canvas state unless you intend to delete sibling layouts or blocks.
Repeaters
Include repeater rows in the parent block’s data using the same shape the client sends:
$column->addBlock('featured-list', [
'title' => ['value' => 'Featured'],
'items' => [
'rows' => [
[
'block' => 'repeater-row',
'data' => ['text' => ['value' => 'Row one']],
'active' => true,
'activate_on' => 0,
'deactivate_on' => 0,
],
],
],
]);
- Rendering — nested
rowsin block data work directly; the parent tree still needs acbCanvas, but you do not have to callsave(). - Persistence —
Block::save(),cbCanvas::fromElementsCanvas(), andupdateContentFromJSON()pass repeater rows toRepeaterBlockService, which stores child rows as individualcbCanvasBlockrecords withparent_blockandrepeater_key. - Loading —
cbCanvas::toElementsCanvas()hydrates child rows back into nesteddata.
Database models
cbCanvas
| Field | Description |
|---|---|
principal_type |
Owner class, e.g. modResource |
principal_id |
Owner ID |
principal_field |
TV/resource field name, usually content |
graph |
Reserved for future use; not read or written today |
Access via $contentBlocks->getCanvas('modResource', $resourceId, 'content').
cbCanvasLayout
| Field | Description |
|---|---|
canvas |
Parent canvas ID |
layout_key |
Definition key from layouts/*.json |
position |
0-based order on the canvas |
title |
Custom layout title from the manager |
data |
Layout modal input values (array) |
active, activate_on, deactivate_on |
Publishing state |
cbCanvasBlock
| Field | Description |
|---|---|
canvas, layout |
Parent references |
column_key |
Column from layout definition |
position |
Order within the column |
block_key |
Definition key from blocks/*.json |
data |
Input values (array); repeater rows are stripped on save |
parent_block, repeater_key |
Repeater row linkage (child blocks) |
active, activate_on, deactivate_on |
Publishing state |
Loading and saving
Render from a stored canvas
/** @var cbCanvas $canvas */
$canvas = $contentBlocks->getCanvas('modResource', $resourceId);
$html = $canvas->generateOutput($contentBlocks->getRenderService());
Internally, cbCanvas::toElementsCanvas() wraps the canvas record and its layout/block rows in Elements classes (including repeater hydration) and passes the result to RenderService.
Persist a single element
$canvas = $cbCanvas->toElementsCanvas();
$layout = $canvas->addLayout('full-width');
$block = $layout->addColumn('main')->addBlock('text', ['text' => ['value' => 'Hello']]);
$layout->save(); // new layout row; does not save child blocks
$block->save(); // new block row, FKs from parents
save() writes only that record. A new block first saves its canvas and layout if those rows do not exist yet. Columns have no table and no save() method.
Persist an Elements tree
/** @var cbCanvas $canvas */
$canvas = $contentBlocks->getCanvas('modResource', $resourceId);
$response = $canvas->fromElementsCanvas($elementsCanvas);
// $response tracks added/updated/removed layout and block IDs
fromElementsCanvas() syncs through the same ID-based upsert path as the manager save, including deletes. Use per-element save() when you only want to write the rows you changed.
Duplicate a canvas
$copy = $sourceCanvas->duplicate();
Copies all layouts and blocks (including repeater children) into a new cbCanvas record via the Elements round-trip. You will need to update the canvas principal values manually.
Conversion paths
| Direction | Method |
|---|---|
| Elements → array | $canvas->toArray() |
| array → Elements | Canvas::fromArray($cbCanvas, $data) |
| Database → Elements | $cbCanvas->toElementsCanvas() |
| One element → Database | $layout->save() / $block->save() |
| Elements → Database (full upsert) | $cbCanvas->fromElementsCanvas($canvas) |
| Elements → HTML | $canvas->generateOutput($renderService) |
| Database → manager JSON | $cbCanvas->getContentAsJSON() |
| manager JSON → Database | $cbCanvas->updateContentFromJSON($json) |
Limitations
- Publishing is scheduled at render time —
RenderServicechecksactiveonly. RunPublishingService::syncCanvasPublishingState()on stored canvases, or setactiveyourself when building Elements for immediate output. - No definition validation — Elements classes do not verify that layout, block, or column keys exist in config.
cbCanvas.graph— reserved; not used yet.Canvas::getLayouts()return shape — returns enriched entries[['layout' => ..., 'key' => ..., 'data' => ...]]forRenderService, not bare layout objects.
API reference (Elements)
Canvas / Elements\CanvasInterface
__construct(cbCanvas $record)getRecord(): cbCanvas/getId(): ?int/save(): booladdLayout(string $key = '', ?int $position = null): LayoutInterfacegetLayouts(): array— enriched entries for renderingtoArray(): array/fromArray(cbCanvas $record, array $data): CanvasInterfacegenerateOutput(RenderService $renderService): stringserialize(): string/deserialize(cbCanvas $record, string $serialized): CanvasInterface
Layout / LayoutInterface
getParent(): CanvasInterface/getRecord(): cbCanvasLayoutgetId(): int|string|null/hasId(): bool/save(): boolsetKey(string)/getKey(): stringsetTitle(?string)/getTitle(): ?stringsetData(array)/getData(): arraysetActive(),setActivateOn(),setDeactivateOn()/ matching gettersaddColumn(string $reference): ColumnInterfacegetColumns(): arraytoArray(): array/fromArray(CanvasInterface $canvas, array $data): LayoutInterfacegenerateOutput(),serialize(),deserialize(CanvasInterface $canvas, string $serialized)
Column / ColumnInterface
__construct(LayoutInterface $parent, string $reference)getParent(): LayoutInterface/getReference(): stringaddBlock(string $key = '', array $data = [], ?int $position = null): BlockInterfacegetBlocks(): BlockInterface[]toArray(): array/fromArray(LayoutInterface $layout, string $reference, array $data): ColumnInterfaceserialize(): string/deserialize(LayoutInterface $layout, string $reference, string $serialized): ColumnInterfacegenerateOutput()- No
save()— columns are not stored as rows
Block / BlockInterface
getParent(): ColumnInterface/getRecord(): cbCanvasBlockgetId(): int|string|null/hasId(): bool/save(): boolsetKey(string)/getKey(): stringsetData(array)/getData(): arraysetActive(),setActivateOn(),setDeactivateOn()/ matching getterstoArray(): array/fromArray(ColumnInterface $column, array $data): BlockInterfacegenerateOutput(),serialize(),deserialize(ColumnInterface $column, string $serialized)
See also
- Parser — template resolution and
RenderServicebehavior - Canvas Structure — client-side element hierarchy
- Data Flow — save and render pipelines
- Publishing plugin — scheduled visibility in the manager