{"id":16817697,"url":"https://github.com/spawnia/sailor","last_synced_at":"2025-05-16T09:03:57.198Z","repository":{"id":41390110,"uuid":"207396174","full_name":"spawnia/sailor","owner":"spawnia","description":"A typesafe GraphQL client for PHP","archived":false,"fork":false,"pushed_at":"2025-04-07T08:54:03.000Z","size":689,"stargazers_count":84,"open_issues_count":4,"forks_count":18,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-05-11T04:13:22.787Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spawnia.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-09-09T20:14:09.000Z","updated_at":"2025-04-17T14:53:10.000Z","dependencies_parsed_at":"2023-11-15T17:31:33.160Z","dependency_job_id":"747f764f-047c-47b2-8e09-3e11f6c2af8a","html_url":"https://github.com/spawnia/sailor","commit_stats":{"total_commits":252,"total_committers":9,"mean_commits":28.0,"dds":0.5277777777777778,"last_synced_commit":"3c167f404e6d5441f8a845b5a78cf5dd5c592c87"},"previous_names":[],"tags_count":60,"template":false,"template_full_name":"spawnia/php-package-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spawnia%2Fsailor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spawnia%2Fsailor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spawnia%2Fsailor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spawnia%2Fsailor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spawnia","download_url":"https://codeload.github.com/spawnia/sailor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254501556,"owners_count":22081528,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T10:47:59.973Z","updated_at":"2025-05-16T09:03:57.166Z","avatar_url":"https://github.com/spawnia.png","language":"PHP","readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"sailor.png\" alt=sailor-logo\"\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n[![CI Status](https://github.com/spawnia/sailor/workflows/Validate/badge.svg)](https://github.com/spawnia/sailor/actions)\n[![codecov](https://codecov.io/gh/spawnia/sailor/branch/master/graph/badge.svg)](https://codecov.io/gh/spawnia/sailor)\n\n[![Latest Stable Version](https://poser.pugx.org/spawnia/sailor/v/stable)](https://packagist.org/packages/spawnia/sailor)\n[![Total Downloads](https://poser.pugx.org/spawnia/sailor/downloads)](https://packagist.org/packages/spawnia/sailor)\n\nA typesafe GraphQL client for PHP\n\n\u003c/div\u003e\n\n## Motivation\n\nGraphQL provides typesafe API access through the schema definition each\nserver provides through introspection. Sailor leverages that information\nto enable an ergonomic workflow and reduce type-related bugs in your code.\n\nThe native GraphQL query language is the most universally used tool to formulate\nGraphQL queries and works natively with the entire ecosystem of GraphQL tools.\nSailor takes the plain queries you write and generates executable PHP code,\nusing the server schema to generate typesafe operations and results.\n\n## Installation\n\nInstall Sailor through composer by running:\n\n```shell\ncomposer require spawnia/sailor\n```\n\nIf you want to use the built-in default Client (see [Client implementations](#client-implementations)):\n\n```shell\ncomposer require guzzlehttp/guzzle\n```\n\nIf you want to use the PSR-18 Client and don't have\nPSR-17 Request and Stream factory implementations (see [Client implementations](#client-implementations)):\n\n```shell\ncomposer require nyholm/psr7\n```\n\n## Configuration\n\nRun `vendor/bin/sailor` to set up the configuration.\nA file called `sailor.php` will be created in your project root.\n\nA Sailor configuration file is expected to return an associative array\nwhere the keys are endpoint names and the values are instances of `Spawnia\\Sailor\\EndpointConfig`.\n\nYou can take a look at the example configuration to see what options\nare available for configuration: [`sailor.php`](sailor.php).\n\nIf you would like to use multiple configuration files,\nspecify which file to use through the `-c/--config` option.\n\nIt is quite useful to include dynamic values in your configuration.\nYou might use [PHP dotenv](https://github.com/vlucas/phpdotenv) to load\nenvironment variables (run `composer require vlucas/phpdotenv` if you do not have it installed already.).\n\n```diff\n# sailor.php\n+$dotenv = Dotenv\\Dotenv::createImmutable(__DIR__);\n+$dotenv-\u003eload();\n\n...\n        public function makeClient(): Client\n        {\n            return new \\Spawnia\\Sailor\\Client\\Guzzle(\n-               'https://hardcoded.url',\n+               getenv('EXAMPLE_API_URL'),\n                [\n                    'headers' =\u003e [\n-                       'Authorization' =\u003e 'hardcoded-api-token',\n+                       'Authorization' =\u003e getenv('EXAMPLE_API_TOKEN'),\n                    ],\n                ]\n            );\n        }\n```\n\n### Client implementations\n\nSailor provides a few built-in clients:\n- `Spawnia\\Sailor\\Client\\Guzzle`: Default HTTP client\n- `Spawnia\\Sailor\\Client\\Psr18`: PSR-18 HTTP client\n- `Spawnia\\Sailor\\Client\\Log`: Used for testing\n\nYou can bring your own by implementing the interface `Spawnia\\Sailor\\Client`.\n\n### Dynamic clients\n\nYou can configure clients dynamically for specific operations or per request:\n\n```php\nuse Example\\Api\\Operations\\HelloSailor;\n\n/** @var \\Spawnia\\Sailor\\Client $client Somehow instantiated dynamically */\n\nHelloSailor::setClient($client);\n\n// Will use $client over the client from EndpointConfig\n$result = HelloSailor::execute();\n\n// Reverts to using the client from EndpointConfig\nHelloSailor::setClient(null);\n```\n\n### Custom types\n\nCustom scalars are commonly serialized as strings, but may also use other representations.\nWithout knowing about the contents of the type, Sailor can not do any conversions or provide more accurate type hints, so it uses `mixed`.\n\nSince enums are only supported from PHP 8.1 and this library still supports PHP 7.4,\nit generates enums as a class with string constants and handles values as `string`.\nYou may leverage native PHP enums by overriding `EndpointConfig::enumTypeConfig()`\nand return an instance of `Spawnia\\Sailor\\Type\\NativeEnumTypeConfig`.\n\nOverwrite `EndpointConfig::configureTypes()` to specialize how Sailor deals with the types within your schema.\nSee [examples/custom-types](examples/custom-types).\n\n### Error conversion\n\nErrors sent within the GraphQL response must follow the [response errors specification](http://spec.graphql.org/October2021/#sec-Errors).\nSailor converts the plain `stdClass` obtained from decoding the JSON response into\ninstances of `\\Spawnia\\Sailor\\Error\\Error` by default.\n\nIf one of your endpoints returns structured data in `extensions`, you can customize how\nthe plain errors are decoded into class instances by overwriting `EndpointConfig::parseError()`.\n\n## Usage\n\n### Introspection\n\nRun `vendor/bin/sailor introspect` to update your schema with the latest changes\nfrom the server by running an introspection query. As an example, a very simple\nserver might result in the following file being placed in your project:\n\n```graphql\n# schema.graphql\ntype Query {\n  hello(name: String): String\n}\n```\n\n### Define operations\n\nPut your queries and mutations into `.graphql` files and place them anywhere within your\nconfigured project directory. You are free to name and structure the files in any way.\nLet's query the example schema from above:\n\n```graphql\n# src/example.graphql\nquery HelloSailor {\n  hello(name: \"Sailor\")\n}\n```\n\nYou must give all your operations unique `PascalCase` names, the following example is invalid:\n\n```graphql\n# Invalid, operations have to be named\nquery {\n  anonymous\n}\n\n# Invalid, names must be unique across all operations\nquery Foo { ... }\nmutation Foo { ... }\n\n# Invalid, names must be PascalCase\nquery camelCase { ... }\n```\n\n### Generate code\n\nRun `vendor/bin/sailor` to generate PHP code for your operations.\nFor the example above, Sailor will generate a class called `HelloSailor`,\nplace it in the configured namespace and write it to the configured location.\n\n```php\nnamespace Example\\Api\\Operations;\n\nclass HelloSailor extends \\Spawnia\\Sailor\\Operation { ... }\n```\n\nThere are additional generated classes that represent the results of calling\nthe operations. The plain data from the server is wrapped up and contained\nwithin those value classes, so you can access them in a typesafe way.\n\n### Execute queries\n\nYou are now set up to run a query against the server,\njust call the `execute()` function of the new query class:\n\n```php\n$result = \\Example\\Api\\Operations\\HelloSailor::execute();\n```\n\nThe returned `$result` is going to be a class that extends `\\Spawnia\\Sailor\\Result` and\nholds the decoded response returned from the server.\nYou can just grab the `$data`, `$errors` or `$extensions` off of it:\n\n```php\n$result-\u003edata       // `null` or a generated subclass of `\\Spawnia\\Sailor\\ObjectLike`\n$result-\u003eerrors     // `null` or a list of `\\Spawnia\\Sailor\\Error\\Error`\n$result-\u003eextensions // `null` or an arbitrary map\n```\n\n### Error handling\n\nYou can ensure an operation returned the proper data and contained no errors:\n\n```php\n$errorFreeResult = \\Example\\Api\\Operations\\HelloSailor::execute()\n    -\u003eerrorFree(); // Throws if there are errors or returns an error free result\n```\n\nThe `$errorFreeResult` is going to be a class that extends `\\Spawnia\\Sailor\\ErrorFreeResult`.\nGiven it can only be obtained by going through validation,\nit is guaranteed to have non-null `$data` and does not have `$errors`:\n\n```php\n$errorFreeResult-\u003edata       // a generated subclass of `\\Spawnia\\Sailor\\ObjectLike`\n$errorFreeResult-\u003eextensions // `null` or an arbitrary map\n```\n\nIf you do not need to access the data and just want to ensure a mutation was successful,\nthe following is more efficient as it does not instantiate a new object:\n\n```php\n\\Example\\Api\\Operations\\SomeMutation::execute()\n    -\u003eassertErrorFree(); // Throws if there are errors\n```\n\n### Queries with arguments\n\nYour generated operation classes will be annotated with the arguments your query defines.\n\n```php\nclass HelloSailor extends \\Spawnia\\Sailor\\Operation\n{\n    public static function execute(string $required, ?\\Example\\Api\\Types\\SomeInput $input = null): HelloSailor\\HelloSailorResult { ... }\n}\n```\n\nInputs can be built up incrementally:\n\n```php\n$input = new \\Example\\Api\\Types\\SomeInput;\n$input-\u003efoo = 'bar';\n```\n\nIf you are using PHP 8, instantiation with named arguments can be quite useful to ensure your\ninput is completely filled:\n\n```php\n\\Example\\Api\\Types\\SomeInput::make(foo: 'bar')\n```\n\n### Partial inputs\n\nGraphQL often uses a pattern of partial inputs - the equivalent of an HTTP `PATCH`.\nConsider the following input:\n\n```graphql\ninput SomeInput {\n  requiredID: Int!\n  firstOptional: Int\n  secondOptional: Int\n}\n```\n\nSuppose we allow instantiation in PHP with the following implementation:\n\n```php\nclass SomeInput extends \\Spawnia\\Sailor\\ObjectLike\n{\n    public static function make(\n        int $requiredID,\n        ?int $firstOptional = null,\n        ?int $secondOptional = null,\n    ): self {\n        $instance = new self;\n\n        $instance-\u003erequiredID = $required;\n        $instance-\u003efirstOptional = $firstOptional;\n        $instance-\u003esecondOptional = $secondOptional;\n\n        return $instance;\n    }\n}\n```\n\nGiven that implementation, the following call will produce the following JSON payload:\n\n```php\nSomeInput::make(requiredID: 1, secondOptional: 2);\n```\n\n```json\n{ \"requiredID\": 1, \"firstOptional\": null, \"secondOptional\": 2 }\n```\n\nHowever, we would like to produce the following JSON payload:\n\n```json\n{ \"requiredID\": 1, \"secondOptional\": 2 }\n```\n\nThis is because from within `make()`, there is no way to differentiate between an explicitly\npassed optional named argument and one that has been assigned the default value.\nThus, the resulting JSON payload will unintentionally modify `firstOptional` too,\nerasing whatever value it previously held.\n\nA naive solution to this would be to filter out any argument that is `null`.\nHowever, we would also like to be able to explicitly set the first optional value to `null`.\nThe following call *should* result in a JSON payload that contains `\"firstOptional\": null`.\n\n```php\nSomeInput::make(requiredID: 1, firstOptional: null, secondOptional: 2);\n```\n\nIn order to generate partial inputs by default, optional named arguments have a special default value:\n\n```php\nSpawnia\\Sailor\\ObjectLike::UNDEFINED = 'Special default value that allows Sailor to differentiate between explicitly passing null and not passing a value at all.';\n```\n\n```php\nclass SomeInput extends \\Spawnia\\Sailor\\ObjectLike\n{\n    /**\n     * @param int $requiredID\n     * @param int|null $firstOptional\n     * @param int|null $secondOptional\n     */\n    public static function make(\n        $requiredID,\n        $firstOptional = 'Special default value that allows Sailor to differentiate between explicitly passing null and not passing a value at all.',\n        $secondOptional = 'Special default value that allows Sailor to differentiate between explicitly passing null and not passing a value at all.',\n    ): self {\n        $instance = new self;\n\n        if ($requiredID !== self::UNDEFINED) {\n            $instance-\u003erequiredID = $requiredID;\n        }\n        if ($firstOptional !== self::UNDEFINED) {\n            $instance-\u003efirstOptional = $firstOptional;\n        }\n        if ($secondOptional !== self::UNDEFINED) {\n            $instance-\u003esecondOptional = $secondOptional;\n        }\n\n        return $instance;\n    }\n}\n```\n\nYou may use `Spawnia\\Sailor\\ObjectLike::UNDEFINED` to omit nullable arguments completely:\n\n```php\nSomeInput::make(\n    requiredID: 1,\n    firstOptional: $maybeNull ?? Spawnia\\Sailor\\ObjectLike::UNDEFINED,\n);\n```\n\nIf `$maybeNull` is `null`, this will result in the following JSON payload:\n\n```json\n{ \"requiredID\": 1 }\n```\n\nIn the very unlikely case where you need to pass exactly the value of `Spawnia\\Sailor\\ObjectLike::UNDEFINED`,\nyou can bypass the logic in `make()` and assign it directly:\n\n```php\n$input = SomeInput::make(requiredID: 1);\n$input-\u003esecondOptional = Spawnia\\Sailor\\ObjectLike::UNDEFINED;\n```\n\n### Events\n\nSailor calls `EndpointConfig::handleEvent()` with the following events during the execution lifecycle:\n\n1. [StartRequest](src/Events/StartRequest.php): Fired after calling `execute()` on an `Operation`, before invoking the client.\n2. [ReceiveResponse](src/Events/ReceiveResponse.php): Fired after receiving a GraphQL response from the client.\n\n### PHP keyword collisions\n\nSince GraphQL uses a different set of reserved keywords, names of fields or types may collide with PHP keywords.\nSailor prevents illegal usages of those names in generated code by prefixing them with a single underscore `_`.\n\n## Testing\n\nSailor provides first class support for testing by allowing you to mock operations.\n\n### Setup\n\nIt is assumed you are using [PHPUnit](https://phpunit.de) and [Mockery](https://docs.mockery.io/en/latest).\n\n```shell\ncomposer require --dev phpunit/phpunit mockery/mockery\n```\n\nMake sure your test class - or one of its parents - uses the following traits:\n\n```php\nuse Mockery\\Adapter\\Phpunit\\MockeryPHPUnitIntegration;\nuse PHPUnit\\Framework\\TestCase as PHPUnitTestCase;\nuse Spawnia\\Sailor\\Testing\\RequiresSailorMocks;\n\nabstract class TestCase extends PHPUnitTestCase\n{\n    use MockeryPHPUnitIntegration; // Makes Mockery assertions work\n    use RequiresSailorMocks; // Prevents stray requests and resets mocks between tests\n}\n```\n\nIf you want to perform some kind of integration test where mocks are not required,\nyou may replace `RequiresSailorMocks` with `UsesSailorMocks`.\n\n### Mock results\n\nMocks are registered per operation class:\n\n```php\nuse Example\\Api\\Operations\\HelloSailor;\n\n/** @var \\Mockery\\MockInterface\u0026HelloSailor */\n$mock = HelloSailor::mock();\n```\n\nWhen registered, the mock captures all calls to `HelloSailor::execute()`.\nUse it to build up expectations for what calls it should receive and mock returned results:\n\n```php\n$name = 'Sailor';\n$hello = \"Hello, {$name}!\";\n\n$mock\n    -\u003eexpects('execute')\n    -\u003eonce()\n    -\u003ewith($name)\n    -\u003eandReturn(HelloSailor\\HelloSailorResult::fromData(\n        data: HelloSailor\\HelloSailor::make(\n            hello: $hello,\n        ),\n    ));\n\n$result = HelloSailor::execute(name: $name)\n    -\u003eerrorFree();\n\nself::assertSame($hello, $result-\u003edata-\u003ehello);\n```\n\nSubsequent calls to `::mock()` will return the initially registered mock instance.\n\n```php\n$mock1 = HelloSailor::mock();\n$mock2 = HelloSailor::mock();\nassert($mock1 === $mock2); // true\n```\n\nYou can also simulate a result with errors:\n\n```php\nHelloSailor\\HelloSailorResult::fromErrors([\n    (object) [\n        'message' =\u003e 'Something went wrong',\n    ],\n]);\n```\n\nFor PHP 8 users, it is recommended to use named arguments to build complex mocked results:\n\n```php\nHelloSailor\\HelloSailorResult::fromData(\n    data: HelloSailor\\HelloSailor::make(\n        hello: 'Hello, Sailor!',\n        nested: HelloSailor\\HelloSailor\\Nested::make(\n            hello: 'Hello again!',\n        ),\n    ),\n))\n```\n\n### Integration\n\nIf you want to perform integration testing for a service that uses Sailor without actually\nhitting an external API, you can swap out your client with the `Log` client.\nIt writes all requests made through Sailor to a file of your choice.\n\n\u003e The `Log` client can not know what constitutes a valid response for a given request,\n\u003e so it always responds with an error.\n\n```php\n# sailor.php\npublic function makeClient(): Client\n{\n    return new \\Spawnia\\Sailor\\Client\\Log(__DIR__ . '/sailor-requests.log');\n}\n```\n\nEach request goes on a new line and contains a JSON string that holds the `query` and `variables`:\n\n```json\n{\"query\":\"{ foo }\",\"variables\":{\"bar\":42}}\n{\"query\":\"mutation { baz }\",\"variables\":null}\n```\n\nThis allows you to perform assertions on the calls that were made.\nThe `Log` client offers a convenient method of reading the requests as structured data:\n\n```php\n$log = new \\Spawnia\\Sailor\\Client\\Log(__DIR__ . '/sailor-requests.log');\nforeach ($log-\u003erequests() as $request) {\n    var_dump($request);\n}\n\narray(2) {\n  [\"query\"]=\u003e\n  string(7) \"{ foo }\"\n  [\"variables\"]=\u003e\n  array(1) {\n    [\"bar\"]=\u003e\n    int(42)\n  }\n}\narray(2) {\n  [\"query\"]=\u003e\n  string(7) \"mutation { baz }\"\n  [\"variables\"]=\u003e\n  NULL\n}\n```\n\nTo clean up the log after performing tests, use `Log::clear()`.\n\n## Examples\n\nYou can find examples of how a project would use Sailor within [examples](examples).\n\n## Changelog\n\nSee [`CHANGELOG.md`](CHANGELOG.md).\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## License\n\nThis package is licensed using the MIT License.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspawnia%2Fsailor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspawnia%2Fsailor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspawnia%2Fsailor/lists"}