Admidio Hooks Catalogue
This page documents every hook actually implemented in Admidio 5.1.
Engine: Admidio\Hooks\Hooks (static). Three primitives — Action (addAction/doAction), Filter
(addFilter/applyFilters/applyTypedFilters), Resolver (addResolver/resolve).
See the developer documentation how to use the hooks documented here in your own code.
1. Entity persistence lifecycle
Implemented once in Entity. Every hookable entity dispatches the same nine stages under its own name
and under the generic entity_* name. A plugin normally subscribes to the entity-specific name; a
cross-cutting plugin (audit, Devhelper) subscribes to the generic one.
Stage pattern
| Stage | Type | Generic name | Entity-specific name | Order | Args |
|---|---|---|---|---|---|
| pre-create | Action | entity_creating | <id>_creating | generic first | (EntityChangeSet $changeSet, ?Entity $entity) |
| post-create (committed) | Action | entity_created | <id>_created | specific first | (EntityChangeSet $changeSet, ?Entity $entity) |
| create failed | Action | entity_create_failed | <id>_create_failed | specific first | (EntityChangeSet $changeSet, ?Entity $entity) |
| pre-update | Action | entity_updating | <id>_updating | generic first | (EntityChangeSet $changeSet, ?Entity $entity) |
| post-update (committed) | Action | entity_updated | <id>_updated | specific first | (EntityChangeSet $changeSet, ?Entity $entity) |
| update failed | Action | entity_update_failed | <id>_update_failed | specific first | (EntityChangeSet $changeSet, ?Entity $entity) |
| pre-delete | Action | entity_deleting | <id>_deleting | generic first | (EntityChangeSet $changeSet, ?Entity $entity) |
| post-delete (committed) | Action | entity_deleted | <id>_deleted | specific first | (EntityChangeSet $changeSet, null) |
| delete failed | Action | entity_delete_failed | <id>_delete_failed | specific first | (EntityChangeSet $changeSet, null) |
| proposed field value | Filter | entity_value | <id>_value | generic first | (mixed $value, Entity $entity, string $columnName, mixed $oldValue) |
| readable name | Filter | entity_readable_name | <id>_readable_name | generic first | (string $name, Entity $entity) |
The second argument is the live entity, so a callback can read a field the change set does not
carry (an unchanged column, a related record) or call a domain method on it — not just observe what
changed. It is null for exactly two stages: deleted and delete_failed. Entity::delete()
clears the object before either can be dispatched — immediately for a failure outside a transaction,
later through the commit/rollback queue for everything else — and a bulk deletion reuses one scratch
object for every row it removes, so the object at that point would as often be the wrong record as an
empty one. EntityChangeSet::getSnapshot() is what those two describe the record from instead.
Committed (*_created/*_updated/*_deleted) fires after the outermost transaction really commits,
never before, and never for a change that is later rolled back. Two saves of the same record in one
transaction coalesce into one event. A create+delete of the same never-persisted record fires nothing.
*_value runs before Admidio's own type canonicalization, so it sees the proposed input, not the
stored form. Bulk-deleted dependent records (Entity::deleteDependentRecords() /
hookBulkDeletion()) dispatch the same deleting/deleted pair per record, with
EntityChangeSet::isCascade() / getCauseHookId() / getCauseId() naming the record whose deletion
caused it.
Entities and their hook ID
| Hook ID (<id>) | Class | Table |
|---|---|---|
announcement | Announcements\Entity\Announcement | adm_announcements |
category | Categories\Entity\Category | adm_categories |
component | Components\Entity\Component | adm_components |
event | Events\Entity\Event | adm_events |
file | Documents\Entity\File | adm_files |
folder | Documents\Entity\Folder | adm_folders |
forum_post | Forum\Entity\Post | adm_forum_posts |
forum_topic | Forum\Entity\Topic | adm_forum_topics |
inventory_field | Inventory\Entity\ItemField | adm_inventory_fields |
inventory_field_select_option | Inventory\Entity\SelectOptions | adm_inventory_field_options |
inventory_item | Inventory\Entity\Item | adm_inventory_items |
inventory_item_borrow_data | Inventory\Entity\ItemBorrowData | adm_inventory_item_borrow_data |
inventory_item_data | Inventory\Entity\ItemData | adm_inventory_item_data |
list_column | Roles\Entity\ListColumns | adm_list_columns |
list_configuration | Roles\Entity\ListConfiguration | adm_lists |
membership | Roles\Entity\Membership | adm_members |
menu_entry | Menu\Entity\MenuEntry | adm_menu |
message | Messages\Entity\Message | adm_messages |
message_content | Messages\Entity\MessageContent | adm_messages_content |
oidc_client | SSO\Entity\OIDCClient | adm_oidc_clients |
organization | Organizations\Entity\Organization | adm_organizations |
photo_album | Photos\Entity\Album | adm_photos |
profile_field | ProfileFields\Entity\ProfileField | adm_user_fields |
profile_field_select_option | ProfileFields\Entity\SelectOptions | adm_user_field_options |
role | Roles\Entity\Role | adm_roles |
role_dependency | Roles\Entity\RolesDependencies | adm_role_dependencies |
role_right | Roles\Entity\RolesRights | adm_roles_rights |
role_right_assignment | Roles\Entity\RolesRightsData | adm_roles_rights_data |
room | Events\Entity\Room | adm_rooms |
saml_client | SSO\Entity\SAMLClient | adm_saml_clients |
sso_key | SSO\Entity\Key | adm_sso_keys |
user | Users\Entity\User | adm_users |
user_data | Users\Entity\UserData | adm_user_data |
user_relation | Users\Entity\UserRelation | adm_user_relations |
user_relation_type | Users\Entity\UserRelationType | adm_user_relation_types |
weblink | Weblinks\Entity\Weblink | adm_links |
Deliberately unhookable (no hook ID, dispatch nothing)
LogChanges, Text, Preferences, AutoLogin, Session, SSOClient (its subclasses OIDCClient/
SAMLClient are hookable), TokenEntity (OIDC access/refresh tokens, auth codes). Infrastructure,
security-sensitive, or too high-frequency to be normal CRUD subjects.
2. Semantic / cross-cutting hooks
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
user_registration_accepted | Action | (User $user, string $method) — $method is UserRegistration::ACCEPTED_BY_APPROVAL or ::ACCEPTED_BY_ASSIGNMENT | UserRegistration::acceptRegistration(), RegistrationService::assignRegistration() |
user_changes_cumulated | Action | (string $userUuid, array $reasons) — reasons are a subset of user, profile, membership | ChangeNotification::sendNotifications(), once per affected user per request |
3. Forms
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
form_built | Action | (FormPresenter $form) | FormPresenter::finalize() — once per form, however many times/from where finalize() is reached |
form_select_options | Filter | (array $values, string $elementId, string $elementType, FormPresenter $form) — must return an array of the same shape | FormPresenter::finalize(), after form_built, only for select / radio / button-group.radio elements |
form_select_options's result is enforced: it is the same element definition validate() checks a POST
against, so removing an entry there really refuses it server-side.
4. Pages
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
page_title | Filter<string> | (string $title, PagePresenter $page) | PagePresenter::show() (or getFilteredTitle() for a caller that needs it earlier) |
page_headline | Filter<string> | (string $headline, PagePresenter $page) | PagePresenter::show() (or getFilteredHeadline()) |
page_before_render | Action | (PagePresenter $page) | PagePresenter::show(), after the two text filters |
Both filters and the action fire exactly once per page regardless of how many times show() /
getFilteredTitle() / getFilteredHeadline() are called on it. PagePresenter::getHtmlID() gives the
stable per-page identifier (adm_<module>[_<view>]) to key behaviour on.
5. Components
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
component_visible | Filter<bool> | (bool $visible, string $componentName) | Component::isVisible() |
component_administrable | Filter<bool> | (bool $administrable, string $componentName) | Component::isAdministrable() |
Typed filters — a callback must return a bool, not a truthy value.
6. Translation
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
translation_text | Filter<string> | (string $text, string $textId, string $language) | Language::get(), on every lookup, after the cache read |
translation_missing | Resolver | (string $textId, string $language), default null | Language::get(), only when neither the requested nor the reference language has the text |
translation_fallback_used | Action | (string $textId, string $language, string $referenceLanguage) | Language::get(), when the reference-language text was used |
translation_unresolved | Action | (string $textId, string $language) | Language::get(), when nothing — requested, reference, or resolver — produced a text |
7. Login, session and diagnostics
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
login_attempt | Action | (string $loginName, string $orgShortName) | ModuleLogin, at the start of a login attempt; throwing refuses it |
login_succeeded | Action | (User $user, string $orgShortName) | ModuleLogin, after a successful login |
login_failed | Action | (string $loginName, string $reason, string $orgShortName) — no password/TOTP | ModuleLogin, for an unknown user as well as a wrong password/TOTP |
logout | Action | (User $user) | system/logout.php |
exception_terminating | Action | (Throwable $exception, bool $isJsonResponse) | handleException() |
8. Email
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
email_recipients | Filter<array> | (array $recipients, string $subject) | Email::sendEmail() (internally deliver()), before sending |
email_sent | Action | (string $subject, int $recipientCount) | Email, after a successful send |
email_failed | Action | (string $subject, int $recipientCount, string $errorMessage) | Email, after a failed send |
No email hook ever receives the Email/PHPMailer object — it carries the SMTP credentials in public
properties.
9. Lists
Proven on one module (contacts) with the stable listId contacts; not yet implemented for any
other list.
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
list_columns | Filter<array> | (array $columnHeadings, string $listId) — must return the same number of entries | contacts.php, before the headings are handed to the page |
list_data | Filter<array> | (array $row, string $listId) | contacts_data.php, right after a row is fetched, before formatting |
list_row_actions | Filter<string> | (string $actionsHtml, string $listId, array $row) | contacts_data.php, right before the action-icons cell is written |
list_rendered_data | Filter<array> | (array $columnValues, string $listId, array $row) | contacts_data.php, right before a row is added to the JSON response |
list_columns can relabel a column but not add or remove one: rows are still built by column position,
there is no shared list/column pipeline yet, and a count change is refused with an exception rather than
silently misaligning every row.
10. Module-specific (not part of the generic vocabulary)
| Hook | Type | Args | Dispatched from |
|---|---|---|---|
category_report_enabled | Filter<bool> | (bool $enabled) | category_report.php |
category_report_config | Filter<array> | (array $config) | category_report.php |
Kept deliberately module-specific: category_report_config is genuine module data with no generic
equivalent, and category_report_enabled lets the module tell a direct URL “disabled” apart from
“no rights” after component_visible already folds the setting into Component::isVisible().
Hook engine API reference
Hooks::addAction(string $name, callable $callback, int $priority = 10, ?int $acceptedArgs = null, ?string $id = null): void Hooks::doAction(string $name, mixed ...$args): void Hooks::doActionCatchErrors(string $name, mixed ...$args): void // failure/diagnostic dispatch only Hooks::addFilter(string $name, callable $callback, int $priority = 10, ?int $acceptedArgs = null, ?string $id = null): void Hooks::applyFilters(string $name, mixed $value, mixed ...$args): mixed Hooks::applyTypedFilters(string $name, mixed $value, mixed ...$args): mixed // return type must match $value's Hooks::addResolver(string $name, callable $callback, int $priority = 10, ?int $acceptedArgs = null, ?string $id = null): void Hooks::resolve(string $name, mixed $default = null, mixed ...$args): mixed // first non-null answer wins Hooks::removeAction(string $name, string|callable $idOrCallback): bool Hooks::removeFilter(string $name, string|callable $idOrCallback): bool Hooks::removeResolver(string $name, string|callable $idOrCallback): bool Hooks::hasAction/hasFilter/hasResolver(string $name): bool Hooks::reset(string $name = ''): void // one hook, or the whole registry
Lower priority runs earlier; same priority runs in registration order. A callback throws to veto an
Action or a Filter; the exception propagates. doActionCatchErrors() logs and swallows instead, used
only at failure/diagnostic sites where the original failure must stay the one Admidio reports.
doAction()/doActionCatchErrors()/applyFilters()/applyTypedFilters()/resolve() are not only for
core's own dispatch sites: a module or plugin can call them at its own extension points to make itself
extensible by other plugins the same way core is — register the names with add*() as usual, then
dispatch them from your own code with these.