fedorgrecha/php-graphql-projection

GraphQL projections generator for Laravel

Installs: 2

Dependents: 0

Suggesters: 0

Security: 0

Stars: 2

Watchers: 1

Forks: 0

Open Issues: 0

pkg:composer/fedorgrecha/php-graphql-projection

0.1.1 2025-10-10 09:29 UTC

This package is auto-updated.

Last update: 2025-10-10 09:31:08 UTC


README

A PHP library for generating type-safe GraphQL queries and projections for Laravel applications. Build GraphQL requests with full IDE autocomplete, type safety, and a fluent interface.

Installation

Install via Composer:

composer require --dev fedorgrecha/php-graphql-projection

Publish configuration:

php artisan vendor:publish --tag=config-graphql-projection

Configuration

Configure in config/graphql-projection.php:

return [
    'build' => [
        'namespace' => env('GRAPHQL_PROJECTION_NAMESPACE', 'Build\\GraphQLProjection\\'),
        'buildDir' => env('GRAPHQL_PROJECTION_PATH', 'build/graphql-projection'),
    ],

    'endpoint' => env('GRAPHQL_PROJECTION_ENDPOINT', '/graphql'),
    
    'schema' => [
        'provider' => env('GRAPHQL_PROJECTION_SCHEMA_PROVIDER', 'lighthouse'),
        'providers' => [
            'lighthouse' => LighthouseSchemaProvider::class,
            'rebing' => RebingSchemaProvider::class,
        ],
    ],
    
    /*
     * Register custom scalar types:
     * 
     * Just for represent data in PHP objects
     */
    'typeMapping' => [
        'Email' => [
            'type' => 'string',
        ],
        'Date' => [
            'type' => Carbon::class,
            'format' => 'Y-m-d',
        ],
    ],
];

Note: Add build/ to .gitignore (contains generated files).

Configure Composer Autoload

Update the autoload-dev section in your root composer.json file:

"autoload-dev": {
    "psr-4": {
        "Build\\GraphQLProjection\\": "build/graphql-projection/",
        // other namespaces
    }
},

Note: Use namespace and path from your configuration.

After updating composer.json, run:

composer dump-autoload

Quick Start

Generate Classes

Generate type-safe classes from your GraphQL schema:

php artisan graphql:projection

This creates:

  • Query classes - Execute GraphQL operations
  • Projection classes - Select fields to retrieve
  • Input classes - Type-safe input data
  • Type classes - Response data models

Build Your First Query

use GraphQLProjection\Client\Serialization\GraphQLQueryRequest;
use GraphQLProjection\Client\Execution\GraphQLTestHelpers;

class ArticleTest extends TestCase
{
    use GraphQLTestHelpers;

    public function test_create_article(): void
    {
        $this->actingAs($user);

        // Build input data
        $input = ArticleInput::builder()
            ->title('My First Article')
            ->content('Article content here...')
            ->active(true)
            ->publishedAt(now())
            ->build();

        // Create query
        $query = CreateArticleGraphQLQuery::newRequest()
            ->input($input)
            ->build();

        // Define projection (select fields)
        $projection = CreateArticleProjection::new()
            ->id()
            ->title()
            ->content()
            ->active()
            ->publishedAt()
            ->author()
                ->name()
                ->email()
            ->getRoot();

        // Execute and get typed response
        $request = new GraphQLQueryRequest($query, $projection);
        $article = $this->graphql()->executeAndExtract($request);

        // Work with typed object
        $this->assertInstanceOf(Article::class, $article);
        $this->assertEquals('My First Article', $article->getTitle());
    }
}

Usage Examples

Nested Objects and Arrays

Work with complex nested structures effortlessly:

$input = ArticleInput::builder()
    ->title('Multilingual Article')
    ->author(
        AuthorInput::builder()
            ->name('John Doe')
            ->email('john@example.com')
            ->build()
    )
    ->translations([
        TranslationInput::builder()
            ->language('en')
            ->title('Article in English')
            ->content('English content...')
            ->build(),
        TranslationInput::builder()
            ->language('es')
            ->title('Artículo en Español')
            ->content('Contenido en español...')
            ->build(),
    ])
    ->tags(['php', 'laravel', 'graphql'])
    ->build();

$projection = ArticleProjection::new()
    ->id()
    ->title()
    ->author()
        ->id()
        ->name()
        ->avatar()
            ->url()
            ->getParent()
        ->getRoot()
    ->translations()
        ->language()
        ->title()
        ->content()
        ->getRoot()
    ->tags()
    ->getRoot();

File Uploads

Upload single files, multiple files, or files within nested inputs:

// Single file
$input = ArticleInput::builder()
    ->title('Article with Cover')
    ->coverImage(UploadedFile::fake()->image('cover.jpg'))
    ->build();

// Multiple files
$input = ArticleInput::builder()
    ->title('Photo Gallery')
    ->images([
        UploadedFile::fake()->image('photo1.jpg'),
        UploadedFile::fake()->image('photo2.jpg'),
        UploadedFile::fake()->image('photo3.jpg'),
    ])
    ->build();

// Files in nested inputs
$input = ArticleInput::builder()
    ->translations([
        TranslationInput::builder()
            ->language('en')
            ->thumbnail(UploadedFile::fake()->image('en-thumb.jpg'))
            ->build(),
        TranslationInput::builder()
            ->language('es')
            ->thumbnail(UploadedFile::fake()->image('es-thumb.jpg'))
            ->build(),
    ])
    ->build();

$query = CreateArticleGraphQLQuery::newRequest()
    ->input($input)
    ->build();

$projection = ArticleProjection::new()
    ->id()
    ->coverImage()
        ->url()
        ->size()
        ->mimeType();

$request = new GraphQLQueryRequest($query, $projection);
$article = $this->graphql()->executeAndExtract($request);

Field Arguments

Pass arguments to specific fields:

$projection = UserProjection::new()
    ->id()
    ->name()
    ->posts(10, PostStatus::PUBLISHED) // Arguments: limit, status
        ->id()
        ->title()
        ->createdAt()
        ->getRoot()
    ->avatar('large') // Argument: size
        ->url()
        ->width()
        ->height()
        ->getRoot();

Union Types

Handle GraphQL union and interface types:

$projection = SearchProjection::new()
    ->results()
        ->onArticle() // Union type: Article
            ->id()
            ->title()
            ->author()
                ->name()
                ->getParent()
            ->getParent()
        ->onUser() // Union type: User
            ->id()
            ->name()
            ->email()
            ->getParent()
        ->onComment() // Union type: Comment
            ->id()
            ->text()
            ->createdAt()
            ->getRoot();

Pagination

Built-in support for paginated queries:

$query = ArticlesGraphQLQuery::newRequest()
    ->first(20)
    ->page(1)
    ->build();

$projection = ArticlesProjection::new()
    ->paginatorInfo()
        ->currentPage()
        ->lastPage()
        ->total()
        ->perPage()
        ->getRoot()
    ->data()
        ->id()
        ->title()
        ->description()
        ->publishedAt();

$request = new GraphQLQueryRequest($query, $projection);
$paginator = $this->graphql()->executeAndExtract($request);

// Work with paginated data
$articles = $paginator->getData(); // Article[]
$currentPage = $paginator->getPaginatorInfo()->getCurrentPage();
$total = $paginator->getPaginatorInfo()->getTotal();

Error Handling

Two approaches for handling errors:

// Option 1: executeAndExtract - throws exception on errors
try {
    $article = $this->graphql()->executeAndExtract($request);
    
    // Work with successful response
    $this->assertEquals('Expected Title', $article->getTitle());
} catch (GraphQLException $e) {
    // Handle GraphQL errors
    foreach ($e->getErrors() as $error) {
        echo $error['message'];
        
        // Access validation errors
        if (isset($error['extensions']['validation'])) {
            $validationErrors = $error['extensions']['validation'];
        }
    }
}

// Option 2: execute - manual error checking
$response = $this->graphql()->execute($request);

if ($response->hasErrors()) {
    // Handle errors
    $errors = $response->getErrors();
    $firstError = $response->getFirstError();
} else {
    // Get raw data or extract to typed object
    $rawData = $response->getData();
}

Working with Enums

Native PHP enum support:

enum ArticleStatus: string
{
    case DRAFT = 'DRAFT';
    case PUBLISHED = 'PUBLISHED';
    case ARCHIVED = 'ARCHIVED';
}

$input = ArticleInput::builder()
    ->title('My Article')
    ->status(ArticleStatus::PUBLISHED)
    ->build();

$query = CreateArticleGraphQLQuery::newRequest()
    ->input($input)
    ->build();

Date and Time Handling

Automatic Carbon conversion:

$input = ArticleInput::builder()
    ->publishedAt(now())
    ->scheduledFor(Carbon::parse('2024-12-25 10:00:00'))
    ->expiresAt(now()->addDays(30))
    ->build();

// Response automatically converts to Carbon
$article = $this->graphql()->executeAndExtract($request);
$publishedDate = $article->getPublishedAt(); // Carbon instance

API Reference

GraphQLTestHelpers (Trait)

Use in your test classes:

use GraphQLProjection\Client\Execution\GraphQLTestHelpers;

class MyTest extends TestCase
{
    use GraphQLTestHelpers;

    public function test_example()
    {
        $executor = $this->graphql(); // Returns GraphQLQueryExecutor
    }
}

GraphQLQueryExecutor

Execute GraphQL requests: Methods:

  • execute(GraphQLQueryRequest $request): GraphQLResponse - Execute query, return raw response
  • executeAndExtract(GraphQLQueryRequest $request): mixed - Execute query, return typed object

GraphQLResponse

GraphQL response wrapper:

Methods:

  • getData(): mixed - Get response data
  • getErrors(): array - Get errors array
  • hasErrors(): bool - Check if response has errors
  • getFirstError(): ?array - Get first error
  • isSuccessful(): bool - Check if request was successful
  • getRawResponse(): array - Get raw response array

GraphQLException

Exception thrown on GraphQL errors:

Methods:

  • getErrors(): array - Get GraphQL errors
  • getRawResponse(): array - Get full response
  • getMessage(): string - Get error message

Projection Navigation

Navigate through nested projections:

Methods:

  • getRoot() - Return to root projection
  • getParent() - Return to parent projection
$projection = RootProjection::new()
    ->field1()
    ->level1()
        ->field2()
        ->level2()
            ->field3()
            ->level3()
                ->field4()
                ->getRoot() // Back to RootProjection
    ->field5()
        ->level6()
        ->getParent() // Back to field5;

Builder Pattern

All Input and Type classes support builder pattern:

$input = ArticleInput::builder()
    ->field1($value1)
    ->field2($value2)
    ->optionalField($value3) // Optional fields
    ->build();

Supported GraphQL Features

Scalar Types

  • String, Int, Float, Boolean, ID
  • Custom scalars (with automatic conversion)

Complex Types

  • Objects - Nested object types
  • Input Objects - Input type support
  • Arrays - Lists of any type
  • Nullable Fields - Optional fields with null support
  • Union Types - GraphQL unions with type discrimination
  • Interface Types - GraphQL interfaces

Special Features

  • File Uploads - GraphQL Multipart Request Spec compliant
  • Pagination - Cursor and offset-based pagination
  • Enums - Native PHP enum support
  • Dates - Automatic Carbon/DateTime conversion
  • Custom Scalars - Extensible scalar handling

Generated Code Structure

Query Class

class CreateArticleGraphQLQuery extends GraphQLQuery
{
    public function getOperationName(): string
    {
        return 'createArticle';
    }

    public function getOperation(): string
    {
        return 'mutation'; // or 'query'
    }

    public function getResponseType(): string
    {
        return Article::class;
    }

    public function isList(): bool
    {
        return false;
    }
}

Input Class

class ArticleInput extends BaseInput
{
    private string $title;
    private bool $active;
    private ?Carbon $publishedAt;

    public function getTitle(): string { return $this->title; }
    public function setTitle(string $title): void { $this->title = $title; }
    
    public static function builder(): ArticleInputBuilder
    {
        return new ArticleInputBuilder();
    }
}

Type Class

class Article
{
    private int|string $id;
    private string $title;
    private Author $author;
    
    public function getId(): int|string { return $this->id; }
    public function getTitle(): string { return $this->title; }
    public function getAuthor(): Author { return $this->author; }
    
    public static function builder(): ArticleBuilder
    {
        return new ArticleBuilder();
    }
}

Requirements

  • PHP 8.2 or higher
  • Laravel 10.x, 11.x, or 12.x
  • Lighthouse or Rebing GraphQL Laravel package

Compatible GraphQL Servers

  • Lighthouse - Laravel GraphQL server
  • Rebing GraphQL Laravel - Laravel GraphQL implementation

License

The MIT License (MIT).