GraphQL in PHP and Symfony


query talkGraphQLInPHP {
	person(id: "renatomefi") {
		name
		twitter
        company
		bioPerLine
	}
	talk(id: "graphql-php-symfony") {
	    title
	    date
	    type
	}
}
                    

{
  "data": {
    "person": {
      "name": "Renato Mendes Figueiredo",
      "twitter": "@renatomefi",
      "company": "@enrise",
      "bioPerLine": [
        "ZCE, ZCPE, ZFCE, LPIC-1, LFCS, LFCE Professional",
        "a.k.a.: A guy who loves linux and software dev!",
        "Co-organizer of @PHPAmersfoort and @rumoazcephp",
        "Maintainer of php-vcr and OverblogGraphQLBundle"
      ]
    },
    "talk": {
      "title": "GraphQL is right in front of us",
      "date": "2017-11-04T15:30:00.042Z",
      "type": "ScotlandPHP 2017"
    }
  }
}
                    

N+1 issue


GET /people?limit=10&start=0

GET /person/luke

GET /planet/tatooine
GET /person/luke/starshipConnection?page=1&limit=2

GET /starship/X-wing
GET /starship/ImperialShuttle
				
“GraphQL prevents HTTP overfetching”

Tailor made endpoint (RPC)

Query

Result


query lukeSkywalker {
	person(id: "cGVvcGxlOjE=") {
		name
		birthYear
		height
		hairColor
		starshipConnection {
			starships {
				name
			}
		}
		homeworld {
			name
		}
	}
}
							

 {
   "data": {
     "person": {
       "name": "Luke Skywalker",
       "birthYear": "19BBY",
       "height": 172,
       "hairColor": "blond",
       "starshipConnection": {
         "starships": [
           {
             "name": "X-wing"
           },
           {
             "name": "Imperial shuttle"
           }
         ]
       },
       "homeworld": {
         "name": "Tatooine"
       }
     }
   }
 }
							

Clients ♥ Backend API

Query

Result


query theGoodMovies {
    allFilms(first: 3) {
       films {
           episodeID
           title
       }
    }
}
                

{
  "data": {
    "allFilms": {
      "films": [
         {
           "episodeID": 4,
           "title": "A New Hope"
         },
         {
           "episodeID": 5,
           "title": "The Empire Strikes Back"
         },
         {
           "episodeID": 6,
           "title": "Return of the Jedi"
         }
      ]
    }
  }
}
                

Query

Result


query threeAfterR2D2 {
	allPeople(first: 3, after: "R2-D2") {
		edges {
			node {
				name
				id
			}
		}
	}
}
							
Almost...

 {
   "data":{
     "allPeople": {
       "edges": [
         {
           "node": {
             "name":"Leia Organa",
             "id":"cGVvcGxlOjU="
           }
         }
         ...
       ]
     }
   }
 }
							
Relay Cursor Connections

Query

Result


query twoAfterR2D2 {
  allPeople(
    first: 2,
    after: "YXJyYXljb25uZWN0aW9uOjI="
  ) {
    edges {
      node {
        name
      }
      cursor
    }
  }
}
                

{
  "data": {
    "allPeople": {
      "edges": [
        {
          "node": {
            "name": "Darth Vader"
          },
          "cursor": "YXJyYXljb25uZWN0aW9uOjM="
        },
        {
          "node": {
            "name": "Leia Organa"
          },
          "cursor": "YXJyYXljb25uZWN0aW9uOjQ="
        }
      ]
    }
  }
}
							

Query

Result


query firstPage {
	allPeople (first: 3) {
		totalCount
		pageInfo {
			hasNextPage
			hasPreviousPage
			startCursor
			endCursor
        }
		edges {
			node {
				name
			}
		}
	}
}
							

 {
   "data": {
     "allPeople": {
       "totalCount": 82,
       "pageInfo": {
         "hasNextPage": true,
         "hasPreviousPage": false,
         "startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
         "endCursor": "YXJyYXljb25uZWN0aW9uOjI="
       },
       "edges": [
			 ... // Nodes
       ]
     }
   }
 }
							

Query

Result


query secondPage {
allPeople (
	first: 3,
	after: "YXJyYXljb25uZWN0aW9uOjI="
) {
		totalCount
		pageInfo {
			hasNextPage
			hasPreviousPage
			startCursor
			endCursor
		}
		edges {
			node {
				name
			}
		}
	}
}
							

 {
   "data": {
     "allPeople": {
       "totalCount": 82,
       "pageInfo": {
         "hasNextPage": true,
         "hasPreviousPage": false,
         "startCursor": "YXJyYXljb25uZWN0aW9uOjM=",
         "endCursor": "YXJyYXljb25uZWN0aW9uOjU="
       },
       "edges": [
			 ... // Nodes
       ]
     }
   }
 }
							
They are good for dynamic data
Don't trust it, it's opaque

Inserting data

Mutations


mutation createReview($episode: String!, $content: String!) {
  createReview(episode: $episode, content: $content) {
    content
    film {
      title
    }
  }
}
                
Variables can also be used on Queries

Mutation

Result


mutation createReview(
  $episode: String!,
  $content: String!,
  $rating: Int!
) {
  content
  film {
    title
    averageRating
  }
}
                    

{
    "episode": "ZmlsbXM6MQ==",
    "content": "Be a Jedi!",
    "rating": 5
}
                    

{
  "data": {
    "review": {
      "content": "...",
      "film": {
        "title": "A New Hope",
        "averageRating": 4
      }
    }
  }
}
                    

{
  "query": "query httpTest { __schema { queryType { fields { name } } } }",
  "variables": { "episode": "III" },
  "operationName": "httpTest"
}
                

{
  "data": {
    "__schema": {
      "queryType": {
        "fields": [
          {
            "name": "droid"
          },
          {
            "name": "hero"
          },
          ...
        ]
      }
    }
  }
}
                

Simple syntax error


{
  "errors": [
    {
      "message": "Variable \"$episode\" of required type \"String!\" was not provided.",
      "locations": [
        {
          "line": 2,
          "column": 16
        }
      ]
    }
  ]
}
                

Runtime error


{
  "data": {
    "review": null
  },
  "errors": [
    {
      "message": "Episode not found",
      "locations": [
        {
          "line": 2,
          "column": 30
        }
      ]
    }
  ]
}
                

Defining an object type


$humanType = new ObjectType([
    'name' => 'Human',
    'description' => 'A humanoid creature in the Star Wars universe.',
    'fields' => [
        'id' => [
            'type' => Type::nonNull(Type::string()),
            'description' => 'The id of the human.',
        ],
        'name' => [
            'type' => Type::string(),
            'description' => 'The name of the human.',
        ]
    ],
    'resolveType' => function ($obj) use (&$humanType) {
        if (isset($humans[$obj['id']])) {
            return $humanType;
        }
    },
]);
                

Query


$queryString = '
    query HeroNameQuery {
        hero {
            name
        }
    }
';

$result = GraphQL::execute(
  StarWarsSchema::build(),
  $queryString
)
                

Declare an interface


# character.types.yml
Character:
  type: interface
  config:
    fields:
      id:
        type: "String"
      name:
        type: "String"
                        

# continuation of character.types.yml
...
  # Character.config.fields:
      friends:
        type: "[Character]"
        description: "The friends of the character."
      appearsIn:
        type: "[Film]"
        description: "Which movies they appear in."
                        

Declare the objects


# human.types.yml
Human:
  type: object
  config:
    fields:
      id:
        type: "String"
      name:
        type: "String"
      homeWorld:
        type: World
    interfaces: [Character]
    isTypeOf: true
                        

# droid.types.yml
Droid:
  type: object
  config:
    fields:
      id:
        type: "String"
      name:
        type: "String"
      homeWorld:
        type: World
    interfaces: [Character]
    isTypeOf: true
                        
Interfaces aren't required

Entry point


# query.types.yml
Query:
  type: object
  config:
    description: "All Star Wars queries"
    fields:
      allHumans:
        type: "[Human]"
        resolve: "@=resolver('allHumans', [args])" # Symfony Expression Language
      human:
        type: "Human"
        resolve: "@=resolver('human', [args['id'])" # Symfony Expression Language
        args:
          id:
            description: "Human's UUID"
            type: "String!"
                

Mutation entry point


# Mutation.types.yml
Mutation:
  type: object
  config:
    description: "All Star Wars mutations"
    fields:
      createReview:
        args:
          episode:
            type: "String!"
          content:
            type: "String!"
          rating:
            type: "Int!"
        type: Episode
        resolve: "@=mutation('reviewEpisode', [args['episode'], ...])"
                

Resolving it...


# services.yml
services:
  graphql.starwars.resolver.human:
    class: Mefi/GraphQL/Starwars/Resolver/HumanResolver
    arguments:
      - "@repository.human"
    tags:
      - { name: overblog_graphql.resolver, alias: "allHumans", method: "resolveAllHumans" }
      - { name: overblog_graphql.resolver, alias: "human", method: "resolveHuman" }
                

# query.types.yml
# Query.config
fields:
  allHumans:
    type: "[Human]"
    resolve: "@=resolver('allHumans', [args])"
                

Resolving it...


namespace Mefi\GraphQL\Starwars\Resolver;

use Overblog\GraphQLBundle\Definition\Argument;

class HumanResolver
{
    // Injected repository, properties and etc ...

    public function resolveAllHumans(Argument $args): array
    {
        // Do something with the $args
        return $this->humanRepository->findAll();
    }

    public function resolveHuman(string $humanId): Human
    {
        // Your business logic could be here, criterias, firewall, etc
        return $this->humanRepository->findById($humanId);
    }
}
                

Throwing errors


class HumanResolver
{
    // Injected repository, properties and etc ...
    // resolveAllHumans ...

    public function resolveHuman(string $humanId): Human
    {
        try {
            return $this->humanRepository->findById($humanId);
        } catch (HumanNotFoundException $e) {
            throw new UserError('This human was eliminated', 0, $e);
        }
    }
}
                

Possible errors Exceptions


throw new UserError('This human was eliminated');

throw new UserWarning('This human is sleeping');

throw new UserErrors(
    [
        new UserError('Human A not found', 0, $previousException),
        'Human B not found',
    ]
);
                

Versioning


 /graphql/v1
 /graphql/v2
 /graphql/v3
 /graphql/v4
				

Just kidding...

Simply as... deprecated


# droid.types.yml
Droid:
  type: object
  config:
    fields:
      id:
        type: "String"
      name:
        type: "String"
        deprecationReason: "Droids don't deserve names"
      homeWorld:
        type: World
    interfaces: [Character]
    isTypeOf: true
                

Query

Result


query droidType {
  __type(name: "Droid") {
    fields(includeDeprecated: true) {
      name
      isDeprecated
      deprecationReason
    }
  }
}
                    

{
  "data": {
    "__type": {
      "fields": [
        {
          "name": "id",
          "isDeprecated": false,
          "deprecationReason": null
        },
        {
          "name": "name",
          "isDeprecated": true,
          "deprecationReason": "No name for it"
        },
        ...
      ]
    }
  }
}
                    

Awesome... drop the mic

Limiting query by depth


/** @var \GraphQL\Validator\Rules\QueryDepth $queryDepth */
$queryDepth = DocumentValidator::getRule('QueryDepth');
$queryDepth->setMaxQueryDepth($maxQueryDepth = 10);

GraphQL::execute(/*...*/);
                
or with OverBlogGraphQLBundle

# app/config/config.yml
overblog_graphql:
  security:
    query_max_depth: 10
                

Limiting query by complexity


/** @var \GraphQL\Validator\Rules\QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setMaxQueryComplexity($maxQueryComplexity = 110);

GraphQL::execute(/*...*/);
                
or with OverBlogGraphQLBundle

# app/config/config.yml
overblog_graphql:
  security:
    query_max_complexity: 1000
                

Custom field complexity


# Query.types.yml

Query:
  type: object
  config:
    fields:
      droid:
        type: "Droid"
        complexity: '@=1000 + childrenComplexity'
        args:
          id:
            description: "id of the droid"
            type: "String!"
        resolve: "@=resolver('character_droid', [args])"
        

Security

Field access Control


# human.types.yml

Human:
  type: object
  config:
    fields:
      id:
        type: "String"
        access: "@=isSuperAdmin()"
                 

Field publicly visible


# human.types.yml

Human:
  type: object
  config:
    fields:
      id:
        type: "String"
        public: "@=service('security.authorization_checker').isGranted('ROLE_ADMIN')"
                 

Human:
  type: object
  fieldsDefaultPublic: "@=service('my_service').isGranted(typeName, fieldName)"
  config:
    fields:
      id:
        type: "String"
                 

Limiting data


namespace Mefi\GraphQL\Starwars\Resolver;

use Overblog\GraphQLBundle\Definition\Argument;

class HumanResolver
{
    // Injected repository, properties and etc ...

    public function resolveAllHumans(Argument $args): array
    {
        return $this->humanRepository->findAll();
        $criteria = Criteria::create()
        $criteria->where(
            Criteria::expr()->eq('race', $this->currentUser->getRace())
        );
        return $this->humanRepository->findAllByCriteria($criteria);
    }
}
                

Jetbrains autocomplete

Atom autocomplete

GraphiQL

GraphDoc

graphqlviz

GraphQL Network for chrome

Useful links

Thank you! Any Questions?

http://talks.mefi.in/graphql-scotphp17

Please leave feedback!

https://joind.in/talk/4919c