Skip to content

Block Bindings

Fill core blocks and Blade blocks with server data in Pollora: #[BlockBinding] sources in PHP, typed meta formatted by type, a field picker and live preview in the editor.

A Block Binding fills an attribute of a block — the text of a paragraph, the URL of a button, the image of an image block — with a value computed on the server, instead of what the editor typed. An event page, a product sheet or a team member card can then be built from core blocks, with no custom block to write.

Block Bindings need WordPress 6.9 or later; Pollora installs WordPress 7. Pollora lets you declare a source as a PHP class, ships sources that read typed meta formatted by their type, and makes your own Blade blocks bindable with one line of block.json.

Experimental. The API may still change before it is declared stable.

A source is a class marked #[BlockBinding]. Each public method marked #[BindingField] is a field, chosen in the block with the field argument:

use App\Cms\PostTypes\Event;
use App\Services\BookingRepository;
use Pollora\Attributes\BlockBinding;
use Pollora\Attributes\BlockBinding\BindingField;
use Pollora\BlockBinding\Domain\Models\BindingContext;
#[BlockBinding('acme/event', label: 'Event', postTypes: 'event')]
final class EventBinding
{
#[BindingField(label: 'Remaining seats')]
public function remainingSeats(BindingContext $context, BookingRepository $bookings): string
{
$event = $context->meta(Event::class);
$left = max(0, $event->capacity - $bookings->countFor($context->postId));
return trans_choice('events.seats', $left, ['count' => $left]);
}
#[BindingField(label: 'Booking link', type: 'url')]
public function bookingUrl(BindingContext $context): string
{
return route('events.book', ['event' => $context->postId]);
}
}

The class is discovered in the application, themes, plugins and modules, and resolved by the container: the constructor and each field receive their dependencies, like BookingRepository above. php artisan pollora:make:binding EventBinding generates one (--theme, --plugin, --module to choose where).

A block binds an attribute to a field in its markup:

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"acme/event","args":{"field":"remaining_seats"}}}}} -->
<p>Seats available</p>
<!-- /wp:paragraph -->
<!-- wp:buttons --><div class="wp-block-buttons">
<!-- wp:button {"metadata":{"bindings":{"url":{"source":"acme/event","args":{"field":"booking_url"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">Book</a></div>
<!-- /wp:button -->
</div><!-- /wp:buttons -->

The text saved in the block is a fallback: it shows when the field returns null.

Nothing to write in JavaScript: the fields come from the class.

  • Choosing a field. Select a bindable block, open Attributes in the block settings, pick the attribute, then a source and one of its fields. A source limited by postTypes is only offered on those post types; pollora/post-meta offers the meta of the post type being edited that it may show.
  • Previewing the value. A bound block shows the value of the post being edited, computed on the server by the same code as the page, and escaped the same way. The values of the blocks on screen are asked in one request, to POST /wp-json/pollora/v1/block-bindings/resolve, which answers only a user who can edit the post.
  • Read-only. A block bound to a Pollora source cannot be edited from the canvas. While its value loads, and when the field gives none, it shows the field’s label. To edit a meta from a block, bind it to WordPress’s core/post-meta.

The editor offers a field to an attribute of the same data type: every field gives a string, so it is offered to text and URL attributes alike, and an attachment meta is also offered to an image’s id.

AttributeParameterDefaultEffect
#[BlockBinding]namerequiredSource name, namespace/name in lowercase, as WordPress requires
#[BlockBinding]labelclass nameLabel shown in the editor
#[BlockBinding]usesContext['postId', 'postType']Block context the source reads
#[BlockBinding]postTypesallThe post types the source answers for; on any other post, the block keeps its content
#[BindingField]namemethod name in snake_caseValue of the field argument: remainingSeats is remaining_seats
#[BindingField]labelmethod name, headlinedLabel of the field
#[BindingField]typetexttext, url or image: a url or image value is sanitized as a URL

A class with no field defines __invoke(BindingContext $context) and receives every call, whatever its arguments.

MemberContent
postId, postTypeThe post of the block context, so the right one inside a query loop
termId, taxonomyThe term of the block context (a terms query)
arg('format', $default)An argument of the binding, besides field
attributeThe bound attribute: content, url, alt…
blockThe WP_Block, for advanced cases
post()The post, as a Pollora\Models\Post (the model bound to its post type, when there is one)
meta(Event::class)The typed meta of a class, on the object that carries them: the post, the term, or the post’s author for #[UserMeta]

A field declares its return type: string, int, float, bool, a Stringable or null. A class whose field has no return type, or returns an array, is reported at discovery, like a name WordPress would refuse or a field declared twice.

Pollora escapes the value for the place it lands in, so a source cannot forget to:

ValueWritten as
Text in a paragraph, a heading, a list itemEscaped: <for> shows as text
A url or image fieldSanitized as a URL
An HtmlStringFiltered like post content (wp_kses_post())
A boolean in a paragraph”Yes” or “No”, translated
An attribute of a Blade blockAs returned: the template escapes it, like any attribute

null keeps what the block holds. A field that throws is logged and treated as null, so a failing source never takes a page down; with APP_DEBUG on, the exception is thrown. With APP_DEBUG on too, a field that takes more than 50 ms is logged as a warning, with its source, its field and its post: every bound block of the page waits for it.

A block rendered on the server becomes bindable by listing its attributes under pollora.bindings in its block.json:

{
"name": "acme/event-card",
"attributes": {
"title": { "type": "string" },
"ctaUrl": { "type": "string" }
},
"pollora": { "bindings": ["title", "ctaUrl"] },
"render": "file:./render.blade.php"
}

WordPress replaces the bound attributes before the block renders, so the view does not change:

<div {!! get_block_wrapper_attributes() !!}>
<h3>{{ $attributes['title'] }}</h3>
<a href="{{ $attributes['ctaUrl'] }}">{{ __('Book', 'acme') }}</a>
</div>

Only a block with a render can be bound: WordPress cannot rewrite the saved HTML of a block that is not one of its own. A block listing bindings without render, or an attribute it does not declare, is reported in the log.

Pollora ships four sources. Those reading meta format the value by its declared type, which core/post-meta does not: a date reads as a date, not 2026-11-14T09:00:00+00:00.

SourceReadsArguments
pollora/post-metaA typed meta of the post of the blockkey, format, decimals, true, false, size, fallback
pollora/term-metaA typed meta of the term of the block, or of the queried term on a term archivesame
pollora/author-metaA typed meta of the author of the postsame
pollora/optionA site optionname, fallback
<!-- wp:heading {"metadata":{"bindings":{"content":{"source":"pollora/post-meta","args":{"key":"starts_at","format":"l j F Y"}}}}} -->
<h2 class="wp-block-heading">Date to come</h2>
<!-- /wp:heading -->

The title of a post, its link or date, and the name of a term are served by WordPress’s own core/post-data and core/term-data.

Meta typeShown asArgument
stringAs stored, escapednone
int, floatA number with the site’s separatorsdecimals
bool”Yes” or “No”, translatedtrue, false for other words
DateThe site’s date format, in its languageformat, a PHP date format
EnumIts label() when the enum defines one, else its valuenone
ArrayIts items, as a list: “rock, jazz, and blues”as for one item
Attachment IDDepends on the bound attribute (see below)size

format: raw gives the value as stored, and fallback a text for an empty meta. An object meta is not shown.

A meta marked #[Meta(media: true)] holds an attachment ID. Bound to an image, it gives what each attribute needs:

#[Meta(showInRest: true, media: true)]
public ?int $coverImageId = null;
<!-- wp:image {"metadata":{"bindings":{
"url":{"source":"pollora/post-meta","args":{"key":"cover_image_id","size":"large"}},
"alt":{"source":"pollora/post-meta","args":{"key":"cover_image_id"}}}}} -->
<figure class="wp-block-image"><img alt=""/></figure>
<!-- /wp:image -->
AttributeValue
url (and any other)The image URL at size (full by default)
altThe alternative text
titleThe attachment title
captionThe caption
idThe ID

A binding that cannot show its value gives no error: the block simply keeps the content it was saved with. php artisan pollora:doctor, and Tools › Site Health in wp-admin, read every binding written in the templates, template parts and patterns of the theme, the Pollora plugins and the modules, and name the file, the block and the reason:

✗ Block bindings — 2 binding(s) can never show a value: the blocks keep their saved content.
theme buzz: templates/single-event.html — core/paragraph, "content" → acme/event: the field "seats" does not exist; acme/event has the fields "remaining_seats", "booking_url"
theme buzz: patterns/event-card.php — core/paragraph, "content" → pollora/post-meta: the meta "internal_ref" is never shown: it is not exposed in REST (showInRest: true) or its key is protected

It reports a source that is not registered, a field the source does not have, a meta no #[Meta] declares, a meta or an option the source may not show, an attribute WordPress does not bind for that block, and a block.json whose pollora.bindings has no render or lists an attribute the block does not declare. Bindings saved in posts, in the database, are not read.

php artisan pollora:binding:list shows what can be bound: each Pollora source with its fields, the meta it may show (by post type or taxonomy) and the options it may read, then every block whose attributes WordPress lets bind. --json gives the same as JSON.

acme/event Event ..................................... EventBinding · event
field: remaining_seats ............................... Remaining seats
field: booking_url ...................................... Booking link
pollora/post-meta Post meta (Pollora) ..................... PostMetaSource
key: starts_at ............................................ Start (event)

Anyone who can edit a post can bind any source to it, so each source only shows what may be public, and the checks live in one place that no source can skip:

  • The post or term must be visible. Nothing is shown of a post the visitor cannot read (unpublished without read_post, waiting for its password) or of a term of a taxonomy that is not publicly queryable — the checks of core/post-meta and core/term-data.
  • Post and term meta: only a meta declared with showInRest: true, under a key that does not start with _, as core/post-meta does.
  • User meta: showInRest is not enough, a user meta may be personal. pollora/author-meta only reads a meta marked #[Meta(public: true)].
  • Options: pollora/option only reads the options listed in config/block-bindings.php, none by default:
config/block-bindings.php
return [
'options' => ['blogdescription'],
];
  • No source reads the logged-in visitor: the value would leak through the page cache.

A field is called once per request for the same post, attribute and arguments, and a source class is only made the first time one of its fields is used. Typed meta are read through WordPress’s meta cache, already primed for the posts of a query loop. In a loop of 20 posts, four bound blocks make 80 calls: a field should not run an uncached query.