========= 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
* a **plugin administration** page under //Administration → Extensions// for installing, enabling, disabling, updating and removing plugins,
* a **plugin store** so that a plugin can be installed and updated from a published catalogue,
* an **upload** for a plugin that arrives as a ZIP archive,
* a **runtime** that loads the enabled plugins into every request, and
* a set of **admidio plugin:*** command line tasks that do the same as the web interface.
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
[[en:entwickler:plugin_manager#what_changed_since_the_first_plugin_manager|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 ======
- Folder and file names containing multiple words use **//kebab-case//** (//dash separated//).
- Class files below the autoload directory (conventionally //src//) use **//UpperCamelCase//** and must not contain ``-`` or ``_``.
- 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__enabled`` | Registered by the core for every installed plugin. It is what enables the plugin for one organization; a plugin does not declare it. |
| ``_plugin_enabled`` | Who may see the overview widget of the plugin: ``0`` nobody, ``1`` everybody, ``2`` only logged-in users. |
| ``_overview_sequence`` | Position of the overview widget on the overview page. |
In ```` 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:
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:
- its ``autoload`` mappings become resolvable classes,
- its **languages/** directory is added to the language search path,
- its ``settings`` and the preference that enables it are registered as ordinary Admidio preferences,
- 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`` | ``_overview_sequence`` |
| ``enabledPreference`` | ``_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 =======
* **languages/** is added to the language search path when the plugin is loaded, so ``$gL10n->get('PLG_HELLO_WORLD_NAME')`` works and the manifest may use language string IDs. The files have the same format as the Admidio language files (``en.xml``, ``de.xml``, …).
* **templates/** is added to the Smarty template search path. ``$plugin->renderTemplate($page, 'plugin.hello-world.tpl', $variables)`` renders one. The deprecated ``HtmlPage`` and ``HtmlTable`` classes should not be used any more.
* **assets/** holds css, js and images. ``$plugin->getAssetUrl('style.css')`` answers the URL of one.
======= Database scripts =======
**db_scripts/** is optional and holds
* ``install.sql`` — run when the plugin is installed,
* ``uninstall.sql`` — run when the plugin is removed,
* ``update_x_y.xml`` — the update scripts, structured exactly like Admidio's own.
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//cli.php``, a plugin registers them in
**plugins//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**:
* **installed** is global, because the database schema of a plugin is global. It is a row in the components table with ``com_type = PLUGIN`` and ``com_name_intern = ``.
* **enabled** is per organization, because an Admidio installation may serve several organizations that do not want the same extensions. It is the preference ``plugin__enabled``.
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 `` 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/``, 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__enabled`` | string |
| ``getAccessSettingName()`` | ``_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.