Custom Types

Create custom types and fields to extend dynamic content in YOOtheme Pro.

The dynamic content feature in YOOtheme Pro uses GraphQL for quering 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. Since YOOtheme Pro is extendable and supports custom fields, the schema is dynamic and can look different on each system. 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.

Note It can be quite useful during development to inspect the cached schema.


Type Definition

Dynamic content sources are defined as GraphQL types. To define GraphQL types a code-first approach is used, which allows to dynamically build a schema depending on the system environment.

The most basic components of a GraphQL schema are object types, which just represent a kind of object and its fields. Use an array to create an object type configuration.

[
   'fields' => [

       'my_field' => [

           'name' => 'my_field',

           'type' => 'String',

           'metadata' => [

               // Label used in the customizer
               'label' => 'My Field',

               // Option group within the dynamic content select box
               'group' => 'My Group'

           ],

           'extensions' => [

                // A static resolver function to resolve the field value
                'call' => 'Namespace\MyResolver::resolveFunc'
           ]

       ]
   ],

   'metadata' => [

       // Label used in the customizer
       'label' => 'My Type',

       // Denotes that this is an object type and makes the type usable as dynamic content source
       'type' => true,

   ]
]

Type Configuration

Option Type Description
fields array An array describing object fields with multiple field configurations.
metadata array An array of metadata used for customizer fields and descriptions.
extensions array An array of extensions used to add resolver functions and services.

Field Configuration

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, etc.
args array An array of arguments configurations.
metadata array An array of metadata used for customizer fields and descriptions.
extensions array An array of 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.

Scalar Types

Like other programming languages GraphQL provides built-in scalar types like String, Int, etc. Usually the String type is used for the fields on your types.

[
    'type' => 'String',
]

Lists of Types

List modifiers can be used to define a list of scalar or object types. Usually repeatable items from multiple items elements like grids and galleries are dynamically created using a list modifier. 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'
    ]
]

Resolving Fields

When data is queried, 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. When you define your own resolve function for a field, you simply override this default resolver.

Field Resolver

A resolve function can be defined with the extensions.call property in the field configuration. It needs to be a callable like global/namespaced function or a static method. 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 resolve function. All arguments will be JSON encoded in the schema definition, so only JSON serializable values are allowed.

Note Closures can't be used because it's not possible to serialize them in the cached schema. Instead you can share data between configuration and resolve function using args option.

[
    'extensions' => [

        // Static function (shorthand)
        'call' => 'Namespace\MyResolver::resolveFn',

        // Static function with arguments
        'call' => [
            'func' => 'Namespace\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.

Adding a Custom Type

The GraphQL schema in YOOtheme Pro can be extended by listening to the source.init event in the bootstrap.php file of the module. The first parameter of the listener is the Source object which carries the schema definition. It's recommended to modularize a type definition with its configuration and resolve functions into PHP classes.

<?php

namespace MyApp;

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)
    {
        // Query some data …

        return 'the data';
    }
}

Resolve Function

Argument Type Description
$obj mixed The parent 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 add a new type to the schema use the Source::objectType($name, $config) method in your source.init event listener. The first argument is the name of the type and the second is its configuration. The naming convention for type names is PascalCase. Use the string helper YOOtheme\Str::camelCase($string, true) to conveniently convert names to pascal case.

<?php

namespace MyApp;

class MyListener
{
    public function initSource($source)
    {
        $source->objectType('MyType', MyType::config());
    }
}

Adding a Custom Query

A GraphQL schema has a top-level entry point which is usually called Query. To make the custom object type MyType available in the Query type, add a field which returns the MyType object type. This can be accomplished by extending the Query invoking Source::queryType($config).

<?php

namespace MyApp;

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 dynamic content select box
                        'label' => 'Custom MyType',

                        // Option group in 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',

                                'labels' => [
                                    'type' => 'My Type',
                                ],
                            ],

                        ]

                    ],

                    'extensions' => [
                        'call' => __CLASS__ . '::resolve',
                    ],

                ],

            ]

        ];
    }

    public static function resolve($item, $args, $context, $info)
    {
        return MyTypeProvider::get($args['id']);
    }
}

The following code merges MyQueryType with the Query in your source.init event listener.

<?php

namespace MyApp;

class MyListener
{
    public function initSource($source)
    {
        $source->queryType(MyQueryType::config());
    }
}

Extending an Existing Type

When Source::objectType gets invoked multiple times with the same type name as argument, the configurations of the type are merged recursively. 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' => [

        'a_new_post_field' => [
            // Field config …
        ]

    ]

]);

Intermediate Types

To avoid field naming conflicts for the Post type either prefix your field names or use an intermediate subtype. The subtype will define all the 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' => 'Namespace\MySubTypeResolver::resolveFn',
            ],

        ]

    ]

]);

Included Types

YOOtheme Pro comes with many built-in types for the dynamic content feature. They are a useful resource to get started when creating your own custom types. They can be found in the following directories.

Path Description
vendor/yootheme/builder-source/src/Source/Type Source transform and builder integration.
vendor/yootheme/builder-joomla-fields/src/Type Custom fields integration.
vendor/yootheme/builder-joomla-source/src/Type Common types for articles, categories and users.
YOOtheme Pro Documentation