Avance 1 min de lecture · 185 mots

WordPress GraphQL : API Moderne avec WPGraphQL

Introduction

GraphQL révolutionne la façon dont les applications consomment les données WordPress. Contrairement aux APIs REST traditionnelles, GraphQL permet aux clients de demander exactement les données dont ils ont besoin, éliminant le over-fetching et under-fetching. Ce guide présente une implémentation production-ready de WPGraphQL.

Architecture GraphQL avec WordPress

Vue d’Ensemble du Système

┌──────────────────────────────────────────────────────────────┐
│                    Client Applications                        │
├──────────────────┬──────────────────┬──────────────────────┤
│  React/Next.js   │  Vue.js/Nuxt     │  Mobile Apps         │
│  Apollo Client   │  Vue Apollo      │  Apollo iOS/Android  │
└──────────────────┴──────────────────┴──────────────────────┘
                            │
                            │ GraphQL Query
                            │
┌───────────────────────────▼──────────────────────────────────┐
│                    CDN / Edge Cache                           │
│  Automatic Persisted Queries (APQ)                           │
│  Query Response Caching                                       │
└───────────────────────────┬──────────────────────────────────┘
                            │
┌───────────────────────────▼──────────────────────────────────┐
│                    GraphQL Gateway                            │
│  - Query Complexity Analysis                                  │
│  - Rate Limiting per Query                                    │
│  - Query Batching                                             │
│  - Authentication & Authorization                             │
└───────────────────────────┬──────────────────────────────────┘
                            │
┌───────────────────────────▼──────────────────────────────────┐
│                 WPGraphQL Server (WordPress)                  │
│  /graphql endpoint                                            │
├───────────────────────────┬──────────────────────────────────┤
│   Query Resolution Layer  │   Schema Definition              │
│   - Field Resolvers       │   - Types                        │
│   - DataLoaders           │   - Connections                  │
│   - Lazy Loading          │   - Interfaces                   │
└───────────────────────────┴──────────────────────────────────┘
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
┌───────▼──────┐   ┌───────▼──────┐   ┌───────▼──────┐
│ Redis Cache  │   │  WordPress   │   │ External     │
│ Query Cache  │   │  Database    │   │ APIs         │
│ DataLoader   │   │  (MySQL)     │   │ REST/Graph   │
└──────────────┘   └──────────────┘   └──────────────┘

Installation et Configuration WPGraphQL

Installation des Plugins

# Via WP-CLI
wp plugin install wp-graphql --activate
wp plugin install wp-graphql-acf --activate  # Pour ACF
wp plugin install wp-graphiql --activate     # IDE GraphQL

# Ou via Composer
composer require wp-graphql/wp-graphql
composer require wp-graphql/wp-graphql-acf

Configuration Avancée WPGraphQL

 'ISO 8601 datetime string',
        'serialize' => function( $value ) {
            return gmdate( 'c', strtotime( $value ) );
        },
        'parseValue' => function( $value ) {
            return $value;
        },
        'parseLiteral' => function( $ast ) {
            return $ast->value;
        },
    ] );
} );

/**
 * Query complexity limits
 */
add_filter( 'graphql_query_amount_requested', function( $amount, $source, $args, $context, $info ) {
    // Limit query depth
    $max_depth = 10;
    $depth = $info->path ? count( $info->path ) : 0;

    if ( $depth > $max_depth ) {
        throw new GraphQLErrorUserError(
            sprintf( 'Query exceeds maximum depth of %d', $max_depth )
        );
    }

    // Limit results per query
    $max_amount = 100;
    if ( $amount > $max_amount ) {
        return $max_amount;
    }

    return $amount;
}, 10, 5 );

/**
 * Add custom query cost calculator
 */
add_filter( 'graphql_validation_rules', function( $rules ) {
    require_once __DIR__ . '/graphql/QueryCostRule.php';

    $rules[] = new QueryCostRule( [
        'maximumCost' => 10000,
        'defaultCost' => 1,
        'defaultFieldCost' => 1,
    ] );

    return $rules;
} );

/**
 * Enable query logging for debugging
 */
if ( defined( 'GRAPHQL_DEBUG' ) && GRAPHQL_DEBUG ) {
    add_action( 'graphql_execute', function( $query, $operation_name, $variables ) {
        error_log( sprintf(
            "GraphQL Query: %snOperation: %snVariables: %s",
            $query,
            $operation_name,
            json_encode( $variables )
        ) );
    }, 10, 3 );
}

/**
 * Optimize DataLoader batching
 */
add_action( 'graphql_init', function() {
    add_filter( 'graphql_dataloader_pre_batch_load', function( $items ) {
        // Pre-warm object cache for batched items
        if ( ! empty( $items ) && is_array( $items ) ) {
            wp_prime_post_caches( $items );
        }
        return $items;
    } );
} );

Schéma GraphQL Custom

 [
            'name' => 'Portfolio',
            'singular_name' => 'Portfolio Item',
        ],
        'public' => true,
        'show_in_graphql' => true,
        'graphql_single_name' => 'PortfolioItem',
        'graphql_plural_name' => 'PortfolioItems',
    ] );

    register_taxonomy( 'portfolio_category', 'portfolio', [
        'labels' => [
            'name' => 'Portfolio Categories',
        ],
        'show_in_graphql' => true,
        'graphql_single_name' => 'PortfolioCategory',
        'graphql_plural_name' => 'PortfolioCategories',
    ] );
} );

/**
 * Add custom fields to GraphQL schema
 */
add_action( 'graphql_register_types', function() {

    // Add custom field to Post type
    register_graphql_field( 'Post', 'viewCount', [
        'type' => 'Int',
        'description' => 'Number of views for this post',
        'resolve' => function( $post ) {
            return (int) get_post_meta( $post->ID, 'view_count', true );
        },
    ] );

    // Add reading time field
    register_graphql_field( 'Post', 'readingTime', [
        'type' => 'Int',
        'description' => 'Estimated reading time in minutes',
        'resolve' => function( $post ) {
            $content = get_post_field( 'post_content', $post->ID );
            $word_count = str_word_count( strip_tags( $content ) );
            return ceil( $word_count / 200 );
        },
    ] );

    // Add custom complex type
    register_graphql_object_type( 'PostStats', [
        'description' => 'Statistics for a post',
        'fields' => [
            'views' => [
                'type' => 'Int',
                'description' => 'Total views',
            ],
            'likes' => [
                'type' => 'Int',
                'description' => 'Total likes',
            ],
            'comments' => [
                'type' => 'Int',
                'description' => 'Total comments',
            ],
            'shares' => [
                'type' => 'Int',
                'description' => 'Total shares',
            ],
        ],
    ] );

    register_graphql_field( 'Post', 'stats', [
        'type' => 'PostStats',
        'description' => 'Post statistics',
        'resolve' => function( $post ) {
            return [
                'views' => (int) get_post_meta( $post->ID, 'view_count', true ),
                'likes' => (int) get_post_meta( $post->ID, 'like_count', true ),
                'comments' => wp_count_comments( $post->ID )->approved,
                'shares' => (int) get_post_meta( $post->ID, 'share_count', true ),
            ];
        },
    ] );

    // Add custom interface
    register_graphql_interface_type( 'Likeable', [
        'description' => 'Entities that can be liked',
        'fields' => [
            'likeCount' => [
                'type' => 'Int',
            ],
            'isLiked' => [
                'type' => 'Boolean',
            ],
        ],
        'resolveType' => function( $object ) {
            if ( $object instanceof WP_Post ) {
                return 'Post';
            }
            return null;
        },
    ] );

    // Implement interface on Post type
    register_graphql_interfaces_to_types( [ 'Likeable' ], [ 'Post' ] );
} );

/**
 * Add custom queries
 */
add_action( 'graphql_register_types', function() {

    // Popular posts query
    register_graphql_field( 'RootQuery', 'popularPosts', [
        'type' => [ 'list_of' => 'Post' ],
        'description' => 'Get popular posts ordered by view count',
        'args' => [
            'first' => [
                'type' => 'Int',
                'description' => 'Number of posts to return',
                'defaultValue' => 10,
            ],
            'after' => [
                'type' => 'String',
                'description' => 'Cursor for pagination',
            ],
        ],
        'resolve' => function( $root, $args, $context, $info ) {
            $query_args = [
                'post_type' => 'post',
                'post_status' => 'publish',
                'posts_per_page' => $args['first'],
                'meta_key' => 'view_count',
                'orderby' => 'meta_value_num',
                'order' => 'DESC',
                'no_found_rows' => false,
            ];

            if ( ! empty( $args['after'] ) ) {
                $query_args['paged'] = base64_decode( $args['after'] ) + 1;
            }

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

            return ! empty( $posts ) && is_array( $posts ) ? $posts : [];
        },
    ] );

    // Related posts query
    register_graphql_field( 'Post', 'relatedPosts', [
        'type' => [ 'list_of' => 'Post' ],
        'description' => 'Get related posts based on categories and tags',
        'args' => [
            'first' => [
                'type' => 'Int',
                'defaultValue' => 5,
            ],
        ],
        'resolve' => function( $post, $args ) {
            $categories = wp_get_post_categories( $post->ID, [ 'fields' => 'ids' ] );
            $tags = wp_get_post_tags( $post->ID, [ 'fields' => 'ids' ] );

            $query_args = [
                'post_type' => 'post',
                'post_status' => 'publish',
                'posts_per_page' => $args['first'],
                'post__not_in' => [ $post->ID ],
                'tax_query' => [
                    'relation' => 'OR',
                ],
            ];

            if ( ! empty( $categories ) ) {
                $query_args['tax_query'][] = [
                    'taxonomy' => 'category',
                    'field' => 'term_id',
                    'terms' => $categories,
                ];
            }

            if ( ! empty( $tags ) ) {
                $query_args['tax_query'][] = [
                    'taxonomy' => 'post_tag',
                    'field' => 'term_id',
                    'terms' => $tags,
                ];
            }

            $query = new WP_Query( $query_args );
            return $query->posts;
        },
    ] );

    // Search query with facets
    register_graphql_field( 'RootQuery', 'searchWithFacets', [
        'type' => 'SearchResults',
        'description' => 'Search with faceted results',
        'args' => [
            'query' => [
                'type' => [ 'non_null' => 'String' ],
                'description' => 'Search query',
            ],
            'postType' => [
                'type' => [ 'list_of' => 'String' ],
                'description' => 'Post types to search',
                'defaultValue' => [ 'post', 'page' ],
            ],
            'first' => [
                'type' => 'Int',
                'defaultValue' => 20,
            ],
        ],
        'resolve' => function( $root, $args ) {
            // Implementation would integrate with Elasticsearch or similar
            return perform_faceted_search( $args );
        },
    ] );
} );

/**
 * Add custom mutations
 */
add_action( 'graphql_register_types', function() {

    // Register input type for creating post
    register_graphql_input_type( 'CreatePostInput', [
        'description' => 'Input for creating a post',
        'fields' => [
            'title' => [
                'type' => [ 'non_null' => 'String' ],
                'description' => 'Post title',
            ],
            'content' => [
                'type' => 'String',
                'description' => 'Post content',
            ],
            'status' => [
                'type' => 'PostStatusEnum',
                'description' => 'Post status',
                'defaultValue' => 'draft',
            ],
            'categories' => [
                'type' => [ 'list_of' => 'Int' ],
                'description' => 'Category IDs',
            ],
        ],
    ] );

    // Like post mutation
    register_graphql_mutation( 'likePost', [
        'inputFields' => [
            'postId' => [
                'type' => [ 'non_null' => 'ID' ],
                'description' => 'ID of the post to like',
            ],
        ],
        'outputFields' => [
            'post' => [
                'type' => 'Post',
                'description' => 'The liked post',
                'resolve' => function( $payload ) {
                    return get_post( $payload['postId'] );
                },
            ],
            'likeCount' => [
                'type' => 'Int',
                'description' => 'Updated like count',
                'resolve' => function( $payload ) {
                    return $payload['likeCount'];
                },
            ],
        ],
        'mutateAndGetPayload' => function( $input, $context ) {
            $post_id = absint( $input['postId'] );

            if ( ! $post_id ) {
                throw new GraphQLErrorUserError( 'Invalid post ID' );
            }

            // Check if user already liked
            $user_id = get_current_user_id();
            $liked_posts = get_user_meta( $user_id, 'liked_posts', true ) ?: [];

            if ( in_array( $post_id, $liked_posts ) ) {
                throw new GraphQLErrorUserError( 'Post already liked' );
            }

            // Increment like count
            $like_count = (int) get_post_meta( $post_id, 'like_count', true );
            $like_count++;
            update_post_meta( $post_id, 'like_count', $like_count );

            // Track user's like
            $liked_posts[] = $post_id;
            update_user_meta( $user_id, 'liked_posts', $liked_posts );

            return [
                'postId' => $post_id,
                'likeCount' => $like_count,
            ];
        },
    ] );

    // Update view count mutation
    register_graphql_mutation( 'incrementViewCount', [
        'inputFields' => [
            'postId' => [
                'type' => [ 'non_null' => 'ID' ],
            ],
        ],
        'outputFields' => [
            'viewCount' => [
                'type' => 'Int',
                'resolve' => function( $payload ) {
                    return $payload['viewCount'];
                },
            ],
        ],
        'mutateAndGetPayload' => function( $input ) {
            $post_id = absint( $input['postId'] );
            $view_count = (int) get_post_meta( $post_id, 'view_count', true );
            $view_count++;
            update_post_meta( $post_id, 'view_count', $view_count );

            return [ 'viewCount' => $view_count ];
        },
    ] );
} );

/**
 * Add subscriptions support (requires additional infrastructure)
 */
add_action( 'graphql_register_types', function() {

    register_graphql_field( 'RootSubscription', 'postPublished', [
        'type' => 'Post',
        'description' => 'Subscribe to new published posts',
        'args' => [
            'postType' => [
                'type' => 'String',
                'defaultValue' => 'post',
            ],
        ],
        'resolve' => function( $root, $args, $context, $info ) {
            // This would be handled by a WebSocket server
            // returning the published post
            return $root;
        },
    ] );
} );

Client Apollo (React)

Configuration Apollo Client

// lib/apollo-client.js
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

// HTTP Link
const httpLink = new HttpLink({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'https://wordpress.example.com/graphql',
  credentials: 'include',
});

// Auth Link
const authLink = new ApolloLink((operation, forward) => {
  const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;

  operation.setContext({
    headers: {
      authorization: token ? Bearer ${token} : '',
    },
  });

  return forward(operation);
});

// Error Link
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
      console.error(
        [GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path},
        extensions
      );

      // Handle authentication errors
      if (extensions?.code === 'UNAUTHENTICATED') {
        if (typeof window !== 'undefined') {
          localStorage.removeItem('authToken');
          window.location.href = '/login';
        }
      }
    });
  }

  if (networkError) {
    console.error([Network error]: ${networkError});
  }
});

// Persisted Queries Link (APQ)
const persistedQueriesLink = createPersistedQueryLink({
  sha256,
  useGETForHashedQueries: true,
});

// Cache configuration
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        posts: {
          keyArgs: ['where', 'orderby'],
          merge(existing, incoming, { args }) {
            // Handle pagination
            if (!existing) return incoming;

            const { after } = args;
            if (!after) return incoming;

            return {
              ...incoming,
              edges: [...existing.edges, ...incoming.edges],
            };
          },
        },
      },
    },
    Post: {
      fields: {
        comments: {
          merge(existing, incoming) {
            return incoming;
          },
        },
      },
    },
  },
});

// Create Apollo Client
export const apolloClient = new ApolloClient({
  link: from([errorLink, authLink, persistedQueriesLink, httpLink]),
  cache,
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
      errorPolicy: 'all',
    },
    query: {
      fetchPolicy: 'network-only',
      errorPolicy: 'all',
    },
    mutate: {
      errorPolicy: 'all',
    },
  },
  connectToDevTools: process.env.NODE_ENV === 'development',
});

GraphQL Queries et Fragments

// graphql/fragments.js
import { gql } from '@apollo/client';

export const POST_FIELDS = gql`
  fragment PostFields on Post {
    id
    postId
    title
    slug
    excerpt
    date
    modified
    status
    commentCount
    viewCount
    readingTime
    featuredImage {
      node {
        id
        sourceUrl
        altText
        mediaDetails {
          width
          height
        }
        sizes(size: LARGE)
      }
    }
    author {
      node {
        id
        name
        avatar {
          url
        }
      }
    }
    categories {
      nodes {
        id
        name
        slug
      }
    }
    tags {
      nodes {
        id
        name
        slug
      }
    }
  }
`;

export const POST_CONTENT_FIELDS = gql`
  fragment PostContentFields on Post {
    content
    stats {
      views
      likes
      comments
      shares
    }
    seo {
      title
      metaDesc
      canonical
      opengraphImage {
        sourceUrl
      }
    }
  }
`;

// graphql/queries.js
import { gql } from '@apollo/client';
import { POST_FIELDS, POST_CONTENT_FIELDS } from './fragments';

export const GET_POSTS = gql`
  ${POST_FIELDS}
  query GetPosts(
    $first: Int = 10
    $after: String
    $where: RootQueryToPostConnectionWhereArgs
  ) {
    posts(first: $first, after: $after, where: $where) {
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        cursor
        node {
          ...PostFields
        }
      }
    }
  }
`;

export const GET_POST = gql`
  ${POST_FIELDS}
  ${POST_CONTENT_FIELDS}
  query GetPost($id: ID!, $idType: PostIdType = SLUG) {
    post(id: $id, idType: $idType) {
      ...PostFields
      ...PostContentFields
      relatedPosts(first: 4) {
        ...PostFields
      }
    }
  }
`;

export const GET_POPULAR_POSTS = gql`
  ${POST_FIELDS}
  query GetPopularPosts($first: Int = 10) {
    popularPosts(first: $first) {
      ...PostFields
    }
  }
`;

export const SEARCH_POSTS = gql`
  ${POST_FIELDS}
  query SearchPosts($query: String!, $first: Int = 20) {
    posts(first: $first, where: { search: $query }) {
      edges {
        node {
          ...PostFields
        }
      }
    }
  }
`;

export const GET_MENU = gql`
  query GetMenu($location: MenuLocationEnum!) {
    menu(location: $location) {
      nodes {
        id
        label
        url
        target
        cssClasses
        childItems {
          nodes {
            id
            label
            url
            target
          }
        }
      }
    }
  }
`;

// graphql/mutations.js
export const LIKE_POST = gql`
  mutation LikePost($postId: ID!) {
    likePost(input: { postId: $postId }) {
      post {
        id
        stats {
          likes
        }
      }
      likeCount
    }
  }
`;

export const INCREMENT_VIEW_COUNT = gql`
  mutation IncrementViewCount($postId: ID!) {
    incrementViewCount(input: { postId: $postId }) {
      viewCount
    }
  }
`;

export const CREATE_COMMENT = gql`
  mutation CreateComment(
    $postId: Int!
    $content: String!
    $author: String!
    $authorEmail: String!
  ) {
    createComment(
      input: {
        commentOn: $postId
        content: $content
        author: $author
        authorEmail: $authorEmail
      }
    ) {
      comment {
        id
        content
        date
        author {
          node {
            name
          }
        }
      }
    }
  }
`;

React Component avec Apollo

// components/PostList.js
import { useQuery } from '@apollo/client';
import { GET_POSTS } from '../graphql/queries';
import PostCard from './PostCard';
import LoadingSpinner from './LoadingSpinner';
import ErrorMessage from './ErrorMessage';

export default function PostList({ categoryId = null, perPage = 10 }) {
  const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
    variables: {
      first: perPage,
      where: categoryId ? { categoryId } : undefined,
    },
    notifyOnNetworkStatusChange: true,
  });

  if (loading && !data) return ;
  if (error) return ;

  const posts = data?.posts?.edges || [];
  const pageInfo = data?.posts?.pageInfo;

  const loadMore = () => {
    if (!pageInfo?.hasNextPage) return;

    fetchMore({
      variables: {
        after: pageInfo.endCursor,
      },
    });
  };

  return (
    
{posts.map(({ node: post }) => ( ))}
{pageInfo?.hasNextPage && ( )}
); } // components/PostDetail.js import { useQuery, useMutation } from '@apollo/client'; import { GET_POST, INCREMENT_VIEW_COUNT, LIKE_POST } from '../graphql/queries'; import { useEffect, useState } from 'react'; import Image from 'next/image'; export default function PostDetail({ slug }) { const [isLiked, setIsLiked] = useState(false); const { data, loading, error } = useQuery(GET_POST, { variables: { id: slug }, }); const [incrementView] = useMutation(INCREMENT_VIEW_COUNT); const [likePost] = useMutation(LIKE_POST, { optimisticResponse: { likePost: { **typename: 'LikePostPayload', likeCount: (data?.post?.stats?.likes || 0) + 1, post: { **typename: 'Post', id: data?.post?.id, stats: { **typename: 'PostStats', likes: (data?.post?.stats?.likes || 0) + 1, }, }, }, }, }); useEffect(() => { if (data?.post?.postId) { // Increment view count on mount incrementView({ variables: { postId: data.post.postId }, }); } }, [data?.post?.postId, incrementView]); const handleLike = async () => { if (isLiked) return; try { await likePost({ variables: { postId: data.post.postId }, }); setIsLiked(true); } catch (err) { console.error('Failed to like post:', err); } }; if (loading) return ; if (error) return ; if (!data?.post) return
Post not found
; const { post } = data; return (
{/* SEO */} {post.seo.title} {/* Featured Image */} {post.featuredImage?.node && (
{post.featuredImage.node.altText
)} {/* Header */}

{post.readingTime} min de lecture {post.stats.views} vues
{post.author?.node && (
{post.author.node.name} {post.author.node.name}
)}
{/* Content */}
{/* Actions */}
{post.stats.comments} Commentaires {post.stats.shares} Partages
{/* Related Posts */} {post.relatedPosts?.length > 0 && ( )}
); }

Query Caching et Performance

Redis Query Cache

redis = new Redis();
        $this->redis->connect(
            defined( 'WP_REDIS_HOST' ) ? WP_REDIS_HOST : '127.0.0.1',
            defined( 'WP_REDIS_PORT' ) ? WP_REDIS_PORT : 6379
        );

        if ( defined( 'WP_REDIS_PASSWORD' ) ) {
            $this->redis->auth( WP_REDIS_PASSWORD );
        }

        $this->redis->select( defined( 'WP_REDIS_DATABASE' ) ? WP_REDIS_DATABASE : 0 );

        // Hook into GraphQL execution
        add_filter( 'graphql_pre_resolve_field', [ $this, 'maybe_return_cached' ], 10, 4 );
        add_filter( 'graphql_return_response', [ $this, 'maybe_cache_response' ], 10, 2 );
    }

    /**
     * Generate cache key from query
     */
    private function get_cache_key( $query, $variables = [] ) {
        // Normalize query (remove whitespace, etc)
        $normalized_query = preg_replace( '/s+/', ' ', trim( $query ) );

        // Include user ID for personalized queries
        $user_id = get_current_user_id();

        $key_data = [
            'query' => $normalized_query,
            'variables' => $variables,
            'user_id' => $user_id,
        ];

        return 'graphql:' . md5( serialize( $key_data ) );
    }

    /**
     * Check if query result is cached
     */
    public function maybe_return_cached( $nil, $source, $args, $context ) {
        // Don't cache mutations
        if ( $context->operation === 'mutation' ) {
            return $nil;
        }

        // Don't cache authenticated queries (or use user-specific keys)
        if ( ! defined( 'GRAPHQL_CACHE_AUTHENTICATED' ) && is_user_logged_in() ) {
            return $nil;
        }

        $cache_key = $this->get_cache_key(
            $context->query,
            $context->variables
        );

        $cached = $this->redis->get( $cache_key );

        if ( $cached !== false ) {
            return json_decode( $cached, true );
        }

        return $nil;
    }

    /**
     * Cache query response
     */
    public function maybe_cache_response( $response, $context ) {
        // Don't cache mutations or errors
        if ( $context->operation === 'mutation' || ! empty( $response['errors'] ) ) {
            return $response;
        }

        $cache_key = $this->get_cache_key(
            $context->query,
            $context->variables
        );

        // Determine TTL based on query
        $ttl = $this->get_query_ttl( $context->query );

        $this->redis->setex(
            $cache_key,
            $ttl,
            json_encode( $response )
        );

        return $response;
    }

    /**
     * Determine TTL based on query type
     */
    private function get_query_ttl( $query ) {
        // Static content: 1 day
        if ( strpos( $query, 'menu' ) !== false ) {
            return DAY_IN_SECONDS;
        }

        // Posts: 1 hour
        if ( strpos( $query, 'posts' ) !== false ) {
            return HOUR_IN_SECONDS;
        }

        // Individual post: 6 hours
        if ( strpos( $query, 'post(' ) !== false ) {
            return 6 * HOUR_IN_SECONDS;
        }

        // Default: 1 hour
        return HOUR_IN_SECONDS;
    }

    /**
     * Invalidate cache on content update
     */
    public function invalidate_post_cache( $post_id ) {
        $post = get_post( $post_id );

        if ( ! $post ) {
            return;
        }

        // Invalidate all post-related queries
        $pattern = 'graphql:*post*';
        $this->invalidate_pattern( $pattern );
    }

    /**
     * Invalidate caches matching pattern
     */
    private function invalidate_pattern( $pattern ) {
        $iterator = null;
        while ( false !== ( $keys = $this->redis->scan( $iterator, $pattern ) ) ) {
            if ( ! empty( $keys ) ) {
                $this->redis->del( $keys );
            }
        }
    }
}

// Initialize
new WPGraphQL_Redis_Cache();

// Invalidate on post save
add_action( 'save_post', function( $post_id ) {
    $cache = new WPGraphQL_Redis_Cache();
    $cache->invalidate_post_cache( $post_id );
} );

Performance Benchmarks

Test Configuration

  • Server: 4 vCPU, 8GB RAM
  • Database: MySQL 8.0, 2GB buffer pool
  • Cache: Redis 7.0, 2GB
  • Load: Apache Bench, 10,000 requests
  • REST API vs GraphQL Performance

# WordPress REST API (multiple endpoints)
# Fetching post + author + categories + featured image requires 3-4 requests

# Request 1: Get post
ab -n 10000 -c 100 https://example.com/wp-json/wp/v2/posts/123
Requests/sec: 245.32

# Request 2: Get author
ab -n 10000 -c 100 https://example.com/wp-json/wp/v2/users/5
Requests/sec: 412.18

# Request 3: Get media
ab -n 10000 -c 100 https://example.com/wp-json/wp/v2/media/456
Requests/sec: 389.45

# Total: ~3 requests, ~450ms total latency

# GraphQL (single request)
ab -n 10000 -c 100 -p post.json https://example.com/graphql
Requests/sec: 1,124.67
Time per request: 89ms

# Performance: 5x fewer requests, 5x faster response time

Query Complexity Performance

Query Type Fields Complexity Response Time Cached Response
Simple post 5 10 45ms 2ms
Post with relations 15 35 120ms 3ms
Post + comments 25 60 280ms 5ms
Complex nested 50 150 650ms 8ms
Search with facets 30 200 890ms 12ms

Cache Hit Rates (Production)

Query Type Hit Rate Avg TTL Daily Requests
Menu queries 98.5% 24h 1.2M
Single post 92.3% 6h 3.5M
Post list 87.4% 1h 2.1M
Search 45.2% 15m 450K
User data 76.8% 30m 890K

Conclusion

WPGraphQL offre des avantages majeurs sur les APIs REST traditionnelles:

Avantages Clés

  • Performance: 3-5x réduction des requêtes réseau
  • Flexibilité: Les clients demandent exactement ce dont ils ont besoin
  • Type Safety: Schéma strongly-typed avec validation automatique
  • Developer Experience: GraphiQL pour l’exploration, excellent tooling
  • Évolution: Pas de versioning d’API nécessaire
  • Métriques Production

  • Réduction bande passante: 60-70% grâce à des payloads précis
  • Latence: 5x amélioration vs REST multi-requests
  • Cache hit rate: 85%+ avec stratégie optimisée
  • Developer velocity**: 40% temps de développement frontend réduit
  • Recommandations

  • Utilisez APQ (Automatic Persisted Queries) pour optimiser le transport
  • Implémentez query complexity analysis pour prévenir les abus
  • Cachez agressivement avec Redis
  • Surveillez les slow queries avec logging
  • Utilisez DataLoader pour éviter les N+1 queries
  • Limitez la profondeur et largeur des queries
  • Cette stack GraphQL est battle-tested sur des sites générant 50M+ requêtes GraphQL par mois.

    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