A basis module for GraphQL queries and mutations. This is also some sort of tutorial on how to write a GraphQL module for OXID.
Object Type is the most frequently used primitive in the GraphQL Schema.
Conceptually Object Type is a collection of Fields. Each field, in turn, has its own type which allows building complex hierarchies.
Every field, object, or argument has a type. GraphQL is a strongly typed language. In the Oxid Schema are 4 kind of types defined specifically for the application:
- System Types
- Base Types
- Common Types
- Custom Types
- Int
- Float
- String
- Boolean
- Id (serialized as String per spec)
- AddressType
- LoginType
- UserType
Your common and custom types are usually built on top of GraphQL system types.
- CategoryType
- and more ...
Types created specifically according to the needs for each project
This README provides step by step instructions on how to implement things.
The graph-ql-base-module is the primary framework for using GraphQL in OXID
We want to be able to get category listings from the shop and also to be able, to add a new category to the shop. So in GraphQL terminology: We want to have some queries (get) and a mutation (push, update, delete).
You should completely separate the domain logic from your GraphQL types. Just think about what information you want to expose through the GraphQL API and encapsulate it into data objects.
A data object should be a dumb object just with getters
and setters. So if you look at the Category
object in the
DataObject
folder, you see just a typical data object
with three properties: title
, id
and parentId
.
This Category
object is the container for the data
we want to expose through our GraphQL API. Now we need
to implement some business logic to fill the instances of this object with data.
In our case here the business logic is quite simple:
Retrieve the data from the database (and later on,
store data to the database). So we do not need a
service layer with complex data logic, but just a
data access object that implements database persistence.
You find the implementation in the Dao
directory, where
the CategoryDao
resides, alongside the appropriate
CategoryDaoInterface
.
Now, before implementing the data access object, think about the interface, you want to provide. In our case it is quite simple:
interface CategoryDaoInterface
{
public function getCategory(string $categoryId, string $lang, int $shopId);
public function getCategories(string $lang, int $shopId, $parentId = null);
public function addCategory(array $names, int $shopId, string $parentId = null);
}
Note that all methods have a language and
a shop id parameter (in addCategory
the language
is implicit because the $names
array is supposed to
hold names for all languages). This is quite important:
The OXID eShop supports a multi-language and multi-shop
environment. So each dao or service class should support
different languages and different shopids. In your
implementation, you should also honor this, even if
you implement your GraphQL route for a shop that only
has one language and no multi-shops. Reusability is
only guaranteed if you support the more advanced features.
Also, the effort is small, if you do it right from the
beginning.
After designing the interface and creating the dao class
with empty methods, you should first write a test class
for your interface. This is called test-driven development
(TDD) and is considered to be good practice. You will
find the test class for the CategoryDao
in the
tests\Integration\Dao
folder. After defining the expected
behavior of your code in the test class, every test
should fail, because you have not implemented anything.
Now implement the complete database logic until all of
your tests are green.
And that's it for domain logic in our small example, because
we straightforward retrieve data from or add data to the database.
If you need to do more complicated stuff, create a two tiered
architecture: Put all the persistence stuff into a data access
object and all the more sophisticated logic into a service,
that gets the dao injected (we talk about dependency injection
below). This service should be created in a separate Service
folder, but the principle is the same: First design an interface,
then write a test and finally implement the logic. Only
now you may write genuine unit tests because you can
mock all the stuff regarding the persistence of your data.
You just must make sure that you inject the dao into your
service, so you can mock it in the test.
Actually, this is quite easy. You just have to do some configuration stuff.
If you look at the CategoryType
(found in the
Type/ObjectType
folder) we want to use in this
example, it just extends the ObjectType
of the underlying
GraphQL framework. It only implements the constructor,
where a config array as created and given to the parent
constructor. If you want to understand in detail what
is happening here, just refer to the
GraphQL documentation,
because this is beyond the scope of this tutorial.
Except with one exception. The type object gets a
class implementing the GenericFieldResolverInterface
injected. This class is part of the OXID GraphQl
framework. It is a handy utility for resolving
fields. Look at the resolveField
instruction in
the configuration. Here an anonymous function is defined that
is called by the framework:
'resolveField' => function ($value, $args, $context, ResolveInfo $info) {
return $this->genericFieldResolver->getField($info->fieldName, $value);
}
Here again, comes dependency injection into play: On the
injected GenericFieldResolver
you can call the getField
method with a fieldname
and a data object. The GenericFieldResolver
then searches
for a getter method on the data object and calls it.
So the $value
this anonymous function receives is our
Category
data object, and from the ResolveInfo
object
we get the field name. Together with the GenericFieldResolver
it is simple to fetch the desired field from the object.
There is not much to test here, so we will test this in conjecture with the provider class that is explained in detail in the next section.
For all the data you want to provide as part of your GraphQL API your need to write a provider class. This binds the part of the API your module is providing to the overall GraphQL Framework. The binding itself is done via dependency injection, but this will be described in the next section. In this step, we describe just the definition and implementation of queries and mutations (if you do not know what queries and mutations are, refer to the GraphQL Documentation)
The provider class must implement at least the
QueryProviderInterface
or the MutationProviderInterface
or both. The interfaces are quite simple (the
MutationProviderInterface
is analogous):
interface QueryProviderInterface
{
/**
* @return array
*/
public function getQueries();
/**
* @return array
*/
public function getQueryResolvers();
}
The getQueries()
or getMutations()
methods just
return an array where you configure the fields
on the query respective mutation GraphQL type. In
a nutshell is just the type of the field,
a description and the arguments.
And then you need to define the resolvers for the fields. This is where you call the business logic that you implemented in Step 1.
Again you have to implement the same function
that you already know from the previous step.
Here is an example of our CategoryProvider
:
'category' => function ($value, $args, $context, ResolveInfo $info) {
/** @var AppContext $context */
$token = $context->getAuthToken();
$this->permissionsService->checkPermission($token, 'mayreaddata');
return $this->categoryDao->getCategory(
$args['categoryId'],
$token->getLang(),
$token->getShopid()
);
},
Now we use the two other parameters of the
function, the $args
array and the $context
object. From the context, you get the
authentication token. You should
always use the permission service to
check if the user has the permission
to do what you implement in the rest
of the function. In this example we
check for the mayreaddata
permission -
the lowest permission that exists and
is available for all logged in users:
$this->permissionsService->checkPermission($token, 'mayreaddata');
This is all you need to do for a permission check. If the user is missing the permission given as a second parameter, an exception is thrown which will result in a GraphQL error that informs the user that he is missing a certain permission.
The mayreaddata
permission is part of the framework,
but you may define your own permissions.
You can find an example in the addCategory
mutation. In the next step, we will show
you how to configure your own permissions.
But you can do more with the token than only just check permissions. The token also provides you with a language and a shop id. And you can be certain, that the token always returns a language and a shop id, even in a one language / one shop environment. This allows you to write your dao/service classes in a generic way that works in all shop environments.
Caveat
If you are using shop models to load data, you can't provide the language / shopid. These are already set on bootstrap. Currently there is not yet a mechanism in the shop bootstrap to set this from an authentication token. But this will be implemented soon.
After checking the permissions, you should keep the logic quite simple. All business logic should already be implemented in your dao / services classes. Here you shouldn't do anything more than perhaps configure service calls according to arguments of the query / mutation.
You can test your type / provider quite
easily. Look at the unit test CategoryTypeTest
.
It inherits from the GraphQlTypeTestCase
which
provides some helper methods. You can set
permissions before executing a query; and the
query will be executed with a default context
unless you do provide your own context
when calling the execute method. The setup
method shows you an example how to set up
the schema for testing the queries and mutations
that you provide for your GraphQL Type.
Now we have all the components together that we need to implement a part of a GraphQL API.
- The business logic in data access objects / services
- The GraphQL type that we want to provide
- The query / mutations that use our GraphQL type
The task is now to put it all together. For this purpose we use the Symfony DI container that comes with the OXID eShop framework.
The configuration is done in a file called
services.yaml
that you put into the root
directory of your module. If you are not
familiar with the Symfony DI container, you
should read the
documentation.
This is the configuration file:
services:
_defaults:
public: false
autowire: true
OxidCommunity\GraphQl\Dao\Common\CategoryDaoInterface:
class: OxidCommunity\GraphQl\Dao\Common\CategoryDao
OxidCommunity\GraphQl\Type\ObjectType\CategoryType:
class: OxidCommunity\GraphQl\Type\ObjectType\CategoryType
OxidCommunity\GraphQl\Type\Provider\CategoryProvider:
class: OxidCommunity\GraphQl\Type\Provider\CategoryProvider
tags: ['graphql_query_provider', 'graphql_mutation_provider']
OxidCommunity\GraphQl\Service\CategoryPermissionsProvider:
class: OxidEsales\GraphQl\Service\PermissionsProvider
calls:
- ['addPermission', ['admin', 'mayaddcategory']]
- ['addPermission', ['shopadmin', 'mayaddcategory']]
tags: ['graphql_permissions_provider']
All of our implementation goes to the services
section
of the configuration. At the beginning we define two defaults:
None of our services should be public (that means you can't
fetch them from the DI container, which is a good thing;
everything is handled within the container) and that
Symfony should try to autowire everything (that means,
if you Type hint the parameters in the constructor of
a service, the container searches for a service that
matches that Type hint when instantiating the service).
The next two entries are trivial: We define our data access object as a service using the interface of the dao as the service key. That helps autowiring when we type hint a dependency with this interface. The next service is equally trivial, it's our category type, and since this does not have an interface, we use the qualified class name itself as a key.
The next entry is a tad more complicated: It defines
our provider class. Again we use the class name itself
as a key. But in fact this key is quite irrelevant
because this key is never used anywhere. Instead, we
add two tags, the graphql_query_provider
and the
graphql_mutation_provider
. These tags are used to
inject the information from the provider into the
GraphQL schema. Since we implemented queries and
a mutation in one provider, we need both tags.
In the last entry we define the permissions
that we want to use. Again, the key is completely
irrelevant, so you may make up something. However, it
should be unique, so using the classpath of your
module together with a descriptive name makes sense.
The class itself is always the same, the PermissionsProvider
class that comes with the OXID GraphQL framework.
And now you can add permissions in the call section.
The method that is called is named appropriately
addPermission
, and the first parameter is the user
group, the second a permission (which is just the
string you use in your permission check in the
query / mutation resolver).
Currently there are four user groups:
- anonymous:
- a logged in user without a user account
- customer:
- a logged in user with a user account
- shopadmin:
- a user with administration rights for a certain shop
- admin:
- a user with global administration rights
Look at the services.yaml
file of the OXID graphql base
module to see which permissions are already set for
these groups.
If everything is tied together, you should write
acceptance tests for all your queries and mutations.
Again there is an example in the Acceptance
folder
under tests. This CategoryTest
inherits from
BaseGraphQlAcceptanceTestCase
that provides a setup
of the DI container. You really need not care about this,
just use the executeQuery
method to run a query / mutation.
You will find the result of query in three properties
of the test case:
$this->queryResult
contains data / errors that the client will receive$this-httpStatus
contains unsurprisingly the http status the client will receive$this->logResult
contains all errors that are normally written to the log file
This allows you to write nearly complete end to end tests without involving an http server.
To run the acceptance tests, you need to activate the module in the test configuration of your OXID project.
So for this example module, the test_config.yml
in the root
directory of your project should have the following entries:
mandatory_parameters:
...
partial_module_paths: 'oe/graphql-base,oxcom/graphql-common'
...
optional_parameters:
...
activate_all_modules: true
...