Modern PHP Engineering
Chapitre 14
14 — WordPress Ecosystem
14 — WordPress Ecosystem
Course : WordPress Ecosystem
1. Introduction à l'Écosystème WordPress
1.1 WordPress aujourd'hui
WordPress alimente 43% du web mondial. Longtemps considéré comme une simple plateforme de blogging, WordPress est devenu un système de gestion de contenu (CMS) complet, un framework de développement et une plateforme d'application (grâce à la REST API et au Block Editor).
1.2 Évolution vers un CMS moderne
2003 : b2/cafelog fork → WordPress 0.7
2005 : WordPress 1.5 (templates, pages)
2010 : WordPress 3.0 (Custom Post Types, Custom Taxonomies)
2015 : WordPress 4.4 (REST API)
2018 : WordPress 5.0 (Block Editor — Gutenberg)
2024 : WordPress 6.x (Full Site Editing, Interactivity API)
1.3 Philosophie WordPress
- Decisions, not options : WordPress fait des choix par défaut
- Plugins over core : les fonctionnalités spécifiques dans des plugins
- Themes for presentation : les thèmes gèrent l'apparence
- Hooks everywhere : actions et filters pour étendre le système
2. WordPress Internals
2.1 Architecture WordPress
/wp-content/
├── plugins/ # Plugins installés
│ └── mon-plugin/
│ ├── mon-plugin.php
│ └── includes/
├── themes/ # Thèmes installés
│ └── mon-theme/
│ ├── style.css
│ ├── index.php
│ └── functions.php
├── uploads/ # Médiathèque
└── languages/ # Traductions
/wp-includes/ # Cœur WordPress (NE PAS MODIFIER)
/wp-admin/ # Interface d'administration
2.2 Hooks : Actions et Filters
// ACTION — exécute du code à un moment précis
add_action('init', function () {
register_post_type('book', [
'labels' => [
'name' => 'Livres',
'singular_name' => 'Livre',
],
'public' => true,
'has_archive' => true,
'supports' => ['title', 'editor', 'thumbnail'],
'show_in_rest' => true,
]);
});
add_action('save_post_book', function (int $post_id) {
// Actions après sauvegarde d'un livre
update_post_meta($post_id, 'last_modified', time());
}, 10, 1); // priorité 10, 1 paramètre
// FILTER — modifie une valeur
add_filter('the_content', function (string $content): string {
return '<div class="post-content">' . $content . '</div>';
}, 10, 1);
add_filter('excerpt_length', function (): int {
return 30; // 30 mots au lieu de 55
});
add_filter('wp_mail_from', function (): string {
return 'noreply@monsite.com';
});
2.3 WP_Query
// Requête personnalisée
$query = new WP_Query([
'post_type' => 'book',
'posts_per_page' => 10,
'meta_query' => [
[
'key' => 'price',
'value' => 20,
'type' => 'NUMERIC',
'compare' => '<=',
],
],
'tax_query' => [
[
'taxonomy' => 'genre',
'field' => 'slug',
'terms' => ['fiction', 'science-fiction'],
],
],
'orderby' => 'meta_value_num',
'meta_key' => 'price',
'order' => 'ASC',
]);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
the_title('<h2>', '</h2>');
the_excerpt();
}
wp_reset_postdata();
}
// Optimisation avec 'fields' et 'no_found_rows'
$titles = new WP_Query([
'post_type' => 'book',
'posts_per_page' => -1,
'fields' => 'ids', // seulement les IDs
'no_found_rows' => true, // pas de pagination
'update_post_meta_cache' => false, // pas de meta cache
'update_post_term_cache' => false, // pas de term cache
]);
2.4 Rewrite API
// Ajouter une règle de réécriture
add_action('init', function () {
add_rewrite_rule(
'^livres/([^/]+)/?$',
'index.php?post_type=book&book_name=$matches[1]',
'top'
);
});
// Ajouter des query vars
add_filter('query_vars', function (array $vars): array {
$vars[] = 'book_name';
return $vars;
});
// Template redirect
add_action('template_redirect', function () {
$bookName = get_query_var('book_name');
if ($bookName) {
$book = get_posts([
'post_type' => 'book',
'name' => $bookName,
]);
if ($book) {
setup_postdata($book[0]);
include get_template_directory() . '/single-book.php';
wp_reset_postdata();
exit;
}
}
});
3. Plugin Development (OOP)
3.1 Structure PSR-4 avec Composer
mon-plugin/
├── composer.json
├── mon-plugin.php # Entry point
├── src/
│ ├── Admin/
│ │ └── SettingsPage.php
│ ├── Frontend/
│ │ └── ShortcodeHandler.php
│ ├── PostTypes/
│ │ └── BookPostType.php
│ └── Contracts/
│ └── Registrable.php
├── tests/
│ └── Unit/
│ └── BookPostTypeTest.php
├── vendor/
└── .gitignore
{
"name": "vendor/mon-plugin",
"description": "Description du plugin",
"type": "wordpress-plugin",
"require": {
"php": ">=8.1",
"composer/installers": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^11",
"wp-phpunit/wp-phpunit": "^6"
},
"autoload": {
"psr-4": {
"MonPlugin\\": "src/"
}
},
"scripts": {
"test": "phpunit"
}
}
3.2 Plugin OOP
<?php
/**
* Plugin Name: Mon Plugin
* Description: Plugin orienté objet
* Version: 1.0.0
* Requires PHP: 8.1
*/
namespace MonPlugin;
use MonPlugin\PostTypes\BookPostType;
use MonPlugin\Admin\SettingsPage;
use MonPlugin\Frontend\ShortcodeHandler;
// Autoload
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
class Plugin
{
private static ?Plugin $instance = null;
private array $services = [];
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function initialize(): void
{
// Register services
$this->services['book_post_type'] = new BookPostType();
$this->services['settings_page'] = new SettingsPage();
$this->services['shortcode'] = new ShortcodeHandler();
// Boot services
foreach ($this->services as $service) {
if ($service instanceof Contracts\Registrable) {
$service->register();
}
}
}
public function getService(string $name): mixed
{
return $this->services[$name] ?? null;
}
}
// Bootstrap
add_action('plugins_loaded', function () {
Plugin::getInstance()->initialize();
});
3.3 PHPUnit pour WordPress
composer require --dev wp-phpunit/wp-phpunit
<phpunit bootstrap="vendor/wp-phpunit/wp-phpunit/bootstrap.php">
<testsuites>
<testsuite name="WordPress Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
class BookPostTypeTest extends \WP_UnitTestCase
{
public function test_post_type_is_registered(): void
{
$this->assertTrue(post_type_exists('book'));
}
public function test_custom_meta_is_saved(): void
{
$post_id = $this->factory->post->create([
'post_type' => 'book',
]);
update_post_meta($post_id, 'price', 25.99);
$this->assertEquals(25.99, get_post_meta($post_id, 'price', true));
}
}
4. Block Editor (Gutenberg)
4.1 block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "mon-plugin/testimonial",
"title": "Témoignage",
"category": "widgets",
"icon": "format-quote",
"description": "Affiche un témoignage client",
"keywords": ["testimonial", "quote", "client"],
"version": "1.0.0",
"textdomain": "mon-plugin",
"attributes": {
"quote": {
"type": "string",
"source": "html",
"selector": "blockquote"
},
"author": {
"type": "string",
"source": "text",
"selector": "cite"
},
"avatarUrl": {
"type": "string",
"default": ""
}
},
"supports": {
"align": ["wide", "full"],
"color": {
"background": true,
"text": true
},
"html": false
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
4.2 render_callback (PHP)
// render.php
<?php
$quote = $attributes['quote'] ?? '';
$author = $attributes['author'] ?? '';
$avatarUrl = $attributes['avatarUrl'] ?? '';
$className = $attributes['className'] ?? '';
?>
<div class="wp-block-mon-plugin-testimonial <?php echo esc_attr($className); ?>">
<?php if ($avatarUrl): ?>
<img
src="<?php echo esc_url($avatarUrl); ?>"
alt="<?php echo esc_attr($author); ?>"
class="testimonial-avatar"
/>
<?php endif; ?>
<blockquote>
<?php echo wp_kses_post($quote); ?>
</blockquote>
<?php if ($author): ?>
<cite>— <?php echo esc_html($author); ?></cite>
<?php endif; ?>
</div>
4.3 Interactivity API
// view.js (Interactivity API)
import { store, getContext } from '@wordpress/interactivity';
store('mon-plugin/testimonial', {
state: {
get isExpanded() {
const context = getContext();
return context.expanded;
},
},
actions: {
toggle() {
const context = getContext();
context.expanded = !context.expanded;
},
},
callbacks: {
logView() {
console.log('Testimonial viewed');
},
},
});
5. Advanced Custom Fields (ACF)
5.1 Field Groups (PHP)
// functions.php ou plugin
add_action('acf/init', function () {
acf_add_local_field_group([
'key' => 'group_book_details',
'title' => 'Détails du livre',
'fields' => [
[
'key' => 'field_book_author',
'label' => 'Auteur',
'name' => 'book_author',
'type' => 'text',
'required' => 1,
],
[
'key' => 'field_book_price',
'label' => 'Prix',
'name' => 'book_price',
'type' => 'number',
'min' => 0,
'step' => 0.01,
],
[
'key' => 'field_book_cover',
'label' => 'Couverture',
'name' => 'book_cover',
'type' => 'image',
'return_format' => 'array',
'preview_size' => 'medium',
],
[
'key' => 'field_book_rating',
'label' => 'Note',
'name' => 'book_rating',
'type' => 'range',
'min' => 0,
'max' => 5,
'step' => 0.5,
],
],
'location' => [
[
[
'param' => 'post_type',
'operator' => '==',
'value' => 'book',
],
],
],
]);
});
5.2 Utilisation dans les templates
// Dans single-book.php
$author = get_field('book_author');
$price = get_field('book_price');
$cover = get_field('book_cover');
$rating = get_field('book_rating');
echo '<h1>' . get_the_title() . '</h1>';
if ($cover) {
echo '<img src="' . esc_url($cover['url']) . '" alt="' . esc_attr($cover['alt']) . '" />';
}
echo '<p>Auteur : ' . esc_html($author) . '</p>';
echo '<p>Prix : ' . esc_html(number_format($price, 2)) . ' €</p>';
// Champ Repeater
if (have_rows('book_reviews')) :
echo '<h2>Avis</h2>';
while (have_rows('book_reviews')) : the_row();
$reviewer = get_sub_field('reviewer_name');
$comment = get_sub_field('review_comment');
echo '<div class="review">';
echo '<strong>' . esc_html($reviewer) . '</strong>';
echo '<p>' . wp_kses_post($comment) . '</p>';
echo '</div>';
endwhile;
endif;
6. REST API
6.1 Routes personnalisées
add_action('rest_api_init', function () {
// GET /wp-json/mon-plugin/v1/books/recommended
register_rest_route('mon-plugin/v1', '/books/recommended', [
'methods' => 'GET',
'callback' => function (WP_REST_Request $request): WP_REST_Response {
$books = get_posts([
'post_type' => 'book',
'posts_per_page' => 5,
'meta_key' => 'book_rating',
'orderby' => 'meta_value_num',
'order' => 'DESC',
]);
$data = array_map(function ($post): array {
return [
'id' => $post->ID,
'title' => $post->post_title,
'author' => get_field('book_author', $post->ID),
'price' => (float) get_field('book_price', $post->ID),
'rating' => (float) get_field('book_rating', $post->ID),
'cover_url' => get_field('book_cover', $post->ID)['url'] ?? '',
];
}, $books);
return new WP_REST_Response($data, 200);
},
'permission_callback' => '__return_true',
]);
// POST /wp-json/mon-plugin/v1/contact
register_rest_route('mon-plugin/v1', '/contact', [
'methods' => 'POST',
'callback' => function (WP_REST_Request $request): WP_REST_Response {
$params = $request->get_json_params();
if (empty($params['email']) || !is_email($params['email'])) {
return new WP_REST_Response([
'message' => 'Email invalide',
], 400);
}
wp_mail(
get_option('admin_email'),
'Nouveau contact : ' . sanitize_text_field($params['subject']),
sanitize_textarea_field($params['message'])
);
return new WP_REST_Response([
'message' => 'Message envoyé',
], 200);
},
'permission_callback' => '__return_true',
]);
});
6.2 Authentification JWT
composer require firebase/php-jwt
add_filter('rest_authentication_errors', function ($result) {
if (!empty($result)) {
return $result;
}
$token = null;
$headers = getallheaders();
$auth = $headers['Authorization'] ?? '';
if (preg_match('/Bearer\s(\S+)/', $auth, $matches)) {
$token = $matches[1];
}
if (!$token) {
return new WP_Error('rest_forbidden', 'Token manquant', ['status' => 401]);
}
try {
$decoded = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key(JWT_SECRET, 'HS256'));
wp_set_current_user($decoded->user_id);
} catch (\Exception $e) {
return new WP_Error('rest_forbidden', 'Token invalide', ['status' => 401]);
}
return $result;
});
7. WP CLI
7.1 Commandes personnalisées
class BookCommand
{
/**
* Importe des livres depuis un fichier CSV.
*
* ## OPTIONS
*
* <file>
* : Chemin vers le fichier CSV
*
* [--dry-run]
* : Simulation sans écrire en base
*
* @subcommand import
*/
public function import(array $args, array $assocArgs): void
{
$file = $args[0];
$dryRun = isset($assocArgs['dry-run']);
if (!file_exists($file)) {
WP_CLI::error("Fichier introuvable : $file");
}
$handle = fopen($file, 'r');
$headers = fgetcsv($handle);
$imported = 0;
while (($row = fgetcsv($handle)) !== false) {
$data = array_combine($headers, $row);
if (!$dryRun) {
$postId = wp_insert_post([
'post_title' => $data['title'],
'post_type' => 'book',
'post_status' => 'publish',
]);
update_field('book_author', $data['author'], $postId);
update_field('book_price', (float) $data['price'], $postId);
}
$imported++;
}
fclose($handle);
$mode = $dryRun ? '[DRY RUN] ' : '';
WP_CLI::success("{$mode}{$imported} livres importés");
}
}
WP_CLI::add_command('book', BookCommand::class);
# Utilisation
wp book import books.csv
wp book import books.csv --dry-run
8. Performance WordPress
8.1 Optimisation des requêtes
// Éviter les requêtes inutiles
$query = new WP_Query([
'posts_per_page' => 10,
'no_found_rows' => true, // désactive COUNT
'update_post_meta_cache' => false, // pas de meta cache
'update_post_term_cache' => false, // pas de term cache
]);
// Pagination optimisée
$paged = get_query_var('paged') ?: 1;
$query = new WP_Query([
'posts_per_page' => 10,
'paged' => $paged,
'cache_results' => true,
]);
8.2 Object Cache (Redis)
// Utilisation de WP Object Cache
$key = 'featured_books';
$cached = wp_cache_get($key, 'mon-plugin');
if ($cached === false) {
$books = new WP_Query(['post_type' => 'book', 'posts_per_page' => 5]);
wp_cache_set($key, $books, 'mon-plugin', 3600);
} else {
$books = $cached;
}
// Transients (cache avec expiration automatique)
if (false === ($popular = get_transient('popular_books'))) {
$popular = new WP_Query([
'post_type' => 'book',
'meta_key' => 'views',
'orderby' => 'meta_value_num',
'posts_per_page' => 5,
]);
set_transient('popular_books', $popular, HOUR_IN_SECONDS);
}
8.3 Lazy Loading
// Images en lazy loading natif
add_filter('wp_get_attachment_image_attributes', function (array $attrs): array {
$attrs['loading'] = 'lazy';
return $attrs;
});
// Defer JS
add_filter('script_loader_tag', function (string $tag, string $handle): string {
if (is_admin()) return $tag;
$defer = ['jquery', 'mon-plugin-public'];
if (in_array($handle, $defer)) {
return str_replace(' src', ' defer src', $tag);
}
return $tag;
}, 10, 2);
9. Résumé
WordPress moderne c'est :
- Internals : hooks, WP_Query, Rewrite API
- Plugins OOP : PSR-4, Composer, PHPUnit
- Gutenberg : block.json, render_callback, Interactivity API
- ACF : field groups, flexible content, options pages
- REST API : routes personnalisées, JWT auth
- WP CLI : commandes automatisées
- Performance : object cache, query optimization, lazy loading
WordPress n'est plus "juste un CMS" — c'est une plateforme d'application complète avec des standards de développement modernes.