Skip to content

PHP SDK

The FraiseQL PHP SDK is a schema authoring SDK: you define GraphQL types using PHP 8 attributes and register queries and mutations using a fluent builder API, and the FraiseQL compiler generates an optimized GraphQL API backed by your SQL views.

Terminal window
composer require fraiseql/fraiseql-php

Requirements: PHP 8.2+

FraiseQL PHP uses PHP 8 attributes for type definitions and a fluent builder API for queries and mutations:

ConstructGraphQL EquivalentPurpose
#[GraphQLType]typeMark a class as a GraphQL output type
#[GraphQLType(isInput: true)]inputMark a class as a GraphQL input type
#[GraphQLField]field metadataConfigure field types, nullability, scopes
StaticAPI::query(name)...Query fieldWire a query to a SQL view
StaticAPI::mutation(name)...Mutation fieldDefine a mutation

Annotate classes with #[GraphQLType] and properties with #[GraphQLField]. Use Scalars:: constants to specify GraphQL scalar types:

schema/User.php
<?php
declare(strict_types=1);
namespace Schema;
use FraiseQL\Attributes\GraphQLType;
use FraiseQL\Attributes\GraphQLField;
use FraiseQL\Scalars;
#[GraphQLType]
class User
{
#[GraphQLField(type: Scalars::ID, nullable: false)]
public string $id;
#[GraphQLField(nullable: false)]
public string $username;
#[GraphQLField(type: Scalars::EMAIL, nullable: false)]
public string $email;
#[GraphQLField(nullable: true)]
public ?string $bio;
#[GraphQLField(type: Scalars::DATE_TIME, nullable: false)]
public string $createdAt;
}
schema/Post.php
<?php
declare(strict_types=1);
namespace Schema;
use FraiseQL\Attributes\GraphQLType;
use FraiseQL\Attributes\GraphQLField;
use FraiseQL\Scalars;
#[GraphQLType]
class Post
{
#[GraphQLField(type: Scalars::ID, nullable: false)]
public string $id;
#[GraphQLField(nullable: false)]
public string $title;
#[GraphQLField(type: Scalars::SLUG, nullable: false)]
public string $slug;
#[GraphQLField(nullable: false)]
public string $content;
#[GraphQLField(nullable: false)]
public bool $isPublished;
#[GraphQLField(type: Scalars::DATE_TIME, nullable: false)]
public string $createdAt;
#[GraphQLField(type: Scalars::DATE_TIME, nullable: false)]
public string $updatedAt;
}

Scalar type constants are defined in FraiseQL\Scalars. Use them in #[GraphQLField(type: ...)]:

use FraiseQL\Scalars;
Scalars::ID // 'ID'
Scalars::EMAIL // 'Email'
Scalars::SLUG // 'Slug'
Scalars::DATE_TIME // 'DateTime'
Scalars::URL // 'URL'
Scalars::DATE // 'Date'
Scalars::UUID // 'UUID'
Scalars::DECIMAL // 'Decimal'
Scalars::JSON // 'Json'
Scalars::PHONE_NUMBER // 'PhoneNumber'

Scalars are string constants only. Validation and serialization happen in the FraiseQL Rust runtime after compilation.

Use scope or scopes on #[GraphQLField] for field-level access control:

#[GraphQLType]
class User
{
#[GraphQLField(type: Scalars::ID, nullable: false)]
public string $id;
#[GraphQLField(
type: Scalars::EMAIL,
nullable: false,
scope: 'read:user.email',
description: 'Email address — requires read:user.email scope'
)]
public string $email;
#[GraphQLField(
type: Scalars::DECIMAL,
nullable: true,
scopes: ['read:user.salary', 'admin'],
description: 'Salary — requires both scopes'
)]
public ?string $salary;
}

Use #[GraphQLType(isInput: true)] to declare a class as a GraphQL input type:

schema/CreatePostInput.php
<?php
declare(strict_types=1);
namespace Schema;
use FraiseQL\Attributes\GraphQLType;
use FraiseQL\Attributes\GraphQLField;
#[GraphQLType(isInput: true)]
class CreatePostInput
{
#[GraphQLField(nullable: false)]
public string $title;
#[GraphQLField(nullable: false)]
public string $content;
#[GraphQLField(nullable: false)]
public bool $isPublished;
}
schema/UpdatePostInput.php
<?php
declare(strict_types=1);
namespace Schema;
use FraiseQL\Attributes\GraphQLType;
use FraiseQL\Attributes\GraphQLField;
use FraiseQL\Scalars;
#[GraphQLType(isInput: true)]
class UpdatePostInput
{
#[GraphQLField(type: Scalars::ID, nullable: false)]
public string $id;
#[GraphQLField(nullable: true)]
public ?string $title;
#[GraphQLField(nullable: true)]
public ?string $content;
#[GraphQLField(nullable: true)]
public ?bool $isPublished;
}

Use StaticAPI::query(name) to create a QueryBuilder, chain configuration methods, and call ->register() to add the query to the schema registry:

schema/setup.php
<?php
declare(strict_types=1);
use FraiseQL\StaticAPI;
use Schema\User;
use Schema\Post;
use Schema\CreatePostInput;
use Schema\UpdatePostInput;
// Register types from annotated classes
StaticAPI::register(User::class);
StaticAPI::register(Post::class);
StaticAPI::register(CreatePostInput::class);
StaticAPI::register(UpdatePostInput::class);
// List query — maps to v_post view
StaticAPI::query('posts')
->returnType('Post')
->returnsList(true)
->sqlSource('v_post')
->argument('isPublished', 'Boolean', nullable: true)
->argument('limit', 'Int', nullable: true, default: 20)
->argument('offset', 'Int', nullable: true, default: 0)
->description('Fetch posts, optionally filtered by published status.')
->register();
// Single-item query — maps to v_post with id filter
StaticAPI::query('post')
->returnType('Post')
->sqlSource('v_post')
->argument('id', 'ID', nullable: false)
->description('Fetch a single post by ID.')
->register();

Query arguments matching columns in the backing view become SQL WHERE clauses automatically. See SQL Patterns → Automatic WHERE Clauses for details.


Use StaticAPI::mutation(name) to create a MutationBuilder. Each mutation maps to a PostgreSQL function using the fn_ naming convention:

schema/setup.php
<?php
declare(strict_types=1);
use FraiseQL\StaticAPI;
// Maps to fn_create_post PostgreSQL function
StaticAPI::mutation('createPost')
->returnType('Post')
->sqlSource('fn_create_post')
->argument('input', 'CreatePostInput', nullable: false)
->operation('insert')
->invalidatesViews(['v_post'])
->description('Create a new blog post.')
->register();
// Maps to fn_publish_post PostgreSQL function
StaticAPI::mutation('publishPost')
->returnType('Post')
->sqlSource('fn_publish_post')
->argument('id', 'ID', nullable: false)
->requiresRole('editor')
->operation('update')
->invalidatesViews(['v_post'])
->description('Publish a draft post.')
->register();
// Maps to fn_delete_post PostgreSQL function
StaticAPI::mutation('deletePost')
->returnType('Post')
->sqlSource('fn_delete_post')
->argument('id', 'ID', nullable: false)
->requiresRole('admin')
->operation('delete')
->invalidatesViews(['v_post'])
->description('Soft-delete a post.')
->register();

Mutations wire to PostgreSQL functions returning mutation_response. Views return (id UUID, data JSONB). See SQL Patterns for complete table, view, and function templates.


Transport annotations are optional. Omit them to serve an operation via GraphQL only. Use the #[Query] and #[Mutation] function-level attributes from FraiseQL\SDK\Attributes with restPath and restMethod named arguments. gRPC endpoints are auto-generated when [grpc] is enabled — no per-operation annotation needed. See gRPC Transport.

use FraiseQL\SDK\Attributes\Query;
use FraiseQL\SDK\Attributes\Mutation;
#[Query(
sqlSource: 'v_post',
restPath: '/posts',
restMethod: 'GET',
)]
function posts(int $limit = 10): array { return []; }
#[Query(
sqlSource: 'v_post',
restPath: '/posts/{id}',
restMethod: 'GET',
)]
function post(string $id): Post { return new Post(); }
#[Mutation(
sqlSource: 'create_post',
operation: 'CREATE',
restPath: '/posts',
restMethod: 'POST',
)]
function createPost(string $title, string $authorId): Post { return new Post(); }

Path parameters in restPath (e.g., {id}) must match function parameter names exactly. A mismatch produces a compile-time error. Duplicate (method, path) pairs are also rejected at compile time.


Use .requiresRole(role) on any query or mutation builder to restrict access to callers who hold the specified role:

StaticAPI::query('adminStats')
->returnType('AdminStats')
->sqlSource('v_admin_stats')
->requiresRole('admin')
->description('Aggregate statistics — admin only.')
->register();
StaticAPI::mutation('deletePost')
->returnType('Post')
->sqlSource('fn_delete_post')
->argument('id', 'ID', nullable: false)
->requiresRole('admin')
->register();

Use ->inject(array) to inject server-side values from the JWT without exposing them as client-controlled arguments:

StaticAPI::query('myPosts')
->returnType('Post')
->returnsList(true)
->sqlSource('v_post')
->argument('limit', 'Int', nullable: true, default: 20)
->inject(['author_id' => 'jwt:sub'])
->description("Fetch the current user's posts.")
->register();

  1. Register all types and operations, then export the schema:

    export-schema.php
    <?php
    declare(strict_types=1);
    require_once __DIR__ . '/vendor/autoload.php';
    require_once __DIR__ . '/schema/setup.php';
    use FraiseQL\StaticAPI;
    $schema = StaticAPI::exportSchema();
    file_put_contents('schema.json', json_encode($schema, JSON_PRETTY_PRINT));
    echo "Schema exported to schema.json\n";
  2. Run the export script:

    Terminal window
    php export-schema.php
  3. Compile with the CLI:

    Terminal window
    fraiseql compile --schema schema.json
  4. Serve the API:

    Terminal window
    fraiseql run