Como extender WPGraphQL con tipos y resolvers propios en PHP en WordPress

En este artículo se explica paso a paso cómo extender WPGraphQL con tipos y resolvers propios en PHP. Se incluyen las principales funciones y hooks del API de WPGraphQL, ejemplos completos de código listos para usar en un plugin o en el functions.php de un tema, prácticas recomendadas de seguridad y rendimiento, y patrones comunes: añadir campos a tipos existentes, crear tipos personalizados, exponer consultas (queries) y mutaciones (mutations), y cómo manejar autenticación y caching.

Contents

Pre-requisitos

  • Instalar y activar el plugin WPGraphQL (https://www.wpgraphql.com/).
  • Conocimientos básicos de PHP y del ciclo de vida de hooks de WordPress.
  • Acceso para añadir un plugin personalizado o modificar functions.php.
  • Entender la estructura de tipos de GraphQL: objetos, listas, entradas y resolvers.

Conceptos clave de WPGraphQL

  • graphql_register_types: hook principal donde registrar tipos y campos.
  • register_graphql_field: añade un campo a un tipo GraphQL existente (por ejemplo, Post, User, RootQuery).
  • register_graphql_object_type: define un tipo de objeto GraphQL personalizado (no relacionado con post type de WP).
  • register_graphql_enum_type y register_graphql_input_type: para enums e inputs complejos.
  • register_graphql_mutation: registrar mutaciones GraphQL (crear/actualizar/acciones).
  • Los resolvers reciben la firma: function(source, args, context, info) { … }.

Buen patrón: usar el hook graphql_register_types

Todas las definiciones deben realizarse dentro de una función conectada al hook graphql_register_types. Así se asegura que el registro se hace cuando WPGraphQL está listo.

Ejemplo mínimo de estructura del plugin


1) Añadir campos a tipos existentes (ej.: Post)

Es común añadir campos calculados o datos externos a tipos que WPGraphQL ya expone, como Post o User. Para ello se utiliza register_graphql_field.

Ejemplo: añadir campo readingTime al tipo Post

Este campo calcula un tiempo de lectura estimado basado en el contenido del post. Devuelve un entero con los minutos de lectura.

add_action( graphql_register_types, function() {
    register_graphql_field( Post, readingTime, [
        type => Int,
        description => Tiempo estimado de lectura en minutos.,
        resolve => function( post, args, context, info ) {
            // post es un objeto WP_Post cuando proviene del resolver de Post
            content = get_post_field( post_content, post->ID )
            words   = str_word_count( wp_strip_all_tags( content ) )
            wpm     = 200 // palabras por minuto
            minutes = (int) max(1, round( words / wpm ))
            return minutes
        }
    ] )
} )

Notas sobre este patrón

  • Si el resolver necesita datos adicionales (metadatos, transients, APIs externas), encárgatelo aquí.
  • Evita consultas pesadas por cada fila: utiliza caching (transients o memoización) si el cálculo es costoso.
  • Si necesitas devolver campos complejos, puedes registrar un tipo objeto y devolver un array asociativo desde el resolve.

2) Registrar un tipo de objeto personalizado

Cuando los datos que quieres exponer no son un Post ni un User, define un tipo con register_graphql_object_type y después registra campos o queries que devuelvan ese tipo.

Ejemplo: tipo Book y query para obtener libros

Definimos un tipo Book con campos simples y registramos dos queries: book(id: ID!) y books (lista).

add_action( graphql_register_types, function() {
    // 1) registrar el tipo Book
    register_graphql_object_type( Book, [
        description => Objeto que representa un libro.,
        fields => [
            id => [
                type => ID,
                description => Identificador del libro.
            ],
            title => [
                type => String
            ],
            author => [
                type => String
            ],
            publishedYear => [
                type => Int
            ]
        ]
    ] )

    // 2) query para obtener un libro por id
    register_graphql_field( RootQuery, book, [
        type => Book,
        args => [
            id => [
                type => ID
            ]
        ],
        resolve => function( source, args, context, info ) {
            // Ejemplo de datos simulados (en un caso real leerías DB o REST API)
            books = [
                1 => [ id => 1, title => Mi libro, author => Autor Uno, publishedYear => 2020 ],
                2 => [ id => 2, title => Segundo libro, author => Autor Dos, publishedYear => 2018 ],
            ]
            id = isset( args[id] ) ? intval( args[id] ) : null
            return id  isset( books[ id ] ) ? books[ id ] : null
        }
    ] )

    // 3) query para obtener lista de libros
    register_graphql_field( RootQuery, books, [
        type => [ list_of => Book ],
        resolve => function() {
            return [
                [ id => 1, title => Mi libro, author => Autor Uno, publishedYear => 2020 ],
                [ id => 2, title => Segundo libro, author => Autor Dos, publishedYear => 2018 ],
            ]
        }
    ] )
} )

Cómo debe estructurarse la respuesta

Para tipos personalizados debes devolver arrays asociativos con claves que coincidan con los nombres de campo (por ejemplo title, author). Si tu campo devuelve múltiples instancias, utiliza type => [list_of => NombreTipo].

3) Mutaciones: crear/editar datos

WPGraphQL tiene una API sencilla para definir mutaciones mediante register_graphql_mutation. Una mutación define inputFields, outputFields y la función mutateAndGetPayload.

Ejemplo: mutation createBook

add_action( graphql_register_types, function() {
    register_graphql_mutation( createBook, [
        inputFields => [
            title => [ type => String ],
            author => [ type => String ],
            publishedYear => [ type => Int ],
        ],
        outputFields => [
            book => [
                type => Book,
                resolve => function( payload ) {
                    return isset( payload[book] ) ? payload[book] : null
                }
            ],
            success => [
                type => Boolean
            ],
        ],
        mutateAndGetPayload => function( input, context, info ) {
            // Validaciones de permisos
            if ( ! current_user_can( edit_posts ) ) {
                throw new GraphQL_Error( No autorizado. )
            }

            // Aquí crearías el registro (DB, Custom Post Type, API externa...)
            new_book = [
                id => rand(1000, 9999),
                title => input[title] ?? ,
                author => input[author] ?? ,
                publishedYear => input[publishedYear] ?? null,
            ]

            // Simulamos persistencia en un caso real guardarías datos
            return [
                book => new_book,
                success => true
            ]
        }
    ] )
} )

4) Resolver campos que devuelven WP_Post o relacionan con tipos nativos

Si tu resolver devuelve un objeto WP_Post (o un ID), WPGraphQL consumirá sus campos nativos (title, content, custom fields expuestos). Por ejemplo, podrías añadir un campo que devuelva el post relacionado y aprovechar los resolvers ya existentes para Post.

Ejemplo: añadir campo relatedPost a un tipo custom Book que retorna un WP_Post

register_graphql_field( Book, relatedPost, [
    type => Post,
    resolve => function( book ) {
        // Si el libro tiene meta con post_id relacionado
        post_id = get_post_meta( book[id], related_post_id, true )
        if ( post_id ) {
            return get_post( intval( post_id ) ) // devolver un WP_Post
        }
        return null
    }
] )

5) Argumentos y validación

Define argumentos en register_graphql_field usando la clave args. Valía los argumentos en el resolver y lanza errores de GraphQL (GraphQL_Error) para comunicar problemas. Evita inyecciones al usar intval, sanitize_text_field, esc_sql cuando accedas a la base de datos directamente.

6) Contexto y autorización

  • Usa las funciones nativas de WordPress para control de permisos (current_user_can, get_current_user_id).
  • No confíes en args para autorizar valida permisos dentro del resolver.
  • Si dependes de cabeceras o tokens, recupéralos desde _SERVER o a través de la integración que uses con WPGraphQL y valida antes de ejecutar acciones sensibles.

7) Performance y caching

  • Evita consultas N 1: si solicitas campos que disparan consultas por cada elemento, agrupa la lógica y usa data loaders/caching.
  • Utiliza transients para almacenar resultados de llamadas externas o cálculos pesados por un tiempo razonable.
  • Considera usar object caching (Redis/Memcached) y liberar transients al actualizar los datos relevantes.

Ejemplo de caching con transients

register_graphql_field( RootQuery, externalData, [
    type => String,
    resolve => function() {
        cache_key = mi_external_data
        cached = get_transient( cache_key )
        if ( cached !== false ) {
            return cached
        }
        // Llamada externa ficticia
        value = wp_remote_retrieve_body( wp_remote_get( https://api.ejemplo.com/data ) )
        if ( value ) {
            set_transient( cache_key, value, HOUR_IN_SECONDS )
        }
        return value
    }
] )

8) Depuración y testing

  • Usa GraphiQL o GraphQL IDE para probar queries y mutaciones (WPGraphQL incluye GraphiQL en algunas instalaciones o usa extensiones).
  • Agrega logs (error_log, WP_DEBUG_LOG) en desarrollos para depurar resolvers. Evita imprimir errores sensibles en producción.
  • Prueba permisos y errores: solicita datos sin autenticar y con distintos roles de usuario.

9) Ejemplo avanzado: registrar un campo con args y resolver que usa WP_Query

Campo para obtener posts relacionados por una taxonomía con paginación simple (args: taxonomy, term, perPage).

add_action( graphql_register_types, function() {
    register_graphql_field( RootQuery, relatedPosts, [
        type => [ list_of => Post ],
        args => [
            taxonomy => [ type => String ],
            term => [ type => String ],
            perPage => [ type => Int, defaultValue => 5 ],
        ],
        resolve => function( source, args ) {
            taxonomy = sanitize_text_field( args[taxonomy] ?? category )
            term     = sanitize_text_field( args[term] ??  )
            per_page = intval( args[perPage] ?? 5 )

            if ( empty( term ) ) {
                return []
            }

            wpq = new WP_Query( [
                post_type => post,
                posts_per_page => per_page,
                tax_query => [
                    [
                        taxonomy => taxonomy,
                        field    => slug,
                        terms    => term,
                    ]
                ]
            ] )

            if ( ! wpq->have_posts() ) {
                return []
            }

            return wpq->posts // WPGraphQL sabe trabajar con WP_Post
        }
    ] )
} )

10) Buenas prácticas y recomendaciones finales

  1. Separa lógica de negocio: escribe funciones PHP reutilizables y llama a esas funciones desde tus resolvers evita lógica monolítica dentro del callback.
  2. Evita cargas innecesarias: solo calcula o consulta lo necesario cuando un campo es solicitado por la query.
  3. Documenta tus campos y tipos: usa description en los registros para facilitar el uso desde GraphiQL y herramientas de cliente.
  4. Manejo de errores consistente: lanza GraphQL_Error para indicarle al cliente qué falló y por qué.
  5. Versionado: si necesitas cambios breaking, crea nombres de campos o tipos con versiones o mantén compatibilidad hacia atrás.

Recursos útiles

Ejemplo completo: plugin de ejemplo que agrega tipo Book, queries y mutation

 Tipo Book de ejemplo,
        fields => [
            id => [ type => ID ],
            title => [ type => String ],
            author => [ type => String ],
            publishedYear => [ type => Int ],
        ]
    ] )

    // Query single book
    register_graphql_field( RootQuery, book, [
        type => Book,
        args => [
            id => [ type => ID ]
        ],
        resolve => function( source, args ) {
            books = books_demo_data()
            id = isset( args[id] ) ? intval( args[id] ) : null
            return id  isset( books[ id ] ) ? books[ id ] : null
        }
    ] )

    // Query books list
    register_graphql_field( RootQuery, books, [
        type => [ list_of => Book ],
        resolve => function() {
            return array_values( books_demo_data() )
        }
    ] )

    // Mutation createBook
    register_graphql_mutation( createBook, [
        inputFields => [
            title => [ type => String ],
            author => [ type => String ],
            publishedYear => [ type => Int ],
        ],
        outputFields => [
            book => [
                type => Book,
                resolve => function( payload ) {
                    return payload[book] ?? null
                }
            ]
        ],
        mutateAndGetPayload => function( input, context ) {
            if ( ! current_user_can( publish_posts ) ) {
                throw new GraphQL_Error( Sin permisos. )
            }
            books = books_demo_data()
            new_id = max( array_keys( books ) )   1
            new_book = [
                id => new_id,
                title => input[title] ?? ,
                author => input[author] ?? ,
                publishedYear => input[publishedYear] ?? null,
            ]
            // En demo no persistimos, simplemente retornamos el nuevo libro
            return [ book => new_book ]
        }
    ] )
}

function books_demo_data() {
    return [
        1 => [ id => 1, title => Libro A, author => Autor A, publishedYear => 2015 ],
        2 => [ id => 2, title => Libro B, author => Autor B, publishedYear => 2019 ],
    ]
}

Con los ejemplos anteriores puedes construir extensiones robustas de WPGraphQL: añadir campos a tipos nativos, definir tipos propios, exponer queries y mutaciones y manejar permisos y caching. Implementa pruebas en entornos de desarrollo antes de desplegar a producción y mantén la lógica de negocio separada de tus resolvers para facilitar pruebas y mantenimiento.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *