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.
Install via Composer:
composer require --dev fedorgrecha/php-graphql-projection
Publish configuration:
php artisan vendor:publish --tag=config-graphql-projection
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).
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
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
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());
}
}
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();
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);
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();
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();
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();
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();
}
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();
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
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
}
}
Execute GraphQL requests: Methods:
execute(GraphQLQueryRequest $request): GraphQLResponse
- Execute query, return raw responseexecuteAndExtract(GraphQLQueryRequest $request): mixed
- Execute query, return typed object
GraphQL response wrapper:
Methods:
getData(): mixed
- Get response datagetErrors(): array
- Get errors arrayhasErrors(): bool
- Check if response has errorsgetFirstError(): ?array
- Get first errorisSuccessful(): bool
- Check if request was successfulgetRawResponse(): array
- Get raw response array
Exception thrown on GraphQL errors:
Methods:
getErrors(): array
- Get GraphQL errorsgetRawResponse(): array
- Get full responsegetMessage(): string
- Get error message
Navigate through nested projections:
Methods:
getRoot()
- Return to root projectiongetParent()
- Return to parent projection
$projection = RootProjection::new()
->field1()
->level1()
->field2()
->level2()
->field3()
->level3()
->field4()
->getRoot() // Back to RootProjection
->field5()
->level6()
->getParent() // Back to field5;
All Input and Type classes support builder pattern:
$input = ArticleInput::builder()
->field1($value1)
->field2($value2)
->optionalField($value3) // Optional fields
->build();
String
,Int
,Float
,Boolean
,ID
- Custom scalars (with automatic conversion)
- 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
- 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
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;
}
}
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();
}
}
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();
}
}
- PHP 8.2 or higher
- Laravel 10.x, 11.x, or 12.x
- Lighthouse or Rebing GraphQL Laravel package
- Lighthouse - Laravel GraphQL server
- Rebing GraphQL Laravel - Laravel GraphQL implementation
The MIT License (MIT).