We are pretty thrilled to announce GraphQLite v4! This v4 release is the culmination of 8 months of hard work. It comes with a whole host of new very exciting features! Come an join us for a ride in the land of v4.
GraphQLite?
You don't know GraphQLite? It is a PHP library aiming at exposing a GraphQL API in PHP dead simple.
GraphQLite exposes your PHP functions as queries or mutations using simple PHP annotations.
/**
* @Query
*/
public function product(string $id): Product
{
// Some code that looks for a product and returns it.
}
This simple code will expose a GraphQL schema with a product
query that accepts an $id
argument and returns a
Product
output type.
This article really focuses on the improvements since GraphQLite 3. If you have not played with GraphQLite yet, you should maybe start by reading more about the GraphQLite philosophy in our initial blog post announcement. Also, do not hesitate to jump directly to the documentation.
So, what's new?
GraphQLite 3 already offered these features:
- Declaration of queries and mutations in "controller" classes
- Declaration of types and fields using annotations (
@Type
and@Field
) - Support for GraphQL input types (
@Factory
annotation) - Mapping PHP inheritance to GraphQL interface
- Support for basic authentication and authorization (
@Logged
and@Right
annotation)
With GraphQLite 4, we focused a lot on:
- improving developer experience
- the few missing GraphQL features remaining
- adding hook points to make GraphQLite extensible
So let's dive into the main new features:
Autowiring services in resolvers
Maybe my favorite feature of this new release, because it simplifies a lot the way you organize your code.
Some frameworks (Symfony in particular) can inject services directly in controller actions.
The interest is somewhat limited since it is pretty easy to inject the services in the controller's constructor.
But in the context of GraphQLite, it starts to make a lot of sense, because resolvers are scattered all over your code (and particularly in your models). And injecting a service in a model is not something you can usually do.
Let's see how this works with a simple sample. Let's assume you are running an international store. You have a Product
class. Each product has many names (depending
on the language of the user).
namespace App\Entities;
use TheCodingMachine\GraphQLite\Annotations\Autowire;
use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Type;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @Type()
*/
class Product
{
// ...
/**
* @Field()
* @Autowire(for="$translator")
*/
public function getName(TranslatorInterface $translator): string
{
return $translator->trans('product_name_'.$this->id);
}
}
Thanks to the @Autowire
annotation, GraphQLite will inject a translation service in the $translator
parameter.
So any time you run a GraphQL query on the Product
type, GraphQLite will inject the services for you:
{
products {
name
}
}
I absolutely love this feature, because it pushes the developer in organizing his/her code in a fashion that looks a lot like Domain Driven Design.
In the example above, a "name" clearly belongs to a product, and if we need an additional service
to be able to retrieve it, it makes sense to pass the service as an argument to the getName
method.
Compare that to performing the translation outside the model. In a REST approach, it is terribly easy to write something like:
return new JsonResponse([
'name' => $this->translateService->trans('product_name_'.$product->getId()),
]);
If you write your code like this, the notion of "product name" becomes external to the Product
class. Looking at
the Product
class code only, a developer does not even know that it "has" a name.
In GraphQLite 3, to do the same thing, you would have had to create an "external type", which led to having
fields declared in 2 classes. Same result in the end, your Product
class is stripped from
possessing a name. Autowiring allows you to keep all your fields in the same class. Use it!
Mapping GraphQL interfaces
You can now map a PHP interface to a GraphQL interface, using the same @Type
annotation you are already using on classes.
/**
* @Type
*/
interface UserInterface
{
/**
* @Field
*/
public function getUserName(): string;
}
This will automatically create a GraphQL interface whose description is:
interface UserInterface {
userName: String!
}
Of course, a PHP class implementing this PHP interface will translate into a GraphQL output type implementing the GraphQL interface automatically!
Validating user input
GraphQL input types can now be validated using simple to use annotations.
I must admit I was first reluctant to add validation to GraphQLite, but this was certainly the most requested feature.
So here we go! The way you validate those will depend on the framework you are using, since we are tapping directly into your framework's validation library.
In Laravel
class MyController
{
/**
* @Mutation
* @Validate(for="$email", rule="email|unique:users")
* @Validate(for="$password", rule="gte:8")
*/
public function createUser(string $email, string $password): User
{
// ...
}
}
In Symfony
... or in any other framework (since you can use the symfony/validator component)
/**
* @Query
* @Assertion(for="email", constraint=@Assert\Email())
*/
public function findByMail(string $email): User
{
// ...
}
Improved security handling
A much requested feature: GraphQLite 4 comes with fine-grained security.
Until v4, you could deny access to users based on user rights. With v4, you can grant or deny access based on the context.
For instance, imagine you are developing a blog platform. When writing a new article, a user can access his blog post,
but no-one else should be allowed to. You can express this in GraphQLite using the new @Security
annotation.
/**
* @Type
*/
class Post {
/**
* @Query
* @Security("this.canAccess(post, user)")
*/
public function getPost(Post $post): array
{
// ...
}
public function canAccess(Post $post, User $user): bool
{
// Some custom logic here to know if $user can access $post
}
}
The @Security
annotation lets you tap into the power of Symfony expression language to write complex expressions.
If you are used to Symfony, you already know this concept. The GraphQLite @Security
annotation is heavily inspired
from Symfony's own @Security
annotation. A big thanks to the Symfony team for this great idea!
Improving performance
A common problem faced by GraphQL implementations is the way to deal with the so called N+1 problem. GraphQLite 4 comes with 2 ways to ways to help you tackle this issue:
- you can inspect the "query plan" (which can help you perform joins preemptively)
- you can easily use the "dataloader" design pattern with our new "prefetch" feature
The prefetch method will let you fetch all the objects of a given type in a single DB call.
/**
* @Type
*/
class Post {
/**
* @Field(prefetchMethod="prefetchUsers")
* @param mixed $prefetchedUsers
* @return User
*/
public function getUser($prefetchedUsers): User
{
// This method will receive the $prefetchedUsers as first argument.
// This is the return value of the "prefetchUsers" method below.
// Using this pre-fetched list, it should be easy to map it
// to the post
}
/**
* @param Post[] $posts
* @return mixed
*/
public function prefetchUsers(iterable $posts)
{
// This function is called only once per GraphQL request
// with the list of posts. You can fetch the list of users
// associated with this posts in a single request,
// for instance using a "IN" query in SQL or a multi-fetch
// in your cache back-end.
}
}
prefetchMethod
technique simplifies a lot the implementation of the dataloader pattern, but still, it
requires some work. The more I look at this problem, the more I am convinced that the issue should be tackled
not by the GraphQL library, but by the underlying ORM. I wrote an article about this idea, check it out.Better error handling
Choosing an HTTP error code
Error handling is a complex topic in GraphQL. A GraphQL query can contain several queries / mutations. Some may succeed, some may fail in various ways. At the end of the query, putting a unique HTTP code on it is hard. Of course, if all queries succeed, the HTTP 200 code is the way to go. But as soon as one query fails, choosing an HTTP code is hard.
It is actually so hard that there is no agreement on what to do in the wider GraphQL ecosystem, with the 2 leading JS server-side implementation (Graph-js and Apollo Server) choosing 2 different strategies.
In GraphQLite, we externalized the choice of the HTTP code in the new HttpCodeDecider
class. You can replace this class
by your own implementation if you have specific needs. By default, GraphQLite will return a HTTP error code (4xx or 5xx)
as soon as there is one query that fails. If many queries fail, it will choose the highest error code.
Easier error throwing
GraphQLite now comes with a GraphQLException
base exception that you can throw.
This exception (just like any exception extending the ClientAware
interface) will be turned into a GraphQL error
.
Furthermore, if you want to output many errors in the GraphQL errors section, you can the new GraphQLAggregateException
class.
This exception aggregates many exceptions. Each exception will be turned into a GraphQL error.
Improved input types
In GraphQLite v3, you could already map a PHP class to a GraphQL input type (using the @Factory
annotation).
/**
* The Factory annotation will create automatically
* a UserInput input type in GraphQL.
* @Factory()
*/
public function fetchUser(int $id): User
{
return $this->userRepository->findById($id);
}
// You can now use the User class in any query / mutation / field arguments:
/**
* @Query
* @return Post[]
*/
public function getPostsByUser(User $user): iterable
{
return $this->postRepository->findByUser($user);
}
The issue is that sometimes, a single class can map to many GraphQL input types.
In the example above, when I inject the User
class in a query / mutation, there could be 2 different meanings:
- I want to provide only the
id
of theUser
and GraphQLite should fetch theUser
instance in DB - or I want to provide a complete
User
object, like in this code sample:
/**
* @Factory()
*/
public function createUser(string $lastName, string $firstName): User
{
return new User($lastName, $firstName);
}
/**
* @Mutation
*/
public function saveUser(User $user): User
{
$this->em->persist($user);
$this->em->flush();
return $user;
}
With GraphQLite 4, a given PHP class can now be mapped by many GraphQL input type. This means that you can add many factories for the same class.
/**
* This factory will be used by default
* @Factory(name="FetchUserInput", default=true)
*/
public function fetchUser(int $id): User
{
return $this->userRepository->findById($id);
}
/**
* @Factory(name="CreateUserInput")
*/
public function createUser(string $lastName, string $firstName): User
{
return new User($lastName, $firstName);
}
In the example above, the first factory for the User
class is marked with the default
attribute. It will be used
by default. The second annotation can be used if you ask for it specifically, using the new @UseInputType
annotation:
/**
* @Mutation
* @UseInputType(for="$user", inputType="CreateUserInput!")
*/
public function saveUser(User $user): User
{
$this->em->persist($user);
$this->em->flush();
return $user;
}
On a side-note, if an input type is provided by a third-party library, you can now extend it using the @Decorate
annotation.
Completely reworked internals
It should be fairly easy for users to migrate from v3 to v4 as must annotations are left untouched.
But internally, there are almost no lines of code that are left in common! GraphQLite 4 is really a huge release.
The important part is that we added a lot of extension points that you can tap into in order to extend GraphQLite.
Most of these extension points have been added using the "middleware" design pattern.
You can:
- alter the way the PHPDoc and the PHP type are turned into a GraphQL type, using the "root type mapper". "Root type mappers" can be used to add support for new custom scalar types.
- add custom annotations to change the way a resolver works, using "field middlewares".
For instance, the
@Logged
and@Right
annotation have been rewritten from the ground as "field middlewares", and you can now add your own. - alter the way arguments are resolved using "parameter type mappers".
For instance, the
@Autowire
annotation is implemented using argument resolvers.
Actually, most of the new features we implemented in v4 are built upon those extension points that you can use yourself!
Symfony specific features
The GraphQLite Symfony bundle has also had a ton of new features.
The most important one is probably that it now offers by default:
- a
login
and alogout
mutation - a
me
query
So out of the box, you can login / logout from your Symfony application using GraphQL. No need to go through the Symfony REST API anymore!
Laravel specific features
The Laravel package has also been improved, with an improved integration with Laravel authentication framework.
Most of all, GraphQLite now supports mapping magic properties to GraphQL fields natively, using the @MagicField
annotation:
/**
* @Type()
* @MagicField(name="id" outputType="ID!")
* @MagicField(name="name" phpType="string")
* @MagicField(name="categories" phpType="Category[]")
*/
class Product extends Model
{
}
This is not a Laravel specific feature per-se, but this new feature makes working with Eloquent a lot easier.
Framework agnostic
At TheCodingMachine, we love framework-agnostic code. GraphQLite is therefore framework agnostic too. You can deploy it in any framework.
Also, GraphQLite now comes with its own PSR-15 middleware so it is relatively easy to setup a working environment with Zend Expressive, Slim or any PSR-15 enabled framework.
What's next?
v4 is a big milestone, but there is still a lot of work to be done.
In the coming months we plan to:
- work on a tutorial: we will write a tutorial to get started with a full-stack environment using GraphQLite. The choice is not completely set, but it will probably be Symfony 5 + GraphQLite + Next.JS + Apollo + Typescript
- work on a TDBM: TDBM is our home-grown ORM. We are working on a pretty unique feature that solves automatically the N+1 performance problem. TDBM + GraphQLite would be a perfect match!
- improve custom Scalar GraphQL types handling: it is already possible to add custom scalar types in GraphQLite 4, but there is some room for simplification.
- add support for subscriptions: using Mercure, we could add support for GraphQL subscriptions quite easily.
You can check the issues targeted at the upcoming 4.1 release here.
Follow us
We hope you will be interested in GraphQLite.
Between v3 and v4, we spent 8 months testing and slowly improving the library. v4 is already used by several of our clients, so I'm confident saying it is stable.
Still, feedbacks are welcome!
You can star the project on Github (we love stars!) or follow me (@david_negrier) on Twitter for GraphQLite related news.
About the author
David is CTO and co-founder of TheCodingMachine and WorkAdventure. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. He is also the lead developper of GraphQLite, a framework-agnostic PHP library to implement a GraphQL API easily.