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.
Installation
Section titled “Installation”composer require fraiseql/fraiseql-phpRequirements: PHP 8.2+
Core Concepts
Section titled “Core Concepts”FraiseQL PHP uses PHP 8 attributes for type definitions and a fluent builder API for queries and mutations:
| Construct | GraphQL Equivalent | Purpose |
|---|---|---|
#[GraphQLType] | type | Mark a class as a GraphQL output type |
#[GraphQLType(isInput: true)] | input | Mark a class as a GraphQL input type |
#[GraphQLField] | field metadata | Configure field types, nullability, scopes |
StaticAPI::query(name)... | Query field | Wire a query to a SQL view |
StaticAPI::mutation(name)... | Mutation field | Define a mutation |
Defining Types
Section titled “Defining Types”Annotate classes with #[GraphQLType] and properties with #[GraphQLField]. Use Scalars:: constants to specify GraphQL scalar types:
<?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;}<?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;}Built-in Scalars
Section titled “Built-in Scalars”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.
Field-Level Scopes
Section titled “Field-Level Scopes”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;}Defining Inputs
Section titled “Defining Inputs”Use #[GraphQLType(isInput: true)] to declare a class as a GraphQL input type:
<?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;}<?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;}Defining Queries
Section titled “Defining Queries”Use StaticAPI::query(name) to create a QueryBuilder, chain configuration methods, and call ->register() to add the query to the schema registry:
<?php
declare(strict_types=1);
use FraiseQL\StaticAPI;use Schema\User;use Schema\Post;use Schema\CreatePostInput;use Schema\UpdatePostInput;
// Register types from annotated classesStaticAPI::register(User::class);StaticAPI::register(Post::class);StaticAPI::register(CreatePostInput::class);StaticAPI::register(UpdatePostInput::class);
// List query — maps to v_post viewStaticAPI::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 filterStaticAPI::query('post') ->returnType('Post') ->sqlSource('v_post') ->argument('id', 'ID', nullable: false) ->description('Fetch a single post by ID.') ->register();How Arguments Become WHERE Clauses
Section titled “How Arguments Become WHERE Clauses”Query arguments matching columns in the backing view become SQL WHERE clauses automatically. See SQL Patterns → Automatic WHERE Clauses for details.
Defining Mutations
Section titled “Defining Mutations”Use StaticAPI::mutation(name) to create a MutationBuilder. Each mutation maps to a PostgreSQL function using the fn_ naming convention:
<?php
declare(strict_types=1);
use FraiseQL\StaticAPI;
// Maps to fn_create_post PostgreSQL functionStaticAPI::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 functionStaticAPI::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 functionStaticAPI::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();The SQL Side
Section titled “The SQL Side”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
Section titled “Transport Annotations”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.
Authorization
Section titled “Authorization”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();JWT Claim Injection
Section titled “JWT Claim Injection”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();Build and Export
Section titled “Build and Export”-
Register all types and operations, then export the schema:
export-schema.php <?phpdeclare(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"; -
Run the export script:
Terminal window php export-schema.php -
Compile with the CLI:
Terminal window fraiseql compile --schema schema.json -
Serve the API:
Terminal window fraiseql run
Next Steps
Section titled “Next Steps”- SDK Overview — how compile-time authoring works
- SQL Patterns — view and function conventions
- Your First API — full tutorial
- All SDKs — compare languages