Ryan Welcher

Developer Advocate

PHP-only block registration in WordPress 7.0

WordPress 7.0 introduces PHP-only block registration—a new way to register blocks using PHP as the source of truth for block metadata, without requiring a block.json file. If you’re building dynamic blocks (or PHP-first plugins), this is a nice workflow upgrade: you can keep registration, metadata, assets, and rendering together. block.json is still a solid default, but it can feel like extra overhead when the block is server-rendered and the bulk of the work already happens in PHP. PHP-only registration in 7.0 fills in that gap by letting you define the block’s metadata directly in the register_block_type() call.

How it works

The flow is the same shape you’re used to:

  1. Hook into init.
  2. Register scripts/styles (optional).
  3. Call register_block_type() with a full metadata array.
  4. Provide a render_callback for dynamic output.

Enabling PHP-only registration with autoRegister

To opt into PHP-only block registration, set autoRegister under supports:

'supports' => array(
    'autoRegister' => true,
),
PHP

That flag tells WordPress to register the block using the metadata you provide in PHP.

Basic registration

Start by registering on init:

add_action(
    'init',
    __NAMESPACE__ . '\register_blocks'
);
PHP

Then register the block. For PHP-only registration, make sure supports.autoRegister is set:

function register_blocks() {

    register_block_type(
        'twitch/php-only-block',
        array(
            'title'           => 'My PHP Only block',
            'icon'            => 'smiley',
            'category'        => 'widgets',
            'description'     => 'A block registered entirely in PHP',
            'keywords'        => array( 'php' ),
            'supports'        => array(
                'autoRegister' => true,
            ),
            'render_callback' => __NAMESPACE__ . '\render_sample_block',
        )
    );
}
PHP

Here’s a simple render callback. The important piece is get_block_wrapper_attributes()—it ensures wrapper-based features (supports, custom class names, etc.) are applied consistently:

function render_sample_block( $attributes ) {
    $title = $attributes['title'];

    ob_start();
    ?>
    <div <?php echo wp_kses_data( get_block_wrapper_attributes( array( 'class' => 'php-only-block' ) ) ); ?>>
        <h2 class="php-only-block-title"><?php echo esc_html( $title ); ?></h2>
    </div>
    <?php
    return ob_get_clean();
}
PHP

Attributes and supports

Once you’re defining the block in PHP, it’s natural to keep the rest of the definition there too—attributes and supports included.

Attributes

From your reference snippet:

'attributes' => array(
    'title'   => array(
        'type'    => 'string',
        'default' => 'Hello World',
    ),
    'count'   => array(
        'type'    => 'integer',
        'default' => 5,
    ),
    'enabled' => array(
        'type'    => 'boolean',
        'default' => true,
    ),
    'size'    => array(
        'type'    => 'string',
        'enum'    => array( 'small', 'medium', 'large' ),
        'default' => 'medium',
    ),
),
PHP

What’s really cool about this is that once we register the attributes, they automagincally get Inspector Controls rendered to manage them.

And in your render callback you can read them:

$title   = $attributes['title'];
$count   = $attributes['count'];
$enabled = $attributes['enabled'];
$size    = $attributes['size'];
PHP

Supports

Supports work the same way they do elsewhere, including opting into built-in design tools:

'supports' => array(
    'autoRegister' => true,
    'color'         => array(
        'background' => true,
    ),
    'spacing'      => array(
        'margin'   => true,
        'padding'  => true,
        'blockGap' => true,
    ),
),
PHP

If you enable wrapper-based supports like spacing and color, make sure your render callback outputs wrapper attributes. Otherwise you’ll have controls in the editor that don’t show up on the front end.

5. Adding scripts and styles

Register assets normally, then attach them to the block using handles.

Register assets

$css_file = plugin_dir_path( __FILE__ ) . '/assets/test.css';

wp_register_style(
    'php-only-blocks-style',
    plugin_dir_url( __FILE__ ) . 'assets/test.css',
    array(),
    filemtime( $css_file )
);

wp_register_script(
    'php-only-blocks-script',
    plugin_dir_url( __FILE__ ) . 'assets/index.js',
    array(),
    filemtime( plugin_dir_path( __FILE__ ) . '/assets/index.js' ),
    true
);

$css_editor_file = plugin_dir_path( __FILE__ ) . '/assets/editor.css';

wp_register_style(
    'php-only-blocks-editor-style',
    plugin_dir_url( __FILE__ ) . 'assets/editor.css',
    array(),
    filemtime( $css_editor_file )
);
PHP

Attach handles to the block

'script_handles'       => array( 'php-only-blocks-script' ),
'style_handles'        => array( 'php-only-blocks-style' ),
'editor_style_handles' => array( 'php-only-blocks-editor-style' ),
PHP

This keeps assets block-scoped, which is usually what you want.

Full Code

Click to see the full code example
// Register the block on the init action.
add_action(
	'init',
	__NAMESPACE__ . '\register_blocks'
);


/**
 * Renders the sample block.
 */
function register_blocks() {

	$css_file = plugin_dir_path( __FILE__ ) . 'assets/test.css';

	wp_register_style(
		'php-only-blocks-style',
		plugin_dir_url( __FILE__ ) . 'assets/test.css',
		array(),
		filemtime( $css_file )
	);

	wp_register_script(
		'php-only-blocks-script',
		plugin_dir_url( __FILE__ ) . 'assets/index.js',
		array(),
		filemtime( plugin_dir_path( __FILE__ ) . '/assets/index.js' ),
		true
	);

	$css_editor_file = plugin_dir_path( __FILE__ ) . 'assets/editor.css';
	wp_register_style(
		'php-only-blocks-editor-style',
		plugin_dir_url( __FILE__ ) . 'assets/editor.css',
		array(),
		filemtime( $css_editor_file )
	);

	register_block_type(
		'twitch/php-only-block',
		array(
			'title'                => 'My PHP Only block',
			'icon'                 => 'smiley',
			'category'             => 'widgets',
			'description'          => 'A block registered entirely in PHP',
			'keywords'             => array( 'php' ),
			'attributes'           => array(
				'title'   => array(
					'type'    => 'string',
					'default' => 'Hello World',
				),
				'count'   => array(
					'type'    => 'integer',
					'default' => 5,
				),
				'width'   => array(
					'type'    => 'number',
					'default' => 200,
				),
				'enabled' => array(
					'type'    => 'boolean',
					'default' => true,
				),
				'size'    => array(
					'type'    => 'string',
					'enum'    => array( 'small', 'medium', 'large' ),
					'default' => 'medium',
				),
			),
			'example'              => array(),
			'render_callback'      => __NAMESPACE__ . '\render_sample_block',
			'supports'             => array(
				'autoRegister' => true,
				'color'         => array(
					'background' => true,
				),
				'spacing'      => array(
					'margin'   => true,
					'padding'  => true,
					'blockGap' => true,
				),
			),
			'script_handles'       => array( 'php-only-blocks-script' ),
			'style_handles'        => array( 'php-only-blocks-style' ),
			'editor_style_handles' => array( 'php-only-blocks-editor-style' ),
		)
	);
}

/**
 * Renders the sample block.
 */
function render_sample_block( $attributes ) {
	$title   = $attributes['title'];
	$count   = $attributes['count'];
	$enabled = $attributes['enabled'];
	$width   = $attributes['width'];
	$size    = $attributes['size'];

	ob_start();
	?>
	<div <?php echo wp_kses_data( get_block_wrapper_attributes( array( 'class' => 'php-only-block' ) ) ); ?>>
		<h2 class="php-only-block-title"><?php echo esc_html( $title ); ?></h2>
		<p>Count: <?php echo esc_html( $count ); ?></p>
		<p>Enabled: <?php echo $enabled ? 'Yes' : 'No'; ?></p>
		<p>Width: <?php echo esc_html( $width ); ?></p>
	</div>
	<?php
	return ob_get_clean();
}
PHP

Gotchas

  1. Don’t forget supports.autoRegister
    If you’re aiming for PHP-only registration, this is the required opt-in.
  2. Inspector controls are generated automatically, but only for certain attribute types
    Wherever possible, WordPress will generate controls in the Inspector sidebar for block attributes. The currently supported attribute types are: string, number, integer, boolean, and enum.
    Controls won’t be generated for attributes with the local role, or for attribute types outside that list.
  3. Use get_block_wrapper_attributes()
    Especially with spacing/color supports. It’s the difference between “controls exist” and “controls work.”
  4. Register asset handles before you reference them
    If you attach handles in register_block_type(), make sure they’ve already been registered.

4 responses to “PHP-only block registration in WordPress 7.0”

  1. Weston Ruter Avatar

    Thanks for the writeup!

    And in your render callback you can read them with fallbacks:

    So the defaults aren’t merged with the provided attributes when passed to the render callback?

    If not, that seems like an improvement which would reduce duplication.

    1. ryanwelcher Avatar

      Great callout Weston! The defaults defined when registering the attribute are indeed used so there is no need to do the fallbacks. I’ve updated that section accordingly.

  2. […] Welcher also explains how PHP-only block registration in WordPress 7.0 lets you skip block.json entirely and define block metadata directly in […]

  3. […] Fonseca explained PHP only block registration process. Also, Ryan Welcher published step by step process for the […]

Leave a Reply

Your email address will not be published. Required fields are marked *