This is an old revision of the document!
How Admidio's Changelog Works and Instructions for Implementations
Introduction
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:
How the Changelog Works Technically
The changelog depends on the use of the Entity
class (previously TableAccess
) and its derived classes for all database access. Direct modifications of the database using SQL statements will not be logged. The Entity class uses setValue
methods to change database columns and the save
method to store them to the database. This is where the changelog hooks in: In addition to storing the change to the database, the save method will also detect and insert a new entry for each changed column into the adm_log_changes database table (using the LogChanges class derived from Entity).
- If an object (=database table row) is created, the
Entity::logCreation()
method is called to insert a creation record into the log. - For all combined modification to an existing record, the
Entity::logModifications($logChanges)
method is called (which will in turn insert a separate changelog entry for each modified column). - If an object (=database table row) is deleted, the
Entity::logDeletion()
method is called.
These methods handle the creation of LogChanges records and storing them to the adm_log_changes database table. Usually it is not needed to change these core functions. See below for instructions how to adjust all aspects of the changelog generation and display.
In addition, the following methods of the Entity class are used:
Entity::readableName()
: Returns a human-readable representation of the database record. By default, if a column 'prefix_name' or 'prefix_headline' exists, it will be used, otherwise the table's key column is returned. Classes likeUser
can override this to return e.g. a string of the form “Lastname, Firstname”.static Entity::setLoggingEnabled($enabled)
: Temporarily enable or disable logging (until called again, or a new request is handled). This state is not persisted, so this function will not affect subsequent page loadings.Entity::getIngoredLogColumns()
: Returns a list of column names, which should not be logged (e.g. the creation / modification time stamps or user IDs)Entity::adjustLogEntry(LogChanges $logEntry)
: After the logCration/logModification/logDeletion methods set up the new LogChanges record, this method can adjust it for custom behaviour, e.g. to add a related record (group memberships have the user as modified record and the group as a related record) or completely change the record away from the default behavior.
Derived subclasses of the Entity base class can override these methods to tweak the changelog entries generated (or even suppress or fundamentally change them).
Database Structure of the Changelog Table
The changelog entries are stored in the database in the adm_log_changes table and contain all information of the affected record (ID, UUID and name), a potentially affected related record (ID/UUID and name), the modified field / dabase column (column name and human-readable name), as well as the previous and the new values.
The table has the following columns, most of which will be automatically filled the methods in the Entity class:
log_id
(auto-increment counter),log_table
(the affected database table without the 'adm_' prefix)log_record_id
,log_record_uuid
,log_record_name
: The record ID, UUID and a human-readable representation of the affected recordlog_record_linkid
: Some tables have no corresponding display page for its records, so we want to link to a different object (e.g. the adm_members table has no view for its records. Instead the affected record should be the user and the group is the related record → the linkid for html links is the user UUID rather than the membership ID or UUID)log_related_id
,log_related_name
: For records that relate to others (e.g. group memberships relate a user to a group, a folder or album potentially relates to its parent folder/album, etc.) these columns give the ID/UUID and the human-readable name of the related namelog_field
,log_field_name
: table column name and human-readable representation of the modified columnlog_action
: MODIFY, CREATED or DELETEDlog_value_old
,log_value_new
: The previous and the new value of the field.log_user_id_create
,log_timestamp_create
: user ID and time of the change (both automatically filled from the current user and date/time)
Part A: Creating Changelog Entries for All Changes
Adding New Tables to the Changelog
By default, all database access via the Entity class can and will be logged, as long as the corresponding preference flag is set. The core admidio tables have their own preference setting, so each table can be individually turned on/off. All other tables (third party modules/plugins or future core modules that have not yet explicitly implemented its logging) are controlled collectively by the preference changelog_table_others
(Preferences → “Change History” → header “Content modules” → “All others (others)”).
All uncustomized tables will log all changes to all columns (except the creation/modification user ids and timestamps) with the raw table column name as field. The changelog entry will not have any links and the human-readable representation will use value returned by the record's Entity::readableName()
method. For example, without any custom implementation, the forum modul would create the following changelog entries out of the box when adding a new topic and a new post:
As one can see, the creation of the topic and the post with ID 1 is properly logged, as well as setting its category and title. For a user, however, this display can be improved by using category and topic names for display, linking to the corresponding pages, and converting the raw database column names to comprehensible labels.
See below for instructions how to add tables for individual selection and customize their logging.
Ignoring Certain Tables from Logging
Some tables (like the adm_auto_login or adm_sessions tables) are meant as transient temporary data storage and should never be logged in the changelog. Others like adm_log_changes are clearly also not meant to be logged in the changelog. Third-party plugins can also use their own table to cache data temporarily without logging.
Admidio holds a global static table of database table names that should not be logged in the static array Entity::$noLogTables
array. If a third-party plugin or module has a table that should never generate changelog entries, you can add the corresponding table names (WITHOUT the table name prefix!) in the constructor of your modules class or even in the main body of a module.
The following code will add the adm_myplugin_cache and adm_session_data tables to the list of tables that should not be logged. This will only affect the current execution and will NOT persist to the next execution. So this code really needs to be executed each time your plugin/module loads.
use Admidio\Changelog\Entity\LogChanges; array_push(LogChanges::$noLogTables, 'myplugin_cache', 'session_data');
Core modules and plugins can directly modify the LogChanges::$noLogTables default setting in the src/Changelog/Entity/LogChanges.php
file.
Ignoring Certain Columns from Logging
By default, the uuid (prefix_uuid) as well as the the create/change timestamp (prefix_timestamp_create/change) and user ID (prefix_usr_id_create/change) columns are ignored. To ignore other columns as well, one must use an Entity-derived subclass for your database record and override the getIgnoredLogColumns()
methods, like the User class does::
use Admidio\Infrastructure\Entity\Entity; class User extends Entity { ... public function getIgnoredLogColumns(): array { return array_merge(parent::getIgnoredLogColumns(), ['usr_pw_reset_id', 'usr_pw_reset_timestamp', 'usr_last_login']); } }
Of course, you then need to use this class instead of Entity to create / modify the database.
Tweaking the changelog creation
All tweaks to the changelog record generation depend on the use of an Entity-drived class for your record creation/modification, similar to the code above to ignore certain database columns in the changelog!
Changing the Displayed Name of a Record
By default, each object (database record) uses the 'prefix_name' column as its display name in the changelog view, if such a column exists (if not, 'prefix_title' and 'prefix_headline' are used, too). To change this, one can simply override the Entity::readableName()
method, like for the User class, which uses a label of the form “Lastname, Firstname” as its display string:
class User extends Entity { ... public function readableName(): string { return $this->mProfileFieldsData->getValue('LAST_NAME') . ', ' . $this->mProfileFieldsData->getValue('FIRST_NAME'); } }
Adding a Related Object
Override the Entity::adjustLogEntry(LogChanges $logEntry)
method in your subclass and call $logEntry→setLogRelated(..)
to add the link to the related object. Here is the code for the File class to set the corresponding Folder as the related object (the UUID and the name of the related object are needed):
<?php use Admidio\Changelog\Entity\LogChanges; class File extends Entity { ... protected function adjustLogEntry(LogChanges $logEntry) { $folEntry = new Folder($this->db, $this->getValue('fil_fol_id')); $logEntry->setLogRelated($folEntry->getValue('fol_uuid'), $folEntry->getValue('fol_name')); } }
By default, the changelog view will link to the same object as the main object (e.g. if a folder links to parent folder, it will work out of the box). To link to a different object type, one needs to modify the changelog display code. See below for instructions.
Modifying the Whole Changelog Record (e.g. Role Dependencies or Group Memberships)
Some database records do not describe objects per se, but relations between records or even more abstract data. For these, the database record should not be logged as a separate record, but rather as a change to a completely different record. E.g. the creation of a Membership record (table adm_members) should not be logged as the creation of a membership record, with each column as a separate modification, but rather as a modification of the corresponding User records with the related group/role. This fundamental modification of the changelog record can also be done in the adjustLogEntry method. E.g. the Membership::ajustLogEntry method set the user as the object for HTML links in the change log, inserts the group as a releated object and also suppresses individual logging of the mem_rol_id, mem_usr_id and mem_uuid columns. As a consequence, the creation of a membership object is logged like a change of the user object related to the group.
class Membership extends Entity { ... public function getIgnoredLogColumns(): array { return array_merge(parent::getIgnoredLogColumns(), ['mem_rol_id', 'mem_usr_id', 'mem_uuid'}]); } protected function adjustLogEntry(LogChanges $logEntry) { global $gDb, $gProfileFields; $usrId = (int)$this->getValue('mem_usr_id'); $user = new User($this->db, $gProfileFields, $usrId); $logEntry->setValue('log_record_name', $user->readableName()); $logEntry->setValue('log_record_uuid', $user->getValue('usr_uuid')); $logEntry->setLogLinkID($usrId); $rolId = $this->getValue('mem_rol_id'); $role = new Role($this->db, $rolId); $logEntry->setLogRelated($role->getValue('rol_uuid'), $role->getValue('rol_name')); } }
Part B: Displaying Changelog Entries to the Admin / User
The page '/adm_program/modules/changelog.php' is used to display the changelog, either for only one or more particular tables (parameter table=table1
or table=table1%2Ctable2%2Ctable3
) or even only one object (parameter uuid=7a854ed2-50db-49ee-9379-31d07f467d47
). If no parameters are given, the whole changelog from all tables is displayed.
Adding an Administration Menu Item for the Changelog
While individual modification or list pages show the “Change History” button for the particular object or object type, sometimes it can be useful to see the complete changelog of all changes. The easiest way is to create an admin menu item for this. Simply to to “Menu” and create a new item with the following settings:
How the Changelog Page Works
The changelog page (code in adm_program/modules/changelog.php
) first checks, whether the current user has either admin rights or at least edit rights for the corresponding tables or object.
It loads all entries from the adm_log_changes table and displays them:
- If a UUID is given, the method
static ChangelogService::getObjectForTable(string $module)
is used to load the object from the database. It's readableName is then used in the headline. E.g. if$module='users'
, a User object is created, if$module='photos'
, an Album object, etc. - For each changelog record, the following columns are displayed:
- 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 thestatic 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. - 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.
return array(..., 'rol_name' => 'SYS_NAME', 'rol_description' => 'SYS_DESCRIPTION', 'rol_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), ... 'lnk_name' => 'SYS_LINK_NAME', 'lnk_description' => 'SYS_DESCRIPTION', 'lnk_url' => array('name' => 'SYS_LINK_ADDRESS', 'type' => 'URL'), 'lnk_cat_id' => array('name' => 'SYS_CATEGORY', 'type' => 'CATEGORY'), }
- The old and the new values of the field are printed by default with their raw values. If the getFieldTranslations() array returns an array with a
'type'
key, its value determines the formatting. The actual formatting is done with thestatic ChangelogService::formatValue($value, $type, $entries = [])
method. New columns can be simply added to the getFieldTranslations() method.- 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 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:
public static function formatValue($value, $type, $entries = []) { ... switch ($type) { ... case 'ROOM': $obj = new Room($gDb, $value); $htmlValue = self::createLink($obj->readableName(), 'rooms', $obj->getValue('room_id'), $obj->getValue('room_uuid')); 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.).
Part C: Steps to Extend the Admidio Core with a New Module / Plugin
Part D: Additional Steps for Third-Party Extensions
TODO
- How to add new tables/objects to the logging (core development or third-party)
- General setup for basic logging
- Translating DB name and columns (display in changelog)
- Adding links to the objects
- adding data types to DB columns
- Ignoring certain tables, columns or changes
- Logging a change as something different (e.g. creation of a membership records as a modification of a user)
- Displaying the changelog button