Zum Inhalt

CCM19 plugins

CCM19 can be extended with various functionalities using plugins.


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 future. If you develop plugins yourself, please make sure that the name is as unique as possible so that there are no name collisions with the manufacturer prefix.

For the plugin directory, the name is combined with 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 have the prefix Ccm19.

A plugin from CCM19 for "extended iframe support" would therefore be called ccm19/extended_iframe or Ccm19ExtendedIframe, a plugin from the manufacturer "Example Vendor" e.g. 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 plugin
  • preview.{png|jpeg|svg|webp|gif} - preview image
  • src/ - PHP source files (ideally in a substructure like in the CCM19-src directory)
  • src/Controller/ - Controller PHP files (optional)
  • config/ - Symfony config files (optional)
  • templates/ - Twig templates (optional)
  • translations/ - translation files (optional)


The composer.json 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 (de)activated for individual domains in the agency version
        "label": {
            "en": "Really special plugin", // Display name in the plugin administration
            "de": "Really special 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": {
            "en": "https://examplevendor.example",
            "en": "https://examplevendor.example"
        "supportLink": {
            "en": "https://examplevendor.example/support_de/",
            "en": "https://examplevendor.example/support_en/"
        "preview": "path/to/preview.png" // OPTIONAL: If the preview image is not in the above-mentioned default location

If translatable specifications are missing in a language, the English specification is used as a fallback.

Plugins can also provide their own dependencies using Composer. To do this, the composer.lock and the vendor/ directory must be included in the main directory of the plugin. The dependencies of the plugin are then automatically loaded when the plugin is activated.


Templates from the templates/ directory of the plugin are integrated via Twig namespaces according to the @plugin:PluginVerzeichnis/ scheme.

In the example plugin above, templates/index.html.twig would therefore be @plugin:ExamplevendorReallySpecialPlugin/index.html.twig.

Change templates of the main system

To add or edit templates of menu items other than your own, the events App\Event\TemplateResolveEvent and App\Event\TemplateRenderEvent can be used.

With App\Event\TemplateResolveEvent, any template file (including those that are included with {% include(...) %}, for example) can be extended or replaced with its own template. For this purpose, $event->extendTemplate('@plugin:ExamplevendorReallySpecialPlugin/....twig') is usually used to replace or supplement parts of the output with Twig blocks.

With App\Event\TemplateRenderEvent, template variables can be read or set with $event->get() and $event->set() and menu item templates (not included 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()) {

        $event->set('someVariable', ...);

Routes and menu items

Routes and menu items can be created via the @Route and @Menu annotations in controllers in src/Controller/. The dependencies for the annotations are already imported from 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;.

Route names**** must start with plugin_manufacturer_pluginname_ so that the rights management takes effect and plugin routes are activated/deactivated depending on whether a plugin is activated for a user. The plugin name and the manufacturer prefix must be written in the same snake_case notation as in composer.json (only with underscore instead of slash).

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 normal translation system.
  • group: string Menu group in which the menu item is to be displayed (optional, by default under "Plugins")
  • icon: string Icon name. Currently glyphicon-... and fa-... are supported.
  • order: integer Order ID with which the position in the menu is determined
  • route: string Route to which the menu item should lead (default: is read from the @route annotation of the method)
  • route_group: string Prefix of all routes that belong to the menu item (default: is read from the @route annotation of the class, if available, otherwise equal to route). This is used to display 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: is determined from the superclass of the controller class).
  • navigation: string In which navigation the menu item should be displayed. Possible values are main, domain and meta. Here, main corresponds to the navigation that the user is shown when logging in. So either domain or meta, depending on the edition and user type. (Default: domain for DomainDependantControllern, otherwise main).
  • editions={...}: string[] Editions in which the menu item should be displayed. (Default: {"extended"} for HostingController, otherwise all editions).
  • envs={...}: string[] Environments in which the menu item should be displayed. E.g. {"dev"} for only in developer mode. (default: all)

Presetting the access rights and the navigation area

If not explicitly set with access and navigation, it depends on the class from which the controller derives who gets to see the menu item and where.

  • DomainDependantController: In the domain navigation. Only visible to users who have domain access (no agency admins)
  • AdminController: In the main navigation. Only visible for admins in the Agency edition and users in the Basic edition.
  • HostingController: In the main navigation. Only visible to admins in the Agency edition.
  • AbstractController: In the main navigation. Only visible for users who have domain access (no agency admins)
  • UniversalController: In the main navigation. Visible to everyone.
  • UnrestrictedController: In the main navigation. Visible to all.

What the main navigation is differs depending on the edition and user type. It is always the navigation that appears after logging in. So the domain navigation for Basic Edition users and the meta/user navigation for Agency customers. For Agency Admins, it is again the "Domain" navigation (even if there is no domain).


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 autowiring can be used. Two additional special autowiring arguments are possible within plugins:

  • App\Model\PluginState returns the plugin state model for the current plugin, which can be used, for example, to check whether the plugin should be active.
  • string $PLUGINPATH returns the full path to the plugin directory.

Menu items in the domain menu (i.e. those whose controller is derived from DomainDependantController) are automatically shown/hidden when the plugin is (de)activated for the domain.

Menu items that are not domain-dependent are automatically hidden in the agency version when the plugin is blocked for a customer.

For more specific requirements, it is also possible to set menu items manually via an event listerner on the event App\Event\MenuGenerationEvent.

React to routes of other controllers

The event ccm19.controller.request.<route_name>, e.g. ccm19.controller.request.app_dashboard, can be used to react to a route before the respective controller is executed.

The event object is a Symfony\Component\HttpKernel\Event\ControllerEvent. The request can also be redirected to another controller using the setController() method.

The event ccm19.controller.response.<route_name>, on the other hand, enables code to be executed after the controller has run through.

Both events are not triggered in the event of unauthorized access, e.g. if a route that requires a logged-in user is called without a valid session.

Important:For all event handlers, always check with $pluginState->isActiveForCurrentDomain() or, depending on the context, $pluginState->isAllowedForCurrentUser() whether an action of the plugin is desired in the current context.