Both sides previous revision Previous revision Next revision | Previous revision |
en:entwickler:changelog_implementation [2025/02/14 17:26] – [Adding an Administration Menu Item for the Changelog] kainhofer | en:entwickler:changelog_implementation [2025/03/21 08:55] (current) – kainhofer |
---|
Starting with Admidio 5.0, all changes to objects (Users, Events, Groups/Roles, Weblinks, Albums, Folders/Files, ...) and settings that are saved in Admidio's database can be logged and the changes displayed in the Change History screen. Logging can be enabled per object type (=database table) in the preferences. Each object or list with changelogs enabled will display a changelog button to view it: | Starting with Admidio 5.0, all changes to objects (Users, Events, Groups/Roles, Weblinks, Albums, Folders/Files, ...) and settings that are saved in Admidio's database can be logged and the changes displayed in the Change History screen. Logging can be enabled per object type (=database table) in the preferences. Each object or list with changelogs enabled will display a changelog button to view it: |
| |
}{{:en:entwickler:changelog:changelog_view_overview.png?400|The new changelog view}}{{:en:entwickler:changelog:changelog_settings.png?400|Settings for the changelog}}{{:en:entwickler:changelog:changelog_button_preferences.png?200|View changelog button}} | {{:en:entwickler:changelog:changelog_view_overview.png?400|The new changelog view}}{{:en:entwickler:changelog:changelog_settings.png?400|Settings for the changelog}}{{:en:entwickler:changelog:changelog_button_preferences.png?200|View changelog button}} |
| |
| |
* The raw database table name is translated into a nice label by the ''static ChangelogService::getTableLabel(string $table)'' method | * The raw database table name is translated into a nice label by the ''static ChangelogService::getTableLabel(string $table)'' method |
* Object name: The display name is already stored in the adm_log_changes table. The changelog page first tries to translate it using ''Language::translateIfTranslationStrId''. It then tries to create a link to the object using the ''%%static ChangelogService::createLink(string $text, string $module, int|string $id, string $uuid = '')%%'' method. If the 'log_record_linkid' is set, then this ID is used in the link rather than the original record ID/UUID. | * Object name: The display name is already stored in the adm_log_changes table. The changelog page first tries to translate it using ''Language::translateIfTranslationStrId''. It then tries to create a link to the object using the ''%%static ChangelogService::createLink(string $text, string $module, int|string $id, string $uuid = '')%%'' method. If the 'log_record_linkid' is set, then this ID is used in the link rather than the original record ID/UUID. |
* If a record has a related record stored in the changelog, the same is done for the related record. By default, the related record uses the same object type. If this is not desired (e.g. a File has its parent folder as its related object), then one has to change the ''changelog.php'' to modify the object type for the related object. | * If a record has a related record stored in the changelog, the same is done for the related record. By default, the related record uses the same object type. If this is not desired (e.g. a File has its parent folder as its related object), then one has to change the ''static ChangelogService::getRelatedTable'' method to modify the object type for the related object. |
* To translate the raw database column names (or field names in general) into nice labels, and define data types for it values, the ''static ChangelogService::getFieldTranslations()'' method defines a mapping table. | * To translate the raw database column names (or field names in general) into nice labels, and define data types for it values, the ''static ChangelogService::getFieldTranslations()'' method defines a mapping table. |
<code php> | <code php> |
* If the value needs no particular formatting, an entry of the form ''%%'column_name' => 'translatable string'%%'' suffices. | * If the value needs no particular formatting, an entry of the form ''%%'column_name' => 'translatable string'%%'' suffices. |
* To add particular formatting of an existing data type, the entry has to be\\ ''%%'column_name' => array('name' => 'translatable string', 'type' => 'BOOL')%%'' | * To add particular formatting of an existing data type, the entry has to be\\ ''%%'column_name' => array('name' => 'translatable string', 'type' => 'BOOL')%%'' |
* To add a new data type that is not yet available, the above entry can be used, but in addition, the new data type has to be added in the ''formatValue($value, $type, $entries = [])'' function, with the $value typically holding the ID of the object: | * To add a new data type that is not yet available, the same code is used, but you can choose any data type name you like. In addition, you have to implement support for this datatype in the ''formatValue'' method. See below. |
| |
| The abovementioned methods of the ''ChangelogService'' class have all existing database tables properly implemented. New modules or new database tables simply need to add code to these functions for proper support, if required (not all objects have a page to link to, some database columns can use the default formatting, etc.). |
| |
| ===== Implement Links to the List Page of a New Table ===== |
| If a new object type and thus a new database table is added to Admidio (either by the core, but included modules or plugins or by third-party extensions), the table's (translatable) name is added to the ''ChangelogService::getTableLabel(string $table)'' array and if a list view for objects of the new table exists, a link can be added in the ''ChangelogService::createLink'' method (file ''src/Changelog/Service/ChangelogService.php''): |
| <code php> |
| class ChangelogService { |
| ... |
| public static function createLink(string $text, string $module, $id, $uuid = '') { |
| switch ($module) { |
| ... |
| case 'rooms': |
| $url = SecurityUtils::encodeUrl(ADMIDIO_URL.FOLDER_MODULES.'/rooms/rooms_new.php', |
| array('room_uuid' => $uuid)); break; |
| ... |
| } |
| } |
| </code> |
| |
| |
| ===== Links to Related Objects of a Different Type ===== |
| |
| By default, if a record has a related object, it will be formatted and linked with the same object type. E.g. if a folder has a parent folder (set as related record in the changelog table), the parent folder will also be formatted as a folder and a link to the folder page created. In many cases this is not desired, e.g. a File is related to its parent folder, which cannot be formatted as File, but rather as folder. This needs to be added to the method ''ChangelogService::getRelatedTable'' (file ''src/Changelog/Service/ChangelogService.php''): |
| <code php> |
| case 'files': |
| return 'folders'; |
| </code> |
| |
| ===== Add Field Value Formatting for a New Data Type ===== |
| |
| To add a new data type for field value formatting (both the previous and the new value), you define the column in the ''ChangelogService::getFieldTranslations'' method (file ''src/Changelog/Service/ChangelogService.php'') with the new data type you desire: |
| |
| <code php> |
| 'fil_fol_id' => array('name' => 'SYS_FOLDER', 'type' => 'FOLDER'), |
| </code> |
| |
| In addition, you also need to implement the HTML output for values this datatype 'FOLDER' in the ''ChangelogService::formatValue($value, $type, $entries = [])'' function (file ''src/Changelog/Service/ChangelogService.php''), with the $value typically holding the ID of the object: |
<code php> | <code php> |
public static function formatValue($value, $type, $entries = []) { | public static function formatValue($value, $type, $entries = []) { |
switch ($type) { | switch ($type) { |
... | ... |
case 'ROOM': | case 'FOLDER': |
$obj = new Room($gDb, $value); | $obj = new Folder($gDb, $value); |
$htmlValue = self::createLink($obj->readableName(), 'rooms', $obj->getValue('room_id'), $obj->getValue('room_uuid')); | $htmlValue = self::createLink($obj->readableName(), 'folders', |
| $obj->getValue('fol_id'), $obj->getValue('fol_uuid')); |
break; | break; |
... | ... |
| |
| |
The abovementioned methods of the ''ChangelogService'' class have all existing database tables properly implemented. New modules or new database tables simply need to add code to these functions for proper support, if required (not all objects have a page to link to, some database columns can use the default formatting, etc.). | ====== Adding a Changelog Button to a List or Edit Page ====== |
| |
| It is very easy to add a changelog button to each list page for a certain type of object, as well as to individual edit pages. The changelog on a list page will display all changes to the objects of that type, while the changelog of a particular edit page will filter the changelog to display only changes to the current object. Both variants are handled by a call to the method '' public static ChangelogService::displayHistoryButton(PagePresenter $page, string $area, string|array $table, bool $condition = true, array $params = array())''. This method adds a history button (only if ''$condition'' is true) to the current page ''$page''. The param ''$table'' defines the database table(s), e.g. ''%%'users,user_data,members'%%'' for user profile data, or 'rooms' for rooms), while ''$params'' defines additional filters that are directly passed on as URL parameters to the link for adm_program/modules/changelog.php. |
| Supported key are 'id', 'uuid' and 'related_to', which all correspond to the columns in the adm_log_changes table. |
| |
| An example for the changelog button on the contacts page (showing all changes to all contacts, if the user has the neccessary permissions) is: |
| <code php> |
| ChangelogService::displayHistoryButton($page, 'contacts', 'users,user_data,members'); |
| </code> |
| |
| The changlog button on the profile edit page of a particular contact is: |
| <code php> |
| // show link to view profile field change history, if we have a user ID and the current user has permissions |
| ChangelogService::displayHistoryButton($page, 'profile', 'users,user_data,user_relations,members', |
| !empty($getUserUuid) && $gCurrentUser->hasRightEditProfile($user), array('uuid' => $getUserUuid)); |
| </code> |
| |
====== Part C: Steps to Extend the Admidio Core with a New Module / Plugin ====== | ====== Part C: Steps to Extend the Admidio Core with a New Module / Plugin ====== |
| |
| At the example of the Forum module's tables (adm_forum_posts and adm_forum_topics), these are the steps to implement full support. Basic changelog support already works out of the box with no required code changes (enabled with the ''changelog_table_others'' preference flag for all unknown or third-party database table). All changes described here are only required to get a nicer changelog view with links and easy-to-understand labels. |
| |
| The example of the forum module is taken directly from the admidio code tree ([[https://github.com/Admidio/admidio/commit/ff61d0a0029be89e88a4bccdfbb227b9af612efc|commit ff61d0a on github]]) |
| |
| - **A: Preparation / General setup for forum logging** |
| - **Register the tables for logging (translated table name)**: Add the tables to the ''ChangelogService::getTableLabel'' translation array (file ''src/Changelog/Service/ChangelogService.php'') |
| - <code php> |
| 'forum_topics' => 'SYS_FORUM_TOPIC', |
| 'forum_posts' => 'SYS_FORUM_POST',</code> |
| - If the translation strings don't exist yet, add them at least to ''adm_program/languages/en.xml''<code xml> |
| <string name="SYS_FORUM_POST">Forum post</string> |
| <string name="SYS_FORUM_TOPIC">Forum topic</string></code> |
| - **Preferences to enable/disable forum logging**: |
| - preference definition in file ''install/db_scripts/preferences.php''<code php>'changelog_table_forum_topics' => '0', |
| 'changelog_table_forum_posts' => '0',</code> |
| - enable/disable checkbox in ''src/UI/View/Preferences.php'', Preferences::createChangelogForm:<code php>array( |
| 'title' => $gL10n->get('SYS_HEADER_CONTENT_MODULES'), |
| 'id' => 'content_modules', |
| 'tables' => array('files', 'folders', 'photos', 'announcements', 'events', 'rooms', |
| 'forum_topics', 'forum_posts', 'links', 'others') |
| ),</code> |
| - **Make the Topic and Post classes available in the changelog code** (file ''src/Changelog/Service/ChangelogService.php'') |
| - <code php> |
| use Admidio\Forum\Entity\Topic; |
| use Admidio\Forum\Entity\Post;</code> |
| - **B: Adjust the creation/content of the changelog entris in the database** |
| - **Ignore certain columns from logging** (e.g. on creation, the topic title or post text is no change; the Topic ID of a Post is a technical detail that is never changed by the user; The view counter of a topic shall not be logged) in the Entity-derived classes Topic and Post: |
| - Add function ''Topic::getIgnoredLogColumns()'' (file ''src/Forum/Entity/Topic.php''):<code php>class Topic extends Entity |
| { |
| ... |
| public function getIgnoredLogColumns(): array |
| { |
| return array_merge(parent::getIgnoredLogColumns(), |
| [$this->columnPrefix . '_views'], |
| ($this->newRecord)?[$this->columnPrefix.'_title']:[]); |
| } |
| }</code> |
| - Add function ''Post::getIgnoredLogColumns()'' (file ''src/Forum/Entity/Post.php''):<code php>class Post extends Entity |
| { |
| ... |
| public function getIgnoredLogColumns(): array |
| { |
| return array_merge(parent::getIgnoredLogColumns(), |
| ['fop_fot_id'], |
| ($this->newRecord)?[$this->columnPrefix.'_text']:[] |
| ); |
| } |
| }</code> |
| - **Set related objects in the log entry**: a forum post is related to a forum topic, and a topic is related to its first post, so we insert the corresponding links in the log table, too. This will make it easier in the changelog view to navigate and link to related objects of the change: |
| - Add function ''Topic::adjustLogEntry()'' (file ''src/Forum/Entity/Topic.php''):<code php>class Topic extends Entity |
| { |
| ... |
| protected function adjustLogEntry(LogChanges $logEntry): void |
| { |
| $fotEntry = new Post($this->db, (int)$this->getValue('fot_fop_id_first_post')); |
| $logEntry->setLogRelated($fotEntry->getValue('fop_uuid'), $fotEntry->getValue('fop_text')); |
| } |
| }</code> |
| - Add function ''Post::adjustLogEntry()'' (file ''src/Forum/Entity/Post.php''):<code php>class Post extends Entity |
| { |
| ... |
| protected function adjustLogEntry(LogChanges $logEntry): void |
| { |
| $fotEntry = new Topic($this->db, $this->getValue('fop_fot_id')); |
| $logEntry->setLogRelated($fotEntry->getValue('fot_uuid'), $fotEntry->getValue('fot_title')); |
| } |
| }</code> |
| - **C: Adjust the display in the Change History View** |
| - **Translation and data types of the table columns** |
| - In function ''ChangelogService::getFieldTranslations'', add the column definitions to the return list (file ''src/Changelog/Service/ChangelogService.php''):<code php> |
| 'fot_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), |
| 'fot_fop_id_first_post' => array('name' => 'SYS_FORUM_POST', 'type' => 'POST'), |
| 'fot_title' => 'SYS_TITLE', |
| 'fop_text' => 'SYS_TEXT', |
| 'fop_fot_id' => array('name' => 'SYS_FORUM_TOPIC', 'type' => 'TOPIC'), |
| </code> |
| - Add the new column data types to the ''ChangelogService::formatValue'' method inside the switch statement (file ''src/Changelog/Service/ChangelogService.php''):<code php> |
| case 'TOPIC': |
| $obj = new Topic($gDb, $value); |
| $htmlValue = self::createLink($obj->readableName(), 'forum_topics', |
| $obj->getValue('fot_id'), $obj->getValue('fot_uuid')); |
| break; |
| case 'POST': |
| $obj = new POST($gDb, $value); |
| $htmlValue = self::createLink($obj->readableName(), 'forum_posts', |
| $obj->getValue('fop_id'), $obj->getValue('fop_uuid')); |
| break; |
| </code> |
| - **Add links in the changelog to forum topics and posts** |
| - In function ''ChangelogService::createLink'', add the link definitions in the switch statement (file ''src/Changelog/Service/ChangelogService.php''):<code php> |
| case 'forum_topics' : |
| $url = SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/forum.php', |
| array('mode' => 'topic', 'topic_uuid' => $uuid)); break; |
| case 'forum_posts' : |
| $url = SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/forum.php', |
| array('mode' => 'post_edit', 'post_uuid' => $uuid)); break; |
| </code> |
| - **Format links to related objects (in the "related to" column) with proper types / links** |
| - In method ''ChangelogService::getRelatedTable'', tinside the switch statement, add the type of the related object for posts to topic and vice versa (file ''src/Changelog/Service/ChangelogService.php''):<code php> |
| case 'forum_posts': |
| return 'forum_topics'; |
| case 'forum_topics: |
| return 'forum_posts'; |
| </code> |
| - **Object name in changelog headline**: Create empty Topic and Post objects for given tables name 'forum_topics' and 'forum_posts' (file ''src/Changelog/Service/ChangelogService.php'') |
| - In method ''ChangelogServer::getObjectForTable(string $module)'':<code php> |
| case 'forum_topic': |
| return new Topic($gDb); |
| case 'forum_post': |
| return new Post($gDb);</code> |
| - **D: Add Change History buttons to the forum pages** (both topic view, as well as individual topic and post edit pages) |
| - File ''src/UI/Presenter/ForumPresenter.php'', <code php>use Admidio\Changelog\Service\ChangelogService;</code>method ''ForumPresenter::createSharedHeader''<code php>ChangelogService::displayHistoryButton($this, 'forum', 'forum_topics,forum_posts', $gCurrentUser->administrateForum());</code> |
| - File ''src/UI/Presenter/ForumTopicPresenter.php''<code php>use Admidio\Changelog\Service\ChangelogService;</code>In ForumTopicPresenter::createCards method (after this->addPageFunctionsMenuItem call):<code php>global $gCurrentUser; |
| ChangelogService::displayHistoryButton($this, 'forum', 'forum_topics,forum_posts', |
| $gCurrentUser->administrateForum(), ['uuid' => $this->topicUUID]);</code>In ForumTopicPresenter::createEditForm (after new FormPresenter); The changelog button should be hidden when a new topic is created (i.e. no uuid exists yet!):<code php>ChangelogService::displayHistoryButton($this, 'forum', 'forum_topics,forum_posts', |
| $this->topicUUID !== '' && $gCurrentUser->administrateForum(), ['uuid' => $this->topicUUID]);</code> |
| - File ''src/UI/Presenter/ForumPostPresenter.php''<code php>use Admidio\Changelog\Service\ChangelogService;</code>In ForumPostPresenter::createEditFormmethod (after new FormPresenter):<code php>global $gCurrentUser; |
| ChangelogService::displayHistoryButton($this, 'forum', 'forum_posts', |
| $this->postUUID !== '' && $gCurrentUser->administrateForum(), ['uuid' => $this->postUUID]); |
| </code> |
| |
| |
====== Part D: Additional Steps for Third-Party Extensions ====== | ====== Part D: Additional Steps for Third-Party Extensions ====== |
| |
| Adding support for changelogs in third-party extensions, where modifying the core Admidio code is not possible, works similar to the above steps. Modifying the changelog entry creation is implemented inside the extension's Entity-derived database access classes, so this part is similar to core modules. However, the formatting in the Change history page view is implemented in the class ''ChangelogService'' for the core modules, which is not directly available for change to third-party extension developers. |
| |
======= TODO ======= | |
| |
* How to add new tables/objects to the logging (core development or third-party) | However, the ChanglogService class additionally provides a way to register mapping tables for table/column names or general callback functions for all the methods that need modifications as described above. The callback functions are registered with the method <code php>/** |
* General setup for basic logging | * Register a callback function or value for the changelog functionality. If the callback is a value (string, array, etc.), it will |
* Translating DB name and columns (display in changelog) | * be returned. If the callback is a function, it will be executed and if the return value is not empty, it will be returned. If the |
* Adding links to the objects | * function returns a null or empty value, the next callback or the default processing of the ChangelogService method will proceed. |
* adding data types to DB columns | * @param string $function The method of the ChangelogService class that should be customized. One of |
* Ignoring certain tables, columns or changes | * 'getTableLabel', 'getTableLabelArray', 'getObjectForTable', 'getFieldTranslations', 'createLink', |
* Logging a change as something different (e.g. creation of a membership records as a modification of a user) | * 'formatValue', 'getRelatedTable', 'getPermittedTables' |
* Displaying the changelog button | * @param string $moduleOrKey The module or type that should be customized. If empty, the callback will be |
* | * executed for all values and it will be used if it evaluates to a non-empty value. |
| * @param mixed $callback The callback function or value. A value will be returned unchanged, a function will |
| be executed (arguments are identical to the ChangelogService's methods) |
| */ |
| static ChangelogService::registerCallback(string $function, string $moduleOrKey, mixed $callback)</code> |
| |
| Using these callback mechanisms, the forum changelog described above could also be implemented with the following code. It should be executed somewhere during php startup when the third-party module is loaded, and before either a changelog page can be displayed or before any of the third-party extension's database records are modified (i.e. before the extension writes data to the database). |
| |
| <code php> |
| |
| ## Translation of database tables |
| ChangelogService::registerCallback('getTableLabelArray', 'forum_topics', 'SYS_FORUM_TOPIC'); |
| ChangelogService::registerCallback('getTableLabelArray', 'forum_posts', 'SYS_FORUM_POST'); |
| |
| ## Translations and type definitions of database columns |
| ChangelogService::registerCallback('getFieldTranslations', '', [ |
| 'fot_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), |
| 'fot_fop_id_first_post' => array('name' => 'SYS_FORUM_POST', 'type' => 'POST'), |
| 'fot_title' => 'SYS_TITLE', |
| 'fop_text' => 'SYS_TEXT', |
| 'fop_fot_id' => array('name' => 'SYS_FORUM_TOPIC', 'type' => 'TOPIC') |
| ]); |
| |
| ## Formatting of new database column types (in many cases not needed) |
| ChangelogService::registerCallback('formatValue', 'TOPIC', function($value, $type, $entries = []) { |
| global $gDb; |
| if (empty($value)) return ''; |
| $obj = new Topic($gDb, $value??0); |
| return ChangelogService::createLink($obj->readableName(), 'forum_topics', |
| $obj->getValue('fot_id'), $obj->getValue('fot_uuid')); |
| }); |
| ChangelogService::registerCallback('formatValue', 'POST', function($value, $type, $entries = []) { |
| global $gDb; |
| if (empty($value)) return ''; |
| $obj = new POST($gDb, $value??0); |
| return ChangelogService::createLink($obj->readableName(), 'forum_posts', |
| $obj->getValue('fop_id'), $obj->getValue('fop_uuid')); |
| }); |
| |
| ## Create HTML links to the object's list view and edit pages |
| ChangelogService::registerCallback('createLink', 'forum_topics', function(string $text, string $module, int|string $id, string $uuid = '') { |
| return SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/forum.php', |
| array('mode' => 'topic', 'topic_uuid' => $uuid)); |
| }); |
| ChangelogService::registerCallback('createLink', 'forum_posts', function(string $text, string $module, int|string $id, string $uuid = '') { |
| return SecurityUtils::encodeUrl( ADMIDIO_URL.FOLDER_MODULES.'/forum.php', |
| array('mode' => 'post_edit', 'post_uuid' => $uuid)); |
| }); |
| |
| ## Object types of related objects (if object relations are used at all!) |
| ChangelogService::registerCallback('getRelatedTable', 'forum_topics', 'forum_posts'); |
| ChangelogService::registerCallback('getRelatedTable', 'forum_posts', 'forum_topics'); |
| |
| |
| ## Create Entity-derived objects to create headlines with proper object names |
| ChangelogService::registerCallback('getObjectForTable', 'forum_topics', function() {global $gDb; return new Topic($gDb);}); |
| ChangelogService::registerCallback('getObjectForTable', 'forum_posts', function() {global $gDb; return new Post($gDb);}); |
| |
| ## Enable per-user detection of access permissions to the tables (based on user's role permission); Admin is always allowed |
| ChangelogService::registerCallback('getPermittedTables', '', function(User $user) { |
| if ($user->administrateForum()) |
| return ['forum_topics', 'forum_posts']; |
| }); |
| </code> |