Creating a GraphQL API is more and more common for PHP developers. The task can seem complex but there are a lot of tools to help.
In this talk given at AFUP Paris PHP Meetup, I'm presenting GraphQL and why it is important. Then, I'm having a look at the existing libraries in PHP. Finally, I'm diving in the details of GraphQLite; a library that creates a GraphQL schema by analyzing your PHP code.
4. GraphQL ?
• GraphQL is a protocol
• It is not:
• A fancy new database
• A database query language like SQL
5. GraphQL ?
• GraphQL is a protocol
• GraphQL is a challenger to those other
protocols:
• REST
• Web-services (SOAP/WSDL based)
6. A bit of history
Web-services
(~1999)
• Strongly typed
• Self-describing (WSDL)
• XML-based
7. A bit of history
Web-services
(~1999)
• Strongly typed
• Self-describing (WSDL)
• XML-based
8. A bit of history
Web-services
(~1999)
REST
(~2005)
• Strongly typed
• Self-describing (WSDL)
• XML-based
• Weakly typed
• Non self-describing
(still OpenAPI for doc)
• Mostly JSON based
9. A bit of history
Web-services
(~1999)
REST
(~2005)
• Strongly typed
• Self-describing (WSDL)
• XML-based
• Weakly typed
• Non self-describing
(still OpenAPI for doc)
• Mostly JSON based
10. A bit of history
Web-services
(~1999)
REST
(~2005)
GraphQL
(2015)
• Strongly typed
• Self-describing (WSDL)
• XML-based
• Weakly typed
• Non self-describing
(still OpenAPI for doc)
• Mostly JSON based
• Strongly typed
• Self-describing
• JSON based
• + Client driven queries
11. GraphQL ?
It is developed by Facebook and was first used in
the Facebook API.
Facebook also provides:
• A JS client (to query a GraphQL server)
• A NodeJS server library
Tons of server implementations exist.
13. What problem does GraphQL solves?
• Your API often changes
• You develop a new feature but your API
does not exactly respond to your needs.
14. What problem does GraphQL solves?
• For instance: you are developing a
marketplace. You need a page to display a
product, along some company information.
/api/product/42 /api/company/35
REST
15. What problem does GraphQL solves?
• Alternative, still REST
• But what if some pages don’t need the
company details?
/api/product/42
REST
16. What problem does GraphQL solves?
• Yet another alternative, still REST
/api/product/42?with_company=true
REST
Flags hell 😨! Probably one
flag per consumer of the API
17. What problem does GraphQL solves?
• GraphQL to the rescue!
• GraphQL is a paradigm shift.
• The client asks for the list of fields it wants.
GET /graphql?query= Single endpoint
The name of the query is « products »
List of fields requested
18. What problem does GraphQL solves?
• GraphQL to the rescue!
• Another request of the same query with a
different set of fields
No need to change the code on the
server-side! All this data in one API call!
GET /graphql?query=
20. Types
GraphQL is strongly typed.
It comes with a « schema
language » but this is rarely
used while developing.
It is however useful to
understand what is going on.
22. Types
Note:
• [Product] ➔ an array of
Products
• String ➔ a string (or null)
• String! ➔ a non-nullable string
Hence:
• [Product!]! ➔ An array (non-
nullable) of products that are also
non-nullable.
23. Types
Some « scalar » types:
• ID: a unique identifier (~=String)
• String
• Int
• Float
• Boolean
No support for « Date » in the
standard (but custom types are
supported by some implementations)
25. Types
Bonus:
• Support for interfaces
• Support for Union types
• Support for “InputType” (to pass complex
objects in queries)
26. Mutations
So far, we mostly talked about queries (because
this is what is fun in GraphQL).
GraphQL can also do mutations (to change the
state of the DB)
28. Transport is out of scope
• You usually do GraphQL over HTTP/HTTPS
• But nothing prevents you from using GraphQL
over UDP, or mail, …
29. Transport is out of scope
• You usually do GraphQL over HTTP/HTTPS
• But nothing prevents you from using GraphQL
over UDP, or mail, or homing pigeon whatever!
30. Authentication is out of scope
• GraphQL does not deal with authentication.
• You are free to deal with it in any way you want:
• Session-based
• or using Oauth2
• or using a token…
• Authentication can happen using a REST API,
or even using a GraphQL mutation!
31. Pagination is out of scope
• GraphQL does not deal with pagination in the
standard.
• You are free to add limit/offset parameters to
your queries
• “Relay” is providing some conventions to deal
with pagination (more on that later)
33. Ecosystem (a small part of…)
Browser
GraphQL
Client
Server
GraphQL
Middleware DB
RelayJS
Apollo
ReactJS
Express-
graphql
NodeJS
Webonyx/
GraphQL-
PHP
PHP
GraphiQL
(for dev!)
34. Zoom on GraphQL in PHP
Core library Wrapper library
• Low level
• Parsing
• Service requests
• Powerful
• Feature complete
• Hard to use (poor DX)
• High level
• Opiniated
• Easy to use
35. Zoom on GraphQL in PHP
Core library Wrapper library
• webonyx/graphql-php
• De-facto standard in PHP
• Youshido/GraphQL
• Abandonned
36. Zoom on GraphQL in PHP
Core library Wrapper library
• API Platform (Symfony)
• Overblog GraphQL Bundle (Symfony)
• Lighthouse (Laravel)
• … and now GraphQLite
50. The idea
The same “echo” method in pure PHP
function echoMsg(string $message): string
{
return $message;
}
51. function echoMsg(string $message): string
{
return $message;
}
The idea
The same “echo” method in pure PHP
Query name
Arguments
Return type
Resolver
52. The idea
The same “echo” method in pure PHP
/**
* @Query
*/
function echoMsg(string $message): string
{
return $message;
}
53. The idea
• PHP is already typed.
• We should be able to get types from PHP and
convert them to a GraphQL schema
PHP
objects
GraphQL
objects
GraphQLite
54. Works well with Doctrine
Bonus:
• It plays nice with Doctrine ORM too
• (we also have native bindings with TDBM, our
in-house ORM)
DB
model
PHP
objects
GraphQL
objects
Doctrine GraphQLite
58. First query
namespace AppController;
use AppEntityPost;
use AppRepositoryPostRepository;
use TheCodingMachineGraphQLiteAnnotationsQuery;
class PostController
{
/**
* @var PostRepository
*/
private $postRepository;
public function __construct(PostRepository $postRepository)
{
$this->postRepository = $postRepository;
}
/**
* @Query()
*/
public function getPosts(?string $search): array
{
return $this->postRepository->findAllFilterBySearch($search);
}
}
62. <?php
namespace AppEntity;
use TheCodingMachineGraphQLiteAnnotationsField;
use TheCodingMachineGraphQLiteAnnotationsType;
/**
* @Type()
*/
class Post
{
//…
/**
* @Field(outputType="ID")
*/
public function getId(): int
{
return $this->id;
}
/**
* @Field()
*/
public function getMessage(): string
{
return $this->message;
}
/**
* @Field()
*/
public function getCreated(): DateTimeImmutable
{
return $this->created;
}
}
64. First query
<?php
namespace AppEntity;
use TheCodingMachineGraphQLiteAnnotationsField;
use TheCodingMachineGraphQLiteAnnotationsType;
/**
* @Type()
*/
class Post
{
// …
/**
* @Field()
*/
public function getAuthor(): User
{
return $this->author;
}
}
66. First query
<?php
namespace AppEntity;
use TheCodingMachineGraphQLiteAnnotationsField;
use TheCodingMachineGraphQLiteAnnotationsType;
/**
* @Type()
*/
class User implements UserInterface
{
// …
/**
* @Field(outputType="ID!")
*/
public function getId(): int
{
return $this->id;
}
/**
* @Field()
*/
public function getLogin(): string
{
return $this->login;
}
// …
69. Improving our sample
What if I want to get the list of comments from my
“post” type?
(Assuming I have no “getComments” method in
the Post class)
70. Improving our sample
Say hello to “type extension”!
You can add fields in an existing type, using the
“@ExtendType” annotation.
71. Improving our sample
namespace AppTypes;
use AppEntityComment;
use AppRepositoryCommentRepository;
use TheCodingMachineGraphQLiteAnnotationsExtendType;
use AppEntityPost;
use TheCodingMachineGraphQLiteAnnotationsField;
/**
* @ExtendType(class=Post::class)
*/
class PostType
{
/**
* @var CommentRepository
*/
private $commentRepository;
public function __construct(CommentRepository $commentRepository)
{
$this->commentRepository = $commentRepository;
}
/**
* @Field()
* @return Comment[]
*/
public function getComments(Post $post): array
{
return $this->commentRepository->findByPost($post);
}
}
72. Improving our sample
namespace AppTypes;
use AppEntityComment;
use AppRepositoryCommentRepository;
use TheCodingMachineGraphQLiteAnnotationsExtendType;
use AppEntityPost;
use TheCodingMachineGraphQLiteAnnotationsField;
/**
* @ExtendType(class=Post::class)
*/
class PostType
{
/**
* @var CommentRepository
*/
private $commentRepository;
public function __construct(CommentRepository $commentRepository)
{
$this->commentRepository = $commentRepository;
}
/**
* @Field()
* @return Comment[]
*/
public function getComments(Post $post): array
{
return $this->commentRepository->findByPost($post);
}
}
It is a service!
73. Improving our samplenamespace AppTypes;
use AppEntityComment;
use AppRepositoryCommentRepository;
use TheCodingMachineGraphQLiteAnnotationsExtendType;
use AppEntityPost;
use TheCodingMachineGraphQLiteAnnotationsField;
/**
* @ExtendType(class=Post::class)
*/
class PostType
{
/**
* @var CommentRepository
*/
private $commentRepository;
public function __construct(CommentRepository $commentRepository)
{
$this->commentRepository = $commentRepository;
}
/**
* @Field()
* @return Comment[]
*/
public function getComments(Post $post): array
{
return $this->commentRepository->findByPost($post);
}
}
Current object
is passed as first
parameter
75. Improving our sample
Even better: fields can have their own arguments.
For instance, you may want to fetch a paginated
list of comments.
76. Improving our sample
namespace AppTypes;
use AppEntityComment;
use AppRepositoryCommentRepository;
use TheCodingMachineGraphQLiteAnnotationsExtendType;
use AppEntityPost;
use TheCodingMachineGraphQLiteAnnotationsField;
/**
* @ExtendType(class=Post::class)
*/
class PostType
{
// …
/**
* @Field()
* @return Comment[]
*/
public function getComments(Post $post, int $limit = 20, int $offset = 0): array
{
return $this->commentRepository->findByPost($post)
->setMaxResults($limit)
->setFirstResult($offset)
->getResult();
}
}
From the 2nd parameter,
arguments are added to the field
79. Native pagination
Actually, you don’t even have to bother adding
pagination as GraphQLite integrates natively with
Porpaginas.
(Porpaginas integrates with Doctrine and TDBM)
80. Native pagination
use PorpaginasDoctrineORMORMQueryResult;
/**
* @ExtendType(class=Post::class)
*/
class PostType
{
// …
/**
* @Field()
* @return Comment[]
*/
public function getComments(Post $post): ORMQueryResult
{
return new ORMQueryResult($this->commentRepository->findByPost($post));
}
}
Needed for Doctrine ORM.
Not even necessary for TDBM 5 ResultIterator’s that can be returned directly.
86. More features!
Everything is clearly documented at:
Thanks @JUN for the help
https://graphqlite.thecodingmachine.io
87. What’s next?
• Release of GraphQLite 4 in June
• Deferred type support
• Injection of services in method
parameters
• Improved performances
• Custom scalar types
• Enum’s support
• …
89. GraphQL everywhere?
• GraphQLite makes it trivial to write a GraphQL
API. It is now easier to start a GraphQL API
than a REST API! o/
• GraphQL makes a lot of sense for most of our
projects because it eases the separation
between front-end and back-end
• And the tooling in JS/TS is awesome
90. GraphQL everywhere?
• Performance warning! GraphQL itself is fast
but…
• N+1 problem
• It is easy to write slow queries ➔ Warning with
front facing websites.
91. GraphQL everywhere?
• Two strategies available to avoid the “N+1”
problem:
• Analyzing the GraphQL query and “joining”
accordingly
• Or the “data-loader” pattern
• + a need to set limits on the queries
complexity to avoid “rogue” queries
94. Zoom on Apollo
• Apollo is a GraphQL client (it is a JS lib).
• It has bindings with:
• Angular
• React
• VueJS
• You bundle a React/Angular/Vue component
in a Apollo component and Apollo takes in
charge the query to the server
100. But where is Redux?
• Apollo comes internally with its own store.
• Redux is really less useful with Apollo and you
can simply scrap ~90% of your reducers.
• Still useful for niche places (like managing the
current logged user)