Skip to content

Instantly share code, notes, and snippets.

@pdclark
Last active July 15, 2023 23:26
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pdclark/b76165fc18d23ec2f13ad7dbdb75c2e4 to your computer and use it in GitHub Desktop.
Save pdclark/b76165fc18d23ec2f13ad7dbdb75c2e4 to your computer and use it in GitHub Desktop.
Short example OOP PHP-rendered WordPress blocks with attributes.
<?php
/**
* Plugin Name: PD Blocks — Example OOP with attributes
* Description: Single-file OOP PHP-rendered WordPress blocks with 5 example blocks.
* Author: Paul David Clark
* Author URI: https://pd.cm
* Plugin URI: https://pd.cm/oop-blocks
* Version: 30
*
* @package pd
*/
namespace PD;
add_action(
'plugins_loaded',
function(){
/**
* Hello World block with a TextControl attribute.
*/
register_block( [
'namespace' => 'pd',
'title' => __( 'Hello world', 'pd' ),
'icon' => 'megaphone', // https://developer.wordpress.org/resource/dashicons/
'category' => 'widgets',
'render_callback' => function( $a /* attributes */, $c /* content */, $o /* Block instance */ ) {
?>
<h1>Hello <?php echo esc_html( $a['name'] ); ?>.</h1>
<?php
},
'attributes' => [
'name' => [
'type' => 'string',
'component' => 'wp.components.TextControl',
'default' => 'world',
'label' => 'Name',
'input_type' => 'text', // text, number, email, or url.
],
],
] );
/**
* WP_Query block with post_per_page and post_type attributes.
*/
register_block( [
'namespace' => 'pd',
'title' => __( 'Example Loop', 'pd' ),
'icon' => 'image-rotate', // https://developer.wordpress.org/resource/dashicons/
'category' => 'widgets',
'render_callback' => function( $a /* attributes */, $c /* content */, $o /* Block instance */ ) {
$q = new \WP_Query(
[
'post_type' => $a['post_type'],
'posts_per_page' => (int) $a['posts_per_page'],
]
);
while ( $q->have_posts() ) {
$q->the_post();
?>
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a><br/>
<?php
}
wp_reset_postdata();
},
'attributes' => [
'post_type' => [
'type' => 'string',
'component' => 'wp.components.SelectControl',
'default' => 'post',
'label' => __( 'Post Type', 'pd' ),
'options' => array_values(
array_map(
function( $post_type ) {
return [
'label' => $post_type,
'value' => $post_type,
];
},
(array) get_post_types()
)
),
],
'posts_per_page' => [
'type' => 'number',
'component' => 'wp.components.RangeControl',
'default' => 3,
'label' => __( 'Number of Posts', 'pd' ),
'min' => -1,
'max' => 200,
],
],
] );
/**
* Shortcodes, toggle, colors, & textarea block.
*/
register_block( [
'namespace' => 'pd',
'title' => __( 'Colors and Shortcodes', 'pd' ),
'icon' => 'admin-appearance', // https://developer.wordpress.org/resource/dashicons/
'category' => 'widgets',
'render_callback' => function( $a /* attributes */, $c /* content */, $o /* Block instance */ ) {
?>
<div style="color: <?php echo esc_attr( $a['text_color'] ); ?>; background-color: <?php echo esc_attr( $a['color_background'] ); ?>;">
Radio Color:
<span style="background-color: <?php echo esc_attr( $a['color_radio'] ); ?>;">
<?php echo esc_html( $a['color_radio'] ); ?>
</span><br/>
Color Picker / Background Color:
<span style="background-color: <?php echo esc_attr( $a['color_background'] ); ?>;">
<?php echo esc_html( $a['color_background'] ); ?>
</span><br/>
Color Palette / Text Color:
<span style="color: <?php echo esc_attr( $a['text_color'] ); ?>;">
<?php echo esc_html( $a['text_color'] ); ?>
</span><br/>
Toggle:
<?php echo ( false === $a['toggle'] ) ? esc_html( __( '❌', 'pd' ) ) : esc_html( __( '✅', 'pd' ) ); ?><br/>
Text Area with shortcodes:
<?php echo apply_shortcodes( wp_kses_post( $a['textarea'] ) ); ?><br/>
</div>
<?php
},
'attributes' => [
'toggle' => [
'type' => 'boolean',
'component' => 'wp.components.ToggleControl',
'default' => false,
'label' => __( 'Toggle', 'pd' ),
],
'textarea' => [
'type' => 'string',
'component' => 'wp.components.TextareaControl',
'label' => __( 'Text / Shortcodes', 'pd' ),
'rows' => 3,
],
'color_radio' => [
'type' => 'string',
'component' => 'wp.components.RadioControl',
'default' => '#eeaaaa',
'label' => __( 'Color radio', 'pd' ),
'help' => __( 'Select a color', 'pd' ),
'options' => [
[
'label' => __( 'Red', 'pd' ),
'value' => '#eeaaaa',
],
[
'label' => __( 'Green', 'pd' ),
'value' => '#aaeeaa',
],
[
'label' => __( 'Blue', 'pd' ),
'value' => '#aaaaee',
],
],
],
'color_background' => [
'type' => 'string',
'component' => 'wp.components.ColorPicker',
'default' => '#eeaaaa',
'label' => __( 'Background Color', 'pd' ),
'help' => __( 'Select a color', 'pd' ),
],
'text_color' => [
'type' => 'string',
'component' => 'wp.components.ColorPalette',
'default' => '#000000',
'label' => __( 'Text Color', 'pd' ),
'colors' => [
[
'name' => 'white',
'color' => '#ffffff',
],
[
'name' => 'black',
'color' => '#000000',
],
],
],
],
] );
/**
* Embed shortcode block.
*/
register_block( [
'namespace' => 'pd',
'title' => __( 'Embed Shortcode', 'pd' ),
'icon' => 'format-video', // https://developer.wordpress.org/resource/dashicons/
'category' => 'widgets',
'render_callback' => function( $a /* attributes */, $c /* content */, $o /* Block instance */ ) {
// If user fills out URL then deletes it, block can render as empty.
// If empty, force the default.
if ( empty( $a['embed_url'] ) ) {
$a['embed_url'] = $o->attributes['embed_url']['default'];
}
// do_shortcode('[embed]...[/embed]') returns false,
// so instantiate the WP_Embed class instead.
$embed = new \WP_Embed();
echo $embed->run_shortcode(
'[embed]' . $a['embed_url'] . '[/embed]'
);
},
'attributes' => [
'embed_url' => [
'type' => 'string',
'component' => 'wp.components.TextControl',
'default' => 'https://www.youtube.com/watch?v=woAHwpOLmyY',
'label' => 'Embed URI',
'input_type' => 'url', // text, number, email, or url.
],
],
] );
/**
* Audio shortcode block.
*/
register_block( [
'namespace' => 'pd',
'title' => 'Audio Shortcode',
'icon' => 'format-audio', // https://developer.wordpress.org/resource/dashicons/
'category' => 'widgets',
'render_callback' => function( $a /* attributes */, $c /* content */, $o /* Block instance */ ) {
// If user fills out URL then deletes it, block can render as empty.
// If empty, use default.
if ( empty( $a['embed_url'] ) ) {
$a['embed_url'] = $o->attributes['embed_url']['default'];
}
echo do_shortcode(
'[audio src="' . $a['embed_url'] . '"]'
);
},
'attributes' => [
'embed_url' => [
'type' => 'string',
'component' => 'wp.components.TextControl',
'default' => 'https://pd.cm/m/what-vbr.mp3',
'label' => 'MP3 URI',
'input_type' => 'url', // text, number, email, or url.
],
],
] );
}
);
/**
* Block registration function.
*
* @param $args array Array of arguments: namespace, title, icon, category, render_callback, attributes.
*
* @return \PD\Block Constructed block object.
*/
function register_block( $a /* args */ ) {
$a['namespace'] = strtolower( $a['namespace'] );
$a['slug'] = sanitize_key( $a['title'] );
if ( empty( $a['icon'] ) ) { $a['icon'] = 'megaphone'; }
if ( empty( $a['category'] ) ) { $a['icon'] = 'widgets'; }
return new Block(
[
'slug_hyphen' => sprintf( '%s-%s', $a['namespace'], $a['slug'] ),
'slug_slash' => sprintf( '%s/%s', $a['namespace'], $a['slug'] ),
'title' => $a['title'],
'icon' => $a['icon'], // https://developer.wordpress.org/resource/dashicons/
'category' => $a['category'],
'render_callback' => $a['render_callback'],
'attributes' => $a['attributes'],
]
);
}
/**
* Block class.
* Allows instantiation of PHP-powered blocks with some attributes.
*
* @author Paul David Clark <pd@pd.cm>
*/
class Block {
/**
* Assign all keys in input array to class vars.
* Register block in PHP on init.
* Register block in JavaScript with wp_ajax_register_block
* Filters for attributes & inspector controls.
*
* @param array $atts Block configuration. See examples above.
*/
public function __construct( $atts ) {
foreach( $atts as $key => $att ) {
$this->$key = $att;
}
add_action( 'init', [ $this, 'init' ] );
add_action( 'wp_ajax_register_block_' . $this->slug_hyphen, [ $this, 'wp_ajax_register_block' ] );
add_filter( 'pd/block/attributes-php/' . $this->slug_hyphen, [ $this, 'block_attributes_php' ] );
add_filter( 'pd/block/attributes-js/' . $this->slug_hyphen, [ $this, 'block_attributes_js' ] );
add_filter( 'pd/block/inspector-controls-js/' . $this->slug_hyphen, [ $this, 'block_inspector_controls_js' ] );
}
/**
* Register: PHP.
*/
public function init() {
wp_register_script(
$this->slug_hyphen,
add_query_arg(
[
'action' => 'register_block_' . $this->slug_hyphen,
'_wpnonce' => wp_create_nonce(),
],
admin_url( 'admin-ajax.php' ) ),
[ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components' ],
microtime(),
true
);
$block_params = [
'editor_script' => $this->slug_hyphen,
/**
* @param array $attributes Optional. Block attributes. Default empty array.
* @param string $content Optional. Block content. Default empty string.
*/
'render_callback' => function( $attributes, $content ) {
ob_start();
call_user_func( $this->render_callback, $attributes, $content, $this );
return ob_get_clean();
},
];
$attributes = apply_filters( 'pd/block/attributes-php/' . $this->slug_hyphen, [] );
if ( ! empty( $attributes ) ) {
$block_params['attributes'] = $attributes;
}
register_block_type( $this->slug_slash, $block_params );
}
/**
* Register: JavaScript.
* Outputs ES2015.
* Calls wp.blocks.registerBlockType.
* Builds attributes and InspectorControls components.
*
* @see https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/creating-dynamic-blocks/
*/
public function wp_ajax_register_block() {
check_ajax_referer();
header( 'Content-Type: text/javascript' );
?>
( function ( el ) {
wp.blocks.registerBlockType( '<?php echo esc_js( $this->slug_slash ); ?>', {
apiVersion: 2,
title: '<?php echo esc_js( $this->title ); ?>',
icon: '<?php echo esc_js( $this->icon ); ?>',
category: '<?php echo esc_js( $this->category ); ?>',
attributes: <?php
echo wp_json_encode(
apply_filters( 'pd/block/attributes-js/' . $this->slug_hyphen, [] ),
JSON_FORCE_OBJECT
);
?>,
edit: function ( props ) {
return [
el(
'div',
{ ...wp.blockEditor.useBlockProps(), 'key': 'block_wrapper' },
el(
wp.serverSideRender,
{
key: 'server_side_render',
block: '<?php echo esc_js( $this->slug_slash ); ?>',
attributes: props.attributes,
}
)
)
<?php
$inspector_controls = apply_filters( 'pd/block/inspector-controls-js/' . $this->slug_hyphen, [] );
if ( ! empty( $inspector_controls ) ) :
?>
, el(
wp.blockEditor.InspectorControls,
{ key: "inspector" },
el(
wp.components.PanelBody,
{ title: "<?php echo esc_js( __( 'Settings', 'pd' ) ); ?>", initialOpen: true },
<?php echo implode( ',', $inspector_controls ); ?>
)
)
<?php
endif;
?>
];
},
} );
} )( wp.element.createElement );
<?php
exit;
}
/**
* Outputs PHP attribute configuration for register_block_type().
*
* Array of attribute types and default values.
* See filter pd/block/attributes-php/
*/
public function block_attributes_php( $attributes = array() ) {
foreach ( (array) $this->attributes as $key => $values ) {
$attributes[ $key ] = [
'type' => $values['type'],
'default' => $values['default'],
];
if ( isset( $values['source'] ) ) {
$attributes[ $key ] = $values['source'];
}
}
return $attributes;
}
/**
* Outputs JavaScript attribute configuration.
*
* Array of attribute default values to be JSON encoded.
* See filter pd/block/attributes-js/
*/
public function block_attributes_js( $attributes = array() ) {
foreach ( (array) $this->attributes as $key => $values ) {
$attributes[ $key ] = [
'value' => $values['default'],
];
}
return $attributes;
}
/**
* Outputs ES5 components according to defined attributes.
* el is wp.element.createElement.
*/
public function block_inspector_controls_js( $controls = [] ) {
foreach ( (array) $this->attributes as $key => $values ) {
ob_start();
switch ( $values['component'] ) {
case 'wp.components.RadioControl':
?>
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
help: "<?php echo esc_js( $values['help'] ); ?>",
selected: props.attributes.<?php echo esc_js( $key ); ?>,
options: <?php echo wp_json_encode( $values['options'] ); ?>,
onChange: function (option) {
return props.setAttributes({
<?php echo esc_js( $key ); ?>: option
});
}
})
<?php
break;
case 'wp.components.SelectControl':
?>
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
value: props.attributes.<?php echo esc_js( $key ); ?>,
options: <?php echo wp_json_encode( $values['options'] ); ?>,
onChange: function (option) {
return props.setAttributes({
<?php echo esc_js( $key ); ?>: option
});
}
})
<?php
break;
case 'wp.components.ColorPicker':
?>
el(
'span',
null,
"<?php echo esc_js( $values['label'] ); ?>"
),
el(
<?php echo esc_js( $values['component'] ); ?>,
{
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
type: 'color',
color: props.attributes.<?php echo esc_js( $key ); ?>,
onChangeComplete: function(newValue) {
props.setAttributes({
<?php echo esc_js( $key ); ?>: newValue.hex
});
},
disableAlpha: true
}
)
<?php
break;
case 'wp.components.ColorPalette':
?>
el(
'span',
null,
"<?php echo esc_js( $values['label'] ); ?>"
),
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
colors: <?php echo wp_json_encode( $values['colors'] ); ?>,
value: props.attributes.<?php echo esc_js( $key ); ?>,
onChange: function(newValue) {
props.setAttributes({
<?php echo esc_js( $key ); ?>: newValue
});
},
disableAlpha: false
})
<?php
break;
case 'wp.components.ToggleControl':
?>
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
checked: props.attributes.<?php echo esc_js( $key ); ?>,
onChange: function(newValue) {
props.setAttributes({
<?php echo esc_js( $key ); ?>: newValue
});
}
})
<?php
break;
case 'wp.components.RangeControl':
?>
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
value: props.attributes.<?php echo esc_js( $key ); ?>,
onChange: function(newValue) {
props.setAttributes({
<?php echo esc_js( $key ); ?>: newValue
});
},
min: <?php echo (int) $values['min']; ?>,
max: <?php echo (int) $values['max']; ?>
})
<?php
break;
case 'wp.components.TextControl':
?>
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
value: props.attributes.<?php echo esc_js( $key ); ?>,
onChange: function(newValue) {
props.setAttributes({
<?php echo esc_js( $key ); ?>: newValue
});
},
type: '<?php echo esc_js( $values['input_type'] ); ?>'
})
<?php
break;
case 'wp.components.TextareaControl':
?>
el( <?php echo esc_js( $values['component'] ); ?>, {
key: '<?php echo esc_js( $key ); ?>',
label: "<?php echo esc_js( $values['label'] ); ?>",
value: props.attributes.<?php echo esc_js( $key ); ?>,
onChange: function(newValue) {
props.setAttributes({
<?php echo esc_js( $key ); ?>: newValue
});
},
rows: <?php echo ( $values['rows'] ) ? (int) $values['rows'] : 4; ?>
})
<?php
break;
}
$controls[] = ob_get_clean();
}
return $controls;
}
}
@pbrocks
Copy link

pbrocks commented Oct 26, 2022

@pdclark This is very cool and informative!! Thanks for sharing. If you want to show previews in the editor, you could add example: {}, on line 361, for example.

@pd-cm
Copy link

pd-cm commented Jul 15, 2023

Thanks for the commend @pbrocks ! I will try to test and revise soon. This version also doesn't have a help attribute on all fields, and default doesn't seem to always work. For now, I've been showing editor previews with an is_admin() check in the callback function, but your solution seems better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment