From 6a42958947e59fec1626e3957d9534899872cc65 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Mon, 7 Oct 2024 10:54:45 +0200 Subject: [PATCH] Escape class names of result types named after PHP reserved keywords --- CHANGELOG.md | 6 +++ README.md | 6 +-- .../expected/Operations/AllCases.php | 44 ++++++++++++++++++ .../expected/Operations/AllCases/AllCases.php | 45 +++++++++++++++++++ .../AllCases/AllCasesErrorFreeResult.php | 18 ++++++++ .../Operations/AllCases/AllCasesResult.php | 41 +++++++++++++++++ .../Operations/AllCases/Cases/_Case.php | 45 +++++++++++++++++++ examples/php-keywords/sailor.php | 42 ++++++++++++----- examples/php-keywords/schema.graphql | 5 +++ .../php-keywords/src/ReservedKeywords.graphql | 6 +++ examples/php-keywords/src/test.php | 18 +++++--- src/Client/Log.php | 4 +- src/Codegen/File.php | 4 +- src/Codegen/Generator.php | 2 - src/Codegen/ObjectLikeBuilder.php | 6 +-- src/Codegen/OperationBuilder.php | 4 +- src/Console/InteractsWithEndpoints.php | 4 +- src/Convert/IDConverter.php | 4 +- src/Convert/NullConverter.php | 4 +- src/Convert/PolymorphicConverter.php | 4 +- src/Error/Location.php | 4 +- src/Error/OriginatesFromEndpoint.php | 4 +- src/ErrorFreeResult.php | 4 +- src/Events/ReceiveResponse.php | 4 +- src/Events/StartRequest.php | 4 +- src/Json.php | 4 +- src/Result.php | 4 +- src/Testing/MockClient.php | 6 +-- src/Type/InputObjectTypeConfig.php | 6 +-- src/Type/TypeConfig.php | 4 +- tests/Unit/Convert/TypeConverterTest.php | 4 +- tests/Unit/Testing/Invokable.php | 7 +-- 32 files changed, 283 insertions(+), 84 deletions(-) create mode 100644 examples/php-keywords/expected/Operations/AllCases.php create mode 100644 examples/php-keywords/expected/Operations/AllCases/AllCases.php create mode 100644 examples/php-keywords/expected/Operations/AllCases/AllCasesErrorFreeResult.php create mode 100644 examples/php-keywords/expected/Operations/AllCases/AllCasesResult.php create mode 100644 examples/php-keywords/expected/Operations/AllCases/Cases/_Case.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ae5e6e..799c8c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## v0.33.1 + +### Fixed + +- Escape class names of result types named after PHP reserved keywords + ## v0.33.0 ### Changed diff --git a/README.md b/README.md index 3193ebe1..dcb92f2d 100644 --- a/README.md +++ b/README.md @@ -202,9 +202,9 @@ holds the decoded response returned from the server. You can just grab the `$dat or `$extensions` off of it: ```php -$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 +$catchResult->data // `null` or a generated subclass of `\Spawnia\Sailor\ObjectLike` +$catchResult->errors // `null` or a list of `\Spawnia\Sailor\Error\Error` +$catchResult->extensions // `null` or an arbitrary map ``` ### Error handling diff --git a/examples/php-keywords/expected/Operations/AllCases.php b/examples/php-keywords/expected/Operations/AllCases.php new file mode 100644 index 00000000..594e6a6f --- /dev/null +++ b/examples/php-keywords/expected/Operations/AllCases.php @@ -0,0 +1,44 @@ + + */ +class AllCases extends \Spawnia\Sailor\Operation +{ + public static function execute(): AllCases\AllCasesResult + { + return self::executeOperation( + ); + } + + protected static function converters(): array + { + static $converters; + + return $converters ??= [ + ]; + } + + public static function document(): string + { + return /* @lang GraphQL */ 'query AllCases { + __typename + cases { + __typename + id + } + }'; + } + + public static function endpoint(): string + { + return 'php-keywords'; + } + + public static function config(): string + { + return \Safe\realpath(__DIR__ . '/../../sailor.php'); + } +} diff --git a/examples/php-keywords/expected/Operations/AllCases/AllCases.php b/examples/php-keywords/expected/Operations/AllCases/AllCases.php new file mode 100644 index 00000000..d9f105e4 --- /dev/null +++ b/examples/php-keywords/expected/Operations/AllCases/AllCases.php @@ -0,0 +1,45 @@ + $cases + * @property string $__typename + */ +class AllCases extends \Spawnia\Sailor\ObjectLike +{ + /** + * @param array $cases + */ + public static function make($cases): self + { + $instance = new self; + + if ($cases !== self::UNDEFINED) { + $instance->cases = $cases; + } + $instance->__typename = 'Query'; + + return $instance; + } + + protected function converters(): array + { + static $converters; + + return $converters ??= [ + 'cases' => new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\ListConverter(new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\PhpKeywords\Operations\AllCases\Cases\_Case))), + '__typename' => new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\StringConverter), + ]; + } + + public static function endpoint(): string + { + return 'php-keywords'; + } + + public static function config(): string + { + return \Safe\realpath(__DIR__ . '/../../../sailor.php'); + } +} diff --git a/examples/php-keywords/expected/Operations/AllCases/AllCasesErrorFreeResult.php b/examples/php-keywords/expected/Operations/AllCases/AllCasesErrorFreeResult.php new file mode 100644 index 00000000..5be9f98d --- /dev/null +++ b/examples/php-keywords/expected/Operations/AllCases/AllCasesErrorFreeResult.php @@ -0,0 +1,18 @@ +data = AllCases::fromStdClass($data); + } + + /** + * Useful for instantiation of successful mocked results. + * + * @return static + */ + public static function fromData(AllCases $data): self + { + $instance = new static; + $instance->data = $data; + + return $instance; + } + + public function errorFree(): AllCasesErrorFreeResult + { + return AllCasesErrorFreeResult::fromResult($this); + } + + public static function endpoint(): string + { + return 'php-keywords'; + } + + public static function config(): string + { + return \Safe\realpath(__DIR__ . '/../../../sailor.php'); + } +} diff --git a/examples/php-keywords/expected/Operations/AllCases/Cases/_Case.php b/examples/php-keywords/expected/Operations/AllCases/Cases/_Case.php new file mode 100644 index 00000000..77b945d7 --- /dev/null +++ b/examples/php-keywords/expected/Operations/AllCases/Cases/_Case.php @@ -0,0 +1,45 @@ +id = $id; + } + $instance->__typename = 'Case'; + + return $instance; + } + + protected function converters(): array + { + static $converters; + + return $converters ??= [ + 'id' => new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\IDConverter), + '__typename' => new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\StringConverter), + ]; + } + + public static function endpoint(): string + { + return 'php-keywords'; + } + + public static function config(): string + { + return \Safe\realpath(__DIR__ . '/../../../../sailor.php'); + } +} diff --git a/examples/php-keywords/sailor.php b/examples/php-keywords/sailor.php index 9a037677..03b4c7b5 100644 --- a/examples/php-keywords/sailor.php +++ b/examples/php-keywords/sailor.php @@ -33,17 +33,37 @@ public function finder(): Finder public function makeClient(): Client { - return new MockClient(static fn(): Response => Response::fromStdClass((object) [ - 'data' => (object) [ - '__typename' => 'Query', - 'print' => (object) [ - '__typename' => 'Switch', - 'for' => _abstract::_class, - 'int' => 42, - 'as' => 69, - ], - ], - ])); + return new MockClient(function (string $query, ?stdClass $variables): Response { + if (str_contains($query, 'print')) { + return Response::fromStdClass((object) [ + 'data' => (object) [ + '__typename' => 'Query', + 'print' => (object) [ + '__typename' => 'Switch', + 'for' => _abstract::_class, + 'int' => 42, + 'as' => 69, + ], + ], + ]); + } + + if (str_contains($query, 'cases')) { + return Response::fromStdClass((object) [ + 'data' => (object) [ + '__typename' => 'Query', + 'cases' => [ + (object) [ + '__typename' => 'Case', + 'id' => 'asdf', + ], + ], + ], + ]); + } + + throw new Exception("Unexpected query: {$query}."); + }); } }, ]; diff --git a/examples/php-keywords/schema.graphql b/examples/php-keywords/schema.graphql index 3135abbb..81081059 100644 --- a/examples/php-keywords/schema.graphql +++ b/examples/php-keywords/schema.graphql @@ -1,5 +1,6 @@ type Query { print: Abstract + cases: [Case!]! } interface Abstract { @@ -18,3 +19,7 @@ enum abstract { input new { unset: Float } + +type Case { + id: ID! +} diff --git a/examples/php-keywords/src/ReservedKeywords.graphql b/examples/php-keywords/src/ReservedKeywords.graphql index edf9a6ac..efe7379f 100644 --- a/examples/php-keywords/src/ReservedKeywords.graphql +++ b/examples/php-keywords/src/ReservedKeywords.graphql @@ -9,3 +9,9 @@ query Catch { } } } + +query AllCases { + cases { + id + } +} diff --git a/examples/php-keywords/src/test.php b/examples/php-keywords/src/test.php index a6def90c..556060b0 100644 --- a/examples/php-keywords/src/test.php +++ b/examples/php-keywords/src/test.php @@ -3,14 +3,22 @@ require __DIR__ . '/../vendor/autoload.php'; use Spawnia\Sailor\PhpKeywords\Operations\_Catch; -use Spawnia\Sailor\PhpKeywords\Operations\_Catch\_Print\_Switch; +use Spawnia\Sailor\PhpKeywords\Operations\AllCases; use Spawnia\Sailor\PhpKeywords\Types\_abstract; -$result = _Catch::execute(); - -$switch = $result->data->print; -assert($switch instanceof _Switch); +$catchResult = _Catch::execute(); +$switch = $catchResult->data->print; +assert($switch instanceof _Catch\_Print\_Switch); assert($switch->for === _abstract::_class); assert($switch->int === 42); assert($switch->as === 69); + +$allCasesResult = AllCases::execute(); + +$cases = $allCasesResult->data->cases; +assert(is_array($cases)); + +$case1 = $cases[0]; +assert($case1 instanceof AllCases\Cases\_Case); +assert($case1->id === 'asdf'); diff --git a/src/Client/Log.php b/src/Client/Log.php index 35a2d8fa..8b0f15bc 100644 --- a/src/Client/Log.php +++ b/src/Client/Log.php @@ -41,8 +41,8 @@ public function request(string $query, ?\stdClass $variables = null): Response /** * @return Generator|null, + * query: string, + * variables: array|null, * }> */ public function requests(): \Generator diff --git a/src/Codegen/File.php b/src/Codegen/File.php index 82896935..2bc63e08 100644 --- a/src/Codegen/File.php +++ b/src/Codegen/File.php @@ -2,9 +2,7 @@ namespace Spawnia\Sailor\Codegen; -/** - * A generated file that should be written to a target. - */ +/** A generated file that should be written to a target. */ class File { public string $content; diff --git a/src/Codegen/Generator.php b/src/Codegen/Generator.php index 1e8b8dfb..e09f5c5a 100644 --- a/src/Codegen/Generator.php +++ b/src/Codegen/Generator.php @@ -160,8 +160,6 @@ protected static function asPhpFile(ClassType $classType, PhpNamespace $namespac * * @param array $documents * - * @throws SyntaxError - * * @return array */ public static function parseDocuments(array $documents): array diff --git a/src/Codegen/ObjectLikeBuilder.php b/src/Codegen/ObjectLikeBuilder.php index 1629063b..dccdb5ef 100644 --- a/src/Codegen/ObjectLikeBuilder.php +++ b/src/Codegen/ObjectLikeBuilder.php @@ -10,9 +10,7 @@ use Nette\PhpGenerator\PhpNamespace; use Spawnia\Sailor\ObjectLike; -/** - * @phpstan-type PropertyArgs array{string, Type, string, string, mixed} - */ +/** @phpstan-type PropertyArgs array{string, Type, string, string, mixed} */ class ObjectLikeBuilder { private bool $isInputType; @@ -32,7 +30,7 @@ class ObjectLikeBuilder public function __construct(string $name, string $namespace, bool $isInputType) { $class = new ClassType( - $name, + Escaper::escapeClassName($name), new PhpNamespace($namespace) // TODO drop escape when min PHP version is 8.0+ ); diff --git a/src/Codegen/OperationBuilder.php b/src/Codegen/OperationBuilder.php index eecfbd98..3ca9f520 100644 --- a/src/Codegen/OperationBuilder.php +++ b/src/Codegen/OperationBuilder.php @@ -10,9 +10,7 @@ use Spawnia\Sailor\ObjectLike; use Spawnia\Sailor\Operation; -/** - * @phpstan-type PropertyArgs array{string, Type, string, string, mixed} - */ +/** @phpstan-type PropertyArgs array{string, Type, string, string, mixed} */ class OperationBuilder { private ClassType $class; diff --git a/src/Console/InteractsWithEndpoints.php b/src/Console/InteractsWithEndpoints.php index 3bd12950..968a8122 100644 --- a/src/Console/InteractsWithEndpoints.php +++ b/src/Console/InteractsWithEndpoints.php @@ -9,9 +9,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -/** - * @mixin Command - */ +/** @mixin Command */ trait InteractsWithEndpoints { /** @return array */ diff --git a/src/Convert/IDConverter.php b/src/Convert/IDConverter.php index 9e22b062..e9608e44 100644 --- a/src/Convert/IDConverter.php +++ b/src/Convert/IDConverter.php @@ -2,9 +2,7 @@ namespace Spawnia\Sailor\Convert; -/** - * https://spec.graphql.org/draft/#sec-ID. - */ +/** @see https://spec.graphql.org/draft/#sec-ID */ class IDConverter implements TypeConverter { /** diff --git a/src/Convert/NullConverter.php b/src/Convert/NullConverter.php index 7439e066..84eeea72 100644 --- a/src/Convert/NullConverter.php +++ b/src/Convert/NullConverter.php @@ -2,9 +2,7 @@ namespace Spawnia\Sailor\Convert; -/** - * Short-circuit conversion of null. - */ +/** Short-circuit conversion of null. */ class NullConverter implements TypeConverter { protected TypeConverter $ofType; diff --git a/src/Convert/PolymorphicConverter.php b/src/Convert/PolymorphicConverter.php index 140aa996..feb725bd 100644 --- a/src/Convert/PolymorphicConverter.php +++ b/src/Convert/PolymorphicConverter.php @@ -4,9 +4,7 @@ use Spawnia\Sailor\ObjectLike; -/** - * @phpstan-type PolymorphicMapping array> - */ +/** @phpstan-type PolymorphicMapping array> */ class PolymorphicConverter implements TypeConverter { /** @var PolymorphicMapping */ diff --git a/src/Error/Location.php b/src/Error/Location.php index c8c3c25b..fccd12ae 100644 --- a/src/Error/Location.php +++ b/src/Error/Location.php @@ -2,9 +2,7 @@ namespace Spawnia\Sailor\Error; -/** - * Beginning point of the syntax element in the GraphQL document associated with the error. - */ +/** Beginning point of the syntax element in the GraphQL document associated with the error. */ class Location { public static function fromStdClass(\stdClass $location): self diff --git a/src/Error/OriginatesFromEndpoint.php b/src/Error/OriginatesFromEndpoint.php index 252f60ed..f3acfb2c 100644 --- a/src/Error/OriginatesFromEndpoint.php +++ b/src/Error/OriginatesFromEndpoint.php @@ -5,9 +5,7 @@ use GraphQL\Error\ClientAware; use Spawnia\Sailor\Configuration; -/** - * @mixin ClientAware - */ +/** @mixin ClientAware */ trait OriginatesFromEndpoint { /** Path to the config file the endpoint is defined in. */ diff --git a/src/ErrorFreeResult.php b/src/ErrorFreeResult.php index 992acb2d..8adf8fb2 100644 --- a/src/ErrorFreeResult.php +++ b/src/ErrorFreeResult.php @@ -4,9 +4,7 @@ use Spawnia\Sailor\Error\ResultErrorsException; -/** - * @property ObjectLike|null $data The result of executing the requested operation. - */ +/** @property ObjectLike|null $data The result of executing the requested operation. */ abstract class ErrorFreeResult { /** Optional, can be an arbitrary map if present. */ diff --git a/src/Events/ReceiveResponse.php b/src/Events/ReceiveResponse.php index f62143eb..09af107f 100644 --- a/src/Events/ReceiveResponse.php +++ b/src/Events/ReceiveResponse.php @@ -4,9 +4,7 @@ use Spawnia\Sailor\Response; -/** - * Fired after receiving a GraphQL response from the client. - */ +/** Fired after receiving a GraphQL response from the client. */ class ReceiveResponse { public Response $response; diff --git a/src/Events/StartRequest.php b/src/Events/StartRequest.php index 48d43b08..1966bfd9 100644 --- a/src/Events/StartRequest.php +++ b/src/Events/StartRequest.php @@ -2,9 +2,7 @@ namespace Spawnia\Sailor\Events; -/** - * Fired after calling `execute()` on an `Operation`, before invoking the client. - */ +/** Fired after calling `execute()` on an `Operation`, before invoking the client. */ class StartRequest { public string $document; diff --git a/src/Json.php b/src/Json.php index 0da7759b..0b3b4ae4 100644 --- a/src/Json.php +++ b/src/Json.php @@ -15,7 +15,7 @@ final class Json { /** - * Convert an JSON-encodable value so that maps are stdClass instances. + * Convert a JSON-encodable value so that maps are stdClass instances. * * @param JsonValue $value any value that can be encoded as JSON * @@ -28,7 +28,7 @@ public static function assocToStdClass($value) } /** - * Convert an JSON encodable value so that maps are associative arrays. + * Convert a JSON-encodable value so that maps are associative arrays. * * @param JsonValue $value any value that can be encoded as JSON * diff --git a/src/Result.php b/src/Result.php index eff54fd5..77c2d479 100644 --- a/src/Result.php +++ b/src/Result.php @@ -5,9 +5,7 @@ use Spawnia\Sailor\Error\Error; use Spawnia\Sailor\Error\ResultErrorsException; -/** - * @property ObjectLike|null $data The result of executing the requested operation. - */ +/** @property ObjectLike|null $data The result of executing the requested operation. */ abstract class Result implements BelongsToEndpoint { /** diff --git a/src/Testing/MockClient.php b/src/Testing/MockClient.php index 6c4bc8f5..eb724f8e 100644 --- a/src/Testing/MockClient.php +++ b/src/Testing/MockClient.php @@ -5,13 +5,11 @@ use Spawnia\Sailor\Client; use Spawnia\Sailor\Response; -/** - * @phpstan-type Request callable(string, \stdClass|null): Response - */ +/** @phpstan-type Request callable(string, \stdClass|null): Response */ class MockClient implements Client { /** @var Request */ - private $request; + protected $request; /** @var array */ public array $storedRequests = []; diff --git a/src/Type/InputObjectTypeConfig.php b/src/Type/InputObjectTypeConfig.php index e860490d..5d98a2e5 100644 --- a/src/Type/InputObjectTypeConfig.php +++ b/src/Type/InputObjectTypeConfig.php @@ -11,9 +11,7 @@ use Spawnia\Sailor\EndpointConfig; use Spawnia\Sailor\ObjectLike; -/** - * https://spec.graphql.org/draft/#sec-Input-Objects. - */ +/** @see https://spec.graphql.org/draft/#sec-Input-Objects */ class InputObjectTypeConfig implements TypeConfig, InputTypeConfig { private EndpointConfig $endpointConfig; @@ -52,7 +50,7 @@ public function generateClasses(): iterable $typeConfigs = $this->endpointConfig->configureTypes($this->schema); $builder = new ObjectLikeBuilder( - Escaper::escapeClassName($this->inputObjectType->name), + $this->inputObjectType->name, $this->endpointConfig->typesNamespace(), true, ); diff --git a/src/Type/TypeConfig.php b/src/Type/TypeConfig.php index f45fb9a6..5fa77adb 100644 --- a/src/Type/TypeConfig.php +++ b/src/Type/TypeConfig.php @@ -5,9 +5,7 @@ use Nette\PhpGenerator\ClassType; use Spawnia\Sailor\Convert\TypeConverter; -/** - * Specifies how Sailor should deal with a GraphQL type. - */ +/** Specifies how Sailor should deal with a GraphQL type. */ interface TypeConfig { /** diff --git a/tests/Unit/Convert/TypeConverterTest.php b/tests/Unit/Convert/TypeConverterTest.php index e0c3abc6..4aad4580 100644 --- a/tests/Unit/Convert/TypeConverterTest.php +++ b/tests/Unit/Convert/TypeConverterTest.php @@ -6,9 +6,7 @@ use Spawnia\Sailor\Json; use Spawnia\Sailor\Tests\TestCase; -/** - * @phpstan-import-type StdClassJsonValue from Json - */ +/** @phpstan-import-type StdClassJsonValue from Json */ abstract class TypeConverterTest extends TestCase { /** diff --git a/tests/Unit/Testing/Invokable.php b/tests/Unit/Testing/Invokable.php index 67a4961e..59a736fc 100644 --- a/tests/Unit/Testing/Invokable.php +++ b/tests/Unit/Testing/Invokable.php @@ -2,11 +2,8 @@ namespace Spawnia\Sailor\Tests\Unit\Testing; -/** - * Stub class to allow creating a partial mock. - */ +/** Stub class to allow creating a partial mock. */ class Invokable { - // @phpstan-ignore-next-line - public function __invoke() {} + public function __invoke() {} // @phpstan-ignore-line }