felixarntz/theme-boilerplate

View on GitHub
inc/library/widgets/class-widget.php

Summary

Maintainability
D
1 day
Test Coverage
<?php
/**
 * Super_Awesome_Theme_Widget class
 *
 * @package Super_Awesome_Theme
 * @license GPL-2.0-or-later
 * @link    https://super-awesome-author.org/themes/super-awesome-theme/
 */

/**
 * Class representing a widget.
 *
 * @since 1.0.0
 */
abstract class Super_Awesome_Theme_Widget extends WP_Widget {

    /**
     * Type field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_TYPE = 'type';

    /**
     * Default field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_DEFAULT = 'default';

    /**
     * Title field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_TITLE = 'title';

    /**
     * Description field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_DESCRIPTION = 'description';

    /**
     * Sanitize callback field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_SANITIZE_CALLBACK = 'sanitize_callback';

    /**
     * Choices field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_CHOICES = 'choices';

    /**
     * Input attributes field argument name.
     *
     * @since 1.0.0
     */
    const FIELD_ARG_INPUT_ATTRS = 'input_attrs';

    /**
     * Identifier for the text field type.
     *
     * @since 1.0.0
     */
    const FIELD_TYPE_TEXT = 'text';

    /**
     * Identifier for the textarea field type.
     *
     * @since 1.0.0
     */
    const FIELD_TYPE_TEXTAREA = 'textarea';

    /**
     * Identifier for the number field type.
     *
     * @since 1.0.0
     */
    const FIELD_TYPE_NUMBER = 'number';

    /**
     * Identifier for the select field type.
     *
     * @since 1.0.0
     */
    const FIELD_TYPE_SELECT = 'select';

    /**
     * Identifier for the radio field type.
     *
     * @since 1.0.0
     */
    const FIELD_TYPE_RADIO = 'radio';

    /**
     * Identifier for the checkbox field type.
     *
     * @since 1.0.0
     */
    const FIELD_TYPE_CHECKBOX = 'checkbox';

    /**
     * Registered fields for the widget.
     *
     * @since 1.0.0
     * @var array
     */
    protected $fields = array();

    /**
     * Parent widgets manager instance.
     *
     * @since 1.0.0
     * @var Super_Awesome_Theme_Widgets
     */
    protected $manager;

    /**
     * Constructor.
     *
     * Sets the widget definition arguments.
     *
     * @since 1.0.0
     *
     * @param Super_Awesome_Theme_Widgets $manager Parent widgets manager instance.
     */
    public function __construct( Super_Awesome_Theme_Widgets $manager ) {
        $this->manager = $manager;

        $slug        = $this->get_slug_from_classname();
        $title       = $this->get_title();
        $description = $this->get_description();

        parent::__construct( 'super_awesome_theme_' . $slug, $title, array(
            'classname'                   => 'widget_' . $slug,
            'description'                 => $description,
            'customize_selective_refresh' => true,
        ) );

        $this->add_fields();
    }

    /**
     * Outputs the content for the current widget instance.
     *
     * @since 1.0.0
     *
     * @param array $args     Display arguments including 'before_title', 'after_title',
     *                        'before_widget', and 'after_widget'.
     * @param array $instance Settings for the current widget instance.
     */
    final public function widget( $args, $instance ) {
        $instance = $this->parse_defaults( $instance );

        if ( ! $this->can_render( $instance ) ) {
            return;
        }

        $title = ! empty( $instance['title'] ) ? $instance['title'] : '';

        /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
        $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );

        echo $args['before_widget']; // WPCS: XSS OK.
        if ( ! empty( $title ) ) {
            echo $args['before_title'] . $title . $args['after_title']; // WPCS: XSS OK.
        }

        $this->render( $instance );

        echo $args['after_widget']; // WPCS: XSS OK.
    }

    /**
     * Handles updating settings for the current widget instance.
     *
     * @since 1.0.0
     *
     * @param array $new_instance New settings for the current instance as input by the user via
     *                            WP_Widget::form().
     * @param array $old_instance Old settings for the current instance.
     * @return array|bool Settings to save or bool false to cancel saving.
     */
    public function update( $new_instance, $old_instance ) {
        foreach ( $this->fields as $id => $args ) {
            $value = isset( $new_instance[ $id ] ) ? $new_instance[ $id ] : null;

            $new_instance[ $id ] = call_user_func( $args[ self::FIELD_ARG_SANITIZE_CALLBACK ], $value, $args );
        }

        return $new_instance;
    }

    /**
     * Renders the widget settings form.
     *
     * @since 1.0.0
     *
     * @param array $instance Settings for the current instance.
     */
    public function form( $instance ) {
        $instance = $this->parse_defaults( $instance );

        ?>
        <div class="widget-form-controls">
            <?php
            foreach ( $this->fields as $id => $args ) {
                $this->render_form_field( $id, $args, $instance[ $id ] );
            }
            ?>
        </div>
        <?php
    }

    /**
     * Renders a single field of the widget settings form.
     *
     * @since 1.0.0
     *
     * @param string $id    Field identifier.
     * @param array  $args  Field definition arguments.
     * @param mixed  $value Current field value.
     */
    protected function render_form_field( $id, $args, $value ) {
        $control_id     = $this->get_field_id( $id );
        $control_name   = $this->get_field_name( $id );
        $type           = $args[ self::FIELD_ARG_TYPE ];
        $title          = $args[ self::FIELD_ARG_TITLE ];
        $description    = $args[ self::FIELD_ARG_DESCRIPTION ];
        $input_attrs    = $args[ self::FIELD_ARG_INPUT_ATTRS ];
        $description_id = $control_id . '-description';

        if ( ! empty( $description ) ) {
            $input_attrs['aria-describedby'] = $description_id;
        }
        if ( ! empty( $input_attrs['required'] ) ) {
            $input_attrs['aria-required'] = 'true';
        }

        ?>
        <?php if ( self::FIELD_TYPE_RADIO === $type ) : ?>
            <fieldset>
                <legend>
                    <?php echo wp_kses_data( $title ); ?>
                </legend>
        <?php else : ?>
            <p>
            <?php if ( self::FIELD_TYPE_CHECKBOX !== $type ) : ?>
                <label for="<?php echo esc_attr( $control_id ); ?>">
                    <?php echo wp_kses_data( $title ); ?>
                </label>
            <?php endif; ?>
        <?php endif; ?>
        <?php
        switch ( $type ) {
            case self::FIELD_TYPE_NUMBER:
                ?>
                <input type="number" id="<?php echo esc_attr( $control_id ); ?>" name="<?php echo esc_attr( $control_name ); ?>" value="<?php echo esc_attr( $value ); ?>"<?php $this->attrs( $input_attrs ); ?> />
                <?php
                break;
            case self::FIELD_TYPE_CHECKBOX:
                ?>
                <input type="checkbox" id="<?php echo esc_attr( $control_id ); ?>" name="<?php echo esc_attr( $control_name ); ?>"<?php checked( $value ); ?><?php $this->attrs( $input_attrs ); ?> />
                <?php
                break;
            case self::FIELD_TYPE_SELECT:
                ?>
                <select id="<?php echo esc_attr( $control_id ); ?>" name="<?php echo esc_attr( $control_name ); ?>"<?php $this->attrs( $input_attrs ); ?>>
                    <?php foreach ( $args[ self::FIELD_ARG_CHOICES ] as $choice_value => $choice_label ) : ?>
                        <option value="<?php echo esc_attr( $choice_value ); ?>"<?php selected( $value, $choice_value ); ?>>
                            <?php echo esc_html( $choice_label ); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
                <?php
                break;
            case self::FIELD_TYPE_RADIO:
                foreach ( $args[ self::FIELD_ARG_CHOICES ] as $choice_value => $choice_label ) {
                    $choice_slug = sanitize_title( $choice_value );
                    ?>
                    <input type="radio" id="<?php echo esc_attr( $control_id . '-' . $choice_slug ); ?>" name="<?php echo esc_attr( $control_name ); ?>"<?php checked( $value, $choice_value ); ?><?php $this->attrs( $input_attrs ); ?> />
                    <label for="<?php echo esc_attr( $control_id . '-' . $choice_slug ); ?>">
                        <?php echo wp_kses_data( $choice_label ); ?>
                    </label>
                    <?php
                }
                break;
            case self::FIELD_TYPE_TEXTAREA:
                ?>
                <textarea id="<?php echo esc_attr( $control_id ); ?>" name="<?php echo esc_attr( $control_name ); ?>"<?php $this->attrs( $input_attrs ); ?>><?php echo esc_textarea( $value ); ?></textarea>
                <?php
                break;
            default:
                ?>
                <input type="<?php echo esc_attr( $type ); ?>" id="<?php echo esc_attr( $control_id ); ?>" name="<?php echo esc_attr( $control_name ); ?>" value="<?php echo esc_attr( $value ); ?>"<?php $this->attrs( $input_attrs ); ?> />
                <?php
        }
        ?>
        <?php if ( ! empty( $description ) ) : ?>
            <span id="<?php echo esc_attr( $description_id ); ?>" class="description">
                <?php echo wp_kses_data( $description ); ?>
            </span>
        <?php endif; ?>
        <?php if ( self::FIELD_TYPE_RADIO === $type ) : ?>
            </fieldset>
        <?php else : ?>
            <?php if ( self::FIELD_TYPE_CHECKBOX === $type ) : ?>
                <label for="<?php echo esc_attr( $control_id ); ?>">
                    <?php echo wp_kses_data( $title ); ?>
                </label>
            <?php endif; ?>
            </p>
        <?php endif; ?>
        <?php
    }

    /**
     * Adds a field for the widget.
     *
     * @since 1.0.0
     *
     * @param string $id   Field identifier.
     * @param array  $args Field definition arguments.
     */
    protected function add_field( $id, array $args = array() ) {
        if ( ! isset( $args[ self::FIELD_ARG_TYPE ] ) ) {
            $args[ self::FIELD_ARG_TYPE ] = self::FIELD_TYPE_TEXT;
        }

        if ( ! isset( $args[ self::FIELD_ARG_TITLE ] ) ) {
            $args[ self::FIELD_ARG_TITLE ] = '';
        }

        if ( ! isset( $args[ self::FIELD_ARG_DESCRIPTION ] ) ) {
            $args[ self::FIELD_ARG_DESCRIPTION ] = '';
        }

        if ( ! isset( $args[ self::FIELD_ARG_INPUT_ATTRS ] ) ) {
            $args[ self::FIELD_ARG_INPUT_ATTRS ] = array();
        }

        if ( in_array( $args[ self::FIELD_ARG_TYPE ], array( self::FIELD_TYPE_SELECT, self::FIELD_TYPE_RADIO ), true ) && ! isset( $args[ self::FIELD_ARG_CHOICES ] ) ) {
            $args[ self::FIELD_ARG_CHOICES ] = array();
        }

        if ( ! isset( $args[ self::FIELD_ARG_DEFAULT ] ) ) {
            switch ( $args[ self::FIELD_ARG_TYPE ] ) {
                case self::FIELD_TYPE_NUMBER:
                    $args[ self::FIELD_ARG_DEFAULT ] = 0.0;
                    break;
                case self::FIELD_TYPE_CHECKBOX:
                    $args[ self::FIELD_ARG_DEFAULT ] = false;
                    break;
                case self::FIELD_TYPE_SELECT:
                case self::FIELD_TYPE_RADIO:
                    if ( ! empty( $args[ self::FIELD_ARG_CHOICES ] ) ) {
                        $keys                            = array_keys( $args[ self::FIELD_ARG_CHOICES ] );
                        $args[ self::FIELD_ARG_DEFAULT ] = $keys[0];
                    } else {
                        $args[ self::FIELD_ARG_DEFAULT ] = '';
                    }
                    break;
                default:
                    $args[ self::FIELD_ARG_DEFAULT ] = '';
            }
        }

        if ( ! isset( $args[ self::FIELD_ARG_SANITIZE_CALLBACK ] ) ) {
            $args[ self::FIELD_ARG_SANITIZE_CALLBACK ] = array( $this, 'default_sanitize_callback' );
        }

        $this->fields[ $id ] = $args;
    }

    /**
     * Prints an attribute string from a list of attributes and their values.
     *
     * @since 1.0.0
     *
     * @param array $attrs Attributes as $attr => $value pairs.
     */
    protected function attrs( $attrs ) {
        $output = '';

        foreach ( $attrs as $attr => $value ) {
            $output .= ' ' . $attr . '="' . esc_attr( $value ) . '"';
        }

        echo $output; // WPCS: XSS OK.
    }

    /**
     * Parses a widget instance with its default values.
     *
     * @since 1.0.0
     *
     * @param array $instance Settings for the current widget instance.
     * @return array Valid widget instance with defaults filled as necessary.
     */
    protected function parse_defaults( $instance ) {
        if ( ! is_array( $instance ) ) {
            $instance = array();
        }

        foreach ( $this->fields as $id => $args ) {
            if ( ! isset( $instance[ $id ] ) ) {
                $instance[ $id ] = $args[ self::FIELD_ARG_DEFAULT ];
            }
        }

        return $instance;
    }

    /**
     * Performs default sanitization on a field value, based on the field type.
     *
     * @since 1.0.0
     *
     * @param mixed $value Value to sanitize.
     * @param array $args  Field definition arguments for the value sanitized.
     * @return mixed Sanitized value.
     */
    protected function default_sanitize_callback( $value, $args ) {
        switch ( $args[ self::FIELD_ARG_TYPE ] ) {
            case self::FIELD_TYPE_NUMBER:
                if ( isset( $args[ self::FIELD_ARG_INPUT_ATTRS ]['step'] ) && is_int( $args[ self::FIELD_ARG_INPUT_ATTRS ]['step'] ) ) {
                    $value = (int) $value;
                } else {
                    $value = (float) $value;
                }
                if ( isset( $args[ self::FIELD_ARG_INPUT_ATTRS ]['min'] ) && $value < $args[ self::FIELD_ARG_INPUT_ATTRS ]['min'] ) {
                    $value = $args[ self::FIELD_ARG_INPUT_ATTRS ]['min'];
                }
                if ( isset( $args[ self::FIELD_ARG_INPUT_ATTRS ]['max'] ) && $value < $args[ self::FIELD_ARG_INPUT_ATTRS ]['max'] ) {
                    $value = $args[ self::FIELD_ARG_INPUT_ATTRS ]['max'];
                }
                break;
            case self::FIELD_TYPE_CHECKBOX:
                $value = (bool) $value;
                break;
            case self::FIELD_TYPE_SELECT:
            case self::FIELD_TYPE_RADIO:
                $value = (string) $value;
                if ( ! isset( $args[ self::FIELD_ARG_CHOICES ][ $value ] ) ) {
                    $value = $args[ self::FIELD_ARG_DEFAULT ];
                }
                break;
            case self::FIELD_TYPE_TEXTAREA:
                $value = trim( (string) $value );
                if ( ! current_user_can( 'unfiltered_html' ) ) {
                    $value = wp_kses_post( $value );
                }
                break;
            default:
                $value = sanitize_text_field( trim( (string) $value ) );
        }

        if ( ! empty( $args[ self::FIELD_ARG_INPUT_ATTRS ]['required'] ) && empty( $value ) ) {
            $value = $args[ self::FIELD_ARG_DEFAULT ];
        }

        return $value;
    }

    /**
     * Adds the commonly-used field for the widget title.
     *
     * @since 1.0.0
     */
    protected function add_title_field() {
        $this->add_field( 'title', array(
            self::FIELD_ARG_TYPE        => self::FIELD_TYPE_TEXT,
            self::FIELD_ARG_TITLE       => __( 'Title', 'super-awesome-theme' ),
            self::FIELD_ARG_INPUT_ATTRS => array( 'class' => 'widefat' ),
        ) );
    }

    /**
     * Gets the title of the widget.
     *
     * @since 1.0.0
     *
     * @return string Title for the widget.
     */
    abstract protected function get_title();

    /**
     * Gets the description of the widget.
     *
     * @since 1.0.0
     *
     * @return string Description for the widget.
     */
    abstract protected function get_description();

    /**
     * Adds the available widget form fields.
     *
     * @since 1.0.0
     */
    abstract protected function add_fields();

    /**
     * Renders the widget for a given instance.
     *
     * @since 1.0.0
     *
     * @param array $instance Settings for the current widget instance.
     */
    abstract protected function render( array $instance );

    /**
     * Checks whether the widget can be rendered for a given instance.
     *
     * @since 1.0.0
     *
     * @param array $instance Settings for the current widget instance.
     * @return bool True if the settings qualify the widget to be rendered, false otherwise.
     */
    protected function can_render( array $instance ) {
        return true;
    }

    /**
     * Gets the widget slug from the class name.
     *
     * @since 1.0.0
     *
     * @return string Unprefixed widget slug.
     */
    private function get_slug_from_classname() {
        $slug = str_replace( 'Super_Awesome_Theme_', '', get_class( $this ) );

        if ( 'Widget_' === substr( $slug, 0, 7 ) ) {
            $slug = substr( $slug, 7 );
        } elseif ( '_Widget' === substr( $slug, -7 ) ) {
            $slug = substr( $slug, 0, -7 );
        }

        return strtolower( $slug );
    }
}