tools/wordpress-widget-generator
WordPress Widget Generator
Build a WP_Widget class with its form, its save routine and its front end — fields sanitized per type, settings names built by get_field_name() so they post where update() reads them, and the flag that makes the block widget editor show a real preview.
snippet.php
<?php
/**
* Widget: My Custom Widget
*
* Generated by wp-skills.com
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* A classic widget, which the block widget editor still shows.
*
* WordPress 5.8 replaced the widgets screen with a block editor, and the usual
* conclusion is that WP_Widget is dead. It is not: a widget registered this way
* appears in /wp/v2/widget-types and is offered in the editor as a Legacy
* Widget block, alongside every one of core's own — verified on WordPress 7.1.
*/
class WP_Skills_Widget_My_Custom_Widget extends WP_Widget {
/**
* Every setting this widget stores, with the type that renders and
* sanitizes it.
*
* Held in one place because three separate passes read it — form(),
* update() and widget() — and a key that exists in only two of them is a
* field that renders and never saves.
*/
private $fields = array(
array(
'id' => 'title',
'label' => 'Title',
'type' => 'text',
'default' => '',
),
array(
'id' => 'message',
'label' => 'Message',
'type' => 'textarea',
'default' => '',
)
);
public function __construct() {
/*
* The first argument is the $id_base, and it is not a label: the
* option row every instance is saved in is 'widget_' . $id_base, each
* instance's id is "$id_base-2", and get_field_name() renders it into
* the name attribute of every input below. Changing it later orphans
* every widget already placed in a sidebar.
*/
parent::__construct(
'my_custom_widget',
__( 'My Custom Widget', 'text-domain' ),
array(
'classname' => 'widget_my_custom_widget',
'description' => __( 'Lorem ipsum dolor sit amet.', 'text-domain' ),
'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
)
);
}
/**
* The front end.
*
* $args comes from register_sidebar() — before_widget and after_widget are
* the theme's wrapper, already built. Echoing them is not optional: a widget
* that skips them loses the theme's markup and the id the sidebar gave it.
*/
public function widget( $args, $instance ) {
$instance = wp_parse_args( (array) $instance, $this->defaults() );
echo $args['before_widget'];
/*
* The title goes through 'before_title'/'after_title' rather than into
* the body: that is where the theme's markup for it lives, and where
* every core widget puts it. The 'widget_title' filter is what plugins
* hook to translate or decorate it.
*/
$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
if ( '' !== $title ) {
echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
}
?>
<div class="widget_my_custom_widget__message"><?php echo wpautop( esc_html( $instance['message'] ) ); ?></div>
<?php
echo $args['after_widget'];
}
/**
* The form on the widgets screen.
*
* get_field_id() and get_field_name() are what tie an input to this
* instance: they render as widget-my_custom_widget-2-key and
* widget-my_custom_widget[2][key]. Hand-writing either one means the
* value posts under a name update() never sees.
*/
public function form( $instance ) {
$instance = wp_parse_args( (array) $instance, $this->defaults() );
foreach ( $this->fields as $field ) {
$value = $instance[ $field['id'] ];
echo '<p>';
printf(
'<label for="%1$s">%2$s</label>',
esc_attr( $this->get_field_id( $field['id'] ) ),
esc_html( $field['label'] )
);
$this->render_field( $field, $value );
echo '</p>';
}
}
private function render_field( $field, $value ) {
switch ( $field['type'] ) {
case 'textarea':
printf(
'<textarea class="widefat" id="%1$s" name="%2$s" rows="4">%3$s</textarea>',
esc_attr( $this->get_field_id( $field['id'] ) ),
esc_attr( $this->get_field_name( $field['id'] ) ),
esc_textarea( $value )
);
break;
default:
printf(
'<input class="widefat" id="%1$s" name="%2$s" type="%3$s" value="%4$s">',
esc_attr( $this->get_field_id( $field['id'] ) ),
esc_attr( $this->get_field_name( $field['id'] ) ),
esc_attr( $field['type'] ),
esc_attr( $value )
);
}
}
/**
* Saves the instance.
*
* Whatever this returns is stored verbatim, so it is the only place the
* values are constrained. Returning $new_instance untouched — the shortest
* thing that works — stores whatever was posted.
*/
public function update( $new_instance, $old_instance ) {
$instance = $this->defaults();
foreach ( $this->fields as $field ) {
$key = $field['id'];
// An unchecked box posts nothing at all, so its absence is the
// value. Skipping it would leave the previous '1' in place forever.
if ( 'checkbox' === $field['type'] ) {
$instance[ $key ] = empty( $new_instance[ $key ] ) ? '0' : '1';
continue;
}
if ( ! isset( $new_instance[ $key ] ) ) {
continue;
}
$instance[ $key ] = $this->sanitize_field( $field, $new_instance[ $key ] );
}
return $instance;
}
private function sanitize_field( $field, $value ) {
switch ( $field['type'] ) {
case 'textarea':
return sanitize_textarea_field( $value );
default:
return sanitize_text_field( $value );
}
}
/**
* Every field's default, keyed the way the instance stores them.
*
* An instance saved before a field was added simply has no value for it, so
* widget() and form() both merge through here rather than indexing into an
* array that may not have the key.
*/
private function defaults() {
$defaults = array();
foreach ( $this->fields as $field ) {
$defaults[ $field['id'] ] = $field['default'];
}
return $defaults;
}
}
add_action(
'widgets_init',
function () {
register_widget( 'WP_Skills_Widget_My_Custom_Widget' );
}
);
/*
* Two ways to render this widget, and they do not take the same arguments.
*
* In a sidebar, WordPress does the work — drop the widget into a widget area
* and the theme's dynamic_sidebar() call renders it. 'before_widget' there gets
* two sprintf arguments, the instance id and the class name, which is why
* register_sidebar() defaults it to '<li id="%1$s" class="widget %2$s">'.
*
* Directly in a template, the_widget() renders it without a sidebar — but it
* builds 'before_widget' with only ONE argument, the class name. Handing it the
* sidebar-shaped string above is an ArgumentCountError, which is a fatal on
* PHP 8. Note the single %s:
*
* the_widget(
* 'WP_Skills_Widget_My_Custom_Widget',
* array( 'title' => 'From a template' ),
* array(
* 'before_widget' => '<section class="widget %s">',
* 'after_widget' => '</section>',
* 'before_title' => '<h2 class="widget-title">',
* 'after_title' => '</h2>',
* )
* );
*/