rdx / graphql-framework
GraphQL framework layer on top of webonyx/graphql-php
Requires
- webonyx/graphql-php: ^15.20
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^2.1
This package is auto-updated.
Last update: 2025-04-25 14:04:50 UTC
README
This is a simple way to make webonyx/graphql-php
more readable.
Set-up
You need to make a handler class:
class MyHandler extends GraphQLHandler { public function makeSchema() : Schema { return new \GraphQL\Type\Schema([ 'query' => GraphQLFactory::type(MyQueryType::class), 'mutation' => GraphQLFactory::type(MyMutationType::class), ]); } protected function isDebug() : bool { // Whether you want extra debug in your GraphQL response (next to `data` + `extensions`) return app()->ip() == '127.0.0.1'; } protected function logInputError(string $label, string $input) : void { // Client sent invalid JSON: $input } protected function logInputQuery(string $label, array $input) : void { // Client sent correct JSON: $input } protected function getDebugQueries() : array { // If you want the response to contain runtime db queries return app()->queries(); } }
And if you want eager loading (you do), you need to define your framework/app's strategy:
class MyEager extends DeferredEager { protected function doLoadAll() : void { // Laravel: ModelCollection::make($this->queue)->load($this->field); // Other framework: $class = get_class($this->queue[0]); call_user_func([$class, 'eager'], $this->queue, $this->field); } }
Controller
And then you can call it in the controller:
$context = new GraphQLContext(); $handler = new MyHandler($context); $handler->execute(); return new JsonResponse($handler->getResult());
Types
GraphQL is about types, and webonyx/graphql-php
is not very user-friendly by default, so this
is how you make types. It does help if you know how webonyx works.
Everything starts with MyQueryType
from MyHandler
:
class MyQueryType extends ParentObjectType { public function fields() : array { $fields = []; // Simple field: $fields['user'] = [ 'type' => GraphQLFactory::type(UserType::class), 'resolve' => function() { return app()->user(); }, ]; // Complex field: $fields['users'] = GraphQLFactory::field(MyQueryUsersField::class); return $fields; } } class MyQueryUsersField extends ParentField { static public function type() : Type { return GraphQLFactory::type(UsersPagerType::class); } static public function args() : array { // All of these are optional: return [ 'ids' => Type::listOf(Type::nonNull(Type::int())), 'organization' => Type::int(), 'emails' => Type::listOf(Type::nonNull(Type::string())), 'changed_after' => Type::int(), 'status' => Type::int(), ]; } static public function argsMapper(array $args) : mixed { // If you want the above args to be a nice DTO, use argsMapper() return new UsersPagerArgs(...$args); } /** * @return AssocArray */ static public function resolve(null $source, UsersPagerArgs $args, BrContext $context) : array { // $source is null, because this is a root field. Deeper fields have a mixed or YourDbalObject etc $source. // Use $args to make cool query $query = ...; // Return fields according to `UsersPagerType`. // Use fn() to lazy load: only execute if queried. return [ 'total' => fn() => $query->getCount(), 'nodes' => fn() => $query->getRecords(), ]; } } class UsersPagerArgs { public function __construct( /** @var list<int> */ public array $ids = [], public int $organization = 0, /** @var list<string> */ public array $emails = [], public int $changed_after = 0, public int $status = 0, ) {} }
UserType
and UsersPagerType
are ParentObjectType
again, with the same logic. Because UserType
is so much simpler, another example:
class UserType extends ParentObjectType { public function fields() : array { // No resolvers, so webonyx's default resolver is used. return [ 'id' => Type::nonNull(Type::int()), 'name' => Type::nonNull(Type::string()), 'email' => Type::nonNull(Type::string()), 'status' => Type::nonNull(Type::int()), 'fav_color' => Type::string(), ]; } }
MyMutationType
works exactly the same as Query types.