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.
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.
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.
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": {
"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": {
"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": { "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``.
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.
{
"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"
}
}
}
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.
Loading one plugin means four things and nothing else:
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 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 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; });
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.
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.
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]*/() { [...] } }
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 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. |
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.
``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 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.
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.
All classes live in the namespace ``Admidio\Infrastructure\Plugins``.
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 |
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 |
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 |
| 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 |
| 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`` |
| 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 |
| 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`` |
| 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 |
| 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`` |
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.