Developing GraphQL APIs for Totara
This page provides detailed background information on how our GraphQL APIs are organised. If you want to implement a GraphQL service, you might be more interested in the Implementing GraphQL services page.
For more background on the structure and format of GraphQL requests, see Using GraphQL APIs.
Endpoint types
We offer a number of different GraphQL APIs, each with its own separate endpoint (URL) where it can be accessed. You can find out more about the differences between these APIs on the Available APIs page.
Each endpoint has its own schema, defined via schema files, which means different services are available in different endpoints.
About schema files
Our implementation of GraphQL uses schema files written in SDL to define the API services that are available. Individual schema files (which are created within components and plugins) are combined by our schema loader into a single schema per endpoint type.
The naming of the objects within the schema must follow a specific convention, and the name that is used will determine the location in the server codebase where the specific object will be handled. See the Locations for files relating to services section below for details.
Building the schema
In order to allow plugins to specify their own schema in an extensible way, the overall schema for an endpoint is built from individual files located across the codebase. Schema building happens dynamically in code when required, but is cached as it can be a time-consuming operation to generate.
There are a few ways for a developer to access the full schema for a specific endpoint type:
Via introspection
Often it is not necessary to download the schema directly, as GraphQL clients can request details on the schema structure via an introspection query to the endpoint itself. In this case, the endpoint will build the schema if necessary and serve the result directly. Note that this is only available for the developer API and the external API when the enable_introspection admin setting is enabled.
Via a schema URL
When the development API is enabled, the full schema can be downloaded via the following URL:
https://YOUR-SITE-URL/totara/webapi/dev_graphql_schema.php
When the external API is enabled, the external schema can be downloaded by a logged-in user with access to the API reference documentation via:
https://YOUR-SITE-URL/totara/api/documentation/schema.php
Build via CLI
There is a command line (CLI) PHP script within the codebase, which can be run to generate schema for a specific endpoint:
Produces a complete GraphQL schema for the specified type by concatenating individual schema files. The specified file will be overwritten if it already exists. php ./server/totara/api/cli/generate_external_schema.php -t=ajax -f=totara.graphqls Options: -h, --help Print out this help -t, --type Endpoint type e.g. 'ajax', 'dev', 'mobile' or 'external' (default) -f, --file Writes the schema to the given file. Use '-' for stdout
Build via API reference docs build script
There is another command line (CLI) PHP script which builds the files needed to generate the API reference documentation. This will generate schema files for all endpoint types along with the required metadata.json files, via the command:
php server/totara/api/cli/prep_api_docs.php
See the Extending API documentation page for more details on how to build the reference documentation.
Locations for files relating to services
The naming of a GraphQL file uses a consistent naming convention, which determines where the code that is executed is stored in the server codebase:
Type in GraphQL | Name in GraphQL | Location in PHP code | Class in PHP code |
---|---|---|---|
query | {$component_name}_{$query_name} | server/{$component_path}/classes/webapi/resolver/query/{$query_name}.php [1] | \$component_name\webapi\resolver\query\{$query_name} |
mutation | {$component_name}_{$mutation_name} | server/{$component_path}/classes/webapi/resolver/mutation/{$mutation_name}.php | \$component_name\webapi\resolver\mutation\{$mutation_name} |
type | {$component_name}_{$type_name} | server/{$component_path}/classes/webapi/resolver/type/{$type_name}.php | \$component_name\webapi\resolver\type\{$type_name} |
input | {$component_name}_{$type_name} | server/{$component_path}/classes/webapi/resolver/type/{$type_name}.php | \$component_name\webapi\resolver\type\{$type_name} |
union | {$component_name}_{$union_name} | server/{$component_path}/classes/webapi/resolver/union/{$union_name}.php | \$component_name\webapi\resolver\union\{$union_name} |
persisted query or mutation | {$component_name}_{$query_name} | server/{$component_path}/webapi/{$endpoint_type}/{$query_name}.graphql [2] | N/A |
schema file | N/A | server/{$component_path}/webapi/{$filename}.graphqls server/{$component_path}/webapi/{$endpoint_type}/{$filename}.graphqls [3] | N/A |
[1]Â {$component_name} is the 'frankenstyle' name made up of the component type plus component name, e.g. 'mod_perform' or 'block_current_learning'. This maps to a specific {$component_path} for the plugin as determined by core_component::get_component_directory()
.
[2] Persisted queries belong to a specific endpoint type (currently either 'ajax' or 'mobile' as these are the endpoint types that support persisted queries). The endpoint the request is made against will determine which path is requested for the persisted query.
[3] Schema files in the root of the webapi/ directory are included in all endpoints, whereas schema files in a named subdirectory are only included in that endpoint type's schema.
Examples
An AJAX request for the persisted query with the operation name 'mod_perform_add_participants' would be found on the server at 'server/mod_perform/webapi/ajax/add_participants.graphql'.
The resolver for the core_user type would be defined in the class file found on the server at 'server/lib/classes/webapi/resolver/type/user.php' with the fully-qualified classname of '\core\webapi\resolver\type\user'.
The resolver for the mutation 'totara_api_delete_client' would be found on the server at 'server/totara/api/classes/webapi/resolver/mutation/delete_client.php' with the fully-qualified classname of '\totara_api\webapi\resolver\mutation\delete_client'.
Totara's use of GraphQL
Our implementation
Our server-side implementation of GraphQL makes use of the graphql-php library. See their documentation for more information about the library itself.
The library itself is included as a dependency to our 'required' package, with package code stored in libraries/required/webonyx/graphql-php
.
Our implementation of the library can be found in the code folder server/totara/webapi/
. Our implementation provides the API endpoints and some Totara-specific wrappers of the library's code.
Some important files within this component are described below:
File path | Purpose |
---|---|
server/totara/webapi/classes/schema_file_loader.php | Class responsible for locating schema files within the Totara codebase. |
server/totara/webapi/classes/schema_builder.php | Class responsible for using the schema file loader to construct a complete API schema for use by GraphQL. |
server/totara/webapi/classes/server.php | Class responsible for handling a specific request. |
server/totara/webapi/classes/default_resolver.php | Given a 'piece' of a GraphQL query, this code works out which class it should be passed to to be resolved. It also handles adding any middleware around the resolver call. Resolvers are called multiple times during a single request. |
server/totara/webapi/classes/processor.php | Class responsible for identifying the actual operation to execute, obtaining the schema and for setting up the GraphQL library correctly. |
server/totara/webapi/classes/serverless.php | In order to allow us to load state directly into pages from GraphQL queries without having to trigger a separate AJAX request, we have this class, which lets us trigger GraphQL requests and obtain the data without wrapping it in a full HTTP response. |
server/totara/webapi/classes/resolver_helper.php | Helper class containing some methods used by default_resolver and other classes. |
server/totara/webapi/classes/request.php | Class that represents a request object, and contains useful helper methods such as validate(). |
server/totara/webapi/classes/endpoint_type/ | Contains the base.php endpoint type class, plus specific child classes for each endpoint type. This controls functionality that varies by endpoint type (for example, whether the endpoint support persistent queries or user specified queries). |
server/totara/webapi/classes/local/util.php | Utility class containing helpful methods used by the other parts of this component. |
server/totara/webapi/classes/controllers/ | Contains the base api_controller.php class plus endpoint_type-specific controllers. |
In addition to the server/totara/webapi/ folder, the implementation of some of the core features are in server/lib/classes/webapi
. For example:
File path | Purpose |
---|---|
execution_context.php | The execution context class is used to store contextual data that lives over the lifetime of the request. It is defined early on in the request lifecycle and can be queried and used to store data during the request processing. |
type_resolver.php | Base classes for core resolver classes - these should be extended when implementing resolvers of a specific type. |
middleware.php middleware_group.php | Interfaces defining how middleware classes should be implemented. |
param.php param/*.php | Implementations of Totara's core param types as GraphQL scalars. |
scalar.php scalar/*.php | Implementations of some additional core GraphQL scalars. |
reference/base_reference_record.php | Abstract base class that can be extended to implement reference classes. See Reference input types under Common GraphQL patterns for more details. |
The lib folder also contains core implementations of some of the features of the API, such as middleware, formatters and resolvers.
Request processing order
A typical successful request will take the following path through the code:
- Endpoint file called (e.g. /api/graphql.php for external API).
- Control passed to endpoint api_controller (see server/totara/api/classes/controllers/) process() method.
- action_graphql_request() method called on api_controller.
- A new webapi server (server/totara/webapi/classes/server.php) is instantiated and handle_request() method is called.
- A new webapi processor (server/totara/webapi/classes/processor.php) is instantiated and process_request() method is called.
- A new StandardServer (class from the php-graphql library) is instantiated and executeRequest() method is called.
- Various asynchronous php-graphql methods are executed to manage the request.
- Eventually control is passed to default_resolver (server/totara/webapi/classes/default_resolver.php) to identify the class to resolve the current part of the request.
- The default_resolver __invoke() method identifies the resolver classname (via the GraphQL operation name) and calls the resolve() method on that class (also adding in any required middleware).
- The resolve() method returns the required data, which is combined with data from other resolve() calls to build the complete response.
- The response is constructed and returned to the user.