wappcode/gqlpdss

Utilidades para crear una api GraphQL

Maintainers

Package info

github.com/wappcode/gql-pdss-lib

pkg:composer/wappcode/gqlpdss

Statistics

Installs: 319

Dependents: 5

Suggesters: 0

Stars: 0

Open Issues: 0


README

Una librería PHP moderna para crear APIs GraphQL escalables con Doctrine ORM, arquitectura modular y funcionalidades avanzadas como DataLoaders y middleware.

Versión PHP Doctrine GraphQL

📚 Documentación Completa

Para información detallada, visita: Quick Start Guide

✨ Características Principales

  • 🚀 API GraphQL completa
  • 🏗️ Arquitectura modular flexible y escalable
  • 🔄 Resolvers automáticos para operaciones CRUD con Doctrine ORM
  • DataLoaders integrados para prevenir el problema N+1
  • 🔧 Middleware pipeline para lógica transversal (auth, logging, cache)
  • 📄 Paginación estilo Relay con cursor-based pagination
  • 🎯 Tipos GraphQL personalizados (DateTime, Date, JSON)
  • 🐳 Entorno Docker preconfigurado para desarrollo
  • 📋 Sistema de filtros avanzado con múltiples operadores

🛠️ Instalación

Usar Composer

1. Crear nuevo proyecto

composer init

2. Instalar la librería

composer require wappcode/gqlpdss:^5.0.0

O agregar al composer.json:

{
    "name": "mi-proyecto/graphql-api",
    "type": "project",
    "require": {
        "wappcode/gqlpdss": "^5.0.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0"
    }
}
composer install

3. Estructura del proyecto

Crea la siguiente estructura de directorios:

mi-proyecto/
├── config/
│   ├── master.config.php
│   ├── doctrine.local.php
│   └── doctrine.entities.php
├── data/
│   └── DoctrineORMModule/
├── modules/
│   └── AppModule/
│       ├── config/
│       │   ├── module.config.php
│       │   └── schema.graphql
│       └── src/
│           ├── AppModule.php
│           ├── Entities/
│           ├── Graphql/
│           └── Services/
├── public/
│   └── index.php
├── cli-config.php
└── composer.json

⚙️ Configuración

1. Configurar el módulo principal

Crear modules/AppModule/config/module.config.php

<?php
return [
    // Configuración específica del módulo
    'version' => '1.0.0',
    'description' => 'Módulo principal de la aplicación'
];

Crear modules/AppModule/src/AppModule.php

<?php

namespace AppModule;

use AppModule\Entities\User;
use DateTime;
use GPDCore\Contracts\AppContextInterface;
use GPDCore\Core\AbstractModule;
use GPDCore\Graphql\ResolverFactory;
use GPDCore\Graphql\ResolverPipelineFactory;

class AppModule extends AbstractModule
{
    /**
     * Configuración del módulo
     */
    public function getConfig(): array
    {
        return require __DIR__ . '/../config/module.config.php';
    }

    /**
     * Schema GraphQL del módulo
     */
    public function getSchema(): string
    {
        $schema = file_get_contents(__DIR__ . '/../config/schema.graphql');
        return $schema ?: '';
    }

    /**
     * Servicios del módulo para ServiceManager
     */
    public function getServices(): array
    {
        return [
            'invokables' => [],
            'factories' => [],
            'aliases' => []
        ];
    }

    /**
     * Tipos GraphQL personalizados
     */
    public function getTypes(): array
    {
        return [];
    }

    /**
     * Middlewares HTTP del módulo
     */
    public function getMiddlewares(): array
    {
        return [];
    }

    /**
     * Rutas REST del módulo (opcional)
     */
    public function getRoutes(): array
    {
        return [];
    }

    /**
     * Resolvers GraphQL del módulo
     */
    public function getResolvers(): array
    {
        // Middleware de ejemplo
        $proxyEcho1 = fn($resolver) => fn($root, $args, $context, $info) => 
            'Proxy 1 ' . $resolver($root, $args, $context, $info);
        $proxyEcho2 = fn($resolver) => fn($root, $args, $context, $info) => 
            'Proxy 2 ' . $resolver($root, $args, $context, $info);
        $echoResolve = fn($root, $args, $context, $info) => $args['msg'];

        return [
            // Resolver simple
            'Query::showDate' => fn($root, $args, AppContextInterface $context, $info) => new DateTime(),
            'Query::echo' => $echoResolve,
            
            // Resolvers con middleware pipeline
            'Query::echoProxy' => ResolverPipelineFactory::createPipeline($echoResolve, [
                ResolverPipelineFactory::createWrapper($proxyEcho1),
            ]),
            'Query::echoProxies' => ResolverPipelineFactory::createPipeline($echoResolve, [
                ResolverPipelineFactory::createWrapper($proxyEcho2),
                ResolverPipelineFactory::createWrapper($proxyEcho1),
            ]),
            
            // Resolvers CRUD automáticos usando ResolverFactory
            'Query::getUsers' => ResolverFactory::forConnection(User::class),
            'Query::getUser' => ResolverFactory::forItem(User::class),
            'Mutation::createUser' => ResolverFactory::forCreate(User::class),
            'Mutation::updateUser' => ResolverFactory::forUpdate(User::class),
            'Mutation::deleteUser' => ResolverFactory::forDelete(User::class),
        ];
    }

    /**
     * Campos Query adicionales (opcional)
     */
    public function getQueryFields(): array
    {
        return [];
    }
}

Configurar el autoload en composer.json

{
    "autoload": {
        "psr-4": {
            "AppModule\\": "modules/AppModule/src/"
        }
    }
}
composer dump-autoload -o

2. Archivos de configuración

Crear config/master.config.php

<?php
return [
    // Configuración general de la aplicación
    'app' => [
        'name' => 'Mi API GraphQL',
        'version' => '1.0.0',
        'debug' => false
    ],
    
];

Crear config/doctrine.entities.php

<?php
return [
    "AppModule\\Entities" => __DIR__ . "/../modules/AppModule/src/Entities",
];

Crear config/doctrine.local.php

<?php
return [
    "driver" => [
        'user'     => 'root',
        'password' => 'password',
        'dbname'   => 'mi_database',
        'driver'   => 'pdo_mysql',
        'host'     => '127.0.0.1',
        'charset'  => 'utf8mb4'
    ],
    "entities" => require __DIR__ . "/doctrine.entities.php"
];

Crear public/index.php

<?php

use AppModule\AppModule;
use GPDCore\Contracts\AppContextInterface;
use GPDCore\Core\AppConfig;
use GPDCore\Core\Application;
use GPDCore\Factory\EntityManagerFactory;
use GraphqlModule\GraphqlModule;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Laminas\ServiceManager\ServiceManager;

require_once __DIR__ . '/../vendor/autoload.php';

// Configuración
$configFile = __DIR__ . '/../config/doctrine.local.php';
$cacheDir = __DIR__ . '/../data/DoctrineORMModule';
$environment = getenv('APP_ENV') ?: AppContextInterface::ENV_DEVELOPMENT;

// Cargar configuración
$masterConfig = require __DIR__ . '/../config/master.config.php';
$config = AppConfig::getInstance()->setMasterConfig($masterConfig);

// Inicializar ServiceManager
$serviceManager = new ServiceManager();

// Crear EntityManager
$entityManagerOptions = file_exists($configFile) ? require $configFile : [];
$isEntityManagerDevMode = $environment !== AppContextInterface::ENV_PRODUCTION;
$entityManager = EntityManagerFactory::createInstance(
    $entityManagerOptions, 
    $cacheDir, 
    $isEntityManagerDevMode
);

// Crear Request PSR-7
$request = ServerRequestFactory::fromGlobals();

// Crear y configurar Application
$app = new Application($config, $entityManager, $environment);

// Registrar módulos
$app->addModule(new GraphqlModule(route: '/api'))    // GraphQL endpoint
   ->addModule(AppModule::class);                      // Módulo principal

// Ejecutar aplicación y emitir respuesta
$response = $app->run($request);
$emitter = new SapiEmitter();
$emitter->emit($response);

Crear cli-config.php (para comandos Doctrine CLI)

<?php

use GPDCore\Factory\EntityManagerFactory;
use Doctrine\ORM\Tools\Console\ConsoleRunner;

require_once __DIR__ . "/vendor/autoload.php";

$options = require __DIR__ . "/config/doctrine.local.php";
$cacheDir = __DIR__ . "/data/DoctrineORMModule";
$entityManager = EntityManagerFactory::createInstance($options, $cacheDir, true);

return ConsoleRunner::createHelperSet($entityManager);

💾 Trabajando con Entidades Doctrine

Ejemplo de Entidad User

Crear modules/AppModule/src/Entities/User.php:

<?php

namespace AppModule\Entities;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use PDSSUtilities\AbstractEntityModelUlid;

#[ORM\Entity()]
#[ORM\Table(name: 'users')]
class User extends AbstractEntityModelUlid
{
    #[ORM\Column(type: 'string', length: 255)]
    private string $name;

    #[ORM\Column(type: 'string', length: 255)]
    private string $email;

    #[ORM\JoinTable(name: 'users_accounts')]
    #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false)]
    #[ORM\InverseJoinColumn(name: 'account_code', referencedColumnName: 'code', nullable: false)]
    #[ORM\ManyToMany(targetEntity: Account::class)]
    private Collection $accounts;

    #[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'user')]
    private Collection $posts;

    public function __construct()
    {
        parent::__construct();
        $this->accounts = new ArrayCollection();
        $this->posts = new ArrayCollection();
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;
        return $this;
    }

    public function getAccounts(): Collection
    {
        return $this->accounts;
    }

    public function getPosts(): Collection
    {
        return $this->posts;
    }
}

Comandos Doctrine útiles

# Generar SQL para actualizar la base de datos
./vendor/bin/doctrine orm:schema-tool:update --dump-sql

# Actualizar la base de datos (⚠️  Solo en desarrollo)
./vendor/bin/doctrine orm:schema-tool:update --force

# Crear migración
./vendor/bin/doctrine migrations:diff

# Ejecutar migraciones
./vendor/bin/doctrine migrations:migrate

# Validar mapping
./vendor/bin/doctrine orm:validate-schema

🚀 Ejecutar la aplicación

Desarrollo local

# Servidor de desarrollo PHP
php -S localhost:8000 public/index.php

Endpoints disponibles

  • GraphQL API:
    • GET/POST http://localhost:8000/api (desarrollo)
    • POST http://localhost:8000/api (producción)

📋 Schema GraphQL básico

Crear modules/AppModule/config/schema.graphql:

type Query {
    # Consultas básicas
    showDate: DateTime!
    echo(msg: String!): String!
    echoProxy(msg: String!): String!
    echoProxies(msg: String!): String!
    
    # CRUD de usuarios
    getUsers(
        pagination: PaginationInput
        filters: [FilterGroupInput!]
        joins: [JoinInput!]
        orderBy: [OrderByInput!]
    ): UserConnection!
    
    getUser(id: ID!): User
}

type Mutation {
    createUser(input: UserInput!): User!
    updateUser(id: ID!, input: UserInput!): User!
    deleteUser(id: ID!): Boolean!
}

type User {
    id: ID!
    name: String!
    email: String!
    accounts: [Account!]!
    posts: [Post!]!
    createdAt: DateTime!
    updatedAt: DateTime!
}

input UserInput {
    name: String!
    email: String!
}

# Tipos de conexión para paginación
type UserConnection {
    totalCount: Int!
    pageInfo: PageInfo!
    edges: [UserEdge!]!
}

type UserEdge {
    cursor: String!
    node: User!
}

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
}

# Tipos escalares personalizados (incluidos automáticamente)
scalar DateTime
scalar Date  
scalar JSONData

📚 API Reference

🔧 ResolverFactory

La clase ResolverFactory simplifica la creación de resolvers CRUD automáticos con Doctrine ORM.

Métodos principales

forConnection(string $entityClass, ?QueryModifierInterface $queryModifier = null): callable

Crea un resolver para consultas paginadas estilo Relay Connection con soporte completo para filtros, ordenamiento y joins.

// Resolver básico
'Query::getUsers' => ResolverFactory::forConnection(User::class)

// Con modificador de query personalizado
'Query::getActiveUsers' => ResolverFactory::forConnection(
    User::class, 
    new class implements QueryModifierInterface {
        public function modify(QueryBuilder $qb, array $args): QueryBuilder {
            return $qb->andWhere('entity.status = :status')
                     ->setParameter('status', 'active');
        }
    }
)

Ejemplo de uso en GraphQL:

query GetUsers {
    getUsers(
        pagination: { first: 10, after: "cursor123" }
        filters: [{
            conditions: [{
                property: "name"
                filterOperator: LIKE
                value: { single: "%John%" }
            }]
        }]
        orderBy: [{ property: "createdAt", direction: DESC }]
    ) {
        totalCount
        pageInfo {
            hasNextPage
            hasPreviousPage
            startCursor
            endCursor
        }
        edges {
            cursor
            node {
                id
                name
                email
                createdAt
            }
        }
    }
}

forItem(string $entityClass): callable

Crea un resolver para obtener un único elemento por ID.

'Query::getUser' => ResolverFactory::forItem(User::class)
query GetUser {
    getUser(id: "user-123") {
        id
        name
        email
    }
}

forCreate(string $entityClass): callable

Crea un resolver para operaciones de creación con validación automática.

'Mutation::createUser' => ResolverFactory::forCreate(User::class)
mutation CreateUser {
    createUser(input: {
        name: "John Doe"
        email: "john@example.com"
    }) {
        id
        name
        email
        createdAt
    }
}

forUpdate(string $entityClass): callable

Crea un resolver para operaciones de actualización.

'Mutation::updateUser' => ResolverFactory::forUpdate(User::class)
mutation UpdateUser {
    updateUser(
        id: "user-123"
        input: {
            name: "Jane Doe"
            email: "jane@example.com"
        }
    ) {
        id
        name
        email
        updatedAt
    }
}

forDelete(string $entityClass): callable

Crea un resolver para operaciones de eliminación (soft delete si está configurado).

'Mutation::deleteUser' => ResolverFactory::forDelete(User::class)
mutation DeleteUser {
    deleteUser(id: "user-123")
}

Resolvers para relaciones (prevención N+1)

forEntity(DataLoaderInterface $dataLoader, string $fieldName): callable

Crea un resolver para relaciones many-to-one usando DataLoader.

use GPDCore\DataLoaders\EntityDataLoader;

$userDataLoader = new EntityDataLoader(User::class, $entityManager);

// En el módulo
'Post::author' => ResolverFactory::forEntity($userDataLoader, 'author')

forCollection(string $entityClass, string $fieldName, string $targetEntity, ?QueryModifierInterface $queryModifier = null): callable

Crea un resolver para relaciones one-to-many usando DataLoader.

'User::posts' => ResolverFactory::forCollection(User::class, 'posts', Post::class)
'User::activePosts' => ResolverFactory::forCollection(
    User::class, 
    'posts', 
    Post::class,
    new class implements QueryModifierInterface {
        public function modify(QueryBuilder $qb, array $args): QueryBuilder {
            return $qb->andWhere('target.status = :status')
                     ->setParameter('status', 'published');
        }
    }
)

🔄 ResolverPipelineFactory

Sistema de middleware para resolvers GraphQL que permite aplicar lógica transversal.

Métodos principales

createPipeline(callable $resolver, array $middlewares): callable

Crea un pipeline de middleware para un resolver.

// Middleware de ejemplo
$authMiddleware = fn($resolver) => fn($root, $args, $context, $info) => {
    if (!$context->isAuthenticated()) {
        throw new UnauthorizedException('Authentication required');
    }
    return $resolver($root, $args, $context, $info);
};

$loggingMiddleware = fn($resolver) => fn($root, $args, $context, $info) => {
    $startTime = microtime(true);
    $result = $resolver($root, $args, $context, $info);
    $duration = microtime(true) - $startTime;
    error_log("Resolver {$info->fieldName} executed in {$duration}s");
    return $result;
};

// Aplicar middlewares (se ejecutan en orden inverso)
'Query::protectedData' => ResolverPipelineFactory::createPipeline($baseResolver, [
    ResolverPipelineFactory::createWrapper($loggingMiddleware),
    ResolverPipelineFactory::createWrapper($authMiddleware),
])

createWrapper(callable $middleware): ResolverPipelineHandlerInterface

Convierte una función middleware en un handler de pipeline.

$cacheMiddleware = fn($resolver) => fn($root, $args, $context, $info) => {
    $cacheKey = "resolver_{$info->fieldName}_" . md5(serialize($args));
    
    if ($cached = $context->getCache()->get($cacheKey)) {
        return $cached;
    }
    
    $result = $resolver($root, $args, $context, $info);
    $context->getCache()->set($cacheKey, $result, 300); // 5 min
    
    return $result;
};

$wrappedMiddleware = ResolverPipelineFactory::createWrapper($cacheMiddleware);

🔒 ResolverTransactionMiddlewareFactory

Fábrica que crea un middleware de transacción de base de datos listo para usar en pipelines de resolvers. Envuelve la ejecución del resolver dentro de una transacción Doctrine: hace commit si todo va bien y rollback automático si ocurre cualquier excepción.

Método principal

createMiddleware(): ResolverMiddlewareInterface

Crea una instancia de middleware que gestiona transacciones de base de datos automáticamente.

use GPDCore\Graphql\ResolverFactory;
use GPDCore\Graphql\ResolverPipelineFactory;
use GPDCore\Graphql\ResolverTransactionMiddlewareFactory;

'Mutation::createUser' => ResolverPipelineFactory::createPipeline(
    ResolverFactory::forCreate(User::class),
    [
        ResolverTransactionMiddlewareFactory::createMiddleware(),
    ]
),

Comportamiento interno

Request
  └─► TransactionMiddleware::beginTransaction()
        └─► resolver($root, $args, $context, $info)
              ├─ Éxito → commit() → return $result
              └─ Excepción → rollBack() → re-lanza la excepción

Posición recomendada en el pipeline

El middleware de transacción debe colocarse al final del array de middlewares (última posición). Dado que el pipeline se ejecuta en orden inverso, esto hace que el middleware de transacción sea el primero en ejecutarse, envolviendo así toda la cadena de lógica (validaciones, autorizaciones, etc.) dentro de una única transacción.

'Mutation::createUser' => ResolverPipelineFactory::createPipeline(
    ResolverFactory::forCreate(User::class),
    [
        // 3° en ejecutarse (más interno, justo antes del resolver): autorización
        ResolverPipelineFactory::createWrapper($authMiddleware),
        // 2° en ejecutarse: validación
        ResolverPipelineFactory::createWrapper($validationMiddleware),
        // 1° en ejecutarse (más externo): envuelve toda la operación dentro de la transacción
        ResolverTransactionMiddlewareFactory::createMiddleware(),
    ]
),

⚠️ Nota: Si ResolverFactory::forCreate, forUpdate o forDelete ya gestionan su propia transacción internamente, usar este middleware añadirá una transacción anidada. Esto es seguro en Doctrine siempre que ambas transacciones finalicen correctamente, pero conviene revisar si la gestión de transacciones debe delegarse completamente al middleware.

🎯 Tipos GraphQL personalizados

La librería incluye tipos escalares personalizados listos para usar:

DateTimeType

  • Nombre: DateTime
  • Descripción: Fecha y hora en formato ISO 8601
  • Ejemplo: "2024-01-15T10:30:00Z"

DateType

  • Nombre: Date
  • Descripción: Fecha en formato ISO (solo fecha)
  • Ejemplo: "2024-01-15"

JSONData

  • Nombre: JSONData
  • Descripción: Datos JSON arbitrarios
  • Ejemplo: {"key": "value", "nested": {"data": 123}}

Registro de tipos en módulos

public function getTypes(): array
{
    return [
        DateType::NAME => DateType::class,
        DateTimeType::NAME => DateTimeType::class,
        JSONData::NAME => JSONData::class,
        // Tus tipos personalizados
        'MyCustomType' => MyCustomType::class,
    ];
}

🔍 Sistema de filtros avanzado

La librería incluye un sistema de filtros robusto que soporta operadores complejos, joins y lógica AND/OR.

Operadores disponibles

enum FilterOperator {
  EQUAL
  NOT_EQUAL  
  BETWEEN
  GREATER_THAN
  LESS_THAN
  GREATER_EQUAL_THAN
  LESS_EQUAL_THAN
  LIKE
  NOT_LIKE
  IN
  NOT_IN
}

Ejemplo de filtros complejos

query GetFilteredUsers {
  getUsers(
    # Filtros con lógica AND/OR
    filters: [{
      groupLogic: AND
      conditionsLogic: OR
      conditions: [
        {
          property: "name"
          filterOperator: LIKE
          value: { single: "%John%" }
        }
        {
          property: "email"
          filterOperator: LIKE  
          value: { single: "%gmail%" }
        }
      ]
    }]
    
    # Joins para filtrar por propiedades relacionadas
    joins: [{
      property: "posts"
      joinType: INNER
      alias: "userPosts"
    }]
    
    # Ordenamiento
    orderBy: [{
      property: "createdAt"
      direction: DESC
    }]
    
    # Paginación
    pagination: {
      first: 20
      after: "cursor123"
    }
  ) {
    totalCount
    edges {
      node {
        id
        name
        email
        posts {
          id
          title
        }
      }
    }
  }
}

🚀 Ejemplos prácticos

1. API completa de Blog

// modules/AppModule/src/AppModule.php
class AppModule extends AbstractModule
{
    public function getResolvers(): array
    {
        return [
            // Consultas básicas
            'Query::getPosts' => ResolverFactory::forConnection(Post::class),
            'Query::getPost' => ResolverFactory::forItem(Post::class),
            'Query::getUsers' => ResolverFactory::forConnection(User::class),
            'Query::getUser' => ResolverFactory::forItem(User::class),
            
            // Mutaciones
            'Mutation::createPost' => ResolverFactory::forCreate(Post::class),
            'Mutation::updatePost' => ResolverFactory::forUpdate(Post::class),
            'Mutation::deletePost' => ResolverFactory::forDelete(Post::class),
            
            // Relaciones (prevención N+1)
            'Post::author' => ResolverFactory::forEntity(
                new EntityDataLoader(User::class, $this->entityManager), 
                'author'
            ),
            'User::posts' => ResolverFactory::forCollection(
                User::class, 
                'posts', 
                Post::class
            ),
            
            // Resolver personalizado con middleware
            'Mutation::publishPost' => ResolverPipelineFactory::createPipeline(
                function($root, $args, AppContextInterface $context, $info) {
                    $post = $context->getEntityManager()
                        ->getRepository(Post::class)
                        ->find($args['id']);
                    
                    if (!$post) {
                        throw new \Exception('Post not found');
                    }
                    
                    $post->setStatus('published');
                    $context->getEntityManager()->flush();
                    
                    return $post;
                },
                [
                    ResolverPipelineFactory::createWrapper($this->getAuthMiddleware()),
                    ResolverPipelineFactory::createWrapper($this->getOwnershipMiddleware()),
                ]
            ),
        ];
    }
    
    private function getAuthMiddleware(): callable
    {
        return fn($resolver) => fn($root, $args, $context, $info) => {
            if (!$context->getCurrentUser()) {
                throw new \Exception('Authentication required');
            }
            return $resolver($root, $args, $context, $info);
        };
    }
    
    private function getOwnershipMiddleware(): callable
    {
        return fn($resolver) => fn($root, $args, $context, $info) => {
            $post = $context->getEntityManager()
                ->getRepository(Post::class)
                ->find($args['id']);
                
            if ($post && $post->getAuthor()->getId() !== $context->getCurrentUser()->getId()) {
                throw new \Exception('Access denied');
            }
            
            return $resolver($root, $args, $context, $info);
        };
    }
}

2. GraphQL Schema completo

# modules/AppModule/config/schema.graphql

type Query {
    # Posts
    getPosts(
        pagination: PaginationInput
        filters: [FilterGroupInput!]
        orderBy: [OrderByInput!]
    ): PostConnection!
    
    getPost(id: ID!): Post
    getPublishedPosts: PostConnection!
    
    # Users  
    getUsers(
        pagination: PaginationInput
        filters: [FilterGroupInput!]
    ): UserConnection!
    
    getUser(id: ID!): User
    me: User
}

type Mutation {
    # Authentication
    login(email: String!, password: String!): AuthPayload!
    register(input: RegisterInput!): AuthPayload!
    
    # Posts
    createPost(input: PostInput!): Post!
    updatePost(id: ID!, input: PostInput!): Post!
    deletePost(id: ID!): Boolean!
    publishPost(id: ID!): Post!
    
    # Users
    updateProfile(input: UserUpdateInput!): User!
}

type User {
    id: ID!
    name: String!
    email: String!
    posts(status: PostStatus): [Post!]!
    createdAt: DateTime!
    updatedAt: DateTime!
}

type Post {
    id: ID!
    title: String!
    content: String!
    status: PostStatus!
    author: User!
    comments: [Comment!]!
    createdAt: DateTime!
    updatedAt: DateTime!
    publishedAt: DateTime
}

enum PostStatus {
    DRAFT
    PUBLISHED
    ARCHIVED
}

type AuthPayload {
    token: String!
    user: User!
}

input PostInput {
    title: String!
    content: String!
    status: PostStatus = DRAFT
}

input UserUpdateInput {
    name: String
    email: String
}

input RegisterInput {
    name: String!
    email: String!
    password: String!
}

📝 Mejores prácticas

1. Organización del código

modules/AppModule/
├── config/
│   ├── module.config.php
│   └── schema.graphql
├── src/
│   ├── AppModule.php
│   ├── Entities/
│   │   ├── User.php
│   │   ├── Post.php
│   │   └── Comment.php
│   ├── Graphql/
│   │   ├── Resolvers/
│   │   │   ├── UserResolvers.php
│   │   │   ├── PostResolvers.php
│   │   │   └── CommentResolvers.php
│   │   ├── Types/
│   │   │   └── CustomScalar.php
│   │   └── Middleware/
│   │       ├── AuthMiddleware.php
│   │       └── RateLimitMiddleware.php
│   └── Services/
│       ├── UserService.php
│       └── PostService.php

2. Uso de DataLoaders

// Evita el problema N+1
class UserResolvers
{
    private EntityDataLoader $userDataLoader;
    
    public function __construct(EntityManager $em)
    {
        $this->userDataLoader = new EntityDataLoader(User::class, $em);
    }
    
    public static function getPostsAuthorResolver(): callable 
    {
        return ResolverFactory::forEntity($this->userDataLoader, 'author');
    }
}

4. Manejo de errores

use GPDCore\Exceptions\GQLException;

'Query::sensitiveData' => function($root, $args, AppContextInterface $context, $info) {
    try {
        if (!$context->getCurrentUser()) {
            throw new GQLException('Not authenticated', 'UNAUTHENTICATED');
        }
        
        if (!$context->getCurrentUser()->hasRole('admin')) {
            throw new GQLException('Insufficient permissions', 'FORBIDDEN');
        }
        
        return $this->getSensitiveData();
        
    } catch (\Exception $e) {
        throw new GQLException(
            'Failed to fetch sensitive data: ' . $e->getMessage(),
            'INTERNAL_ERROR'
        );
    }
}

🤝 Contribuir

  1. Fork el proyecto
  2. Crea una rama para tu feature (git checkout -b feature/amazing-feature)
  3. Commit tus cambios (git commit -m 'Add amazing feature')
  4. Push a la rama (git push origin feature/amazing-feature)
  5. Abre un Pull Request

📄 Licencia

Este proyecto está bajo la Licencia MIT. Ver el archivo LICENSE para más detalles.

🆘 Soporte

¿Te ha sido útil esta librería? ⭐ ¡Danos una estrella en GitHub!