Zum Inhalt


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 no name collisions occur with the manufacturer prefix.

For the plugin-directory, the name is composed with the manufacturer-prefix in CamelCase-spelling.

As a 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 in the Agency-version can be activated for individual domains (de-)
        "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 standard-location

If translatable information is missing in a language, the English information 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 above example-plugin, 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;.

Routes-namesmustbegin 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 indicate 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 super class 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: Navigation in the domain-. 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 the user in the Basic-Edition.
  • HostingController: In the main navigation. Only visible for 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 all.
  • 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

The Symfony-autowiring can be used. Two special autowiring-arguments are also possible within plugins:

  • App\Model\PluginState returns the plugin-status-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 activated for the domain (de-).

Menu items that are not domain-dependent are automatically hidden in the Agency-version if 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 use $pluginState->isActiveForCurrentDomain() or, depending on the context, $pluginState->isAllowedForCurrentUser() to check whether an action of the plugin is desired in the current context.