Skip to main content

🧩 Plugin Development Guide

emlog supports a plugin mechanism, allowing developers to easily add needed functions to the system.

Implementation Principle

Throughout the operation of emlog, we have defined some action events. When these events are encountered, emlog will automatically call all plugin functions bound to that event, thereby implementing the plugin's functionality.

Hook Function: doAction

The doAction function is built into the emlog core code and serves as the so-called plugin mounting point (hook).

// This is the mounting point for the index head. When the homepage loads, the plugin functions mounted on this point will be executed.
doAction('index_head')

Plugin Hook: addAction

addAction is used by plugins to mount their own functions to a mounting point. It is written in the plugin file. It takes two parameters: the mounting point name and the plugin's own function name.

// The plugin's add_some_style function is mounted to the system's index_head mounting point.
// Whenever the system executes to the index_head mounting point, the add_some_style function will be called.

addAction('index_head','add_some_style');

function add_some_style() {
// Add some styles, etc.
}

Development Standards

File Structure

  • Plugin Directory: /content/plugins. Each folder under the plugin directory represents a plugin.
  • Plugin English Alias: For example, the system's built-in tips plugin has the alias: tips. Only plugins with the directory structure "Plugin English Alias/Plugin English Alias.php" are recognized, e.g., tips/tips.php.
FileDescription
xxx.phpMain plugin file
xxx_callback.phpEvent callback related function file
xxx_setting.phpPlugin backend settings page (Visible only to administrators)
xxx_user.phpPlugin backend settings page (Visible to everyone)
xxx_show.phpPlugin frontend page
preview.jpgPlugin icon, used for backend plugin list display, size: 75x75 pixels

The xxx in the table above is the plugin English alias. Detailed introductions to plugin files are below.

Main Plugin File

The file named pluginname.php inside the plugin folder is the main plugin file. For example: the default tips plugin has a folder name of tips, and the main plugin file name is tips.php.

The comment content at the beginning of the tips.php file contains necessary information about the plugin, which will be displayed in the backend plugin management interface. Please make sure to fill it in completely. Reference as follows:

<?php
/*
Plugin Name: Tips
Version: 3.0
Plugin URL: https://www.emlog.net/plugin/detail/xxx
Description: Displays a small usage tip on the backend homepage, can also serve as a demo for plugin development.
Author: emlog
Author URL: https://www.emlog.net
*/
tip

For Plugin URL and Author URL, please use the application link and author page from the official website emlog.net. Other non-official links will not be displayed as hyperlinks in the backend plugin list.

Event Callbacks

In the emlog backend plugin management page, users can enable, disable, delete, and update plugins. Some of these operations trigger corresponding callback functions. Developers can add a file named pluginname_callback.php to the plugin to define callback functions for specific events, implementing operations such as plugin initialization, plugin data cleanup, and data structure updates.

EventTrigger Function
Enable Plugincallback_init()
Delete Plugincallback_rm()
Update Plugincallback_up()

Example:

tips_callback.php

<?php
!defined('EMLOG_ROOT') && exit('access denied!');

// Called when the plugin is enabled, can be used for configuration initialization
function callback_init() {
$plugin_storage = Storage::getInstance('plugin_name');
$r = $plugin_storage->getValue('key');
if (empty($r)) {
$default_data = [
'ip' => [],
'time' => [],
'attempt' => [],
];
$plugin_storage->setValue('temp', json_encode($default_data), 'string');
}
}

// Called when the plugin is deleted, can be used for data cleanup
function callback_rm() {
$plugin_storage = Storage::getInstance('plugin_name'); // Initialize a storage instance using the plugin's English name
$ak = $plugin_storage->deleteAllName('YES'); // Delete all data created by this plugin. Please pass uppercase "YES" to confirm deletion.
}

// Called when the plugin is updated, can be used for database changes, etc.
function callback_up() {
...
}

☘️ Green Plugin

Use the event callback mechanism to create a "Green Plugin". A so-called green plugin should:

  1. Not modify, add, or delete core database table fields during plugin startup and use.
  2. Not require adding extra custom plugin mounting points for installation; use official reserved mounting points (if individual themes lack mounting points, guide users to add official mounting points).
  3. Clean up all data of the plugin when deleted, including custom database tables and configuration information.

Plugin Backend Settings Page (Visible only to administrators)

If you want the plugin to have a settings page in the backend, you can:

  1. Add a file named pluginname_setting.php to the plugin.
  2. This file must contain a function named plugin_setting_view, where you can output the settings content. At this time, the plugin's backend configuration address is: https://yourdomain/admin/plugin.php?plugin=pluginname
  3. The plugin settings interface can be built directly based on Bootstrap4. Please refer to the default tips plugin.

Plugin Backend Feature Page (Visible to all users)

If you want the plugin to have a feature page in the backend, you can:

  1. Add a file named pluginname_user.php to the plugin.
  2. This file must contain a function named plugin_user_view, where you can output the feature content. At this time, the plugin's backend feature address is: https://yourdomain/admin/plugin_user.php?plugin=pluginname
  3. The plugin settings interface can be built directly based on Bootstrap4. Please refer to the default tips plugin.

This page can be used to build some backend functions for ordinary registered users. For example, the article collection plugin uses this feature.

Plugin Frontend Page

If you want the plugin to output a page on the frontend, you can add a file named pluginname_show.php to the plugin. At this time, the plugin's frontend display address is: https://yourdomain/?plugin=pluginname or https://yourdomain/plugin/pluginname (Pseudo-static rules need to be enabled). This way, you can build the plugin's frontend display page in the pluginname_show.php file.

Naming Rules

Plugin English Alias

Please use a combination of lowercase English letters, numbers, underscores (_), and hyphens (-), and it must start with a letter.

Example: tips, em_ai

Custom Function Naming Inside Plugin

Functions should use "Plugin English Alias_" as a prefix, such as: tips_init, where tips is the plugin English alias.

function tips_init() {
global $array_tips;
$i = mt_rand(0, count($array_tips) - 1);
$tip = $array_tips[$i];
echo "<div id=\"tip\"> $tip</div>";
}

Adopting such a naming convention can avoid conflicts with functions of other plugins.

Plugin File Name

  • It is recommended to use a custom prefix for plugin file naming to avoid conflicts with other plugins, such as: myprefix_tips, where myprefix_ is the custom prefix.
  • The main plugin file name must be the same as the folder name where the plugin is located, such as:
myprefix_tips/
myprefix_tips.php
myprefix_tips_setting.php
myprefix_tips_callback.php

Security

Add restriction statements at the beginning of plugin files. Plugin function files need to add:

!defined('EMLOG_ROOT') && exit('access denied!');

If this statement is not added, directly accessing the plugin's PHP program file will expose the blog's physical path, posing a threat to the blog's security.

If your plugin needs to receive some parameters, please strictly filter the data of every variable. For example: getting an int type parameter from external sources, $id = $_GET['id']; Writing it this way is unsafe. It should be changed to: $id = intval($_GET['id']);

If it is a string type parameter, $action = $_GET['action']; Writing it this way is also unsafe. It should be changed to: $action = addslashes($_GET['action']);

Plugin Data Storage (1): Storage

If a plugin needs to save settings and other information, it can use the system-provided Storage class to complete data storage and reading. Data will be stored in the storage table of the MySQL database. This storage method is suitable for storing key-value pair data, such as plugin settings items.

Writing Data

	$plugin_storage = Storage::getInstance('plugin_name');// Initialize a storage instance using the plugin's English name
$plugin_storage->setValue('key', 'xxx'); // Set the value of key to xxx. The maximum storage length is 65,535 characters.

Set write data type: Data storage also supports a third parameter to specify the type of stored data. When reading, it will return the corresponding data type. Currently, 4 types are supported, with the default being string type.

  • string // Returns string when reading
  • number // Returns float type when reading
  • boolean // Returns boolean type when reading
  • array // Returns array

Example:

	$plugin_storage = Storage::getInstance('plugin_name');
$data = ['name' => 'tom', 'age' => 19];
$plugin_storage->setValue('key', $data, 'array'); // Stored as array type. The array will be serialized and stored in the database, and automatically deserialized when reading.

Reading Data

    $plugin_storage = Storage::getInstance('plugin_name'); // Initialize a storage instance using the plugin's English name
$ak = $plugin_storage->getValue('key'); // Read key value

// If reading an array, please check if the read value is empty first to avoid warning errors
$config = $plugin_storage->getValue('config');
$test_key = !empty($config) ? $config['test_key'] : '';

Cleaning Up/Deleting Data

    $plugin_storage = Storage::getInstance('plugin_name'); // Initialize a storage instance using the plugin's English name
$ak = $plugin_storage->deleteName('key') // Delete a row of data named key created by this plugin
$ak = $plugin_storage->deleteAllName('YES'); // Delete all data created by this plugin. Please pass uppercase "YES" to confirm deletion. Generally used in plugin deletion callback functions.

Plugin Data Storage (2): Custom Database Tables

If the Storage data storage method above cannot meet more complex data structure storage requirements, plugins can create their own database tables to store data.

Creating Plugin Data Tables

Use the [Event Callback] mechanism mentioned above to implement creating the plugin's own table in the custom callback function. A simple example is given below.

<?php
!defined('EMLOG_ROOT') && exit('access denied!');

// Initialize plugin data table
function callback_init() {
$db = MySql::getInstance();
$charset = 'utf8mb4';
$type = 'InnoDB';
$table = DB_PREFIX . 'stats';
$add = "ENGINE=$type DEFAULT CHARSET=$charset;";
$sql = "
CREATE TABLE IF NOT EXISTS `$table` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`gid` int(11) unsigned NOT NULL,
`title` varchar(255) NOT NULL default '',
`views` bigint(11) unsigned NOT NULL default 0,
`comments` bigint(11) unsigned NOT NULL default 0,
`date` date NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `date_gid` (`date`,`gid`)
)" . $add;
$db->query($sql);
}

// Delete plugin data table when plugin is deleted
function callback_rm() {
$sql = "DROP TABLE IF EXISTS `" . DB_PREFIX . "stats`";
$db = MySql::getInstance();
$db->query($sql);
}

Complete Example of Custom Data Table

The PHP code below is a complete callback example for maintaining a plugin's custom database table. It can be directly used in your own plugin's xxxx_callback.php by modifying the corresponding table creation statement.


<?php
/**
* Plugin Callback
*/
!defined('EMLOG_ROOT') && exit('error!');

/**
* Plugin Activation Callback
*/
function callback_init(){
Init_Database_Callback::instance()->pluginInit();
}

/**
* Plugin Update Callback
*/
function callback_up(){
Init_Database_Callback::instance()->pluginUp();
}

/**
* Plugin Deletion Callback
*/
function callback_rm(){
Init_Database_Callback::instance()->pluginRm();
}

/**
* Data Table Operation Class
*/
class Init_Database_Callback {
// Instance
private static $instance;
// Database Instance
private $db;
// Data Table Configuration
private $option = [
// Data Table Name
"tableName" => DB_PREFIX."toEverColor_list",
// Whether to delete the data table when uninstalling the plugin - true/false corresponds to Delete/Do Not Delete. Default is false (Do Not Delete)
"checkDeleteTable" => false,
// Data Table Field Information, Field => SQL Statement. Please do not make mistakes, the program creates and checks fields based on this.
"fieldData" => [
"id" => "`id` int(50) NOT NULL AUTO_INCREMENT",
"gid" => "`gid` int(50) NOT NULL COMMENT 'Article ID'",
"color" => "`color` varchar(200) DEFAULT NULL COMMENT 'Color'",
"weight" => "`weight` enum('n','y') DEFAULT 'n' COMMENT 'Whether to bold (Default not bold)'",
"font_size" => "`font_size` int(50) DEFAULT NULL COMMENT 'Font Size'",
"line_through" => "`line_through` enum('n','y') DEFAULT 'n' COMMENT 'Strikethrough'",
]
];

/**
* Private constructor to ensure singleton
*/
private function __construct(){
// Database instance assignment
$this->db = Database::getInstance();
}

/**
* Singleton Entry
*/
public static function instance(){
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}

/**
* Check if data table exists
*/
public function checkDataTable() {
if (isset($this->option['tableName'])) {
$query = $this->db->query("SHOW TABLES LIKE '{$this->option['tableName']}'");
if ($this->db->num_rows($query) > 0) {
return true;
}
return false;
}
return false;
}

/**
* Check if field exists in data table - Specify field name
*/
public function checkDataField($fieldName = '') {
if (!empty($fieldName) && $this->checkDataTable()) {
$query = $this->db->query("SHOW COLUMNS FROM {$this->option['tableName']} LIKE '{$fieldName}'");
if ($this->db->num_rows($query) > 0) {
return true;
}
return false;
}
return false;
}

/**
* Data Table Creation Function
*/
private function addDataTable() {
if (!empty($this->option) && is_array($this->option) && isset($this->option['fieldData']) && is_array($this->option['fieldData'])) {
$sql = "CREATE TABLE IF NOT EXISTS {$this->option['tableName']} (";
foreach ($this->option['fieldData'] as $field => $fieldSql) {
$sql .= $fieldSql . ',';
}
$sql .= " PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Title Color Change Table';";
$this->db->query($sql);
}
}

/**
* Check if data table field exists, create field if not
*/
private function addDataTableField() {
if (!empty($this->option) && is_array($this->option) && isset($this->option['fieldData']) && is_array($this->option['fieldData'])) {
$preForeachData = '';
foreach ($this->option['fieldData'] as $field => $fieldSql) {
if (!$this->checkDataField($field)) {
$after = !empty($preForeachData) ? " AFTER {$preForeachData}" : '';
$this->db->query("ALTER TABLE {$this->option['tableName']} ADD COLUMN {$fieldSql}{$after}");
}
$preForeachData = $field;
}
}
}

/**
* Plugin Enable Execution Function
*/
public function pluginInit() {
if ($this->checkDataTable()) {
$this->addDataTableField();
} else {
$this->addDataTable();
}
}

/**
* Plugin Update Execution Function
*/
public function pluginUp() {
$this->addDataTableField();
}

/**
* Plugin Uninstall Execution Function
*/
public function pluginRm() {
if (isset($this->option['checkDeleteTable']) && $this->option['checkDeleteTable'] === true) {
$this->db->query("DROP TABLE {$this->tableName}");
}
}
}

Reading Plugin Data Tables

<?php
// Read plugin data
function getDetail($id) {
$db = MySql::getInstance();
$row = $db->once_fetch_array("SELECT * FROM " . DB_PREFIX . "stats WHERE id = " . $id);

$row['xxxx']
……
}

Plugin Data Storage (3): Extending Core Table Fields

Not supported yet. Currently, you can use the above two methods as alternatives. It will be supported in the future.

🔴 Important Notice

danger

Plugins must not modify emlog core database tables and fields, including adding fields to core tables. Especially adding fields without default values.

Hook Types

1. Insertion Hook

  • Execution Principle: Sequentially execute functions hung on the hook, supports multiple parameters.
  • Applicable Scene: Insert specified content at the mounting point position, or execute certain actions.
// Hook Name: adm_main_top
doAction('adm_main_top');
// Plugin Development Example: Mount the tips function to the adm_main_top mounting point as shown above to implement inserting a sentence in the management backend.
addAction('adm_main_top', 'tips');
function tips() {
echo "<div>Hello World</div>";
}

Hooks with parameters will pass parameters to the functions mounted on them in order. As shown in the example below:

// Hook Name: save_log, the mounting point for saving articles, with multiple parameters, including article ID, etc.
doAction('save_log', $blogid, $pubPost, $logData)
// Plugin Development Example: Mount the function test_foo to the save_log mounting point as shown above, and receive the passed parameters $blogid, $pubPost, $logData
addAction('save_log', 'test_foo');
function test_foo($blogid, $pubPost, $logData) {
var_dump($blogid, $pubPost, $logData); // You can try to print the parameters to determine if the values are received correctly
}

Hook List (Insertion Hook)

HookLocation FileDescription
doAction('adm_main_top')admin/views/header.phpBackend homepage top area extension. The official tips plugin uses this mounting point.
doAction('adm_head')admin/views/header.phpBackend header extension: Can be used to add backend CSS styles, load JS, etc.
doAction('adm_menu')admin/views/header.phpBackend sidebar first-level menu extension, visible only to administrators.
doAction('login_head')admin/views/user_head.phpLogin/Registration page header extension, can be used to add login style CSS, etc.
doAction('user_menu')admin/views/uc_header.phpPersonal center top menu extension, visible only to registered users.
doAction('adm_footer')admin/views/footer.phpBackend footer extension: Can be used to add backend JS, etc.