Slides for the talk given at the Berlin PHP user group meetup
How to guarantee consistency of PHP GraphQL implementation to the schema definition with the help of code generation.
5. Rise-your-hands game
Who is well versed with the GraphQL specification?
Who tried to implement GraphQL server with PHP?
Who is supporting GraphQL server in a production environment?
6. GraphQL
Server implementations are available for multiple languages, including Haskell,
JavaScript, Python, Ruby, Java, C#, Scala, Go, Elixir, Erlang, PHP, and Clojure
- API specification and runtime
- developed by Facebook
- published in 2015
7. RPC
API Interaction is just a remote
procedure call:
- It has “function name”
- It has arguments
- It has return value (response)
REST
All data is a resource:
- It can be created / read / updated /
deleted
- Resource is identified by URL
- It can be connected to another
resources through relation
Concepts of API
8. RPC REST
- Versioning of resource schema
- Limited set of operations with
resources
Limitations
- Versioning of endpoints and
response data
- Pre-designed structure of input
and output
9. Why do we like GraphQL?
Validation of input / output data
Tools: automatic documentation based on schema
Ready-to-be-used specification
Schema defines: data types, abstractions, relations
Operations defined by root Query/Mutation objects
All this can be found in other specifications
10. Why do we like GraphQL?
Query is disambiguous:
- no wildcard fields – you don’t need resource versions,
- type assertions, interfaces and unions – easy to extend with new types
Nice:
- built-in deprecation mechanism
Instead of implementing complete endpoints, developer defines the way to
resolve relations – easier to reuse code.
Schema definition language is simple and robust
11. Transport layer
How do we GraphQL?
webonyx/graphql-php
Request
Query
validation
Mapping to
resolvers
Assembling
result
Response
validation &
formatting
Response
Your app API layer
?
12. Global field resolver to resolve object properties
class DefaultFieldResolver
{
// ...
public function __invoke($source, $args, $context, ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
if ($source instanceof TypeInterface) {
$method = 'get' . ucfirst($fieldName);
if (!method_exists($source, $method)) {
throw new FieldNotImplemented('Field <' . $fieldName . '> is not implemented for type <' . $info->parentType . '>');
}
$property = call_user_func_array([$source, $method], $args);
}
$fieldValue = $property instanceof Closure ? $property($source, $args, $context, $info) : $property;
return $fieldValue;
}
}
13. Adapters to represent API objectTypes
class TaxonomyType extends AbstractTaxonomyType
{
/** @var TaxonomyDTO */
private $taxonomy;
// ...
/** @return int */
public function getTreeDepth()
{
return $this->taxonomy->getTreeDepth();
}
/** @return TaxonType[] */
public function getTaxa()
{
return $this->dataLoaderRegistry->get('taxonByParentTaxon')->load($this->taxonomy->getRootTaxonId());
}
}
14. What are challenges there
Return values is up to your resolver implementation, but still duplicates schema
Resolvers receive validated data, but it is still presented as associative array
All inconsistencies can be spotted in the runtime only
Enum values needs to be duplicated
Input and output types field names needs to be duplicated
You define a set of resolver functions that implement application BL
17. What schema defines
Input Object Type
input FeedbackInput {
message: String!
type: FeedbackType!
source: FeedbackSource!
}
Query Type
type Query {
user(eid: ID!): User
}
Mutation Type
type Mutation {
submitFeedback(feedback: FeedbackInput!): Boolean
}
Enum
enum Stage {
preclinic
clinic
doctor
}
Interface
interface Entity {
eid: ID!
}
Object Type
type User implements Entity {
eid: ID!
stage: Stage!
firstName: String
lastName: String
}
18. Code generation: Object Type
type User implements Entity {
eid: ID!
stage: Stage!
firstName: String
lastName: String
}
abstract class AbstractUserType implements EntityInterface
{
/** @return string */
abstract public function getEid();
/** @return string */
abstract public function getStage();
/** @return null|string */
abstract public function getFirstName();
/** @return null|string */
abstract public function getLastName();
}
Abstract class can be extended
by multiple different classes
19. Code generation: Object Type
class UserType extends AbstractUserType
{
private $user;
/** @return string */
public function getEid()
{
return $this->user->getUuid();
}
...
}
class AdminType extends AbstractUserType
{
private $admin;
/** @return string */
public function getEid()
{
return $this->admin>getExternalId();
}
...
}
20. Code generation: Interface
interface Entity {
eid: ID!
}
interface EntityInterface
{
/**
* @return string | int
*/
public function getEid();
}
Abstract class implements this
interface automatically
21. Code generation: Input Object Type
input FeedbackInput {
message: String!
type: FeedbackType!
source: FeedbackSource!
}
class FeedbackInputType
{
/** @var string */
private $message;
/** @var string */
private $type;
/** @var FeedbackSourceType */
private $source;
public function __construct(array $inputValues)
{
$this->message = $inputValues['message'];
$this->type = $inputValues['type'];
$this->source = new FeedbackSourceType(
$inputValues['source']
);
}
Input is wrapped to value object
recursively
22. Code generation: Enum
enum Stage {
preclinic
clinic
doctor
}
class StageEnum extends Enum
{
const PRECLINIC = 'preclinic';
const CLINIC = 'clinic';
const DOCTOR = 'doctor';
/** @inheritdoc */
public function getValues()
{
return [
self::PRECLINIC,
self::CLINIC,
self::DOCTOR,
];
}
}
Constants can be used to
guarantee the consistency
of schema via static analysis
23. Code generation: Union
union NodeContent = File | Folder
type Node {
eid: ID!
firstName: String
content: NodeContent
}
class NodeType extends AbstractNodeType
{
/** @return FileType|FolderType */
public function getContents()
{
...
}
}
Union definitions are used to
declare @return in the docblcok
24. Benefits
- Easier to kick-off new type – just extend and use IDE to create stubs
- Ready-to-be-used value objects for Enum and Input
- Automatic interfaces implementation
- Docblock support for types defined by schema
- Schema inconsistencies can be detected with static code analysis