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.


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 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.

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

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.


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

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.


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.


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.


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

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()

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.


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.


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().


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.

  • en/entwickler/hooks/catalogue.txt
  • Last modified: 2026/08/26 21:04
  • by kainhofer