Skip to main content

Creating an Integrations Hub page

Jadu Central's Integrations hub can be further extended by registering custom integrations. Alongside secure storage of integration credentials, custom integrations can make use of the integration hub user interface, and a service manager to request integration details within homepage widgets and custom scripts.

Creating an Integrations Hub page

Custom Integrations Hub pages follow the MVC architecture. From Jadu Central 4.0 onwards, to add your own page to the Hub to capture integration configuration you will need to create a Symfony bundle.

tip

In versions less than Jadu Central 4.0, the steps to create an integration hub page are different. Please contact us for more information.

The bundle is essentially a directory which contains everything your integration needs in one place.

To create an Integration page, the bundle will need:

  • An integration definition
  • A View (twig)
  • A Symfony controller (and associated configuration)
  • A service locator
  • A module page resolver
  • A migration to update services configuration

Jadu Central provides storage for integration settings, so in most cases you do not need to add your own database tables or model classes.

Adding a Symfony bundle

  • Create a folder - /path/to/jadu/jadu/custom/MyIntegrationBundle
  • Add a file into this folder, called MyIntegrationBundle.php:
namespace Jadu\Custom\MyIntegrationBundle;

use Jadu\Symfony\Kernel\Bundle\Bundle;

class MyIntegrationBundle extends Bundle
{
}

This file (and the class it contains) determine the name of the Bundle. This is usually the same as the name of the bundle's folder, which, in this example, is MyIntegrationBundle.

  • Create a sub folder within the bundle called DependencyInjection. The full path to this should be /path/to/jadu/jadu/custom/MyIntegrationBundle/DependencyInjection
  • In this sub folder, create a new class called MyIntegrationExtension in the file MyIntegrationExtension.php:
namespace Jadu\Custom\MyIntegrationBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class MyIntegrationExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
}
}

Once we have created our custom bundle, we need to register it. This is achieved by creating a new file, /path/to/jadu/config/bundles.xml:

<?xml version="1.0" encoding="utf-8"?>
<bundles xmlns:config="http://www.jadu.co.uk/schema/config">
<core config:type="array">
<item key="custom.my_integration">Jadu\Custom\MyIntegrationBundle\MyIntegrationBundle</item>
</core>
<cc config:type="array" />
<frontend config:type="array" />
</bundles>

This file registers the bundle definition, and loads it in the Control Center and on the front end. These are the correct places to load the bundle for Jadu Central forms, as they do not support Galaxies Sites.

The bundles.xml file can either be placed under version control and included in Meteor patches, or be created/modified by a filesystem migration.

Integration definition

This is used by the Integrations Hub user interface to present your integration to the user.

Create a class that extends Jadu\Integrations\AbstractIntegration and implement all of the required methods.

namespace Jadu\Custom\MyIntegrationBundle\Integration;

use Jadu\Integrations\AbstractIntegration;

class MyIntegrationDefinition extends AbstractIntegration
{
public function getTitle()
{
return 'My Integration';
}

public function getDescription()
{
return 'Link Jadu Central with the MyIntegration Server';
}

public function getImage()
{
return '/jadu/custom/images/myintegration.png';
}

public function getUrl()
{
return SECURE_JADU_PATH . '/integrations/my-integration';
}

public function getMachineName()
{
return 'my-integration';
}

/**
* Indicate whether this integration is available for Galaxies sites
*/
public function galaxySiteEnabled()
{
return true;
}
}

View

Create a twig template to capture your integration settings. Some key things to include:

  • extend the integrations base template
  • set the active_integration to the machine key of your integration
  • place your form and any other content in the integration_content block

For more information and examples of implementing forms, please see the Pulsar Forms component documentation.

{% extends '@assets/utilities/integrations/base.html.twig' %}

{% set active_integration = 'my-integration' %}
{% block integration_content %}
{{
form.create({
'class': 'form piano__form',
'method': 'POST',
'action': SECURE_JADU_PATH ~ '/integrations/my-integration',
})
}}

{% if errors is not empty %}
{% set _errors = [] %}
{% for key, value in errors %}
{% set _errors = _errors|merge([{
'label': value,
'href': key,
}]) %}
{% endfor %}
{{
form.error_summary({
'heading': 'There is a problem',
'errors': _errors,
})
}}
{% endif %}

{{
form.text({
[...]
})
}}

{% if pageAccess.updateContent %}
{{
form.hidden({
'name': '__token',
'value': csrfToken
})
}}

{{
form.end({
'class': 'form__actions--flush',
'actions': [
form.submit({
'label': 'Save',
'class': 'btn btn--primary'
})
]
})
}}
{% else %}
{{ form.end() }}
{% endif %}
{% endblock %}

The twig template should be located under Resources\views in your bundle, e.g. Jadu\Custom\MyIntegrationBundle\Resources\views\index.html.twig

Controller

Add a Symfony controller class to handle loading and saving of the integration details:

n<?php

declare(strict_types=1);

namespace Jadu\Custom\MyIntegrationBundle\Controller;

use Jadu\ControlCentre\PageUtil;
use Jadu\ControlCentre\Response\Renderer;
use Jadu\Integrations\Details\DataMapper as IntegrationDetailsMapper;
use Jadu\Integrations\IntegrationControllerHelper;
use Jadu\Integrations\IntegrationDetailHelper;
use Jadu\Security\CSRFToken;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ManageController
{
use IntegrationDetailHelper;
use IntegrationControllerHelper;

public function __construct(
private Renderer $renderer,
private IntegrationDetailsMapper $dataMapper,
private PageUtil $utils,
private CSRFToken $csrfToken
) {
}

public function __invoke(Request $request): Response
{
$apiKey = null;
$enabled = null;
$errors = [];

if ($request->getMethod() === 'POST') {
if (!$this->utils->get('pageAccess')->updateContent) {
return $this->redirectToError();
}

if (!$this->csrfToken->isValid($request->request->get('__token'))) {
$this->renderer->flash()->set('error', 'There was an error processing the form.');
return new RedirectResponse($this->getSitePrefix() . '/jadu/integrations/my-integration');
}

$enabled = true;
$apiKey = trim($request->request->get('my-integration-api-key', ''));
$errors = $this->validate($apiKey);
if (empty($errors)) {
$this->setIntegrationValue('my-integration-api-key', $apiKey);

$this->createAdminPageAction('Updated My Integration');
$this->renderer->flash()->set('success', 'Your changes have been saved successfully.');
return new RedirectResponse($this->getSitePrefix() . '/jadu/integrations/my-integration');
}
}

$params = [
'tab_visibility' => 'is-selected',
'errors' => $errors,
'values' => [
'my-integration-api-key' => $apiKey ?? $this->getIntegrationValue('my-integration-api-key'),
]
];

return $this->renderer->render(
'@MyIntegration/index.html.twig',
array_merge($params, $this->getIntegrationsViewParams())
);
}

private function validate(string $apiKey): array
{
// TODO
return [];
}
}

There are a few parts to note in the above __invoke method:

        if (!$this->utils->get('pageAccess')->updateContent) {
return $this->redirectToError();
}

This checks the user can update this page, and redirects if not. The redirectToError method is defined in the IntegrationControllerHelper trait included at the top of the class.

            if (!$this->csrfToken->isValid($request->request->get('__token'))) {
$this->renderer->flash()->set('error', 'There was an error processing the form.');
return new RedirectResponse($this->getSitePrefix() . '/jadu/integrations/my-integration');
}

This checks the CSRF token, and sets a flash error message across the page if invalid. This is only visible on page reload, so we return a Symfony RedirectResponse here to refresh the page.

            $this->setIntegrationValue('my-integration-api-key', $apiKey);

This method is defined in the IntegrationDetailHelper trait, and saves credentials to the database securely. They can be retrieved by the method $this->getIntegrationValue('my-integration-api-key') (from the same trait).

            $this->createAdminPageAction('Updated My Integration');

This helper method is in IntegrationControllerHelper, and writes a log to show what the admin is doing on the page for auditing purposes.

        return $this->renderer->render(
'@MyIntegration/index.html.twig',
array_merge($params, $this->getIntegrationsViewParams())
);

Here we render the twig template for the page, passing our $params array. We access the twig using the @MyIntegration syntax. Our bundle is MyIntegrationBundle, so Symfony removes the word Bundle and looks for the template in the standard location (the Resources/views directory of the bundle). We also merge $params with $this->getIntegrationsViewParams() - this is another method from IntegrationControllerHelper, which ensures all other integrations are loaded into the sidebar of the page.

Registering the Controller

To register the controller, add a new file called services.yml to your bundle under Resources/config. In this file, add your controller definition:

services:
_defaults:
autowire: true

Jadu\Custom\MyIntegrationBundle\Controller\ManageController:
public: true

Tip: You can use this YAML file for other services relating to your integration, e.g. an API client.

You'll also need to define a route to it, so it can be accessed at a certain URL. To do this, add a routing.yml in the same directory:

my_integration_manage:
path: /integrations/my-integration
defaults:
_controller: Jadu\Custom\MyIntegrationBundle\Controller\ManageController

Service Locator

This class is used by Jadu Central to register your Integration:

namespace Jadu\Custom\MyIntegrationBundle\Locator;

use Jadu\Response\HtmlResponse;
use Jadu\Service\Container;
use JaduFramework\Service\AbstractLocator;

class ServiceLocator extends AbstractLocator
{
protected $serviceContainer;

public function __construct(Container $serviceContainer)
{
parent::__construct($serviceContainer);

// Register the definition
$definition = $this->serviceContainer
->getInjector()
->make('Jadu\Custom\MyIntegrationBundle\Integration\MyIntegrationDefinition');

$this->serviceContainer
->getIntegrationsContainer()
->register($definition);
}

public function init()
{
}
}
note

The init() method must be defined but is unused at present. In a future version of Jadu Central this may be called a single time when an instance of the class is created. At present, it is recommended to place any logic that is required to be called when an instance is created into the constructor (or a method called within the constructor).

Module Page Resolver

A module page resolver is necessary to ensure your new route adheres to Admin Privileges:

declare(strict_types=1);

namespace Jadu\Custom\MyIntegrationBundle\ModulePageResolver;

use Jadu\ControlCentre\Handler\ModulePage\ResolverInterface;

class MyIntegrationModulePageResolver implements ResolverInterface
{
/**
* @var array
*/
private $list = [
'/integrations' => [
'my_integration_manage',
]
];

public function getList():array
{
return $this->list;
}
}

To take effect, this must be registered in Resources/config/services.yml:

    Jadu\Custom\MyIntegrationBundle\ModulePageResolver\MyIntegrationModulePageResolver:
tags:
- { name: jadu_cc.module_page_resolver }

services.xml Configuration

Once all of the above are completed, you must register your integration within the configuration file config/services.xml.

You should add a migration to your project to apply this consistently to each environment:

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

class Version20230102034545 extends AbstractMigration
{
public const CONFIG_FILE = 'config/services.xml';

public function up(Schema $schema): void
{
$config = $this->version->getConfiguration();
$configPath = $config->getJaduPath() . '/' . self::CONFIG_FILE;

if (!file_exists($configPath)) {
$xml = '<' . '?xml version="1.0" encoding="utf-8" ?>
<system xmlns:config="http://www.jadu.co.uk/schema/config">
<services config:type="array">
<item key="my-integration">Jadu\Custom\MyIntegrationBundle\Locator\ServiceLocator</item>
</services>
</system>';

file_put_contents($configPath, $xml);
} else {
$xml = simplexml_load_file($configPath);

if (!isset($xml->services)) {
$servicesElement = $xml->addChild('services', null);
$servicesElement->addAttribute('config:type', 'array');
} else {
$servicesElement = $xml->services;
}

$found = false;
foreach ($servicesElement->item as $itemElement) {
if (isset($itemElement['key']) && (string) $itemElement['key'] === 'my-integration') {
$found = true;
break;
}
}

if (!$found) {
$itemElement = $servicesElement->addChild('item', 'Jadu\Custom\MyIntegrationBundle\Locator\ServiceLocator');
$itemElement['key'] = 'my-integration';
}

file_put_contents($configPath, $xml->asXML());
}
}

public function down(Schema $schema): void
{
$config = $this->version->getConfiguration();
$configPath = $config->getJaduPath() . '/' . self::CONFIG_FILE;

if (!file_exists($configPath)) {
return;
}

$xml = simplexml_load_file($configPath);

if (!isset($xml->services)) {
return;
}

$index = 0;
foreach ($xml->services->item as $itemElement) {
if (isset($itemElement['key']) && (string) $itemElement['key'] === 'my-integration') {
unset($xml->services->item[$index]);
file_put_contents($configPath, $xml->asXML());

return;
}

++$index;
}
}
}