CCM19-Plugins
CCM19 can be extended with various functionalities through plugins.
Naming Conventions
Each plugin has a unique name consisting of a manufacturer-prefix and the actual plugin-name. The plugin-name should consist of English words.
Manufacturer-prefixes will be assigned centrally in the future. If you are developing plugins yourself, please ensure that the name is as unique as possible at this time to avoid name conflicts with the manufacturer prefix.
For the plugin-directory, the name is composed of the manufacturer-prefix in CamelCase-notation.
As the plugin-ID in the name-field of composer.json, the name is written as manufacturer/pluginname in snake_case.
Plugins developed by the CCM19-development team carry the prefix Ccm19.
A CCM19 plugin for "extended iframe-support" would therefore be named ccm19/extended_iframe or Ccm19ExtendedIframe, while a plugin from the vendor “Example Vendor” would be named, for example, examplevendor/really_special_plugin or ExamplevendorReallySpecialPlugin.
Basic Structure
A plugin is created as a folder in the plugins directory and contains the following files and folders:
composer.json– metadata about the pluginpreview.{png|jpeg|svg|webp|gif} – Preview imagesrc/– PHP-source files (ideally in a substructure like the one in the CCM19-src-directory)src/Controller/– Controller-PHP-files (optional)config/– Symfony-config-files (optional)templates/– Twig-templates (optional)translations/– translation files (optional)
Composer.json
The composer.json file looks like this:
{
"name": "examplevendor/really_special_plugin",
"description": "This plugin does something really special that should be described here.",
"version": "1.0",
"type": "ccm19-plugin",
"license": "proprietary",
"authors": [
{
"name": "ExampleVendor GmbH",
"role": "Manufacturer"
}
],
"extra": {
"copyright": "(c) Copyright...",
"activatePerDomain": true, // Plugin can be activated for individual domains in the Agency-version (de-)
"label": {
"en": "Really special plugin", // Display name in the plugin management section
"de": "Wirklich besonderes Plugin",
"fr": "Plugin vraiment spécial"
},
"description": {
"de": "This plugin does something very special that should be described here.",
"fr": "Ce plugin fait quelque chose de vraiment spécial qui devrait être décrit ici."
},
"manufacturerLink": {
"de": "https://examplevendor.example",
"en": "https://examplevendor.example"
},
"supportLink": {
"de": "https://examplevendor.example/support_de/",
"en": "https://examplevendor.example/support_en/"
},
"preview": "path/to/preview.png" // OPTIONAL: If the preview image is not located at the standard-location mentioned above
}
}
If translatable entries are missing in a language, the English entry is used as a fallback.
Plugins can also include their own dependencies via Composer. To do this, the composer.lock file and the vendor/-directory must be included in the plugin’s main directory. The plugin’s dependencies are then automatically loaded when the plugin is activated.
Templates
Templates from the plugin’s templates/-directory are included via Twig-namespaces according to the @plugin:PluginDirectory/ scheme.
In the example above with the-plugin, templates/index.html.twig would therefore be @plugin:ExamplevendorReallySpecialPlugin/index.html.twig.
Modifying the Main System’s Templates
To add to or edit templates for menu items other than your own, you can use the App\Event\TemplateResolveEvent and App\Event\TemplateRenderEvent events.
With App\Event\TemplateResolveEvent, any template-file (including those included using {% include(…) %}, for example) can be extended or replaced with a custom template. To do this, you typically use $event->extendTemplate('@plugin:ExamplevendorReallySpecialPlugin/….twig') to replace or supplement parts of the output using Twig-blocks.
With App\Event\TemplateRenderEvent, template-variables can be read using $event->get() and set using $event->set(), and menu item-templates (excluding sub--templates) can also be extended or replaced.
class BackendTemplateListener implements EventSubscriberInterface
{
private $pluginState;
public function __construct(PluginState $pluginState)
{
$this->pluginState = $pluginState;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
TemplateRenderEvent::nameForView('domain/index.html.twig') => ['onRenderDomainIndex', 100],
];
}
/**
* @return void
*/
public function onRenderDomainIndex(TemplateRenderEvent $event)
{
if (!$this->pluginState->isActiveForCurrentDomain()) {
return;
}
$event->extendTemplate('@plugin:ExamplevendorReallySpecialPlugin/domain_index.html.twig');
$event->set('someVariable', ...);
}
}
Routes and Menu Items
Routes and menu items can be created in controllers in src/Controller/ using the @Route and @Menu-annotations. The dependencies for the annotations are already imported by the main system. In the plugin, you therefore only need to import the annotations with use Symfony\Component\Routing\Annotation\Route; and use App\Component\Menu\Annotation\Menu;.
Routes-names must begin with plugin_manufacturer_plugin_name_ so that access control takes effect and plugin-routes are enabled or disabled depending on whether a plugin is activated for a user. The plugin-name and the vendor prefix must be written in the same snake_case-notation as in composer.json (using underscores instead of slashes).
@Menu-Annotation
The Menu-annotation creates a menu item for a route.
It can have the following parameters:
- first parameter or
name: string The name used to display the menu item. This is translated using the standard translation system. group: string Menu group in which the menu item should be displayed (optional; defaults to "Plugins")icon: string Icon-name. Currently,glyphicon-…andfa-…are supported.order: integer Order-ID that determines the position in the menuroute: string Route to which the menu item should lead (default: read from the method’s@Route-annotation)route_group: string Prefix of all routes belonging to the menu item (default: read from the@Route-annotation of the class, if present; otherwise, it is the same asroute). This is used to determine whether the menu item is still active.access={…}: string[] Who should have access to the menu item. Possible values are"admin","user", and"subuser". (Default: determined from the superclass of the controller-class).navigation: string In which navigation the menu item should be displayed. Possible values aremain,domain, andmeta. Here,maincorresponds to the navigation the user sees upon logging in. Thus, eitherdomainormeta, depending on the edition and user type. (Default:domainforDomainDependantControllers, otherwisemain).editions={…}: string[] Editions in which the menu item should be displayed. (Default:{"extended"}forHostingController, otherwise all editions).envs={…}: string[] Environments in which the menu item should be displayed. E.g.,{"dev"}for developer mode only. (Default: all)
Default Access Rights and Navigation Area Settings
Unless explicitly set with access and navigation, who can see the menu item and where depends on the class from which the controller is derived.
DomainDependantController: In the-navigation. Visible only to users who have domain access (not Agency-admins)AdminController: In the main navigation. Visible only to admins in the Agency-Edition and to users in the Basic-Edition.HostingController: In the main navigation. Visible only to admins in the Agency-Edition.AbstractController: In the main navigation. Visible only to users who have domain access (excluding Agency-admins)UniversalController: In the main navigation. Visible to everyone.UnrestrictedController: In the main navigation. Visible to everyone.
What constitutes the main navigation varies depending on the edition and user type. It is always the navigation that appears after logging in. This means the domain-navigation for Basic-Edition-users and the Meta/User-navigation for Agency-customers. For Agency-admins, it reverts to the "Domain"-navigation (even if there is no domain).
Example:
namespace Plugins\ExamplevendorReallySpecialPlugin;
use App\Component\Menu\Annotation\Menu;
use App\Controller\DomainDependantController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/domains/{_domainId}/plugins/examplevendor/really_special/", name="plugin_examplevendor_really_special_plugin_")
*/
class MainController extends DomainDependantController
{
/**
* @Menu("Really special", icon="fa-plug")
* @Route("", name="index", methods={"HEAD", "GET"})
*/
public function index(...): Response
{
...
}
/**
* @Route("", name="save")
*/
public function indexSave(...): Response
{
...
}
}
Symfony's-autowiring can be used. Within plugins, two additional special autowiring-arguments are available:
App\Model\PluginStateprovides the plugin-status-model for the current plugin, which can be used, for example, to check whether the plugin should be active.string $PLUGINPATHreturns the full path to the plugin-directory.
Menu items in the domain-menu (i.e., those whose controllers extend DomainDependantController) are automatically-/hidden when the plugin is activated for the domain (de-).
Menu items that are not domain-dependent are automatically hidden in the Agency-version when the plugin is disabled for a client.
For more specific requirements, it is also possible to manually set menu entries via an event-listener for the App\Event\MenuGenerationEvent.
Responding to Routes of Other Controllers
Using the event ccm19.controller.request.<route_name>—for example, ccm19.controller.request.app_dashboard—you can respond to a route before the respective controller is executed.
The - event object is a Symfony\Component\HttpKernel\Event\ControllerEvent. The setController() method can be used to redirect the request to a different controller.
The ccm19.controller.response.<route_name> event, on the other hand, allows code to be executed after the controller has completed.
Neither event is triggered in the event of unauthorized access—for example, when a route that requires a logged-in user is called without a valid session.
Important: For all event-handlers, you must always check whether a plugin action is desired in the current context using $pluginState->isActiveForCurrentDomain() or, depending on the context, $pluginState->isAllowedForCurrentUser().