CCM19-Plugins
CCM19 can be extended with various functionalities using plugins.
Naming
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 pluginpreview.
{png
|jpeg
|svg
|webp
|gif
} - preview imagesrc/
- 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)
Composer.json
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
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 also menu item-templates (not included sub-templates) can 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 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).
@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 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. Currentlyglyphicon-...
andfa-...
are supported.order
: integer Order-ID, with which the position in the menu is determinedroute
: 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 toroute
). 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 super class of the controller-class).navigation
: string In which navigation the menu item should be displayed. Possible values aremain
,domain
andmeta
. Here,main
corresponds to the navigation that the user is shown when logging in. So eitherdomain
ormeta
, depending on the edition and user type. (Default:domain
forDomainDependantController
n, 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 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 to users who can access the domain (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).
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
{
...
}
}
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 manually set menu items 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.