WordPress development has long been divided into two distinct worlds: the PHP-driven backend that millions of developers built their careers on, and the JavaScript-powered Gutenberg editor that arrived in WordPress 5.0 and redefined content creation.
For years, building a custom Gutenberg block meant learning React, JSX, webpack, and a full Node.js toolchain. That was a steep climb for developers who are perfectly comfortable with PHP and had no particular need for JavaScript frameworks. Many simply avoided the block editor entirely, sticking with shortcodes and widgets rather than engaging with a build process that felt foreign.
WordPress 7.0 changes that equation completely. With WordPress 7.0, PHP-only block development is no longer an experimental workaround or a limited fallback โ it is a fully supported, production-ready approach. In this guide, we’ll walk through everything you need to build PHP-only Gutenberg blocks in WordPress 7.0: from your first block to dynamic real-world implementations.
Requirement
PHP-only blocks require Gutenberg 22.3 or higher and WordPress 7 while PHP 8.1 is the minimum PHP version for WordPress 7.
1. What Are PHP-Only Blocks and Why Do They Matter in WordPress 7.0?
A PHP-only Gutenberg block is a custom block that you register and render entirely using PHP. No JavaScript files. No React components. No npm install. No webpack configuration. No compiled assets.
The block still appears in the Gutenberg editor just like any other block. Users can insert it from the block inserter, configure it through the sidebar panel, and see a live preview of the output. The key difference is that all of this is driven by PHP running on the server rather than JavaScript running in the browser.
This matters for several concrete reasons:
- Lower barrier to entry โ PHP developers can now contribute to the block ecosystem without learning a front-end JavaScript framework.
- No build step โ your plugin ships without a node_modules folder, a package.json, or compiled bundles to maintain across dependency updates.
- Simpler maintenance โ block logic lives in one language, in one place, readable by any PHP developer who inherits the project.
- Better fit for dynamic content โ blocks that run database queries, call external APIs, or depend on server-side state are a natural fit for PHP rendering.
- Performance โ no unnecessary front-end JavaScript is loaded for blocks that have nothing to do on the client side.
PHP-only blocks are not a replacement for every kind of block. Complex interactive blocks with real-time UI feedback, drag-and-drop, or live editing still belong in JavaScript. But for the vast majority of custom blocks in real WordPress projects โ dynamic listings, content widgets, data displays, plugin output โ the PHP path is the cleaner, simpler, and more maintainable choice.
2. What Changed in WordPress 7.0 for Block Development?
PHP-only blocks existed in earlier versions of WordPress as “dynamic blocks,” but the developer experience had a significant gap: you still needed JavaScript to register the block on the editor side, define the edit interface, and handle attribute binding. Even a block that rendered entirely in PHP still required a JavaScript build step to function in the editor.
WordPress 7 closes that gap. The core changes that make genuine PHP-only development possible are:
- Auto-registration support flag โ setting ‘autoRegister’ => true in the block’s supports array tells Gutenberg to automatically pass block metadata from PHP to the editor client, eliminating the need for a JavaScript entry point.
- Server-Side Render as the default edit view โ when no JavaScript edit function exists, Gutenberg uses ServerSideRender automatically to show the PHP-rendered preview in the editor. What the editor shows is exactly what visitors see.
- Automatic inspector controls from attributes โ Gutenberg now generates sidebar settings UI automatically from your attribute definitions. A string attribute becomes a text field. A boolean becomes a checkbox. An enum becomes a select dropdown. No React required.
- Native block supports via PHP โ color, spacing, typography, border, and shadow controls are all available through the supports array in PHP and wired up by WordPress automatically.
While PHP-only blocks are production-ready in WordPress 7, the auto-generated inspector controls feature (autoRegister) is still evolving. Advanced UI components like RichText editors, RangeControl sliders, and custom panel layouts still require JavaScript. For these, the traditional Gutenberg development path applies.
3. How to Build Your First PHP-Only Block?
Let’s build from scratch. The simplest PHP-only block requires just two things: a PHP function that returns HTML, and a call to register_block_type() with the right configuration.
Plugin Structure
Your entire plugin can be a single PHP file:
my-php-blocks/
โโโ my-php-blocks.php
No src/ folder. No node_modules. No build output. That’s the complete project.
The Code
my-php-blocks.php
<?php
/**
* Plugin Name: My PHP Blocks
* Description: PHP-only Gutenberg blocks โ no JavaScript required.
* Version: 1.0.0
* Requires at least: 7.0
* Requires PHP: 8.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function my_php_block_render( $attributes ) {
return '<div>
<h3>PHP-only Block</h3>
<p>This block was created with only PHP!</p>
</div>';
}
add_action( 'init', function() {
register_block_type( 'my-plugin/php-only-block', array(
'title' => 'My PHP Block',
'icon' => 'welcome-learn-more',
'category' => 'text',
'render_callback' => 'my_php_block_render',
'supports' => array(
'autoRegister' => true,
),
) );
} );
The autoRegister flag is the WordPress 7 key. It instructs Gutenberg to pass the block metadata from PHP to the editor client automatically, so the block appears in the inserter and renders its preview โ all without a single JavaScript file.
Activate the plugin, open any page in the editor, search for “My PHP Block” in the inserter, and insert it. It works.
What just happened?
WordPress 7 automatically used ServerSideRender to display the PHP output as the editor preview. No JavaScript edit function needed. What you see in the editor is exactly what visitors see on the front end.
4. Using Attributes to Auto-Generate Block Settings UI
A block that always renders the same output is rarely useful. Attributes let users configure the block, and in WordPress 7, they also define the settings UI automatically.
Gutenberg maps PHP attribute types to editor controls without any JavaScript:
| Attribute Type | Auto-Generated Control |
| ‘type’ => ‘string’ | Text input field |
| ‘type’ => ‘integer’ | Number input field |
| ‘type’ => ‘boolean’ | Toggle / Checkbox |
| ‘type’ => ‘string’ + ‘enum’ array | Select dropdown |
Here’s a block with all four types in action:
my-php-blocks.php โ Extended Example
<?php
function my_php_block_render( $attributes ) {
$title = esc_html( $attributes['blockTitle'] );
$count = intval( $attributes['itemCount'] );
$enabled = $attributes['isEnabled'];
$size = esc_attr( $attributes['displaySize'] );
$font_size = match( $size ) {
'large' => '20px',
'small' => '12px',
default => '16px',
};
$output = sprintf(
'<div style="font-size: %s; border: 1px solid #ccc; padding: 15px;">',
$font_size
);
$output .= sprintf( '<h3>%s</h3>', $title );
if ( $enabled ) {
$output .= '<ul>';
for ( $i = 1; $i <= $count; $i++ ) {
$output .= sprintf( '<li>Item %d</li>', $i );
}
$output .= '</ul>';
} else {
$output .= '<p><em>List is currently disabled.</em></p>';
}
$output .= '</div>';
return $output;
}
add_action( 'init', function() {
register_block_type( 'my-plugin/php-only-block', array(
'title' => 'My PHP Block',
'icon' => 'welcome-learn-more',
'category' => 'text',
'render_callback' => 'my_php_block_render',
'attributes' => array(
'blockTitle' => array(
'type' => 'string',
'default' => 'My PHP Block',
),
'itemCount' => array(
'type' => 'integer',
'default' => 3,
),
'isEnabled' => array(
'type' => 'boolean',
'default' => true,
),
'displaySize' => array(
'type' => 'string',
'enum' => array( 'small', 'medium', 'large' ),
'default' => 'medium',
),
),
'supports' => array(
'autoRegister' => true,
),
) );
} );
Save, reload the editor, and insert the block. The sidebar will show a text field for the title, a number field for the item count, a toggle for enabling/disabling the list, and a dropdown for size โ all generated automatically by WordPress 7 from the attributes array.
No React. No JavaScript. Just PHP describing data types.
5. Real-World Example: A Dynamic Testimonials Block
Let’s build something production-ready: a block that pulls customer testimonials from a custom post type and renders them as quote cards. This is a perfect fit for PHP โ the content lives in the database, needs no client-side interactivity, and should always reflect the latest approved testimonials without any manual updates.
Plugin: Testimonials Block
testimonials-block.php
<?php
/**
* Plugin Name: Testimonials Block
* Description: PHP-only block displaying customer testimonials as quote cards.
* Version: 1.0.0
* Requires at least: 7.0
* Requires PHP: 8.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function testimonials_block_render( $attributes ) {
$count = intval( $attributes['count'] ?? 3 );
$service_type = sanitize_text_field( $attributes['serviceType'] ?? '' );
$args = array(
'post_type' => 'testimonial',
'posts_per_page' => $count,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
);
if ( ! empty( $service_type ) ) {
$args['tax_query'] = array(
array(
'taxonomy' => 'testimonial_service',
'field' => 'slug',
'terms' => $service_type,
),
);
}
$testimonials = new WP_Query( $args );
$wrapper_attributes = get_block_wrapper_attributes(
array( 'class' => 'testimonials-block' )
);
ob_start();
?>
<div <?php echo wp_kses_data( $wrapper_attributes ); ?>>
<?php if ( $testimonials->have_posts() ) : ?>
<ul class="testimonials-block__list">
<?php while ( $testimonials->have_posts() ) : $testimonials->the_post(); ?>
<li class="testimonials-block__card">
<blockquote class="testimonials-block__quote">
<?php the_content(); ?>
</blockquote>
<footer class="testimonials-block__footer">
<?php if ( has_post_thumbnail() ) : ?>
<div class="testimonials-block__avatar">
<?php the_post_thumbnail( 'thumbnail' ); ?>
</div>
<?php endif; ?>
<div class="testimonials-block__author">
<strong><?php the_title(); ?></strong>
<span class="testimonials-block__company">
<?php echo esc_html(
get_post_meta( get_the_ID(), '_testimonial_company', true )
); ?>
</span>
<span class="testimonials-block__rating">
<?php
$rating = intval( get_post_meta( get_the_ID(), '_testimonial_rating', true ) );
echo str_repeat( 'โ
', $rating ) . str_repeat( 'โ', 5 - $rating );
?>
</span>
</div>
</footer>
</li>
<?php endwhile; wp_reset_postdata(); ?>
</ul>
<?php else : ?>
<p><?php esc_html_e( 'No testimonials found.', 'testimonials-block' ); ?></p>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
add_action( 'init', function() {
register_block_type( 'my-plugin/testimonials', array(
'title' => 'Testimonials',
'icon' => 'format-quote',
'category' => 'widgets',
'render_callback' => 'testimonials_block_render',
'attributes' => array(
'count' => array(
'type' => 'integer',
'default' => 3,
),
'serviceType' => array(
'type' => 'string',
'default' => '',
),
),
'supports' => array(
'autoRegister' => true,
'color' => array( 'background' => true, 'text' => true ),
'spacing' => array( 'margin' => true, 'padding' => true ),
'typography' => array( 'fontSize' => true ),
),
) );
} );
Because this block uses WP_Query, testimonials are always fresh โ publish a new one and it appears on every page using this block immediately. The star rating is rendered server-side using the _testimonial_rating meta field, with no JavaScript needed to display it.
The supports array adds native color, spacing, and typography controls to the editor sidebar. Users can change the background color, text color, padding, and font size without writing a line of CSS โ and WordPress 7 handles all of that through the block wrapper.
6. Wrapping Legacy Shortcodes as PHP-Only Blocks
PHP-only blocks are an ideal migration path for existing shortcodes. The render logic is often identical โ the only difference is where the parameters come from.
Before: Classic Shortcode
functions.php โ Original Shortcode
<?php
function my_alert_shortcode( $atts ) {
$atts = shortcode_atts( array(
'type' => 'info',
'message' => 'Default alert message',
), $atts );
$styles = array(
'info' => 'background:#d1ecf1;color:#0c5460;border-color:#bee5eb;',
'warning' => 'background:#fff3cd;color:#856404;border-color:#ffeeba;',
'error' => 'background:#f8d7da;color:#721c24;border-color:#f5c6cb;',
);
$style = $styles[ $atts['type'] ] ?? $styles['info'];
return sprintf(
'<div style="%s padding:20px;border:1px solid;border-radius:6px;margin:10px 0;">
<strong>%s:</strong> %s
</div>',
esc_attr( $style ),
esc_html( strtoupper( $atts['type'] ) ),
esc_html( $atts['message'] )
);
}
add_shortcode( 'my_alert', 'my_alert_shortcode' );
After: PHP-Only Block Wrapper
alert-block.php โ Block Wrapper
<?php
function alert_block_render( $attributes ) {
$type = esc_attr( $attributes['alertType'] ?? 'info' );
$message = esc_attr( $attributes['alertMessage'] ?? '' );
$shortcode = sprintf( '[my_alert type="%s" message="%s"]', $type, $message );
$wrapper = get_block_wrapper_attributes(
array( 'class' => 'wp-block-alert-wrapper' )
);
return sprintf(
'<div %s>%s</div>',
wp_kses_data( $wrapper ),
do_shortcode( $shortcode )
);
}
add_action( 'init', function() {
register_block_type( 'my-plugin/alert-block', array(
'title' => 'Alert Box',
'icon' => 'warning',
'category' => 'widgets',
'render_callback' => 'alert_block_render',
'attributes' => array(
'alertType' => array(
'type' => 'string',
'enum' => array( 'info', 'warning', 'error' ),
'default' => 'info',
),
'alertMessage' => array(
'type' => 'string',
'default' => 'Type your message here.',
),
),
'supports' => array(
'autoRegister' => true,
'spacing' => array( 'margin' => true, 'padding' => true ),
),
) );
} );
The migration steps are straightforward:
1. Map each shortcode attribute to a block attribute with the same type and default.
2. In the render callback, reconstruct the shortcode string from $attributes and call do_shortcode().
3. Wrap the output with get_block_wrapper_attributes().
4. Keep the original shortcode registered for backward compatibility during transition.
The result is an intuitive block UI that replaces manual shortcode syntax, while the rendering logic doesn’t change at all. Existing content using the shortcode continues to work. New content uses the block.
7. How WordPress 7.0 is Changing the Developer Landscape
PHP-only blocks in WordPress 7 are not just a convenience feature โ they represent a philosophical shift in how the project defines who gets to build for it.
Since Gutenberg’s introduction in WordPress 5.0, the momentum in WordPress development has trended toward JavaScript. React components, REST API consumption, client-side rendering โ building blocks meant buying into that full ecosystem. For a significant portion of the WordPress developer community โ plugin authors, theme developers, agencies running classic PHP stacks โ that friction was real enough to push them toward avoidance rather than adoption.
WordPress 7 acknowledges that this was a problem worth solving. The strategic impact extends in several directions:
- Broader ecosystem participation โ PHP developers who previously avoided block development can now contribute blocks with the skills they already have.
- Simpler plugin packages โ plugins that ship PHP-only blocks don’t need build toolchains, Node.js dependencies, or compiled JavaScript bundles. Smaller packages, fewer moving parts, easier maintenance.
- A natural migration path โ teams maintaining shortcodes, widgets, and classic PHP templates have a clear, low-friction route into the block editor without a full rewrite.
- Alignment with WordPress’s core mission โ democratizing publishing has always been WordPress’s north star. PHP-only blocks extend that mission to democratizing block development itself.
The JavaScript path isn’t going away. Blocks with rich interactive UIs, live editing experiences, and complex client-side behavior will continue to be built in React. But for the broad middle ground of WordPress blocks โ dynamic content displays, plugin integrations, template components โ PHP is now a clean, supported, first-class choice.
If you’ve been sitting on the sidelines of block development because the JavaScript overhead felt like a barrier, WordPress 7 has removed that barrier. The API is clean, the tooling is minimal, and the only language you need is one you already know.
Summary
PHP-only Gutenberg blocks in WordPress 7.0 give you:
- Block registration entirely in PHP โ no JavaScript entry point needed
- Automatic editor UI generation from attribute type definitions
- Live server-rendered previews in the Gutenberg editor
- Full access to native block supports: color, spacing, typography, border, shadow
- A clean migration path from shortcodes and classic widgets
- No build step, no node_modules, no webpack configuration
Start with the basic example in Section 3, explore attributes in Section 4, then work toward dynamic content blocks and shortcode migration as your confidence grows. The path from zero to a production-ready PHP block is shorter in WordPress 7 than it has ever been.
(Please note that WordPress 7.0 has not been officially released yet, so this feature isnโt recommended for use in production environments. This article is based on the information currently available, and details may evolve by the time of the final release. Weโll do our best to keep this article updated as things progress.)
Also read: wp_body_open โ WordPress action that every theme should use