Inhaltsverzeichnis

Plugin Manager

The Plugin Manager is the part of Admidio that knows which plugins exist, in which state they are and what has to happen to move them from one state to the next. It provides

This page describes the plugin system of Admidio 6.0 and later. It is a rewrite of the plugin manager that was introduced for Admidio 5.0 and is not compatible with it — see What changed.

A complete, commented example plugin is part of the Admidio distribution: plugins/hello-world/. It shows every convention in one place, each of them usable on its own, and its own ``README.md`` is the short form of this page. Copy that directory, rename it, and you have a working plugin.

What a plugin is

A plugin is a directory below plugins/ that contains a manifest plugin.json and an entry file plugin.php. Nothing else is required. A plugin that only registers a hook is exactly those two files: no class, no base class, no interface, no settings, no page and no language file.

The directory name is the plugin ID and the only identity a plugin ever has. It is used in preference names, in URLs and in the components table, so it must consist of lowercase letters, digits and single hyphens (e.g. ``hello-world``). The display name may change, be translated or be duplicated by another plugin without any consequence.

Folder structure

plugins/hello-world/
├── plugin.json                        the manifest — the only required metadata
├── plugin.php                         the entry file — included once when the plugin is loaded
├── cli.php                            optional, registers command line tasks of the plugin
├── modules/index.php                  optional pages, ordinary Admidio module files
├── src/Greeting.php                   optional classes, reached through the "autoload" mapping
├── languages/en.xml                   optional language files, added to the language search path
├── templates/plugin.hello-world.tpl   optional Smarty templates, added to the template search path
├── assets/                            optional css/js/images, reachable through getAssetUrl()
├── db_scripts/                        optional install.sql, uninstall.sql and update_x_y.xml
└── tests/                             optional tests, run with the Admidio suite, not distributed

None of the subdirectories has to exist and none of them has to be declared in the manifest. They are conventions the core looks for.

Naming conventions

  1. Folder and file names containing multiple words use kebab-case (dash separated).
  2. Class files below the autoload directory (conventionally src) use UpperCamelCase and must not contain ``-`` or ``_``.
  3. The plugin ID — the directory name — is lowercase letters, digits and single hyphens.

The manifest

Each plugin has to provide a JSON manifest plugin.json. Only ``name`` and ``version`` really matter to the core; everything else is optional. A manifest that cannot be read, is not valid JSON or declares no version makes the plugin broken: it is still listed and can still be removed, but it is never loaded and cannot be installed.

Key Mandatory Description
``name`` X Display name. May be a language string ID such as ``PLG_BIRTHDAY_PLUGIN_NAME``.
``version`` X Version of the files. Compared against the installed version to detect an update.
``description`` Short description shown in the plugin administration. May be a language string ID.
``author`` The author of the plugin.
``url`` A URL to a wiki page or a project page.
``icon`` Bootstrap icon name, e.g. ``bi-cake2``.
``menu`` ``false`` suppresses the menu entry below Extensions. A plugin without a page never gets one anyway.
``requires`` Version constraints and dependencies, see below.
``autoload`` PSR-4 mappings, as namespace prefix ⇒ directory inside the plugin.
``preferences`` Where the settings panel of the plugin appears: ``section`` and ``sequence``.
``settings`` The preferences the plugin owns.

A manifest may carry further keys of its own; they are kept and are readable through ``$plugin→manifest``.

requires

"requires": {
    "admidio": ">=5.1",
    "php": ">=8.2",
    "extensions": ["curl"],
    "plugins": {"hello-world": ">=1.1"},
    "modules": ["events"]
}

A constraint is a space separated list of terms that all have to match, e.g. ``>=5.1 <6.0``. A term is an operator (``>=``, ``>``, ``⇐``, ``<``, ``=``, ``!=``) followed by a version; a term without an operator means ``>=``.

``admidio``, ``php``, ``extensions`` and ``plugins`` are requirements: an unmet one makes the plugin broken, so it cannot be installed.

``modules`` is deliberately not a requirement. A switched-off Admidio module is neither the plugin's fault nor permanent — the plugin is fine, it simply has nothing to show until the module is switched on again. The plugin administration therefore tells the administrator about it instead of refusing the plugin. A module is named the way Admidio names its component (``documents-files``, ``photos``, ``messages``), not the way its preference is spelled.

autoload

"autoload": {
    "AdmidioPlugin\\HelloWorld\\": "src/"
}

The prefix must not start with ``Admidio\``, which belongs to the core and could otherwise be shadowed, and the directory has to stay inside the plugin directory. Both are verified when the manifest is read; a violation makes the plugin broken.

preferences

"preferences": { "section": "overview_extensions", "sequence": 20 }

``section`` names one of the preference tabs and ``sequence`` places the panel inside it. The tabs are ``system``, ``login_security``, ``user_management``, ``communication``, ``content_management``, ``overview_extensions`` and ``extensions``. A plugin is not restricted to the last two: a module that used to be part of the Admidio core keeps its own tab once it becomes a plugin. A plugin that names no section, or one that no longer exists, lands in ``extensions``.

settings

The ``settings`` object declares the preferences the plugin owns. They become ordinary Admidio preferences when the plugin is loaded, get a row in every organization when it is installed and are removed again when it is uninstalled.

Each setting declares a ``type`` (``string``, ``boolean``, ``integer``, ``enum`` or ``array``), a ``default``, and a ``label`` and ``description`` that may be language string IDs. That is enough for Admidio to build the settings form by itself, so a plugin only writes a preferences presenter when it wants more than the manifest can express.

A setting without a ``label`` is not put into that form. A preference the plugin owns is not necessarily one this form edits — the position of an overview widget is edited in the overview preferences, and a plugin may keep state of its own that nobody is meant to type into.

An ``enum`` lists its permitted values, and may instead name them so that the generated form has something readable to show:

"values": ["ASC", "DESC"]
"values": {"first_name": "PLG_HELLO_WORLD_ADDRESS_FIRST_NAME", "full_name": "…"}

An ``integer`` may bound what the generated form offers with ``min``, ``max`` and ``step``. The value is stored as an integer either way; a bound only restricts the input field.

"hello_world_repeat": { "type": "integer", "default": 3, "min": 0, "max": 20, "step": 1 }

Three preference names are conventions the core understands:

Preference Meaning
``plugin_<id>_enabled`` Registered by the core for every installed plugin. It is what enables the plugin for one organization; a plugin does not declare it.
``<id>_plugin_enabled`` Who may see the overview widget of the plugin: ``0`` nobody, ``1`` everybody, ``2`` only logged-in users.
``<id>_overview_sequence`` Position of the overview widget on the overview page.

In ``<id>`` the hyphens of the plugin ID are replaced by underscores.

A complete manifest

{
    "name": "PLG_HELLO_WORLD_NAME",
    "description": "PLG_HELLO_WORLD_DESCRIPTION",
    "version": "1.1.0",
    "author": "The Admidio Team",
    "url": "https://www.admidio.org",
    "icon": "bi-globe-americas",
    "requires": {
        "admidio": ">=5.1",
        "php": ">=8.2"
    },
    "autoload": {
        "AdmidioPlugin\\HelloWorld\\": "src/"
    },
    "settings": {
        "hello_world_greeting": {
            "type": "string",
            "default": "Hello world",
            "label": "PLG_HELLO_WORLD_GREETING",
            "description": "PLG_HELLO_WORLD_GREETING_DESC"
        },
        "hello_world_address": {
            "type": "enum",
            "default": "first_name",
            "values": {
                "first_name": "PLG_HELLO_WORLD_ADDRESS_FIRST_NAME",
                "full_name": "PLG_HELLO_WORLD_ADDRESS_FULL_NAME",
                "login_name": "PLG_HELLO_WORLD_ADDRESS_LOGIN_NAME"
            },
            "label": "PLG_HELLO_WORLD_ADDRESS",
            "description": "PLG_HELLO_WORLD_ADDRESS_DESC"
        },
        "hello_world_decorate_headline": {
            "type": "boolean",
            "default": false,
            "label": "PLG_HELLO_WORLD_DECORATE_HEADLINE",
            "description": "PLG_HELLO_WORLD_DECORATE_HEADLINE_DESC"
        }
    }
}

The entry file

plugin.php registers what the plugin contributes and returns nothing. There is no plugin class, no interface and no base class. It runs inside the fully initialized Admidio request, so it can call any public Admidio API — normally it registers hooks:

<?php
use Admidio\Hooks\Hooks;
use AdmidioPlugin\HelloWorld\Greeting;
 
/*
 * This file is included by the plugin loader and is never an entry point of its own. Without the
 * Admidio bootstrap it could do nothing anyway; the guard only turns a PHP error into a message.
 */
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
    exit('This page may not be called directly!');
}
 
Hooks::addFilter('page_headline', array(Greeting::class, 'decorateHeadline'), 20, 2, 'hello-world');

A plugin with more to announce puts that into a class of its own and calls it from the entry file, which is what the built-in plugins do:

use AdmidioPlugin\Birthday\Birthday;
 
Birthday::register();

The entry file is included in a scope of its own, so a variable it assigns cannot collide with anything in the loader. Top-level code should stay cheap: it runs in every request of every organization the plugin is enabled for.

What loading a plugin means

Loading one plugin means four things and nothing else:

  1. its ``autoload`` mappings become resolvable classes,
  2. its languages/ directory is added to the language search path,
  3. its ``settings`` and the preference that enables it are registered as ordinary Admidio preferences,
  4. its plugin.php is included once.

A plugin that fails while it is being loaded is reported and skipped. A broken extension must not be able to take the whole installation down, because then it could not be disabled any more. This is the opposite of the policy for hooks, where a failing callback fails the operation it extends: an operation can be aborted, a request that never reaches the plugin administration cannot be repaired.

Hooks

Hooks are the general extension mechanism of Admidio and the normal way for a plugin to change what the core does. Three primitives are offered by ``Admidio\Hooks\Hooks``:

Primitive Purpose Dispatch
Action Notify that something is happening or has happened. Every callback is called, return values are ignored. ``Hooks::doAction()``
Filter Transform a value. Every callback receives the result of the previous one. ``Hooks::applyFilters()``
Resolver Ask for one answer. Callbacks are asked in priority order until one returns something other than ``null``. ``Hooks::resolve()``
// transform a value
Hooks::addFilter('entity_value', function (mixed $value, string $column) {
    return ($column === 'usd_value') ? trim($value) : $value;
});
 
// observe an event
Hooks::addAction('user_created', array($myPlugin, 'onUserCreated'), 20);
 
// ask for one answer
Hooks::addResolver('translation_missing', 'myTranslationProvider');

A lower priority runs earlier, registrations of the same priority run in registration order. A callback may be registered with an explicit ID — the last argument in the examples on this page is the plugin ID — which is unique within one hook regardless of the priority, so registering the same ID again replaces the previous registration. Exceptions of a callback are logged and rethrown: a failing extension fails the operation it extends instead of leaving it half done.

The overview widget

The overview page used to include one PHP file per plugin through a Smarty function, so a plugin could only contribute there by being a file at a place the core knew. A widget is now an ordinary hook: the overview asks for the widgets, and every plugin that wants one adds it.

A plugin that follows the Admidio conventions — one widget, an ``_overview_sequence`` preference for its position and a ``_plugin_enabled`` preference for who may see it — does not write that hook itself but calls ``PluginWidget::register()``:

PluginWidget::register($plugin, array(self::class, 'renderWidget'), array(
    'sequence' => self::DEFAULT_SEQUENCE
));
Option Default
``name`` The name of the plugin, translated if it is a language string ID
``icon`` The icon of the plugin
``sequencePreference`` ``<plugin id>_overview_sequence``
``enabledPreference`` ``<plugin id>_plugin_enabled``
``sequence`` Position of a widget whose preference has no value

The render callback receives the overview page and the plugin and returns the HTML of the widget, or an empty string for a widget that has nothing to show. ``PluginWidget::render()`` renders a template of the plugin for it.

The declaration is what the overview preferences list, so a widget that is switched off is still shown there and can be switched on again.

A plugin that needs more control adds the hook itself:

Hooks::addFilter(PluginWidget::HOOK, function (array $widgets, PagePresenter $page): array {
    global $gValidLogin, $gL10n;
 
    if (!$gValidLogin) {
        return $widgets;   // a widget that decides not to appear simply does not add itself
    }
 
    $plugin = PluginRegistry::get('birthday');
    $widgets[] = array(
        'id' => $plugin->id,
        'name' => $gL10n->get($plugin->name),
        'sequence' => 20,
        'html' => PluginWidget::render($page, $plugin, 'plugin.birthday.tpl', array('birthdays' => $rows))
    );
 
    return $widgets;
});

The preferences panel

A plugin that brings preferences shows them in the Admidio preferences, in the tab its manifest names. It used to get there by being scanned for: the core looked through the autoload directories for a file called ``*PreferencesPresenter.php`` and guessed the method to call from its class name. A panel is now declared, like a widget, so that a plugin says what it offers instead of the core deducing it from file names.

For a plugin whose form is generated from the manifest, the manifest is all there is:

"preferences": { "section": "overview_extensions", "sequence": 20 }

A plugin that builds its form itself registers the callback that builds it:

Hooks::addFilter(
    PluginPanel::HOOK,
    static function (array $panels) use ($plugin): array {
        global $gL10n;
 
        $panels[] = array(
            'id' => PluginPanel::normalizeId($plugin->id),
            'title' => $gL10n->get($plugin->name),
            'icon' => $plugin->icon,
            'sequence' => self::DEFAULT_SEQUENCE,
            'create' => array(BirthdayPreferencesPresenter::class, 'createForm')
        );
 
        return $panels;
    },
    PluginPanel::DEFAULT_SEQUENCE,
    1,
    $plugin->id
);

The panel takes its section and its sequence from the manifest of the plugin it is named after, so only a plugin that declares several panels has to name a ``section`` here as well.

The ``create`` callback receives the ``PreferencesPresenter`` and returns the HTML of the panel. It is only called for the panel the administrator actually opens, because the preferences page fetches one panel per request.

The settings of a plugin are additionally reachable as a dialog from the plugin administration itself, so a plugin does not have to be found in the preferences tabs to be configured.

Pages

modules/index.php is a normal Admidio module file: ``require_once`` of ``system/common.php``, ``admFuncVariableIsValid()``, ``PagePresenter``, ``FormPresenter``, services, ``Entity``, ``handleException()``. Nothing about it is plugin-specific.

Only the files directly below ``modules/`` are entry points; the directory is not searched recursively, so what a page includes stays private to the plugin. Everything else in the plugin — the entry file, the classes below ``src/`` — is refused if it is requested directly.

A page is always reachable at its real location:

plugins/hello-world/modules/index.php

When the administrator switches on the preference plugin_module_pages, Admidio additionally publishes it under the ordinary module URL:

modules/hello-world/index.php

Admidio has no front controller and no rewrite rules — every module URL is a real file — so publishing writes one generated stub per page, which loads the Admidio bootstrap and hands over to the page inside the plugin. The page itself is unchanged and works through either URL, because ``DIR`` is per file: the relative includes of the page still resolve against its real location. Because this writes into the Admidio core tree, which a Git-managed or read-only deployment does not want, it is off by default. A generated directory carries a marker file naming the plugin it belongs to, and nothing without that marker is ever deleted, so a core module directory cannot be removed by a plugin whatever the plugin is called.

Never hardcode either form — ask ``$plugin→getUrl('index.php')`` which one is live.

A plugin with a page gets a menu entry below Extensions when it is installed. A plugin whose pages are not destinations — the calendar answers one month of itself for its own buttons — says ``“menu”: false`` in its manifest and is left out of the menu.

Languages, templates and assets

Database scripts

db_scripts/ is optional and holds

The update scripts are resolved through the plugin ID, and they may call plugin-specific update steps. Those live in a class ``UpdateStepsCode`` in the Service namespace of the plugin — that is the first ``autoload`` prefix of the manifest followed by ``Service\``, so for the mapping ``“AdmidioPlugin\\HelloWorld\\”: “src/“`` the class is ``AdmidioPlugin\HelloWorld\Service\UpdateStepsCode`` in ``src/Service/UpdateStepsCode.php``:

final class UpdateStepsCode
{
    private static Database $db;
 
    public static function setDatabase(Database $database)
    {
        self::$db = $database;
    }
 
    public static function /*[method name used in update_x_y.xml]*/()
    {
        [...]
    }
}

Command line tasks

A plugin owns the CLI command namespace named after its directory, exactly like a module. Where a module registers its commands in ``modules/<module>/cli.php``, a plugin registers them in plugins/<plugin>/cli.php. The file is loaded for every enabled plugin after the plugin runtime, so the autoload mappings, settings and languages of the plugin are available. It only registers metadata and callbacks; the command itself runs later, after the actor and the component have been authorized.

CliTaskRegistry::register(
    'hello-world:greet',
    'PLUGINS',
    array(Greeting::class, 'cliGreet'),
    'Print the greeting of the plugin.',
    'hello-world:greet [--name=NAME]'
);

A command name must have the form ``extension:task`` and must name an Admidio component; a plugin cannot replace a command that is already registered, and a broken ``cli.php`` is reported without taking the rest of the command line down.

The plugin:* commands

The plugin manager itself is fully available from the command line:

Command Purpose
``plugin:list [–state=STATE] [–installed] [–updates]`` List the plugins and their state.
``plugin:show PLUGIN`` Show the manifest and the state of a plugin.
``plugin:install SOURCE [–replace]`` Install a plugin from the store or from an archive file.
``plugin:inspect FILE`` Show what a plugin archive holds, without installing it.
``plugin:enable PLUGIN`` Enable a plugin for the current organization, preparing it if that has not happened yet.
``plugin:disable PLUGIN`` Disable a plugin for the current organization, keeping its data.
``plugin:update PLUGIN`` Update a plugin, fetching newer files where the store publishes them.
``plugin:remove PLUGIN [–yes]`` Remove a plugin from every organization, with its data and its files.
``plugin:archive PLUGIN [–output=FILE] [–overwrite]`` Build the distributable ZIP archive of a plugin.
``plugin:store-list [–available] [–updates]`` List the plugins the store publishes for this Admidio.
``plugin:store-show PLUGIN`` Show what the store publishes for one plugin.
``plugin:store-refresh`` Fetch the plugin catalogue now instead of waiting for the cache to expire.

The lifecycle

A plugin has two independent states:

The plugin administration groups every plugin it knows by six states:

State Meaning
``available`` The files are there, but the plugin was never installed.
``disabled`` Installed, but not enabled for the current organization.
``enabled`` Installed and enabled for the current organization.
``update`` Installed, but the files declare a newer version than the database.
``broken`` The manifest is missing, malformed or the requirements are not met.
``orphaned`` Installed in the database, but the files are gone.

The operations are deliberately separate and mean different things:

Operation Scope Touches data
install installation creates the schema and the preferences of the plugin
update installation runs ``db_scripts/update_x_y.xml`` up to the new version
enable organization nothing, only the preference that loads the plugin
disable organization nothing
remove installation removes the preferences, the data, the component record and the files

Disabling is therefore always safe and reversible. Enabling a plugin that was never installed installs it first, so the administrator has one decision to make and not two. Removing is the only destructive operation there is: it takes the plugin away from every organization at once, runs ``uninstall.sql`` and deletes the files. A plugin of the Admidio distribution is refused, because the next core update would put its files back and quietly undo the decision.

Reading a plugin never executes plugin code and never throws. An unreadable, malformed or incompatible manifest produces a plugin whose ``error`` describes the problem, so the plugin administration can list, disable and remove a broken plugin instead of breaking with it. The reason comes from the manifest reader and from ``checkRequirements()``, which answer in English for a developer, so it is shown as the diagnostic it is and not as a translated member-facing message.

Distributing a plugin

The archive

``admidio plugin:archive <plugin>`` builds the ZIP archive that distributes a plugin. The archive holds a single top-level directory named after the plugin ID, and it is checked with the very gate the installer uses before it is handed back.

These are left out of the archive: ``.git``, ``.svn``, ``.hg``, ``node_modules``, ``vendor``, ``tests``, ``phpunit.xml``, ``phpunit.xml.dist``, ``.phpunit.result.cache``, ``.DS_Store``, ``Thumbs.db``, ``.gitignore`` and ``.gitattributes``. The tests are excluded because Admidio runs the tests of every plugin that is present, and a distributed one would run in installations that have nothing to do with developing the plugin.

Everything an archive contains is written by somebody the installation does not know, so it is checked before a single byte of it reaches the plugins directory. An archive may hold at most 5000 entries, 32 MB compressed and 128 MB extracted, and a failure at any point removes the temporary directory rather than leaving a partial plugin behind.

The plugin store

The store is one JSON file on a web server — by default ``plugins.json`` next to the ``update.txt`` that the version check already reads. Admidio fetches it, caches it in ``adm_my_files`` and refreshes it at most once a day or when an administrator asks. The remote call never happens while a page is being built, and a catalogue that cannot be reached or cannot be understood makes the store unavailable rather than breaking the page.

``plugin:update`` and the Update action fetch newer files where the store publishes them and then run the update scripts — which is all that is needed for a plugin whose files somebody replaced by hand. Updating is one action whatever the plugin needs.

Tests

Every test suite of Admidio also reads ``plugins/*/tests/<suite>``, so a plugin is tested from where it lives and with the same command as the core. The example plugin ships ``plugins/hello-world/tests/Unit/ManifestTest.php`` as a starting point.

API reference

All classes live in the namespace ``Admidio\Infrastructure\Plugins``.

Plugin

One plugin as it exists on disk. Reading it never executes plugin code and never throws.

Property Description
``id`` Plugin ID, which is the directory name
``path`` Absolute path of the plugin directory
``manifest`` The decoded manifest, so a plugin may carry metadata the class does not interpret
``name``, ``description``, ``version``, ``icon``, ``author``, ``homepage`` Metadata
``autoload`` PSR-4 mappings, as prefix ⇒ absolute directory
``settings`` The preferences the plugin owns, as name ⇒ definition
``requires`` ``admidio``, ``php``, ``extensions``, ``plugins``, ``modules``
``preferences`` ``section`` and ``sequence`` of the settings panel
``error`` Why the plugin cannot be used, or ``null``
Method Description Return
``read(string $directory)`` Read the plugin in a directory Plugin
``isValidId(string $id)`` Whether a directory name is a usable plugin ID bool
``isValid()`` Whether the plugin can be used at all bool
``getEntryFile()`` Absolute path of ``plugin.php`` string
``getDirectory(string $directory)`` Absolute path of a conventional subdirectory, or ``null`` ?string
``getPages()`` / ``hasPages()`` The PHP files directly below ``modules/`` array / bool
``wantsMenuEntry()`` Whether the plugin should get a menu entry bool
``getUrl(string $page = '')`` URL of a page, in whichever form is currently live string
``getSettingValues()`` Every declared preference with its value, cast to the declared type array
``renderTemplate(PagePresenter $page, string $template, array $variables)`` Render a template of the plugin string
``getAssetUrl(string $file)`` URL of a file below ``assets/`` string
``getEnabledSettingName()`` ``plugin_<id>_enabled`` string
``getAccessSettingName()`` ``<id>_plugin_enabled`` string
``checkRequirements(array $installedPlugins)`` One English diagnostic per unsatisfied requirement array
``getDisabledModules()`` The Admidio modules the plugin needs that are switched off array
``versionMatches(string $version, string $constraint)`` Check a version against a constraint bool

PluginRegistry

The only place that answers “which plugins are there” and “may this one run”. It never includes plugin code. Discovery and the installation states are read once per request.

Method Description Return
``all()`` Every plugin below ``plugins/`` array
``get(string $id)`` One plugin, or ``null`` ?Plugin
``getState(Plugin or string $plugin)`` One of the six ``STATE_*`` constants string
``isInstalled(string $id)`` Installed in this installation bool
``isEnabled(string $id)`` Enabled for the current organization bool
``isBuiltIn(string $id)`` A plugin of the Admidio distribution bool
``getInstalledVersion(string $id)`` The version the database records string
``getComponentId(string $id)`` / ``getComponentUuid(string $id)`` Component record of the plugin int / string
``getEnabledOrganizations(string $id)`` Which organizations have it enabled array
``getLoadable()`` The plugins that may be loaded in this request array
``getEnabledVersions()`` pluginId ⇒ version of every installed and enabled plugin array
``getOwnerOfSetting(string $name)`` Which plugin owns a preference ?Plugin
``requireEnabled(string $id)`` Refuse the request unless the plugin is enabled Plugin
``requirePage(string $id, string $page)`` Guard a plugin page void
``getScriptPluginId()`` The plugin the running script belongs to ?string

PluginLoader

Loads the plugins that are enabled for the current request.

Method Description Return
``loadEnabled()`` Load every enabled plugin void
``load(Plugin $plugin)`` Load one plugin bool
``getLoaded()`` / ``isLoaded(string $id)`` What was loaded in this request array / bool
``getFailures()`` pluginId ⇒ why it could not be loaded array
``getTemplateDirectories()`` The template search path contributed by plugins array
``registerLanguages(Plugin $plugin)`` Add the language directory of a plugin void

PluginInstaller

Method Description Return
``install(Plugin $plugin, bool $addMenuEntry = true)`` Run ``install.sql``, create the component record, seed the preferences, publish the pages, add the menu entry void
``update(Plugin $plugin)`` Run the update scripts up to the version the files declare void
``updateWithNewerFiles(Plugin $plugin)`` Fetch newer files where the store publishes them, then update Plugin
``remove(Plugin or string $plugin)`` Remove the plugin from every organization, with its data and its files bool
``enable(Plugin $plugin)`` / ``disable(Plugin $plugin)`` The per-organization decision void
``getPreferenceNames(Plugin $plugin)`` Every preference the plugin owns array

PluginWidget

Method Description Return
``register(Plugin $plugin, callable $render, array $options)`` Declare the widget and add the hook that renders it void
``render(PagePresenter $page, Plugin $plugin, string $template, array $variables)`` Render a template as the widget body string
``getDeclarations()`` Every declared widget, whether visible in this request or not array
``isVisible(string $preference)`` / ``getAccess(string $preference)`` Who may see the widget bool / int
``getSequence(string $preference, int $default)`` Position of the widget int
Constants ``HOOK``, ``DEFAULT_SEQUENCE``, ``ACCESS_NOBODY``, ``ACCESS_EVERYBODY``, ``ACCESS_REGISTERED_USERS``

PluginPanel

Method Description Return
``collect()`` Every declared preferences panel array
``inSection(string $section)`` The panels of one preference tab array
``get(string $id)`` One panel declaration ?array
``create(string $id, PreferencesPresenter $presenter)`` Build the HTML of one panel string
``normalizeId(string $id)`` The panel ID a plugin ID maps to string

PluginPages

Method Description Return
``isAllowed()`` Whether ``plugin_module_pages`` permits publishing bool
``isWritable()`` Whether the modules directory can be written bool
``getObstacle(Plugin $plugin)`` Why a plugin cannot be published, or ``null`` ?string
``isPublished(Plugin or string $plugin)`` Whether the stubs exist bool
``getUrl(Plugin $plugin, string $page)`` The URL that is currently live string
``publish(Plugin $plugin)`` / ``unpublish(Plugin or string $plugin)`` Write or remove the stubs bool / void
``reconcile()`` / ``syncAll()`` Bring the modules directory in line with the preference array
Constants ``SETTING`` (``plugin_module_pages``), ``SETTING_APPLIED``

PluginStore

Method Description Return
``getPlugins()`` What the catalogue publishes for this Admidio array
``getEntry(string $id)`` One catalogue entry ?array
``getNewerRelease(string $id)`` A published release newer than the installed one ?array
``install(string $id)`` / ``updateFiles(string $id)`` Fetch and place the files string
``refresh()`` Fetch the catalogue now array
``getUrl()`` / ``isDeveloperCatalogue()`` / ``getError()`` Where it comes from and why it failed string / bool / ?string

PluginPackage

Method Description Return
``install(string $archivePath, bool $replace = false)`` Put the plugin of an archive on disk. Nothing is written to the database and the plugin is not enabled string
``describe(string $archivePath)`` The plugin an archive holds Plugin
``create(Plugin $plugin, string $directory)`` Build the distributable archive string
``inspect(string $archivePath)`` The gate: everything it rejects is rejected before a file is written string
Constants ``MAX_ARCHIVE_BYTES``, ``MAX_EXTRACTED_BYTES``, ``MAX_ENTRIES``, ``EXCLUDED``

What changed since the first plugin manager

The plugin manager of Admidio 5.0 was built around a ``PluginAbstract`` base class and a ``plugin.json`` that described it. The runtime was replaced, and a plugin written for it has to be converted. The differences that matter to a plugin author:

Then Now
A main plugin class extending ``PluginAbstract``, with ``doRender()``, ``getInstance()`` and overridable lifecycle methods No plugin class at all. ``plugin.php`` registers what the plugin contributes and returns nothing
The main file was named by ``mainFile``, or guessed as ``index.php`` / ``PluginName.php`` The entry file is always ``plugin.php``; pages are ordinary module files below ``modules/``
``”dependencies”``: a list of Admidio class names that had to exist ``“requires”``: version constraints for ``admidio`` and ``php``, plus ``extensions``, ``plugins`` and ``modules``
``“defaultConfig”``, each entry with ``name``, ``description``, ``type``, ``value`` ``“settings”``, each entry with ``type``, ``default``, ``label``, ``description``, plus ``values`` for an enum and ``min``/``max``/``step`` for an integer
``“overviewPlugin”: true`` made a plugin an overview plugin The plugin registers an overview widget with ``PluginWidget::register()``
A preferences presenter was found by scanning the autoload directories for ``*PreferencesPresenter.php`` The panel is declared through the ``PluginPanel`` hook, or generated from the manifest and not written at all
``“preferencesFile”`` and ``“hasSubcards”`` Replaced by the declared panel; a plugin that wants several cards declares several panels
``classes/`` with ``Presenter/``, ``Entity/``, ``Service/`` Any layout the ``autoload`` mapping names; ``src/`` by convention
Install and uninstall were one pair of operations Four distinct operations: install and update are installation-wide, enable and disable are per organization, remove is the only destructive one
``db-install.sql`` / ``db-uninstall.sql`` ``install.sql`` / ``uninstall.sql`` in ``db_scripts/``
Plugins were installed by copying files Plugin store, ZIP upload and ``admidio plugin:*`` commands

The generated settings form is the largest simplification: a plugin that only needs ordinary preference fields declares them in the manifest and writes no presenter, no template and no form code at all.