Lorsque j’ai créé mon plugin WordPress Novo-Map, j’ai toujours trouvé assez facilement les infos dont j’avais besoin sur le net. WordPress est un outil tellement utilisé que lorsque je bloquais sur quelque chose, il me suffisait généralement de googler un peu pour trouver une réponse à ma question.
Mais l’arrivée de Gutenberg est en train de bouleverser la façon dont sont développés les plugins et thèmes WP. C’est évidemment une bonne chose car il intègre des technologies modernes comme react.js ou encore redux.js à WordPress. Mais Gutenberg se développe et change à une telle vitesse que c’est parfois difficile à suivre… et surtout, je trouve qu’il y a encore assez peu d’infos en ligne sur Gutenberg (et react/redux) ce qui fait que j’ai été parfois bloqué assez longtemps sur certains sujets.
Bref, comme je me suis mis en tête de mettre à jour mon plugin WordPress pour Gutenberg en ce début d’année, je me suis dit que j’allais aussi documenter un peu les sujets qui m’ont posé problème par la même occasion… en espérant que ça puisse être utile à la communauté des devs WordPress.
Note: Cet article est assez technique et est destiné principalement aux développeurs qui souhaitent créer des plugins en utilisant les nouvelles technologies qu’intègre Gutenberg.
Les outils que j’ai utilisé pour créer mon plugin WP
Pour bien comprendre cet article et les problèmes que j’ai rencontrés, il faut que je vous explique rapidement la structure de mon plugin.
Comme beaucoup de développeurs, j’ai utilisé le WordPress plugin Boilerplate (WPPB) comme base pour créer mon plugin de manière organisée, standardisée et en programmation orientée objet (POO). Le WPPB est une référence et si vous voulez en savoir plus sur ce sujet, il existe beaucoup de tutos en ligne comme celui-ci.
Mon plugin est donc organisé en classes qui gèrent des objets et leurs méthodes (dans mon cas des cartes, des marqueurs, des infoboxes etc…). Chacune de ces classes a un “manager” (ou un gestionnaire) qui permet d’accéder, modifier, sauvegarder ou effacer ces objets dans des tables personnalisées de la base de données (donc mon plugin ajoute des tables en plus des 12 tables de base de WordPress). Si vous voulez en savoir plus sur la POO en PHP, je vous conseille ce tuto gratuit qui est top.
Jusqu’à WordPress 5.0 et l’arrivée de Gutenberg, le 95% de mon plugin était écrit en PHP et j’avais juste besoin d’un tout petit peu de JavaScript (ou Jquery) pour quelques détails et rendre le tout plus interactif dans le navigateur.
Gutenberg: une application front-end moderne
Donc depuis Décembre 2018, Gutenberg est le nouvel éditeur par défaut de WordPress et react/redux ont été intégrés dans WordPress core. Gutenberg est ce qu’on appelle une Single Page App (SPA) cad une application moderne qui fonctionne directement dans le navigateur et qui n’a pas besoin de recharger la page pendant son fonctionnement.
Gutenberg a donc été créé sur la base de React et de Redux (tout en ajoutant une petite couche d’abstraction pour mieux gérer les changements des API de ces outils). Sans rentrer dans les détails (et aussi car je ne suis pas un expert de ces sujets), react permet de créer des composants réutilisables et de mettre à jour facilement une partie du DOM en fonction de certaines actions sans recharger toute la page. Quant à redux il permet d’organiser/partager facilement les données et leur état entre les différents composants de l’application.
Comme Gutenberg est une application front-end (qui fonctionne donc dans le navigateur sans accès direct à la base de donnée), il utilise l’API REST de WordPress pour accéder et/ou modifier les données qui sont stockées dans la base de donnée.
Note: Comme vous le voyez, Gutenberg introduit énormément de nouveautés (react, redux et toute la suite d’outils qu’il faut utiliser comme npm, webpack, babel etc…) et la courbe d’apprentissage est assez élevée pour des débutants avec peu d’expérience en JavaScript comme moi. Le but de cet article n’est pas de parler de tout ces outils en détail (je vous recommande ce cours sur Udemy que j’ai suivi pour ça) mais d’un problème que j’ai rencontré en particulier. D’ailleurs comme tous ces éléments sont encore assez nouveaux pour moi, n’hésitez pas à me corriger en commentaire si je dis des bêtises
Mon problème: accéder à mes tables dans la bdd et avoir accès à mes données dans mes composants
Donc en créant mon premier block Gutenberg pour mon plugin, je me suis rapidement retrouvé confronté à un problème! Je ne savais pas comment avoir accès aux données de mon plugin (toutes les infos sur les cartes, les marqueurs, les infoboxes etc…stockées dans des tables personnalisées dans la bdd) à l’intérieur de mes composants. Bon j’avais bien une petite idée et je pensais bien qu’il fallait que je crée des “Endpoints” personnalisés dans l’API REST de WordPress pour accéder (ou modifier) mes données depuis mes composants.
Mais comment faire ça à la façon Gutenberg (donc react) et avoir accès à mes données à l’intérieur de mes blocks?
J’ai trouvé très peu d’infos sur le net à ce sujet et je me suis retrouvé à devoir beaucoup fouiller dans le code source de Gutenberg pour finalement avoir une solution fonctionnelle que je vais détailler ci-dessous. Dans la suite, je pars du principe que vous savez créer un plugin pour enregistrer un bloc basique dans Gutenberg (même si j’essaierai d’ajouter des liens vers des ressources utiles).
Créer mes Endpoints personnalisés pour l’API REST de WP
La 1ère chose à faire est donc de créer l’interface entre mes blocs Gutenberg côté client et mes classes PHP et la base de donnée côté serveur. Cette interface va me permettre d’accéder à mes données en utilisant mes classes existantes et de les exposer via l’API REST de WP.
En POO cette interface prend la forme d’une classe API dans laquelle on utilise la hook rest_api_init
et la fonction register_rest_route()
pour créer nos Endpoints personnalisés comme suit (avec ici un exemple d’une méthode qui récupère une liste de toutes les cartes gmap disponibles):
<?php /** * Class Api exemple */ class Api { private $version; private $namespace; private $plugin_dir_path; private $plugin_name; public function __construct($plugin_name) { $this->plugin_name = $plugin_name; $this->version = '1'; $this->namespace = $plugin_name . '/v' . $this->version; $this->plugin_dir_path = plugin_dir_path( dirname( __FILE__ ) ); } public function run() { add_action( 'rest_api_init', array( $this, 'register_gmap_list' ) ); } public function register_gmap_list() { register_rest_route( $this->namespace, '/my-unique-url', array( 'methods' => 'GET', 'callback' => array( $this, 'get_gmap_list' ), 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); } public function get_gmap_list() { require_once $this->plugin_dir_path . 'includes/class-gmap.php'; require_once $this->plugin_dir_path . 'includes/class-gmap-manager.php'; global $wpdb; $gmap_manager = new Gmap_Manager($wpdb); $gmap_list = $gmap_manager->get_list(); return $gmap_list; } }
Si vous utilisez le WPPB, vous pourrez initialiser l’API depuis la classe principale de votre plugin comme suit:
$api = new Api( $this->get_plugin_name() ); $api->run();
Quelques remarques concernant cette classe et plus en particulier la fonction register_rest_route(). Donc si vous faîtes une requête à l’adresse 'namespace/my-unique-url'
(et que vous avez la permission ‘edit_posts’), la fonction de callback get_gmap_list()
va être activée et retourner une liste d’objets gmap qui sera retournée au format JSON via l’API.
Comme nous utilisons une API JSON (JavaScript Object Notation), je vous conseille aussi d’ajouter à la classe qui définit les objets que vous envoyez via l’API (en l’occurrence ma classe Gmap) l’interface JsonSerializable
qui permet de contrôler comment les donnée sont transmises en JSON:
class Gmap implements \JsonSerializable { /** * définition de toutes les propriétés de ma classe */ /** * Définit comment les propriétés privées de ma classe sont exportées en Json */ public function jsonSerialize(){ $vars = get_object_vars($this); return $vars; } }
Encore une chose, vous aurez peut-être besoin de passer des paramètres à votre Endpoint (par exemple une id pour une carte ou je ne sais quoi d’autre). la fonction register_rest_route()
permet de le faire de la manière suivant:
register_rest_route( $this->namespace, '/my-unique-url/(?P<id>\d+)/(?P<string>[a-zA-Z0-9-]+)', array( 'methods' => 'GET', 'callback' => array( $this, 'my_custom_callback' ), 'args' => array( 'id' => array( 'default' => 0, 'required' => true, 'validate_callback' => function($param, $request, $key) { return is_numeric( $param ); } ), 'string' => array( 'required' => true, 'validate_callback' => function($param, $request, $key) { // validate $param return $param } ) ), 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, ) );
De cette façon, si vous effectuez une requête à l’adresse 'namespace/my-unique-url/id/string'
, vous pourrez valider les paramètres envoyés et les utiliser dans votre function my_custom_callback()
comme suit:
public function my_custom_callback($request) { $id = $request['id'] $txt = $request['string'] }
Utiliser les données de l’API dans mes blocs Gutenberg et composants
Bon… jusque là je dois dire que tout allait bien pour moi vu que tout a été fait en PHP (comme je le disais plus haut, je fais partie des développeurs PHP + un petit peu de JS… mais j’essaie d’apprendre le JS en profondeur comme le conseillait Matt Mullenweg en 2015 déjà 😉 ). C’est donc maintenant que les choses se compliquent pour moi. D’ailleurs, si vous pensez que j’ai fait un mauvais choix pour une raison ou pour une autre, n’hésitez pas à me laisser un commentaire.
Je ne vais pas rentrer dans les détails de comment ajouter un nouveau bloc dans Gutenberg ni de toute la stack d’outils que ça implique (il y a plein de tutos comme le cours que je vous recommandais avant qui expliquent ça très bien sur le net)… Mais pour cet exemple je pars du principe que vous savez créer un plugin et enregistrer un bloc comme suit:
import { registerBlockType } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import Edit from './edit'; registerBlockType( 'your-plugin-blocks/block-name', { title: __( 'Your block title', 'domain' ), description: __( 'Your block description', 'domain' ), category: 'widgets', icon: 'admin-network', keywords: [ __( 'map', 'domain' ), __( 'novo-map', 'domain' ), ], attributes: attrs, edit: Edit, save() { return <p>Saved Content</p>; }, } );
Dans le cadre de cet article, on ne va s’intéresser qu’à la fonction Edit qui définit ce qui se passe dans l’éditeur lorsque le bloc est utilisé. C’est justement dans les props (propriétés en react) de cette fonction qu’on aimerait avoir accès aux données de notre plugin.
État local d’un composant vs état global de l’application
Bon en soit maintenant pour avoir accès aux données de mon plugin stockées en base de donnée côté client, on pourrait simplement utiliser le module WordPress api-fetch pour récupérer nos données depuis un composant à l’intérieur de notre fonction edit.
Pourtant c’est en me renseignant sur ce sujet que j’ai commencé à voir apparaître dans mes recherches des choses comme les HOC (higher-order components) withSelect
ou withDispatch
et que j’ai entendu parler du module @wordpress/data.
Comme Gutenberg est une application avec une structure complexe, elle a besoin d’un module comme wp.data
(qui est basé sur Redux mais avec quelques différences) pour gérer son état global et pour organiser/partager ses données au sein de WordPress et des plugins. Grâce à ce module, il est possible d’accéder facilement aux données de WordPress depuis n’importe quel composant… mais aussi de les modifier et de faire refléter ces changements au sein de toute l’application (c’est pour ça qu’on appelle ça un état global). Pour comprendre un peu mieux comment le module wp.data fonctionne, je vous recommande de jeter un oeil à cette vidéo:
Vous voyez que grâce à wp.data, il est possible d’accéder aux données de WordPress avec quelque chose comme ça (vous pouvez essayer cette commande dans la console de votre navigateur lorsque vous êtes logué dans votre admin):
wp.data.select('core/editor').selector()
Cette commande vous permet d’accéder au store 'core/editor'
et de récupérer les données de votre choix en utilisant un sélecteur. Et ce module vous permet aussi d’enregistrer votre propre store et d’accéder à ses données de la même manière depuis n’importe quel composant.
Donc la question maintenant est:
Est-ce que j’ai réellement besoin d’utiliser le module wp.data et d’enregistrer mon propre “store” dans mon plugin?
La réponse va dépendre de la complexité de votre application. Si vous créez un bloc tout simple qui peut gérer l’état de ses données dans un seul composant, vous n’avez certainement pas besoin de créer votre propre store (et vous pouvez donc vous contenter d’un état local). Par contre, en ce qui concerne mon plugin, a terme je risque d’avoir plusieurs blocs ou composants qui vont partager des données… et même un jour toute la partie admin de mon plugin sera peut-être créée en JavaScript… il me paraissait donc plus logique de créer mon propre store et c’est ce que je vais développer dans la partie qui suit.
Enregistrer notre propre store dans wp.data
Qu’on se le dise, on pourrait écrire plusieurs articles rien que sur le fonctionnement de wp.data (et donc de redux) et je vous conseille de lire cette excellente série d’articles si vous avez envie de comprendre dans le détail comment ce module fonctionne.
Ci-dessous je vais partager avec vous mon code avec quelques remarques mais sans rentrer dans tous les détails (cet article est déjà très long). Voici donc comment j’ai enregistré mon propre store qui me permet juste de récupérer une liste de tous mes objets Gmap pour le moment:
import apiFetch from '@wordpress/api-fetch'; import { registerStore } from '@wordpress/data'; const DEFAULT_STATE = { gmapList: {}, }; const STORE_NAME = 'plugin-name-blocks/store-name'; const actions = { setGmapList( gmapList ) { return { type: 'SET_GMAP_LIST', gmapList, }; }, getGmapList( path ) { return { type: 'GET_GMAP_LIST', path, }; }, }; const reducer = ( state = DEFAULT_STATE, action ) => { switch ( action.type ) { case 'SET_GMAP_LIST': { return { ...state, gmapList: action.gmapList, }; } default: { return state; } } }; const selectors = { getGmapList( state ) { const { gmapList } = state; return gmapList; }, }; const controls = { GET_GMAP_LIST( action ) { return apiFetch( { path: action.path } ); }, }; const resolvers = { *getGmapList() { const gmapList = yield actions.getGmapList( '/namespace/my-unique-url/' ); return actions.setGmapList( gmapList ); }, }; const storeConfig = { reducer, controls, selectors, resolvers, actions, }; registerStore( STORE_NAME, storeConfig ); export { STORE_NAME };
Vous voyez qu’on utilise tout en bas la fonction registerStore()
pour ajouter notre store avec le nom 'plugin-name-blocks/store-name'
en 1er argument et en 2ème un objet storeConfig
un peu compliqué qui définit l’état des données, comment y accéder et comment le modifier. Intéressons nous d’un peu plus prêt à ce 2ème argument pour mieux comprendre.
storeConfig
contient donc 5 éléments:
- actions: Contient tous les objets action (ou action creator en anglais) qui vont décrire au reducer comment il doit modifier l’état. Un créateur d’action peut aussi être utilisé en association avec un resolver et un controler pour générer des “side effects”.
- selectors: Contient tous les sélecteurs qui permettent de récupérer des données dans notre store. Un sélecteur est simplement une fonction qui prend au minimum l’état courant comme argument et qui renvoi la partie de l’état qu’on souhaite récupérer. En l’occurrence
getGmapList
me permet de récupérer une liste de tous mes objets Gmap. L’exemple de code que je vous donnais plus haut à utiliser dans la console dans l’admin de votre site était un sélecteur. - resolvers: Ce sont des objets qui permettent d’associer une fonction ou une chaîne d’actions (appelé “side effect” en anglais) à des sélecteurs. Un resolver doit obligatoirement avoir le même nom que son sélecteur associé. Même si ce n’est pas obligatoire, ce sont souvent des fonctions générateurs associées à un control.
- controls: Définit une logique associée à un certain type d’action. Ils sont particulièrement utile pour être utilisé depuis un resolver pour obtenir ou sauvegarder des données dans la base de donnée en utilisant l’API REST de WP. En l’occurence, vous voyez que notre control envoit une requête à l’adresse définie dans notre API pour récupérer les liste des Gmap dans le resolver.
- reducer: Décrit la forme de vos données et comment leur état change en réponse aux actions dispatchées dans le store. Le reducer reçoit toujours l’ancien état et un objet action et renvoi l’état mis à jour.
Je dois avouer que j’ai trouvé le fonctionnement d’un store assez complexe et que j’ai du pas mal lire et rechercher sur ce sujet pour comprendre un peu mieux ce concept. Je ne peux pas m’empêcher de voir des similitudes entre ce store et ma classe manager qui gère l’interface entre mes objets et la base de donnée côté serveur avec des fonctions “getters” et “setters”. Mais en plus, un store permet de mettre à jour tous les composants qui y sont connectés en fonction de l’état de la donnée.
Bref je vous conseille d’essayer d’enregistrer votre propre store et de jouer avec pour mieux comprendre le concept. J’ai moi aussi probablement encore beaucoup de chose à découvrir à ce sujet.
Accéder aux données du store dans mes composants
Disons maintenant que je souhaite afficher dans mon bloc un tag <select>
contenant toutes mes Gmaps dans ses <option>nom de ma Gmap</option>
. La première façon de faire est d’utiliser le HOC withSelect
pour enrober notre fonction Edit comme suit:
import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { Spinner } from '@wordpress/components'; import { STORE_NAME } from '../../stores/novo-map-gmap'; class Edit extends Component { render() { const { className, attributes, gmapList } = this.props; // return a Spinner while the maps are loading if ( ! Object.keys( gmapList ).length ) { return ( <div className={ className }> <Spinner /> { __( 'Loading Maps', 'novo-map-blocks' ) } </div> ); } // display the map selector return ( <div className={ className }> <select> <option value={ 0 }> { __( 'Choose a Novo-map', 'novo-map-blocks' ) } </option> { Object.values( gmapList ).map( ( gmap ) => { return ( <option key={ gmap.id } value={ gmap.id }> { gmap.name } </option> ); } ) } ; </select> </div> ); } } export default compose( [ withSelect( ( select ) => { return { gmapList: select( STORE_NAME ).getGmapList(), }; } ), ] )( Edit );
Il y a plusieurs choses à noter dans cette fonction Edit:
- Notre fonction Edit est enrobée par le HOC withSelect qui renvoie un objet contenant les props dont on aimerait avoir accès à l’intérieur de nos composants. C’est à cet endroit qu’on accède à notre store en utilisant notre sélecteur. Ainsi,
gmapList
devient accessible dans nos props à l’intérieur de notre fonction render. - Vous remarquerez que j’ai laissé le
compose()
autour duwithSelect
. Il permet de composer plusieurs HOC en un seul pour des questions de lisibilité. Dans ce simple exemple, on utilise uniquementwithSelect
pour accéder à des données de notre store (donc j’aurais pu enlever le compose). Mais souvent on utilisera aussiwithDispatch
pour modifier l’état des données de notre store ou encore d’autres HOC. - Enfin vous voyez qu’il y a 2
return
à l’intérieur de notre render. C’est parce que notregmapList
est encore vide tant que la requête API n’est pas terminée (si vous recherchez l’état par défautDEFAULT_STATE
, vous voyez qu’il est vide avant notre requête API). Donc on affiche un composant spinner en indiquant qu les maps sont en train de charger. Puis, une fois quegmapList
continent nos maps, on affiche un élément select contenant toutes nos maps à l’intérieur.
Pour terminer, j’aimerai juste ajouter que withSelect
et withDispatch
ne sont pas les seuls moyen d’interagir avec notre store. Plus récemment, des éléments appelés “Hooks” ont été introduits dans React et les Hooks useSelect
et useDispatch
ont été introduites dans Gutenberg. Elles permettent d’interagir avec notre store sans forcément écrire des classes. Vous pourrez en apprendre plus sur ce sujet par ici.
Voilà comme ça m’a pris pas mal de temps à mettre tout ces éléments ensemble et que j’ai eu de la peine à trouver ces infos en ligne, j’espère que cet article aidera d’autres devs de la communauté WP. Dans les semaines à venir, je vais continuer à travailler sur mon plugin et je documenterais les thèmes qui me paraissent pertinents en cours de route. N’hésitez pas à me laisser un commentaire si vous avez des remarques ou des questions!
- Très bon cour sur Gutenberg et react sur Udemy: J’ai suivi plusieurs cours sur Gutenberg et je trouve que c’est de loin le plus complet. Il explique en détails toute la stack d’outils qu’il faut utiliser avec Gutenberg (webpack, babel, npm etc…) et comment créer des blocs complexes
- Série d’article sur le module wp.data: C’est de loin la meilleure ressource que j’ai trouvé sur ce module. Elle a été écrite Darren Ethier qui travail pour Automattic (l’entreprise qui se cache derrière WordPress)
- la documentation officielle du module wp.data
- Un très bon article d’un développeur qui donnent quelques conseils essentiels pour commencer à créer des blocs Gutenberg
Note: Cet article contient des liens affiliés vers un cours Udemy que j’ai suivi et que je trouve très qualitatif. Si vous passez par ces liens pour acheter le cours, nous toucherons une petit commission qui nous aidera à continuer de produire des articles gratuits et indépendants comme celui-ci.