MFormations
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>&mdash; <?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.