Avance 2 min de lecture · 272 mots

WordPress Multisite : Architecture et Gestion à Grande Échelle

Introduction : Multisite pour les Applications d’Entreprise

WordPress Multisite permet de gérer des centaines, voire des milliers de sites depuis une seule installation. En 2025, cette architecture est devenue la norme pour les organisations gérant des réseaux de sites : franchises, universités, médias, portails régionaux, et plateformes SaaS basées sur WordPress.

Cet article explore l’architecture, les stratégies de scalabilité, et les patterns avancés pour gérer des réseaux Multisite de niveau entreprise avec des performances optimales et une maintenance centralisée.

Architecture Fondamentale de Multisite

Structure de Base de Données

WordPress Multisite utilise une architecture de tables partagées et isolées.

-- Tables globales (partagées)
wp_users                    -- Tous les utilisateurs du réseau
wp_usermeta                 -- Métadonnées utilisateurs
wp_blogs                    -- Liste des sites du réseau
wp_blogmeta                 -- Métadonnées des sites
wp_site                     -- Sites du réseau (pour multi-network)
wp_sitemeta                 -- Métadonnées du réseau
wp_registration_log         -- Log des inscriptions
wp_signups                  -- Inscriptions en attente

-- Tables par site (isolées)
wp_1_posts                  -- Posts du site 1
wp_1_postmeta              -- Métadonnées posts site 1
wp_1_comments              -- Commentaires site 1
wp_1_commentmeta           -- Métadonnées commentaires site 1
wp_1_terms                 -- Termes taxonomies site 1
wp_1_term_taxonomy         -- Relations taxonomies site 1
wp_1_term_relationships    -- Relations posts-termes site 1
wp_1_termmeta              -- Métadonnées termes site 1
wp_1_options               -- Options site 1
wp_1_links                 -- Liens site 1

-- Site 2, 3, etc. suivent le même pattern avec préfixe wp_2*, wp_3*...

Service de Gestion des Sites

wpdb = $wpdb;
    }

    /**
     * Crée un nouveau site dans le réseau
     *
     * @param array $config Configuration du site
     * @return Site
     * @throws SiteCreationException
     */
    public function createSite(array $config): Site {
        $defaults = [
            'domain' => '',
            'path' => '/',
            'title' => '',
            'user_id' => get_current_user_id(),
            'public' => 1,
            'meta' => [],
        ];

        $config = array_merge($defaults, $config);

        // Validation
        $this->validateSiteConfig($config);

        // Vérifier que le domaine/chemin n'existe pas déjà
        if ($this->siteExists($config['domain'], $config['path'])) {
            throw new SiteCreationException(
                "Site already exists: {$config['domain']}{$config['path']}"
            );
        }

        // Créer le site
        $siteId = wpmu_create_blog(
            $config['domain'],
            $config['path'],
            $config['title'],
            $config['user_id'],
            $config['meta'],
            get_current_network_id()
        );

        if (is_wp_error($siteId)) {
            throw new SiteCreationException(
                "Failed to create site: {$siteId->get_error_message()}"
            );
        }

        // Appliquer la configuration post-création
        $this->configureSite($siteId, $config);

        return $this->getSiteById($siteId);
    }

    /**
     * Récupère un site par son ID
     *
     * @param int $siteId
     * @return Site
     * @throws SiteNotFoundException
     */
    public function getSiteById(int $siteId): Site {
        $blogDetails = get_blog_details($siteId);

        if (!$blogDetails) {
            throw new SiteNotFoundException("Site not found: {$siteId}");
        }

        return Site::fromBlogDetails($blogDetails);
    }

    /**
     * Liste tous les sites avec pagination
     *
     * @param array $args Arguments de filtrage
     * @return array
     */
    public function listSites(array $args = []): array {
        $defaults = [
            'number' => 100,
            'offset' => 0,
            'public' => null,
            'archived' => 0,
            'deleted' => 0,
            'spam' => 0,
            'orderby' => 'registered',
            'order' => 'DESC',
        ];

        $args = wp_parse_args($args, $defaults);

        $sites = get_sites($args);

        return array_map(
            fn($site) => Site::fromWpSite($site),
            $sites
        );
    }

    /**
     * Met à jour un site
     *
     * @param int $siteId
     * @param array $data Données à mettre à jour
     * @return Site
     */
    public function updateSite(int $siteId, array $data): Site {
        // Basculer vers le site
        switch_to_blog($siteId);

        // Mettre à jour les options
        if (isset($data['blogname'])) {
            update_option('blogname', $data['blogname']);
        }

        if (isset($data['blogdescription'])) {
            update_option('blogdescription', $data['blogdescription']);
        }

        if (isset($data['siteurl'])) {
            update_option('siteurl', $data['siteurl']);
        }

        if (isset($data['home'])) {
            update_option('home', $data['home']);
        }

        // Mettre à jour les métadonnées du site
        if (isset($data['meta'])) {
            foreach ($data['meta'] as $key => $value) {
                update_blog_option($siteId, $key, $value);
            }
        }

        // Restaurer le contexte
        restore_current_blog();

        // Mettre à jour la table wp_blogs si nécessaire
        if (isset($data['public']) || isset($data['archived']) ||
            isset($data['deleted']) || isset($data['spam'])) {

            $updateData = array_intersect_key($data, array_flip([
                'public', 'archived', 'deleted', 'spam', 'mature', 'lang_id'
            ]));

            if (!empty($updateData)) {
                $this->wpdb->update(
                    $this->wpdb->blogs,
                    $updateData,
                    ['blog_id' => $siteId],
                    array_fill(0, count($updateData), '%d'),
                    ['%d']
                );
            }
        }

        return $this->getSiteById($siteId);
    }

    /**
     * Supprime un site
     *
     * @param int $siteId
     * @param bool $drop Supprimer les tables (true) ou marquer comme deleted (false)
     * @return bool
     */
    public function deleteSite(int $siteId, bool $drop = false): bool {
        if ($siteId === 1) {
            throw new InvalidArgumentException('Cannot delete main site');
        }

        if ($drop) {
            wpmu_delete_blog($siteId, true);
        } else {
            update_blog_status($siteId, 'deleted', 1);
        }

        return true;
    }

    /**
     * Clone un site existant
     *
     * @param int $sourceSiteId Site source
     * @param array $targetConfig Configuration du nouveau site
     * @param array $options Options de clonage
     * @return Site
     */
    public function cloneSite(
        int $sourceSiteId,
        array $targetConfig,
        array $options = []
    ): Site {
        $defaults = [
            'clone_content' => true,
            'clone_media' => true,
            'clone_plugins' => true,
            'clone_theme_settings' => true,
            'clone_users' => false,
        ];

        $options = array_merge($defaults, $options);

        // Créer le nouveau site
        $newSite = $this->createSite($targetConfig);

        if ($options['clone_content']) {
            $this->cloneContent($sourceSiteId, $newSite->getId());
        }

        if ($options['clone_media']) {
            $this->cloneMedia($sourceSiteId, $newSite->getId());
        }

        if ($options['clone_plugins']) {
            $this->clonePluginSettings($sourceSiteId, $newSite->getId());
        }

        if ($options['clone_theme_settings']) {
            $this->cloneThemeSettings($sourceSiteId, $newSite->getId());
        }

        if ($options['clone_users']) {
            $this->cloneUsers($sourceSiteId, $newSite->getId());
        }

        return $newSite;
    }

    /**
     * Valide la configuration d'un site
     *
     * @throws SiteCreationException
     */
    private function validateSiteConfig(array $config): void {
        if (empty($config['domain'])) {
            throw new SiteCreationException('Domain is required');
        }

        if (empty($config['title'])) {
            throw new SiteCreationException('Title is required');
        }

        // Valider le domaine
        if (!preg_match('/^[a-z0-9]+([-.]{1}[a-z0-9]+)*.[a-z]{2,}$/i', $config['domain'])) {
            throw new SiteCreationException('Invalid domain format');
        }

        // Valider le chemin
        if (!preg_match('/^/[a-z0-9-/]*/?$/i', $config['path'])) {
            throw new SiteCreationException('Invalid path format');
        }
    }

    /**
     * Vérifie si un site existe
     */
    private function siteExists(string $domain, string $path): bool {
        return (bool) domain_exists($domain, $path);
    }

    /**
     * Configure un site après création
     */
    private function configureSite(int $siteId, array $config): void {
        switch_to_blog($siteId);

        // Activer le thème par défaut du réseau
        if (isset($config['theme'])) {
            switch_theme($config['theme']);
        }

        // Activer les plugins réseau
        if (isset($config['plugins'])) {
            foreach ($config['plugins'] as $plugin) {
                activate_plugin($plugin);
            }
        }

        // Configuration des permaliens
        if (isset($config['permalink_structure'])) {
            update_option('permalink_structure', $config['permalink_structure']);
        }

        // Configuration timezone
        if (isset($config['timezone'])) {
            update_option('timezone_string', $config['timezone']);
        }

        restore_current_blog();
    }

    /**
     * Clone le contenu d'un site
     */
    private function cloneContent(int $sourceId, int $targetId): void {
        global $wpdb;

        // Tables à cloner
        $tables = [
            'posts', 'postmeta', 'comments', 'commentmeta',
            'terms', 'term_taxonomy', 'term_relationships', 'termmeta'
        ];

        foreach ($tables as $table) {
            $sourceTable = $wpdb->get_blog_prefix($sourceId) . $table;
            $targetTable = $wpdb->get_blog_prefix($targetId) . $table;

            // Vider la table cible
            $wpdb->query("TRUNCATE TABLE {$targetTable}");

            // Copier les données
            $wpdb->query("INSERT INTO {$targetTable} SELECT * FROM {$sourceTable}");
        }
    }

    /**
     * Clone les médias d'un site
     */
    private function cloneMedia(int $sourceId, int $targetId): void {
        $sourceUploadDir = wp_upload_dir();
        switch_to_blog($sourceId);
        $sourceUploadDir = wp_upload_dir();
        restore_current_blog();

        switch_to_blog($targetId);
        $targetUploadDir = wp_upload_dir();
        restore_current_blog();

        // Copier récursivement le dossier uploads
        $this->recursiveCopy(
            $sourceUploadDir['basedir'],
            $targetUploadDir['basedir']
        );
    }

    /**
     * Clone les paramètres de plugins
     */
    private function clonePluginSettings(int $sourceId, int $targetId): void {
        switch_to_blog($sourceId);
        $activePlugins = get_option('active_plugins', []);
        $pluginOptions = [];

        // Récupérer toutes les options de plugins
        global $wpdb;
        $results = $wpdb->get_results(
            "SELECT option_name, option_value FROM {$wpdb->options}
             WHERE option_name NOT LIKE '*%'
             AND option_name NOT IN ('siteurl', 'home', 'blogname')",
            ARRAY_A
        );

        foreach ($results as $row) {
            $pluginOptions[$row['option_name']] = maybe_unserialize($row['option_value']);
        }

        restore_current_blog();

        // Appliquer au site cible
        switch_to_blog($targetId);

        foreach ($pluginOptions as $key => $value) {
            update_option($key, $value);
        }

        update_option('active_plugins', $activePlugins);

        restore_current_blog();
    }

    /**
     * Clone les paramètres du thème
     */
    private function cloneThemeSettings(int $sourceId, int $targetId): void {
        switch_to_blog($sourceId);

        $theme = get_option('stylesheet');
        $themeMods = get_option('theme_mods*' . $theme);
        $widgetSettings = [];

        // Récupérer les widgets
        $sidebars = wp_get_sidebars_widgets();

        restore_current_blog();

        switch_to_blog($targetId);

        // Appliquer le thème
        switch_theme($theme);

        // Appliquer les mods
        if ($themeMods) {
            update_option('theme_mods*' . $theme, $themeMods);
        }

        // Appliquer les widgets
        if ($sidebars) {
            wp_set_sidebars_widgets($sidebars);
        }

        restore_current_blog();
    }

    /**
     * Clone les utilisateurs
     */
    private function cloneUsers(int $sourceId, int $targetId): void {
        $sourceUsers = get_users([
            'blog_id' => $sourceId,
            'fields' => 'all_with_meta',
        ]);

        foreach ($sourceUsers as $user) {
            // Récupérer le rôle sur le site source
            switch_to_blog($sourceId);
            $userRoles = $user->roles;
            restore_current_blog();

            // Ajouter au site cible
            if (!empty($userRoles)) {
                add_user_to_blog($targetId, $user->ID, $userRoles[0]);
            }
        }
    }

    /**
     * Copie récursive de répertoire
     */
    private function recursiveCopy(string $source, string $dest): void {
        if (!file_exists($dest)) {
            mkdir($dest, 0755, true);
        }

        $dir = opendir($source);

        while (($file = readdir($dir)) !== false) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            $srcPath = $source . '/' . $file;
            $destPath = $dest . '/' . $file;

            if (is_dir($srcPath)) {
                $this->recursiveCopy($srcPath, $destPath);
            } else {
                copy($srcPath, $destPath);
            }
        }

        closedir($dir);
    }
}

/**
 * Value Object représentant un site
 */
class Site {
    private int $id;
    private string $domain;
    private string $path;
    private string $url;
    private string $name;
    private bool $public;
    private bool $archived;
    private bool $deleted;
    private bool $spam;

    public function **construct(
        int $id,
        string $domain,
        string $path,
        string $url,
        string $name,
        bool $public = true,
        bool $archived = false,
        bool $deleted = false,
        bool $spam = false
    ) {
        $this->id = $id;
        $this->domain = $domain;
        $this->path = $path;
        $this->url = $url;
        $this->name = $name;
        $this->public = $public;
        $this->archived = $archived;
        $this->deleted = $deleted;
        $this->spam = $spam;
    }

    public static function fromBlogDetails($blogDetails): self {
        return new self(
            (int) $blogDetails->blog_id,
            $blogDetails->domain,
            $blogDetails->path,
            get_site_url($blogDetails->blog_id),
            $blogDetails->blogname,
            (bool) $blogDetails->public,
            (bool) $blogDetails->archived,
            (bool) $blogDetails->deleted,
            (bool) $blogDetails->spam
        );
    }

    public static function fromWpSite(WP_Site $site): self {
        return new self(
            (int) $site->blog_id,
            $site->domain,
            $site->path,
            get_site_url($site->blog_id),
            get_blog_option($site->blog_id, 'blogname', ''),
            (bool) $site->public,
            (bool) $site->archived,
            (bool) $site->deleted,
            (bool) $site->spam
        );
    }

    public function getId(): int { return $this->id; }
    public function getDomain(): string { return $this->domain; }
    public function getPath(): string { return $this->path; }
    public function getUrl(): string { return $this->url; }
    public function getName(): string { return $this->name; }
    public function isPublic(): bool { return $this->public; }
    public function isArchived(): bool { return $this->archived; }
    public function isDeleted(): bool { return $this->deleted; }
    public function isSpam(): bool { return $this->spam; }
}

Scalabilité et Performance

Cache Distribué pour Multisite

cache = $cache;
        $this->currentBlogId = get_current_blog_id();
    }

    /**
     * Récupère une valeur du cache avec isolation par site
     */
    public function get(string $key, ?int $blogId = null): mixed {
        $blogId = $blogId ?? $this->currentBlogId;
        $cacheKey = $this->buildKey($key, $blogId);

        return $this->cache->get($cacheKey);
    }

    /**
     * Stocke une valeur dans le cache avec isolation par site
     */
    public function set(string $key, mixed $value, int $ttl = 3600, ?int $blogId = null): bool {
        $blogId = $blogId ?? $this->currentBlogId;
        $cacheKey = $this->buildKey($key, $blogId);

        return $this->cache->set($cacheKey, $value, $ttl);
    }

    /**
     * Supprime une clé du cache
     */
    public function delete(string $key, ?int $blogId = null): bool {
        $blogId = $blogId ?? $this->currentBlogId;
        $cacheKey = $this->buildKey($key, $blogId);

        return $this->cache->delete($cacheKey);
    }

    /**
     * Vide tout le cache d'un site
     */
    public function flushSite(int $blogId): bool {
        // Pattern pour identifier toutes les clés du site
        $pattern = "site:{$blogId}:*";

        // Nécessite une implémentation de cache supportant les patterns (Redis)
        if (method_exists($this->cache, 'deletePattern')) {
            return $this->cache->deletePattern($pattern);
        }

        return false;
    }

    /**
     * Vide le cache de tous les sites
     */
    public function flushAll(): bool {
        return $this->cache->flush();
    }

    /**
     * Récupère ou génère une valeur cachée
     */
    public function remember(
        string $key,
        callable $callback,
        int $ttl = 3600,
        ?int $blogId = null
    ): mixed {
        $value = $this->get($key, $blogId);

        if ($value !== false) {
            return $value;
        }

        $value = $callback();
        $this->set($key, $value, $ttl, $blogId);

        return $value;
    }

    /**
     * Cache de requêtes globales (partagées entre sites)
     */
    public function getGlobal(string $key): mixed {
        $cacheKey = "global:{$key}";
        return $this->cache->get($cacheKey);
    }

    /**
     * Stockage de cache global
     */
    public function setGlobal(string $key, mixed $value, int $ttl = 3600): bool {
        $cacheKey = "global:{$key}";
        return $this->cache->set($cacheKey, $value, $ttl);
    }

    /**
     * Construit une clé de cache avec isolation par site
     */
    private function buildKey(string $key, int $blogId): string {
        return "site:{$blogId}:{$key}";
    }
}

/**
 * Cache pour Object Cache Redis avec Multisite
 */
class RedisMultisiteCache implements CacheInterface {
    private Redis $redis;
    private string $prefix;

    public function **construct(Redis $redis, string $prefix = 'wp:') {
        $this->redis = $redis;
        $this->prefix = $prefix;
    }

    public function get(string $key): mixed {
        $value = $this->redis->get($this->prefix . $key);

        if ($value === false) {
            return false;
        }

        return unserialize($value);
    }

    public function set(string $key, mixed $value, int $ttl = 3600): bool {
        return $this->redis->setex(
            $this->prefix . $key,
            $ttl,
            serialize($value)
        );
    }

    public function delete(string $key): bool {
        return (bool) $this->redis->del($this->prefix . $key);
    }

    public function flush(): bool {
        $keys = $this->redis->keys($this->prefix . '*');

        if (empty($keys)) {
            return true;
        }

        return (bool) $this->redis->del($keys);
    }

    /**
     * Supprime les clés correspondant à un pattern
     */
    public function deletePattern(string $pattern): bool {
        $keys = $this->redis->keys($this->prefix . $pattern);

        if (empty($keys)) {
            return true;
        }

        return (bool) $this->redis->del($keys);
    }
}

/**
 * Invalidation automatique du cache
 */
class CacheInvalidation {
    private MultisiteCacheManager $cache;

    public function **construct(MultisiteCacheManager $cache) {
        $this->cache = $cache;
        $this->registerHooks();
    }

    private function registerHooks(): void {
        // Invalider le cache à la modification de post
        add_action('save_post', [$this, 'invalidatePostCache'], 10, 2);

        // Invalider à la modification de terme
        add_action('edited_term', [$this, 'invalidateTermCache'], 10, 3);

        // Invalider à la modification d'option
        add_action('updated_option', [$this, 'invalidateOptionCache'], 10, 3);

        // Invalider tout le cache du site lors d'un changement de thème
        add_action('switch_theme', [$this, 'invalidateSiteCache']);
    }

    public function invalidatePostCache(int $postId, WP_Post $post): void {
        $blogId = get_current_blog_id();

        // Invalider le cache du post
        $this->cache->delete("post:{$postId}", $blogId);

        // Invalider les archives si publié
        if ($post->post_status === 'publish') {
            $this->cache->delete('posts:archive', $blogId);
            $this->cache->delete("posts:type:{$post->post_type}", $blogId);
        }

        // Invalider le cache des taxonomies associées
        $taxonomies = get_object_taxonomies($post->post_type);
        foreach ($taxonomies as $taxonomy) {
            $terms = wp_get_post_terms($postId, $taxonomy, ['fields' => 'ids']);
            foreach ($terms as $termId) {
                $this->cache->delete("term:{$termId}:posts", $blogId);
            }
        }
    }

    public function invalidateTermCache(int $termId, int $ttId, string $taxonomy): void {
        $blogId = get_current_blog_id();

        $this->cache->delete("term:{$termId}", $blogId);
        $this->cache->delete("term:{$termId}:posts", $blogId);
        $this->cache->delete("taxonomy:{$taxonomy}", $blogId);
    }

    public function invalidateOptionCache(string $option, mixed $oldValue, mixed $value): void {
        $blogId = get_current_blog_id();

        $this->cache->delete("option:{$option}", $blogId);

        // Invalider tout si option critique
        $criticalOptions = ['blogname', 'siteurl', 'home', 'permalink_structure'];
        if (in_array($option, $criticalOptions)) {
            $this->cache->flushSite($blogId);
        }
    }

    public function invalidateSiteCache(): void {
        $this->cache->flushSite(get_current_blog_id());
    }
}

Optimisation des Requêtes Cross-Site

 'DESC'];
    private array $metaQuery = [];
    private array $taxQuery = [];

    public function **construct(wpdb $wpdb) {
        $this->wpdb = $wpdb;
    }

    /**
     * Définit les sites à interroger
     */
    public function sites(array $siteIds): self {
        $this->siteIds = $siteIds;
        return $this;
    }

    /**
     * Définit le type de post
     */
    public function postType(string $postType): self {
        $this->postType = $postType;
        return $this;
    }

    /**
     * Définit le statut
     */
    public function status(string $status): self {
        $this->postStatus = $status;
        return $this;
    }

    /**
     * Définit la limite
     */
    public function limit(int $limit): self {
        $this->limit = $limit;
        return $this;
    }

    /**
     * Définit l'offset
     */
    public function offset(int $offset): self {
        $this->offset = $offset;
        return $this;
    }

    /**
     * Définit le tri
     */
    public function orderBy(string $field, string $order = 'DESC'): self {
        $this->orderBy = [$field => $order];
        return $this;
    }

    /**
     * Ajoute une condition sur les métadonnées
     */
    public function whereMeta(string $key, mixed $value, string $compare = '='): self {
        $this->metaQuery[] = [
            'key' => $key,
            'value' => $value,
            'compare' => $compare,
        ];
        return $this;
    }

    /**
     * Exécute la requête multi-sites
     */
    public function get(): array {
        if (empty($this->siteIds)) {
            $this->siteIds = $this->getAllSiteIds();
        }

        $results = [];

        foreach ($this->siteIds as $siteId) {
            $siteResults = $this->getFromSite($siteId);
            $results = array_merge($results, $siteResults);
        }

        // Tri global
        $results = $this->sortResults($results);

        // Pagination globale
        $results = array_slice($results, $this->offset, $this->limit);

        return $results;
    }

    /**
     * Exécute une requête optimisée avec UNION
     */
    public function getOptimized(): array {
        if (empty($this->siteIds)) {
            $this->siteIds = $this->getAllSiteIds();
        }

        $queries = [];
        $values = [];

        foreach ($this->siteIds as $siteId) {
            $prefix = $this->wpdb->get_blog_prefix($siteId);

            $query = "
                SELECT
                    {$siteId} as blog_id,
                    p.ID,
                    p.post_title,
                    p.post_content,
                    p.post_excerpt,
                    p.post_date,
                    p.post_author
                FROM {$prefix}posts p
                WHERE p.post_type = %s
                AND p.post_status = %s
            ";

            $values[] = $this->postType;
            $values[] = $this->postStatus;

            // Ajouter les conditions meta
            if (!empty($this->metaQuery)) {
                foreach ($this->metaQuery as $index => $meta) {
                    $alias = "pm{$index}";
                    $query .= " INNER JOIN {$prefix}postmeta {$alias}
                                ON p.ID = {$alias}.post_id
                                AND {$alias}.meta_key = %s
                                AND {$alias}.meta_value {$meta['compare']} %s";

                    $values[] = $meta['key'];
                    $values[] = $meta['value'];
                }
            }

            $queries[] = $query;
        }

        // Combiner avec UNION ALL
        $unionQuery = implode(' UNION ALL ', $queries);

        // Ajouter le tri et la limite
        [$orderField, $orderDirection] = each($this->orderBy);
        $unionQuery .= " ORDER BY {$orderField} {$orderDirection}";
        $unionQuery .= " LIMIT %d OFFSET %d";

        $values[] = $this->limit;
        $values[] = $this->offset;

        // Préparer et exécuter
        $preparedQuery = $this->wpdb->prepare($unionQuery, ...$values);
        $results = $this->wpdb->get_results($preparedQuery);

        return $this->hydrateResults($results);
    }

    /**
     * Compte le total de résultats
     */
    public function count(): int {
        if (empty($this->siteIds)) {
            $this->siteIds = $this->getAllSiteIds();
        }

        $total = 0;

        foreach ($this->siteIds as $siteId) {
            switch_to_blog($siteId);

            $args = [
                'post_type' => $this->postType,
                'post_status' => $this->postStatus,
                'posts_per_page' => -1,
                'fields' => 'ids',
            ];

            if (!empty($this->metaQuery)) {
                $args['meta_query'] = $this->metaQuery;
            }

            $query = new WP_Query($args);
            $total += $query->found_posts;

            restore_current_blog();
        }

        return $total;
    }

    /**
     * Récupère les posts d'un site spécifique
     */
    private function getFromSite(int $siteId): array {
        switch_to_blog($siteId);

        $args = [
            'post_type' => $this->postType,
            'post_status' => $this->postStatus,
            'posts_per_page' => $this->limit * 2, // Buffer pour tri global
            'offset' => 0,
        ];

        if (!empty($this->metaQuery)) {
            $args['meta_query'] = $this->metaQuery;
        }

        if (!empty($this->orderBy)) {
            $args['orderby'] = array_keys($this->orderBy)[0];
            $args['order'] = array_values($this->orderBy)[0];
        }

        $query = new WP_Query($args);
        $posts = $query->posts;

        // Ajouter le blog_id à chaque post
        foreach ($posts as $post) {
            $post->blog_id = $siteId;
            $post->site_url = get_site_url($siteId);
        }

        restore_current_blog();

        return $posts;
    }

    /**
     * Trie les résultats combinés
     */
    private function sortResults(array $results): array {
        $field = array_keys($this->orderBy)[0];
        $order = array_values($this->orderBy)[0];

        usort($results, function($a, $b) use ($field, $order) {
            $comparison = $a->$field <=> $b->$field;
            return $order === 'DESC' ? -$comparison : $comparison;
        });

        return $results;
    }

    /**
     * Hydrate les résultats en objets WP_Post
     */
    private function hydrateResults(array $results): array {
        $hydrated = [];

        foreach ($results as $result) {
            $post = new WP_Post((object) $result);
            $post->blog_id = $result->blog_id;
            $hydrated[] = $post;
        }

        return $hydrated;
    }

    /**
     * Récupère tous les IDs de sites
     */
    private function getAllSiteIds(): array {
        return get_sites([
            'fields' => 'ids',
            'number' => 10000,
        ]);
    }
}

// Utilisation
$crossSiteQuery = new CrossSiteQueryBuilder($wpdb);

$posts = $crossSiteQuery
    ->sites([1, 2, 3, 4])
    ->postType('post')
    ->status('publish')
    ->whereMeta('featured', '1')
    ->orderBy('post_date', 'DESC')
    ->limit(20)
    ->getOptimized();

foreach ($posts as $post) {
    echo "Site {$post->blog_id}: {$post->post_title}n";
}

Gestion Centralisée des Plugins et Thèmes

 !is_wp_error($result),
                'error' => is_wp_error($result) ? $result->get_error_message() : null,
            ];

            restore_current_blog();
        }

        return $results;
    }

    /**
     * Désactive un plugin sur plusieurs sites
     */
    public function deactivateOnSites(string $plugin, array $siteIds): array {
        $results = [];

        foreach ($siteIds as $siteId) {
            switch_to_blog($siteId);

            deactivate_plugins($plugin);

            $results[$siteId] = ['success' => true];

            restore_current_blog();
        }

        return $results;
    }

    /**
     * Récupère les plugins actifs sur chaque site
     */
    public function getActivePluginsBySite(array $siteIds = []): array {
        if (empty($siteIds)) {
            $siteIds = get_sites(['fields' => 'ids']);
        }

        $report = [];

        foreach ($siteIds as $siteId) {
            switch_to_blog($siteId);

            $report[$siteId] = [
                'site_name' => get_bloginfo('name'),
                'active_plugins' => get_option('active_plugins', []),
            ];

            restore_current_blog();
        }

        return $report;
    }

    /**
     * Synchronise la configuration d'un plugin sur tous les sites
     */
    public function syncPluginSettings(
        string $optionName,
        mixed $value,
        array $siteIds = []
    ): int {
        if (empty($siteIds)) {
            $siteIds = get_sites(['fields' => 'ids']);
        }

        $updated = 0;

        foreach ($siteIds as $siteId) {
            switch_to_blog($siteId);

            if (update_option($optionName, $value)) {
                $updated++;
            }

            restore_current_blog();
        }

        return $updated;
    }

    /**
     * Audit de sécurité des plugins
     */
    public function auditPlugins(): array {
        $allSites = get_sites(['fields' => 'ids']);
        $pluginVersions = [];
        $outdatedSites = [];

        // Récupérer les versions de tous les sites
        foreach ($allSites as $siteId) {
            switch_to_blog($siteId);

            $activePlugins = get_option('active_plugins', []);

            foreach ($activePlugins as $plugin) {
                $pluginData = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin);

                if (!isset($pluginVersions[$plugin])) {
                    $pluginVersions[$plugin] = [];
                }

                $version = $pluginData['Version'];

                if (!isset($pluginVersions[$plugin][$version])) {
                    $pluginVersions[$plugin][$version] = [];
                }

                $pluginVersions[$plugin][$version][] = $siteId;
            }

            restore_current_blog();
        }

        // Identifier les sites avec versions obsolètes
        foreach ($pluginVersions as $plugin => $versions) {
            if (count($versions) > 1) {
                $latestVersion = max(array_keys($versions));

                foreach ($versions as $version => $sites) {
                    if (version_compare($version, $latestVersion, '<')) {
                        $outdatedSites[$plugin][$version] = $sites;
                    }
                }
            }
        }

        return [
            'plugin_versions' => $pluginVersions,
            'outdated_sites' => $outdatedSites,
        ];
    }
}

Monitoring et Analytics Multisite

wpdb = $wpdb;
    }

    /**
     * Statistiques globales du réseau
     */
    public function getNetworkStats(): array {
        $sites = get_sites(['number' => 10000]);

        $stats = [
            'total_sites' => count($sites),
            'public_sites' => 0,
            'archived_sites' => 0,
            'deleted_sites' => 0,
            'spam_sites' => 0,
            'total_posts' => 0,
            'total_pages' => 0,
            'total_users' => 0,
            'total_comments' => 0,
            'storage_usage' => 0,
        ];

        foreach ($sites as $site) {
            if ($site->public) $stats['public_sites']++;
            if ($site->archived) $stats['archived_sites']++;
            if ($site->deleted) $stats['deleted_sites']++;
            if ($site->spam) $stats['spam_sites']++;

            switch_to_blog($site->blog_id);

            $stats['total_posts'] += wp_count_posts('post')->publish;
            $stats['total_pages'] += wp_count_posts('page')->publish;
            $stats['total_comments'] += wp_count_comments()->approved;

            // Calcul du stockage
            $uploadDir = wp_upload_dir();
            if (file_exists($uploadDir['basedir'])) {
                $stats['storage_usage'] += $this->getDirectorySize($uploadDir['basedir']);
            }

            restore_current_blog();
        }

        // Utilisateurs uniques
        $stats['total_users'] = count(get_users(['fields' => 'ID']));

        // Convertir le stockage en MB
        $stats['storage_usage'] = round($stats['storage_usage'] / 1024 / 1024, 2);

        return $stats;
    }

    /**
     * Top sites par activité
     */
    public function getTopSitesByActivity(int $limit = 10): array {
        $sites = get_sites(['number' => 10000]);
        $activity = [];

        foreach ($sites as $site) {
            switch_to_blog($site->blog_id);

            // Calculer un score d'activité
            $posts = wp_count_posts('post')->publish;
            $comments = wp_count_comments()->approved;
            $recentPosts = $this->getRecentPostsCount($site->blog_id, 30);

            $score = ($posts * 1) + ($comments * 0.5) + ($recentPosts * 5);

            $activity[] = [
                'site_id' => $site->blog_id,
                'site_name' => get_bloginfo('name'),
                'site_url' => get_site_url(),
                'posts' => $posts,
                'comments' => $comments,
                'recent_posts' => $recentPosts,
                'activity_score' => $score,
            ];

            restore_current_blog();
        }

        // Trier par score
        usort($activity, fn($a, $b) => $b['activity_score'] <=> $a['activity_score']);

        return array_slice($activity, 0, $limit);
    }

    /**
     * Sites inactifs
     */
    public function getInactiveSites(int $days = 90): array {
        $sites = get_sites(['number' => 10000]);
        $inactive = [];
        $threshold = strtotime("-{$days} days");

        foreach ($sites as $site) {
            $prefix = $this->wpdb->get_blog_prefix($site->blog_id);

            $lastPost = $this->wpdb->get_var(
                "SELECT post_date FROM {$prefix}posts
                 WHERE post_type = 'post'
                 AND post_status = 'publish'
                 ORDER BY post_date DESC
                 LIMIT 1"
            );

            if (!$lastPost || strtotime($lastPost) < $threshold) {
                $inactive[] = [
                    'site_id' => $site->blog_id,
                    'site_name' => get_blog_option($site->blog_id, 'blogname'),
                    'site_url' => get_site_url($site->blog_id),
                    'last_post_date' => $lastPost,
                    'days_inactive' => $lastPost ?
                        floor((time() - strtotime($lastPost)) / 86400) :
                        null,
                ];
            }
        }

        return $inactive;
    }

    /**
     * Rapport d'utilisation du stockage
     */
    public function getStorageReport(): array {
        $sites = get_sites(['number' => 10000]);
        $report = [];

        foreach ($sites as $site) {
            switch_to_blog($site->blog_id);

            $uploadDir = wp_upload_dir();
            $size = 0;

            if (file_exists($uploadDir['basedir'])) {
                $size = $this->getDirectorySize($uploadDir['basedir']);
            }

            $report[] = [
                'site_id' => $site->blog_id,
                'site_name' => get_bloginfo('name'),
                'storage_mb' => round($size / 1024 / 1024, 2),
            ];

            restore_current_blog();
        }

        // Trier par utilisation
        usort($report, fn($a, $b) => $b['storage_mb'] <=> $a['storage_mb']);

        return $report;
    }

    private function getRecentPostsCount(int $siteId, int $days): int {
        $prefix = $this->wpdb->get_blog_prefix($siteId);

        return (int) $this->wpdb->get_var($this->wpdb->prepare(
            "SELECT COUNT(*) FROM {$prefix}posts
             WHERE post_type = 'post'
             AND post_status = 'publish'
             AND post_date > DATE_SUB(NOW(), INTERVAL %d DAY)",
            $days
        ));
    }

    private function getDirectorySize(string $path): int {
        $size = 0;
        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
        );

        foreach ($files as $file) {
            if ($file->isFile()) {
                $size += $file->getSize();
            }
        }

        return $size;
    }
}

Conclusion

WordPress Multisite en 2025 offre une architecture robuste pour gérer des réseaux de sites à grande échelle. Les pratiques essentielles incluent :

Une remarque, un retour ?

Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.

Laisser un commentaire