Add dynamic content sources to the YOOtheme Pro page builder.
Sources in YOOtheme Pro use GraphQL for querying data. In GraphQL a static and hierarchical schema with strong typing is used to define an API. GraphQL makes it possible to generate precise queries for dynamic content fields, which deliver exactly the data that is needed. YOOtheme Pro uses webonyx/graphql-php as its GraphQL PHP library.
Handling dynamic content in YOOtheme Pro is split into two steps: schema generation and data resolving. Because it's not clear which parts of the schema are needed before evaluating a query, the whole schema is required at every request. A schema is dynamic and can look different on each system depending on the data structure. To avoid repeating the schema generation on every request, it's only generated in the YOOtheme Pro customizer and is cached for front-end requests. The cache mechanism serializes the schema into the GraphQL schema language and stores it in the cache directory cache/schema.gql
of YOOtheme Pro. It can be quite useful during development to inspect the cached schema.
Dynamic content sources are defined by their object type configurations and their field resolution which returns the actual data. These object types are part of the type hierarchy which forms the GraphQL schema.
Content sources can be added to the page builder by using a custom module. The easiest way to get started is to try out the example module which adds a custom content source or take a look at the included YOOtheme Pro content sources.
The example module on GitHub demonstrates how to add custom content sources. Simply download and unzip the example module. The quickest way to try it out is using a child-theme.
YOOtheme Pro comes with many built-in content sources. They are a useful resource to get started when creating your own custom content sources. They can be found in the respective module directory under packages
in YOOtheme Pro.
Path | Description |
---|---|
builder-source/src/Source/Type |
Source transform and builder integration |
builder-source-filesystem/src/Type |
File system integration |
builder-joomla-fields/src/Type |
Joomla custom fields integration |
builder-joomla-source/src/Type |
Joomla common types for articles, categories and users |
builder-joomla-zoo/src/Type |
ZOO extension integration |
builder-wordpress-source/src/Type |
WordPress common types for pages, posts and taxonomies |
builder-wordpress-acf/src/Type |
Advanced custom fields plugin integration |
builder-wordpress-popular-posts/src/Type |
Popular posts plugin integration |
builder-wordpress-toolset/src/Type |
Toolset plugin integration |
Object types are the most basic components of a GraphQL schema. They represent a kind of object and its fields. To define GraphQL object types a code-first approach is used. It allows to dynamically build a schema depending on the system environment. Use an array
to create a simple object type configuration.
[
// Set type fields and their configuration
'fields' => [
],
// Set metadata to define the UI in the customizer
'metadata' => [
// Label used in the customizer
'label' => 'My Type',
// Make the type usable as dynamic content source
'type' => true,
]
]
The following property keys can be used in the object type configuration array.
Option | Type | Description |
---|---|---|
fields | array |
Set type fields and their configuration. |
metadata | array |
Set metadata to define the UI in the customizer. |
extensions | array |
Set extensions used to add resolver functions and services. |
Fields of an object type can be defined in the fields
array. Just add a field name and its field definition.
[
'fields' => [
'my_field' => [
'type' => 'String',
'metadata' => [
// Label shown in the field mapping select box in the customizer
'label' => 'My Field',
// Option group within the field mapping select box
'group' => 'My Group'
],
'extensions' => [
// A static resolver function to resolve the field value
'call' => 'MyResolver::resolveFn'
]
]
]
]
Every field is defined by the following properties.
Option | Type | Description |
---|---|---|
name | string |
Name of the field. When not set, inferred from args array key. |
type | string |
Type of the field like object, scalar or list of types |
args | array |
Set arguments configurations. |
metadata | array |
Set metadata to define the UI in the customizer. |
extensions | array |
Set extensions used to add resolver functions and services. |
Note The naming convention for field names is snake_case. Spaces or hyphens in field names will break the schema. Use the string helper YOOtheme\Str::snakeCase($string)
to conveniently convert names to snake case.
Like other programming languages GraphQL provides built-in scalar types like String
, Int
, etc. Usually the String
type is used for the fields.
[
'type' => 'String',
]
List modifiers can be used to define a list of scalar or object types. The resolver function of a repeatable field has to return an array
containing objects of the specified type. A field can be of any type which is defined in the schema including your own custom types.
[
'type' => [
'listOf' => 'MyType'
]
]
All fields get resolved using a resolver function. Every field has a default resolver which returns the value of a property with the same name. To define a custom resolver function for a field, simply override the default resolver.
A resolver function can be defined with the extensions.call
property in the field configuration. It needs to be a callable like a global or namespaced function or a static method.
[
'extensions' => [
'call' => 'MyResolver::resolveFn'
]
]
Note Closures can't be used because it's not possible to serialize them in the cached schema. Instead, access the data from the configuration in the resolver function using the args
option.
In order to pass static arguments to a resolver function, define the args
option. The arguments will be available in the $args
argument in the resolver function. All arguments have to be JSON serializable, because they are JSON encoded in the schema definition.
[
'extensions' => [
// Static function with arguments
'call' => [
'func' => 'MyResolver::resolveFn',
'args' => [
'arg1' => 'foo',
'arg2' => 'bar',
]
]
]
]
Option | Type | Description |
---|---|---|
func | string |
Name of the callable function |
args | array |
An array of arguments which will be serialized as JSON |
The MyResolver::resolveFn()
method has four parameters.
public static function resolveFn($obj, $args, $context, $info)
{
// Add code to query the data here
return 'the data';
}
Parameter | Type | Description |
---|---|---|
$obj | mixed |
The result of the previous resolver |
$args | array |
An array of field arguments |
$context | mixed |
A context object which is an array of parameters passed to the builder |
$info | GraphQL\Type\Definition\ResolveInfo |
An object that contains information about the current GraphQL operation |
To extend the GraphQL schema in YOOtheme Pro with a custom source, register an event handler SourceListener::initSource
to the source.init
event.
include_once __DIR__ . '/src/SourceListener.php';
include_once __DIR__ . '/src/MyTypeProvider.php';
include_once __DIR__ . '/src/Type/MyType.php';
include_once __DIR__ . '/src/Type/MyQueryType.php';
return [
'events' => [
'source.init' => [
SourceListener::class => ['initSource']
]
]
];
The YOOtheme\Builder\Source
service carries the schema definition and is passed as first argument to the initSource
listener.
Use the YOOtheme\Builder\Source::objectType($name, $config)
method to add a custom type. The first argument is the name of the type and the second its configuration. The naming convention for type names is UpperCamelCase. Use the string helper YOOtheme\Str::camelCase($string, true)
to conveniently convert names to upper camel case.
A GraphQL schema has a top-level entry point which is usually called the Query type. To make MyType
queryable as custom source in the Dynamic Content option, extend the Query type by invoking the YOOtheme\Builder\Source::queryType($config)
method. The method merges the MyQueryType
configuration with the Query type.
class SourceListener
{
public function initSource($source)
{
$source->objectType('MyType', MyType::config());
$source->queryType(MyQueryType::config());
}
}
It's recommended to modularize a type definition with its configuration and resolver functions into a PHP class.
class MyType
{
public static function config()
{
return [
'fields' => [
'my_field' => [
'type' => 'String',
'metadata' => [
'label' => 'My Field'
],
'extensions' => [
'call' => __CLASS__ . '::resolve'
]
]
],
'metadata' => [
'type' => true,
'label' => 'My Type'
]
];
}
public static function resolve($obj, $args, $context, $info)
{
// Add code to query the data here
return $obj->my_field;
}
}
To make the object type MyType
available in the Query type, add a field which returns the MyType
object type.
class MyQueryType
{
public static function config()
{
return [
'fields' => [
'custom_my_type' => [
'type' => 'MyType',
// Arguments passed to the resolver function
'args' => [
'id' => [
'type' => 'String'
],
],
'metadata' => [
// Label in the dynamic content select box
'label' => 'Custom MyType',
// Option group in the dynamic content select box
'group' => 'Custom',
// Fields to input arguments in the customizer
'fields' => [
// The array key corresponds to a key in the `args` array above
'id' => [
// Field label
'label' => 'Type ID',
// Field description
'description' => 'Input a type ID.',
// Default or custom field types can be used
'type' => 'text'
],
]
],
'extensions' => [
'call' => __CLASS__ . '::resolve',
],
],
]
];
}
public static function resolve($item, $args, $context, $info)
{
return MyTypeProvider::get($args['id']);
}
}
The MyTypeProvider is used to query MyType objects.
class MyTypeProvider
{
public static function get($id)
{
// Query objects
return (object) ['my_field' => 'the data'];
}
}
The type configurations are merged recursively when Source::objectType
is invoked multiple times with the same type name as argument. This allows defining new fields for any existing object type. The following code example adds a new field to an object type called Post
.
$source->objectType('Post', [
'fields' => [
'my_new_post_field' => [
// Add code for the field configuration here
]
]
]);
To avoid field naming conflicts for the Post
type, either prefix your field names or use an intermediate subtype. The subtype will define all field names without overriding any existing Post
type fields.
Note Mind that the intermediate subtype lacks a 'type' => true
metadata property.
$source->objectType('MySubType', [
'fields' => [
'title' => [
'type' => 'String',
'metadata' => [
'label' => 'My Title',
],
],
],
'metadata' => [
'label' => 'My SubType'
],
]);
$source->objectType('Post', [
'fields' => [
'my_subtype' => [
'type' => 'MySubType',
'extensions' => [
'call' => 'MySubTypeResolver::resolveFn',
],
]
]
]);