Skip to content

#[AsBlock]

Register a class as a native Gutenberg block.

Signature

php
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class AsBlock
{
    public function __construct(
        public string $name,
        public string $title,
        public string $category = 'widgets',
        public ?string $icon = null,
        public ?string $description = null,
        public array $keywords = [],
        public array $supports = [],
        public ?string $parent = null,
        public array $ancestor = [],
        public bool $interactivity = false,
        public ?string $interactivityNamespace = null,
        public ?string $template = null,
        public array $allowedBlocks = [],
        public array $innerBlocksTemplate = [],
        public string|bool|null $innerBlocksTemplateLock = null,
    ) {}

    public function getInteractivityNamespace(): string {}

    public static function hasInnerBlocks(
        array $allowedBlocks,
        array $innerBlocksTemplate,
        string|bool|null $innerBlocksTemplateLock,
    ): bool {}
}

Parameters

ParameterTypeDefaultDescription
namestringBlock name with namespace (required)
titlestringDisplay title (required)
categorystring'widgets'Block category
icon?stringnullDashicon name or SVG
description?stringnullBlock description
keywordsstring[][]Search keywords
supportsarray[]Block supports configuration
parent?stringnullParent block name
ancestorstring[][]Ancestor block names
interactivityboolfalseEnable WordPress Interactivity API
interactivityNamespace?stringBlock nameCustom interactivity namespace
template?stringAuto-resolvedTemplate path
allowedBlocksstring[][]Block names allowed as inner blocks
innerBlocksTemplatearray[]InnerBlocks template
innerBlocksTemplateLockstring|bool|nullnullInnerBlocks lock: 'all', 'insert', 'contentOnly' or false

Setting any of the three allowedBlocks / innerBlocksTemplate / innerBlocksTemplateLock parameters makes the block a container: the editor renders InnerBlocks instead of a server-rendered preview, and the inner markup reaches the Twig template as content.

Assets

There is no parameter for a block's stylesheet or script. Both are found by naming them after the block, and are loaded when the files exist:

FileLoadedWordPress argument
assets/css/blocks/callout.cssfront end and editorstyle_handles
assets/js/blocks/callout.jsfront end, when the block is usedview_script_module_ids

For a block named theme/callout, the file name is the part after the namespace — callout. Both paths are theme-relative and resolved with get_theme_file_path(), so a child theme can override either file.

Because the assets are attached to the block type rather than enqueued globally, WordPress loads them only on pages that actually render the block, and loads the stylesheet into the editor as well — which is what makes the server-rendered preview look like the front end.

The script is registered as a script module, so it is served with type="module". It can use import, including bare specifiers such as @wordpress/interactivity, which WordPress resolves through the import map it prints for registered modules. Modules are deferred and run in strict mode, so load order needs no thought.

A block with no such files needs no configuration, and a file that does not exist registers nothing: registering an absent asset would emit a 404 on every page using the block.

Usage

Basic Block

php
<?php

namespace App\Blocks\Alert;

use Studiometa\Foehn\Attributes\AsBlock;
use Studiometa\Foehn\Contracts\BlockInterface;
use Studiometa\Foehn\Contracts\ViewEngineInterface;
use WP_Block;

#[AsBlock(
    name: 'theme/alert',
    title: 'Alert',
    category: 'widgets',
    icon: 'warning',
)]
final readonly class AlertBlock implements BlockInterface
{
    public function __construct(
        private ViewEngineInterface $view,
    ) {}

    public static function attributes(): array
    {
        return [
            'type' => ['type' => 'string', 'default' => 'info'],
            'message' => ['type' => 'string', 'default' => ''],
        ];
    }

    public function compose(array $attributes, string $content, WP_Block $block): array
    {
        return [
            'type' => $attributes['type'],
            'message' => $attributes['message'],
        ];
    }

    public function render(array $attributes, string $content, WP_Block $block): string
    {
        return $this->view->render('blocks/alert', $this->compose($attributes, $content, $block));
    }
}

Interactive Block

php
<?php

namespace App\Blocks\Counter;

use Studiometa\Foehn\Attributes\AsBlock;
use Studiometa\Foehn\Contracts\InteractiveBlockInterface;
use WP_Block;

#[AsBlock(
    name: 'theme/counter',
    title: 'Counter',
    interactivity: true,
)]
final readonly class CounterBlock implements InteractiveBlockInterface
{
    public static function attributes(): array
    {
        return [
            'initialCount' => ['type' => 'number', 'default' => 0],
        ];
    }

    public static function initialState(): array
    {
        return ['totalClicks' => 0];
    }

    public function initialContext(array $attributes): array
    {
        return ['count' => $attributes['initialCount']];
    }

    public function compose(array $attributes, string $content, WP_Block $block): array
    {
        return ['context' => $this->initialContext($attributes)];
    }

    public function render(array $attributes, string $content, WP_Block $block): string
    {
        // ...
    }
}

With Supports

php
#[AsBlock(
    name: 'theme/card',
    title: 'Card',
    supports: [
        'align' => ['wide', 'full'],
        'color' => ['background' => true, 'text' => true],
        'spacing' => ['padding' => true],
        'html' => false,
    ],
)]

Required Interfaces

  • Basic blocks: BlockInterface
  • Interactive blocks: InteractiveBlockInterface

Released under the MIT License.