Comment créer un widget avec les custom post types

Introduction

Ce tutoriel fait suite aux deux précédents qui traitaient des custom post types (types d’article personnalisés) et des custom fields (champs personnalisés). Cette fois-ci, nous traitons des widgets et nous verrons pratiquement comment créer un widget qui affiche notre custom post typeimmo, pour les biens immobiliers – et un champ personnalisé associé – le prix.

Certes, il existe déjà des plugins qui font ce travail (exemples ci dessous) mais l’exercice du jour est de se familiariser avec la programmation d’un widget et de comprendre comment, à partir des sources de WordPress, nous pouvons élaborer nos propres widgets. Il ne s’agit donc pas de réinventer la roue mais de comprendre la bonne pratique de programmation pour WordPress. Et quoi de mieux que de reprendre le code originel de WordPress ! 🙂

Dans le tutoriel, j’ai évoqué très rapidement la différence entre un plugin et un widget et je n’y reviendrai pas. J’ai aussi introduit très succinctement les notions de classe, d’objet et de sous-classe. En fin de tutoriel, j’aborde la question de la localisation (traduction de l’anglais vers le français). Je vous laisse découvrir ces sujets dans la vidéo car je me concentrerai dans cet article sur le code pour la gestion d’un widget.

Avant de commencer toute programmation, il est toujours bon de lire la doc (le codex) et vous pouvez la consulter dans l’API Widget de WordPress !

Ici, nous partirons de la classe WP_Widget de WordPress et de sa sous classe WP_Widget_Recent_Posts qui est le widget par défaut de WordPress pour afficher les articles récents (les N dernières parutions d’article).

En effet, dans un premier temps, notre objectif sera d’afficher les dernières parutions de nos biens immobiliers avec le CPT – immo. Cette fonctionnalité est très proche de celle du widget qui, justement, affiche les articles récents. Une seule modification fonctionnelle est à changer en remplaçant la date de parution par le prix du bien immobilier (sans compter évidemment le changement de type d’article de post en immo).

Custom Post Type dans un Widget

Dans un second temps, nous généraliserons le widget afin de pouvoir sélectionner le Custom Post Type de notre choix.

Custom Post Type dans un Widget

La classe WP_Widget de WordPress

Vous pourrez retrouver le source de cette classe dans le dossier wp-includes de WordPress et dans le fichier class-wp-widget.php.

Voici le squelette de cette classe.

Classe WP_Widget

Ce qu’il faut retenir de cette description est liée aux commentaires des lignes 88 et 140. Ces commentaires indiquent quelles sont les fonctions (les méthodes de la classe) que le nouveau widget devra surcharger (autrement dit remplacer) et celles dont on aura besoin.

Les fonctions à surcharger sont au nombre de trois et nous allons les décrire. Il est toujours bon de lire les commentaires au dessus de la fonction qui renseignent sur la fonctionnalité, les arguments et valeur de retour de la fonction.

widget

La première fonction widget est appelée lors de l’affichage du widget sur le site internet (front end) dans la barre latérale où il a été instancié. Elle prend deux arguments $args et $instance. Le premier argument est un tableau qui comprend, entre autres, les paramètres before_widget et after_widget, before_title et after_title.

D’où proviennent ces paramètres ? Un widget s’intègre dans une barre latérale (sidebar). Lors de la déclaration de cette sidebar, on indique comment encadrer les widgets en son sein en définissant les balises HTML  qui s’afficheront avant et après le widget (en règle générale une section), avant et après le titre du widget (en générale une balise h2).

Dans notre étude de cas, nous avons développé un thème enfant prénommée babelweb, fils du thème twentysixteen (2016). C’est dans le fichier function.php que nous retrouvons la déclaration des sidebars et de ses paramètres.

functions.php
/**
 * Registers a widget area.
 *
 * @link https://developer.wordpress.org/reference/functions/register_sidebar/
 *
 * @since Twenty Sixteen 1.0
 */
function twentysixteen_widgets_init() {
	register_sidebar( array(
		'name'          => __( 'Sidebar', 'twentysixteen' ),
		'id'            => 'sidebar-1',
		'description'   => __( 'Add widgets here to appear in your sidebar.', 'twentysixteen' ),
		'before_widget' => '<section id="%1$s" class="widget %2$s">',
		'after_widget'  => '</section>',
		'before_title'  => '<h2 class="widget-title">',
		'after_title'   => '</h2>',
	) );

	register_sidebar( array(
		'name'          => __( 'Content Bottom 1', 'twentysixteen' ),
		'id'            => 'sidebar-2',
		'description'   => __( 'Appears at the bottom of the content on posts and pages.', 'twentysixteen' ),
		'before_widget' => '<section id="%1$s" class="widget %2$s">',
		'after_widget'  => '</section>',
		'before_title'  => '<h2 class="widget-title">',
		'after_title'   => '</h2>',
	) );

	register_sidebar( array(
		'name'          => __( 'Content Bottom 2', 'twentysixteen' ),
		'id'            => 'sidebar-3',
		'description'   => __( 'Appears at the bottom of the content on posts and pages.', 'twentysixteen' ),
		'before_widget' => '<section id="%1$s" class="widget %2$s">',
		'after_widget'  => '</section>',
		'before_title'  => '<h2 class="widget-title">',
		'after_title'   => '</h2>',
	) );
}
add_action( 'widgets_init', 'twentysixteen_widgets_init' );

Le deuxième argument, $instance, est un tableau qui nous donne les paramètres du widget lors de la validation du formulaire depuis l’interface admin. Nous y reviendrons dans les fonctions suivantes.

form

La fonction form est appelée lors de l’affichage du widget et de son paramétrage dans l’interface admin (back end) . Elle prend un seul argument $instance. Cet argument est un tableau avec les valeurs qui ont été précédemment sauvegardées en base de données. Il permet précisément d’afficher ces valeurs dans le formulaire.

Cette fonction se résume à l’affichage des champs (et des libellés) correspondants aux paramètres du widget. Elle ne décrit pas le formulaire HTML dans son entier, c’est WordPress qui s’en charge. Le développeur se concentre donc sur l’essentiel ! 🙂

update

Enfin, la fonction update est appelée lors de la validation des paramètres du widget dans l’interface admin (back end) . Elle prend deux arguments $new_instance et $old_instance. $new_instance est un tableau qui comprend les nouvelles valeurs tandis que $old_instance est le tableau des valeurs précédemment sauvegardées.

De la même façon que la fonction form, cette fonction ne prend pas en charge la sauvegarde en base de données. Il suffit juste de retourner le tableau des valeurs à sauvegarder. Autrement dit, WordPress nous prémache le boulot et c’est fun ! 😉

Les fonctions dont nous aurons besoin

Dernier point à aborder, quelles sont les autres fonctions qui nous serons utiles ?

La première de toute est __construct (notez bien les deux underscores avant le nom). Cette fonction est toujours appelée lors de la création d’un objet d’une classe. C’est aussi le moment que l’on indique comme instanciation. Elle sert notamment à initialiser tous les paramètres de l’objet. Lorsque nous déclarerons notre fonction __construct dans notre classe widget, nous ferons appel à cette même fonction de la classe mère mais nous y reviendrons plus tard.

Enfin, nous utiliserons deux autres méthodes get_field_id et get_field_name. Ces deux fonctions nous permettront de créer respectivement les identifiant et nom des attributs des différents champs correspondant aux paramètres de notre formulaire.

Procédure de création d’un widget

Maintenant que nous avons vu comment était constitué un widget, la question de savoir comment l’on crée un nouveau widget se pose ?

La structure est donc déjà existante et il faut l’utiliser. En programmation orientée objet (POO), on peut étendre une classe en une sous-classe. Cette sous-classe conserve la structure initiale de sa classe mère. Cette opération est très simple en PHP, elle prend la forme suivante :

Déclaration d'une sous-classe
class BWP_CPT_Widget extends WP_Widget {
    // votre code ici : propriétés et méthodes
}

Il nous reste à créer les fonctions widget, update, form et __construct.

Comme le widget Articles récents est très proche de ce que l’on veut réaliser, nous partirons de ce code que l’on adaptera à nos propres besoin.

Vous trouverez le source dans le dossier wp-includes/widgets de WordPress et dans le fichier class-wp-widget-recent-posts.php. Vous constaterez que les quatre fonctions que nous voulions sont bien là ! :).

Il suffit de copier/coller ce fichier dans l’arborescence des plugins de WordPress wp-content/plugins/nom-de-votre-plugin (sans oublier de renommer le nom de la classe).

Pour que votre widget soit reconnu, il faut mettre à jour les informations le concernant dans le premier bloc de commentaire. Attention le deux points ‘:’ doivent être collés au texte.

Description du plugin
/*
Plugin Name: CPT WIDGET
Plugin URI: http://www.babel-web.info/wordpress/plugins/cpt-widget
Description: Display Custom Post Type in a widget
Version: 1.0
Author: Olivier SPADI
Author URI: http://www.babel-web.info/wordpress/plugins/
License: GPL2
Text Domain: bwp-cpt-widget
Domain Path: /lang

Original Author: Olivier SPADI

Copyright 2016  Olivier SPADI (email : olivier.spadi@babel-web.info)
*/

Vous trouverez dans ce qui suit, le code mis à jour des différentes fonctions. Les lignes surlignées en jaune indiquent que nous y avons porté une modification ou est un ajout de notre part.

__construct
public function __construct() {
      $widget_ops = array(
            'classname' => 'bwp-cpt-widget',
            'description' => __( 'Display the custom post types in a widget', 'bwp-cpt-widget' ),
            'customize_selective_refresh' => true,
      );
      parent::__construct( 'bwp-cpt-widget', __( 'Custom Post Type Widget', 'bwp-cpt-widget' ), $widget_ops );
      $this->alt_option_name = 'widget_bwp_cpt_widget';
}

Le point important ici est la ligne 7. Cette ligne de code fait appel à la fonction __construct de la classe WP_Widget en lui passant des arguments.

Je reprends ici la documentation de PHP sur les fonctions d’objet.

PHP permet aux développeurs de déclarer des constructeurs pour les classes. Les classes qui possèdent une méthode constructeur appellent cette méthode à chaque création d’une nouvelle instance de l’objet, ce qui est intéressant pour toutes les initialisations dont l’objet a besoin avant d’être utilisé.
Note: Les constructeurs parents ne sont pas appelés implicitement si la classe enfant définit un constructeur. Si vous voulez utiliser un constructeur parent, il sera nécessaire de faire appel à parent::__construct() depuis le constructeur enfant. Si l’enfant ne définit pas un constructeur alors il peut être hérité de la classe parent, exactement de la même façon qu’une méthode le serait (si elle n’a pas été déclarée comme privée).

Vous pouvez ensuite procéder par étapes pour chacune des fonctions suivantes :

  • formulaire (form),
  • validation (update),
  • affichage (widget).
form
public function form( $instance ) {
        $title      = isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : '';
        $number     = isset( $instance['number'] ) ? absint( $instance['number'] ) : 5;
        $show_price = isset( $instance['show_price'] ) ? (bool) $instance['show_price'] : false;
?>
        <p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
        <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo $title; ?>" /></p>

        <p><label for="<?php echo $this->get_field_id( 'number' ); ?>"><?php _e( 'Number of posts to show:' ); ?></label>
        <input class="tiny-text" id="<?php echo $this->get_field_id( 'number' ); ?>" name="<?php echo $this->get_field_name( 'number' ); ?>" type="number" step="1" min="1" value="<?php echo $number; ?>" size="3" /></p>

        <p><input class="checkbox" type="checkbox"<?php checked( $show_price ); ?> id="<?php echo $this->get_field_id( 'show_price' ); ?>" name="<?php echo $this->get_field_name( 'show_price' ); ?>" />
        <label for="<?php echo $this->get_field_id( 'show_price' ); ?>"><?php _e( 'Display price?', 'bwp-cpt-widget' ); ?></label></p>
<?php
}

 

update
public function update( $new_instance, $old_instance ) {
        $instance = $old_instance;
        $instance['title'] = sanitize_text_field( $new_instance['title'] );
        $instance['number'] = (int) $new_instance['number'];
        $instance['show_price'] = isset( $new_instance['show_price'] ) ? (bool) $new_instance['show_price'] : false;
        return $instance;
}

 

widget
public function widget( $args, $instance ) {
        if ( ! isset( $args['widget_id'] ) ) {
                $args['widget_id'] = $this->id;
        }

        $title = ( ! empty( $instance['title'] ) ) ? $instance['title'] : __( 'Recent Custom Post Type' );

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

        $number = ( ! empty( $instance['number'] ) ) ? absint( $instance['number'] ) : 5;
        if ( ! $number )
                $number = 5;
        $show_price = isset( $instance['show_price'] ) ? $instance['show_price'] : false;

        $r = new WP_Query( apply_filters( 'widget_posts_args', array(
                'post_type'           => 'immo',
                'posts_per_page'      => $number,
                'no_found_rows'       => true,
                'post_status'         => 'publish',
                'ignore_sticky_posts' => true
        ) ) );

        if ($r->have_posts()) :
            ?>
            <?php echo $args['before_widget']; ?>
            <?php if ( $title ) {
                echo $args['before_title'] . $title . $args['after_title'];
            } ?>
            <ul>
            <?php while ( $r->have_posts() ) : $r->the_post(); ?>
                <li>
                    <a href="<?php the_permalink(); ?>"><?php get_the_title() ? the_title() : the_ID(); ?></a>
                <?php if ( $show_price ) : ?>
                    <span class="post-price"><?php echo number_format_i18n(get_field("prix")); ?> €</span>
                <?php endif; ?>
                </li>
            <?php endwhile; ?>
            </ul>
            <?php echo $args['after_widget']; ?>
            <?php
            // Reset the global $the_post as this query will have stomped on it
            wp_reset_postdata();

        endif;
}

Je crois que l’on peut dire que, si l’on maitrise un tant soit peu le PHP, ces modifications sont relativement simples à mettre en place. Qu’en pensez-vous ?

Encore un petit effort

Oui, mais je ne vois toujours pas mon widget s’afficher dans l’interface admin. 🙁

En effet, si l’on en restait là, WordPress n’est pas encore en mesure d’intégrer complétement notre nouveau widget. Il faut donc une étape supplémentaire qui consiste à raccrocher notre code (en l’occurrence notre classe widget) au cœur de WordPress.

WordPress a encore une fois tout prévu et met à notre disposition des hook (crochets) qui permettent d’intégrer nos développements à WP. On distingue deux types de hook : le hook d’action, le hook de filtre. Pour notre cas, il nous faut utiliser un hook d’action widgets_init et l’on procède comme suit.

Charger notre widget dans WP
// register BWP_CPT_Widget
function register_bwp_cpt_widget() {
	register_widget( 'BWP_CPT_Widget' );
}

// Start the widget
add_action( 'widgets_init', 'register_bwp_cpt_widget' );

Dans son processus de lancement, WordPress déclenche un certain nombre d’évènements ou d’actions. Lorsqu’il déclenche le hook d’action widgets_init, il va procéder à toutes les initialisations des widgets présents dans les différents plugins ou thèmes. Dans notre cas, il appellera la fonction register_bwp_cpt_widget qui elle-même se chargera d’initialiser notre widget avec la fonction register_widget.

Une fois cette étape réalisée, WordPress reconnait notre widget et nous pouvons le visualiser aussi bien côté front end que back end !!! 🙂

Généraliser le widget à tous nos custom post types

Le dernier point que nous aborderons dans cet article est la généralisation à tous les custom post types créés dans notre thème ou les plugins installés.

Autrement dit, la création d’un menu déroulant avec tous les CPT dans le formulaire de paramétrage de notre widget.

Il suffit d’ajouter ces lignes dans la fonction form.

Liste des CPT
	$cpt 		= isset( $instance['cpt'] ) ? esc_attr( $instance['cpt'] ) : '';
	$args_cpt = array(
		'public'   => true,
		'_builtin' => false
	);

	$cpts = get_post_types($args_cpt);
	if (count($cpts)) :
?>
		<p><label for="<?php echo $this->get_field_id( 'cpt' ); ?>"><?php _e( 'CPT:', 'bwp-cpt-widget' ); ?></label>
		<select class="select" id="<?php echo $this->get_field_id( 'cpt' ); ?>" name="<?php echo $this->get_field_name( 'cpt' ); ?>">
			<option value=""><?php _e('Choice an option...', 'bwp-cpt-widget'); ?></option>

<?php
		foreach ($cpts as $posttype) {
			$selected = false;
			if ($posttype == $cpt)
				$selected = true;
?>
			<option value="<?php echo $posttype; ?>" <?php echo $selected ? "selected" : ""; ?>><?php echo $posttype; ?></option>
<?php
		}
?>
		</select>
<?php
	endif;

Ici, nous faisons appel à la fonction get_post_types qui nous ramène tous les CPT puis nous construisons le menu déroulant.

Si vous voulez en savoir plus, je vous invite à visualiser la vidéo.

Téléchargement des sources

Vous pouvez télécharger les sources sur GitHub.

Liste de widgets déjà existants