Welcome to a tutorial and example on how to create a PHP plugin module system. So you have a project that has grown massively large and needs to get things done in an organized manner.
The general steps and considerations for developing a plugin system are:
-
- First, develop a core system that has a set of “base functions” and manages the loading of plugin modules.
- Then, build the modules on top of the base system. E.G. Email, users, products, newsletters, orders, etc…
- The modules should communicate with each other to speed up development. For example, get a list of emails with the user module, and send newsletters out using the email module.
But just how does this entire “plugin system” work? Let us walk through an example module system in this guide – Read on!
TABLE OF CONTENTS
DOWNLOAD & NOTES
Here is the download link to the example code, so you don’t have to copy-paste everything.
EXAMPLE CODE DOWNLOAD
Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
SORRY FOR THE ADS...
But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.
Buy Me A Coffee Code Boxx eBooks
SYSTEM CORE DEVELOPMENT
All right, let us now get started with the core of the PHP plugin system.
PART 1) CREATE LIB FOLDER & ACCESS RESTRICTION
Deny from all
First, let us create a lib
folder to contain all our library/module files, and add a .htaccess
protect it. For you guys who are new to the Apache web server – This will prevent users from directly accessing and messing with the system files. That is, directly accessing http://site.com/lib
will throw a 403 unauthorized error.
P.S. We can still load the files in the lib
folder via PHP without any issues.
P.P.S. If you are using IIS or NGINX, it is also possible to put this access restriction in place.
PART 2) CORE STARTER
<?php
// (A) ERROR HANDLING
error_reporting(E_ALL & ~E_NOTICE);
ini_set("display_errors", 1);
//ini_set("log_errors", 0);
//ini_set("error_log", "PATH/error.log");
// (B) DATABASE SETTINGS - CHANGE TO YOUR OWN
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8mb4");
define("DB_USER", "root");
define("DB_PASSWORD", "");
// (C) AUTO FILE PATHS
define("PATH_LIB", __DIR__ . DIRECTORY_SEPARATOR);
define("PATH_BASE", dirname(PATH_LIB) . DIRECTORY_SEPARATOR);
// (D) START SESSION
session_start();
// (E) INIT SYSTEM CORE
require PATH_LIB . "LIB-Core.php";
$_CORE = new Core();
Next, we have a “core starter script” to contain all the “settings and initialize stuff”:
- Set how PHP handles errors.
- Define the database settings.
- Automatically detect the system file paths.
- Start the session.
- Load our core library and create a
$_CORE = new Core()
object.
Simply require lib/CORE-Go.php
at the top of your scripts to kickstart the engine.
PART 3) CORE LIBRARY
3A) DATABASE FUNCTIONS
<?php
class Core {
// (A) PROPERTIES
public $error = ""; // last error message
public $pdo = null; // database connection
public $stmt = null; // sql statement
public $loaded = []; // loaded modules
// (B) CONNECT TO DATABASE
function __construct () {
$this->pdo = new PDO(
"mysql:host=". DB_HOST .";charset=". DB_CHARSET .";dbname=". DB_NAME,
DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
// (C) CLOSE CONNECTION WHEN DONE
function __destruct () {
if ($this->stmt !== null) { $this->stmt = null; }
if ($this->pdo !== null) { $this->pdo = null; }
}
// (D) RUN SQL QUERY
// $sql : sql query
// $data : array of parameters
function exec ($sql, $data=null) : void {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
}
// (F) FETCH SINGLE ROW
// $sql : sql query
// $data : array of parameters
function fetch ($sql, $data=null) {
$this->exec($sql, $data);
return $this->stmt->fetch();
}
// (F) FETCH MULTIPLE ROWS
// $sql : sql query
// $data : array of parameters
// $arrange : (string) arrange by [$arrange] => results
// (array) arrange by [$arrange[0] => $arrange[1]]
// (none) default - whatever is set in pdo
function fetchAll ($sql, $data=null, $arrange=null) {
// (F1) RUN SQL QUERY
$this->exec($sql, $data);
// (F2) FETCH ALL AS-IT-IS
if ($arrange===null) { return $this->stmt->fetchAll(); }
// (F3) ARRANGE BY $DATA[$ARRANGE] => RESULTS
else if (is_string($arrange)) {
$data = [];
while ($r = $this->stmt->fetch()) { $data[$r[$arrange]] = $row; }
return $data;
}
// (F4) ARRANGE BY $DATA[$ARRANGE[0]] => $ARRANGE[1]
else {
$data = [];
while ($r = $this->stmt->fetch()) { $data[$r[$arrange[0]]] = $r[$arrange[1]]; }
return $data;
}
}
}
This can be a little intimidating to beginners, but keep calm and look closely – This is nothing but a database core library.
- (B & C) When
$_CORE = new Core()
is created, the constructor will automatically connect to the database. The destructor closes the connection. - (D)
exec()
A helper function to run an SQL query. - (E)
fetch()
Get a single row from the database. - (F)
fetchAll()
Get multiple rows from the database (and arrange the data).
3B) MODULE LOADER
// (G) LOAD SPECIFIED MODULE
// $module : module to load
function load ($module) : void {
// (G1) CHECK IF MODULE IS ALREADY LOADED
if (isset($this->loaded[$module])) { return; }
// (G2) EXTEND MODULE
$file = PATH_LIB . "LIB-$module.php";
if (file_exists($file)) {
require $file;
$this->loaded[$module] = new $module($this);
} else { throw new Exception("$module module not found!"); }
}
// (H) "MAGIC LINK" TO MODULE
function __get ($name) {
if (isset($this->loaded[$name])) { return $this->loaded[$name]; }
}
Some people build complicated plugin systems with all sorts of namespace and object-oriented mambo stuff… But no, I prefer to keep things simple. A quick code trace to explain how this “module system” work, when we call $_CORE->load("Module")
:
- It will automatically
require "lib/LIB-Module.php"
. - Create an object
$_CORE->loaded["Module"] = new Module()
. - The magic
__get()
will literally “link” the modules back to the core object. That is,$_CORE->Module
will refer to$_CORE->loaded["Module"]
.
3C) MODULE LINKING
// (I) ALL MODULES SHOULD EXTEND THIS CORE CLASS
class Ext {
// (I1) LINK MODULE TO CORE
public $Core;
public $error;
function __construct ($core) {
$this->Core =& $core;
$this->error =& $core->error;
}
// (I2) MAKE MODULES LINKING EASIER
function __get ($name) {
if (isset($this->Core->loaded[$name])) { return $this->Core->loaded[$name]; }
}
}
Finally, all “future modules” only need to extend this class. For those who don’t catch it, these few lines of code simplified the development of plugin modules to:
- Create the new library file –
lib/LIB-Module.php
. - Define
class Module extends Ext
, add the functions. - Call
$_CORE->load("Module")
and use it$_CORE->Module->functions()
.
Since all modules are linked back to the core, we can use the core functions and even access other modules to speed up development.
BUILDING MODULES
Now that we have a working core system, let us walk through developing a dummy user module on top of it.
PART 4) USER TABLE
CREATE TABLE `users` (
`user_id` bigint(20) NOT NULL,
`user_name` varchar(255) NOT NULL,
`user_email` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `users`
ADD PRIMARY KEY (`user_id`),
ADD UNIQUE KEY `user_email` (`user_email`),
ADD KEY `user_name` (`user_name`);
ALTER TABLE `users`
MODIFY `user_id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
Field | Description |
user_id |
The user ID. Primary key, auto-increment. |
user_name |
The user’s name. |
user_email |
The user’s email. |
Yep, just a simple user table that we will use as an example for this section.
PART 5) USER MODULE LIBRARY
<?php
class User extends Ext {
// (A) GET ALL USERS
function getAll () {
return $this->Core->fetchAll(
"SELECT * FROM `users`", null, "user_id"
);
}
// (B) GET USER BY ID OR EMAIL
// $id : user id or email
function get ($id) {
return $this->Core->fetch(
"SELECT * FROM `users` WHERE `user_". (is_numeric($id)?"id":"email") ."`=?",
[$id]
);
}
// (C) ADD A NEW USER
function add ($name, $email) {
// (C1) CHECK IF ALREADY REGISTERED
if (is_array($this->get($email))) {
$this->error = "$email is already registered!";
return false;
}
// (C2) PROCEED ADD
return $this->Core->exec(
"INSERT INTO `users` (`user_name`, `user_email`) VALUES (?, ?)",
[$name, $email]
);
}
// (D) UPDATE AN EXISTING USER
function edit ($name, $email, $id) {
return $this->Core->exec(
"UPDATE `users` SET `user_name`=?, `user_email`=? WHERE `user_id`=?",
[$name, $email, $id]
);
}
// (E) DELETE USER
function del ($id) {
return $this->Core->exec(
"DELETE FROM `users` WHERE `user_id`=?",
[$id]
);
}
}
Once again, keep calm and look carefully. This is quite literally a collection of SQL queries.
- Remember from earlier that
$_CORE->load("Module")
will loadlib/LIB-Module.php
and create$_CORE->loaded["Module"] = new Module()
? - So calling
$_CORE->load("User")
will loadlib/LIB-User.php
and create$_CORE->loaded["User"] = new User()
. - There is also a pointer back to the core object –
$_CORE->loaded["User"]->Core =& $_CORE
. - So in this module, we are pretty much just using the core database functions
$this->Core->exec()
,$this->Core->fetch()
,$this->Core->fetchAll()
to do all the user database work.
PART 6) DONE – USE IT!
<?php
// (A) LOAD CORE LIBRARY
require "lib/CORE-Go.php";
// (B) LOAD USER MODULE
$_CORE->load("User");
// (C) ADD USER
echo $_CORE->User->add("Jon Doe", "jon@doe.com") ? "OK" : $_CORE->error;
// (D) GET USER
$user = $_CORE->User->get("jon@doe.com");
print_r($user);
With that, the module is ready for use. Don’t think this needs any explanation.
EXTRA) LINKED MODULES
<?php
class Newsletter extends Ext {
function send () {
// (A) GET USERS/SUBSCRIBERS
$this->Core->load("User");
$users = $this->User->getSubscribers();
// (B) SEND NEWSLETTER
$this->Core->load("Email");
foreach ($users as $u) {
$this->Email->send(...);
}
}
}
For you guys who still don’t see how this is a modular plugin system – Consider how easy we can build more modules, and use them to help each other.
EXTRAS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
MANY WAYS TO CREATE A PLUGIN SYSTEM
Before the trolls start to spew rubbish – There are no rules on “a plugin system must be made this way”, and there are endless ways to create a plugin system. This is ultimately a sharing of the mechanics behind my own modular system called “Core Boxx“.
So yep, if you cannot accept “evil pointers” and want to call this “unorthodox OOP” – Go ahead. If there are parts that you don’t like, discard them. Take the parts that work for you and create your own system.
LINKS & REFERENCES
THE END
Thank you for reading, and we have come to the end of this guide. I hope that it has helped you with your project, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!
Ahh, totally my style, I am an old programmer but I still use the same old approach for all projects, and write code that can be re-used. Thank you for the source.
PS: I watched your video on YouTube, your voice is so relaxing and the way you explain is really great.
Glad it helped. The YouTube videos are narrated by AI text to speech though. 🙂
From one Maverick to another, I’ve been here a few times already and I have to tell you – I really like the way you do things.
You choose your approach about how to get things done in a good way and you’re not concerned about trends or what other people are considering as “the best way”, which I can really relate.
I will keep visiting every now and then. Keep up the good work! 👌🏼👏🏽