Sailor
A typesafe GraphQL client for PHP
Install / Use
/learn @spawnia/SailorREADME
A typesafe GraphQL client for PHP
</div>Motivation
GraphQL provides typesafe API access through the schema definition each server provides through introspection. Sailor leverages that information to enable an ergonomic workflow and reduce type-related bugs in your code.
The native GraphQL query language is the most universally used tool to formulate GraphQL queries and works natively with the entire ecosystem of GraphQL tools. Sailor takes the plain queries you write and generates executable PHP code, using the server schema to generate typesafe operations and results.
Installation
Install Sailor through composer by running:
composer require spawnia/sailor
If you want to use the built-in default Client (see Client implementations):
composer require guzzlehttp/guzzle
If you want to use the PSR-18 Client and don't have PSR-17 Request and Stream factory implementations (see Client implementations):
composer require nyholm/psr7
Configuration
Run vendor/bin/sailor to set up the configuration.
A file called sailor.php will be created in your project root.
A Sailor configuration file is expected to return an associative array
where the keys are endpoint names and the values are instances of Spawnia\Sailor\EndpointConfig.
You can take a look at the example configuration to see what options
are available for configuration: sailor.php.
If you would like to use multiple configuration files,
specify which file to use through the -c/--config option.
It is quite useful to include dynamic values in your configuration.
You might use PHP dotenv to load
environment variables (run composer require vlucas/phpdotenv if you do not have it installed already.).
# sailor.php
+$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
+$dotenv->load();
...
public function makeClient(): Client
{
return new \Spawnia\Sailor\Client\Guzzle(
- 'https://hardcoded.url',
+ getenv('EXAMPLE_API_URL'),
[
'headers' => [
- 'Authorization' => 'hardcoded-api-token',
+ 'Authorization' => getenv('EXAMPLE_API_TOKEN'),
],
]
);
}
Client implementations
Sailor provides a few built-in clients:
Spawnia\Sailor\Client\Guzzle: Default HTTP clientSpawnia\Sailor\Client\Psr18: PSR-18 HTTP clientSpawnia\Sailor\Client\Log: Used for testing
You can bring your own by implementing the interface Spawnia\Sailor\Client.
Dynamic clients
You can configure clients dynamically for specific operations or per request:
use Example\Api\Operations\HelloSailor;
/** @var \Spawnia\Sailor\Client $client Somehow instantiated dynamically */
HelloSailor::setClient($client);
// Will use $client over the client from EndpointConfig
$result = HelloSailor::execute();
// Reverts to using the client from EndpointConfig
HelloSailor::setClient(null);
Custom types
Custom scalars are commonly serialized as strings, but may also use other representations.
Without knowing about the contents of the type, Sailor can not do any conversions or provide more accurate type hints, so it uses mixed.
Since enums are only supported from PHP 8.1 and this library still supports PHP 7.4,
it generates enums as a class with string constants and handles values as string.
You may leverage native PHP enums by overriding EndpointConfig::enumTypeConfig()
and return an instance of Spawnia\Sailor\Type\NativeEnumTypeConfig.
Overwrite EndpointConfig::configureTypes() to specialize how Sailor deals with the types within your schema.
See examples/custom-types.
Error conversion
Errors sent within the GraphQL response must follow the response errors specification.
Sailor converts the plain stdClass obtained from decoding the JSON response into
instances of \Spawnia\Sailor\Error\Error by default.
If one of your endpoints returns structured data in extensions, you can customize how
the plain errors are decoded into class instances by overwriting EndpointConfig::parseError().
Usage
Introspection
Run vendor/bin/sailor introspect to update your schema with the latest changes
from the server by running an introspection query. As an example, a very simple
server might result in the following file being placed in your project:
# schema.graphql
type Query {
hello(name: String): String
}
Define operations
Put your queries and mutations into .graphql files and place them anywhere within your
configured project directory. You are free to name and structure the files in any way.
Let's query the example schema from above:
# src/example.graphql
query HelloSailor {
hello(name: "Sailor")
}
You must give all your operations unique PascalCase names, the following example is invalid:
# Invalid, operations have to be named
query {
anonymous
}
# Invalid, names must be unique across all operations
query Foo { ... }
mutation Foo { ... }
# Invalid, names must be PascalCase
query camelCase { ... }
Generate code
Run vendor/bin/sailor to generate PHP code for your operations.
For the example above, Sailor will generate a class called HelloSailor,
place it in the configured namespace and write it to the configured location.
namespace Example\Api\Operations;
class HelloSailor extends \Spawnia\Sailor\Operation { ... }
There are additional generated classes that represent the results of calling the operations. The plain data from the server is wrapped up and contained within those value classes, so you can access them in a typesafe way.
Execute queries
You are now set up to run a query against the server,
just call the execute() function of the new query class:
$result = \Example\Api\Operations\HelloSailor::execute();
The returned $result is going to be a class that extends \Spawnia\Sailor\Result and
holds the decoded response returned from the server.
You can just grab the $data, $errors or $extensions off of it:
$result->data // `null` or a generated subclass of `\Spawnia\Sailor\ObjectLike`
$result->errors // `null` or a list of `\Spawnia\Sailor\Error\Error`
$result->extensions // `null` or an arbitrary map
Error handling
You can ensure an operation returned the proper data and contained no errors:
$errorFreeResult = \Example\Api\Operations\HelloSailor::execute()
->errorFree(); // Throws if there are errors or returns an error free result
The $errorFreeResult is going to be a class that extends \Spawnia\Sailor\ErrorFreeResult.
Given it can only be obtained by going through validation,
it is guaranteed to have non-null $data and does not have $errors:
$errorFreeResult->data // a generated subclass of `\Spawnia\Sailor\ObjectLike`
$errorFreeResult->extensions // `null` or an arbitrary map
If you do not need to access the data and just want to ensure a mutation was successful, the following is more efficient as it does not instantiate a new object:
\Example\Api\Operations\SomeMutation::execute()
->assertErrorFree(); // Throws if there are errors
Queries with arguments
Your generated operation classes will be annotated with the arguments your query defines.
class HelloSailor extends \Spawnia\Sailor\Operation
{
public static function execute(string $required, ?\Example\Api\Types\SomeInput $input = null): HelloSailor\HelloSailorResult { ... }
}
Inputs can be built up incrementally:
$input = new \Example\Api\Types\SomeInput;
$input->foo = 'bar';
If you are using PHP 8, instantiation with named arguments can be quite useful to ensure your input is completely filled:
\Example\Api\Types\SomeInput::make(foo: 'bar')
Partial inputs
GraphQL often uses a pattern of partial inputs - the equivalent of an HTTP PATCH.
Consider the following input:
input SomeInput {
requiredID: Int!
firstOptional: Int
secondOptional: Int
}
Suppose we allow instantiation in PHP with the following implementation:
class SomeInput extends \Spawnia\Sailor\ObjectLike
{
public static function make(
int $requiredID,
?int $firstOptional = null,
?int $secondOptional = null,
): self {
$instance = new self;
$instance->requiredID = $required;
$instance->firstOptional = $firstOptional;
$instance->secondOptional = $secondOptional;
return $instance;
}
}
Given that implementation, the following call will produce the following JSON payload:
SomeInput::make(requiredID: 1, secondOptional: 2);
{ "requiredID": 1, "firstOptional": null, "secondOptional": 2 }
However, we would like to produce the following JSON payload:
{ "requiredID": 1, "secondOptional": 2 }
This is because from within make(), there is no way to differentiate between an explicitly
passed optional named argument and one that has been assigned the default value.
Thus, the resulting JSON payload will unintentionally modify `firstOptional
Related Skills
node-connect
352.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
352.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
352.5kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
