How to Build a PHP Plugin Module System (Step-By-Step Example)

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. For example, email, users, products, newsletters, orders, etc…
  • The modules should be able to communicate with each other to speed up development. For example, the newsletter module gets information from the user module, sends the emails 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!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

Download & Notes Core Development Module Development
Useful Bits & Links The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

EXAMPLE CODE DOWNLOAD

Click here to download the source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

 

SYSTEM CORE DEVELOPMENT

All right, let us now get started with the core of the PHP plugin system.

 

STEP 1) CREATE LIB FOLDER & ACCESS RESTRICTION

lib/.htaccess
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 webserver – This will prevent users from directly accessing and messing with the system files. E.g. Directly accessing http://site.com/lib/LIB-Core.php 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.

 

STEP 2) CORE STARTER

lib/core.php
<?php
// (A) ERROR HANDLING
error_reporting(E_ALL & ~E_NOTICE);
ini_set("display_errors", 1); // FOR DEVELOPMENT
//ini_set("log_errors", 0);
//ini_set("error_log", "PATH/error.log");

// (B) DATABASE SETTINGS - CHANGE THESE TO YOUR OWN
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8");
define("DB_USER", "root");
define("DB_PASSWORD", "");

// (C) AUTO FILE PATHS
define("PATH_LIB", __DIR__ . DIRECTORY_SEPARATOR);
define("PATH_ROOT", dirname(PATH_LIB));

// (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 user session.
  • Load our core library and create a $_CORE = new Core() object.

Simply include this at the top of your project scripts to kickstart the engine.

 

 

STEP 3) CORE LIBRARY & LOAD FUNCTION

lib/LIB-Core.php
<?php
class Core {
  // (A) PROPERTIES
  public $error = ""; // LAST ERROR MESSAGE
  public $pdo = null; // DATABASE CONNECTION
  public $stmt = null; // SQL STATEMENT
  public $lastID = null; // LAST INSERT/UPDATE ID

  // (B) LOAD SPECIFIED MODULE
  //  $module : module to load
  function load ($module) {
    // (B1) CHECK IF MODULE IS ALREADY LOADED
    if (isset($this->$module)) { return true; }

    // (B2) EXTEND MODULE ON CORE OBJECT
    $file = PATH_LIB . "LIB-$module.php";
    if (file_exists($file)) {
      require $file;
      $this->$module = new $module();
      // EVIL POINTER - ALLOW OBJECTS TO ACCESS EACH OTHER
      $this->$module->core =& $this;
      $this->$module->error =& $this->error;
      $this->$module->pdo =& $this->pdo;
      $this->$module->stmt =& $this->stmt;
      return true;
    } else {
      $this->error = "$file not found!";
      return false;
    }
  }
}

Some people may be thinking that plugin systems are extremely complicated with all sorts of namespace, object-oriented, and JSON mambo stuff… But no, I prefer to keep things simple. class Core and function load() is literally the heart of the entire system. A quick code trace to explain how this “module system” work – If we call load("Module"):

  • It will automatically require "lib/LIB-Module.php".
  • Create an object $this->Module = new Module().
  • “Link” the module back to the core object $this->Module->core =& $this.

For those who don’t catch what this means, these few lines of code simplified the development of plugin modules to:

  • Create the new library file – lib/LIB-Module.php.
  • Define class Module, add the functions.
  • Done. Call $_CORE->load("Module") and use it $_CORE->Module->functions().
  • Since the module is linked back to the core, it can use the core functions and even access other modules to speed up development. More on that later.

 

 

STEP 4) CORE DATABASE FUNCTIONS

lib/LIB-Core.php
// (C) CONNECT TO DATABASE
function __construct () {
  try {
    $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
    ]);
  } catch (Exception $ex) { exit ($ex->getMessage()); }
}

// (D) CLOSE CONNECTION WHEN DONE
function __destruct () {
  if ($this->stmt !== null) { $this->stmt = null; }
  if ($this->pdo !== null) { $this->pdo = null; }
}

// (E) RUN SQL QUERY
//  $sql : SQL query
//  $data : array of parameters
function exec ($sql, $data=null) {
  try {
    $this->stmt = $this->pdo->prepare($sql);
    $this->stmt->execute($data);
    $this->lastID = $this->pdo->lastInsertId();
    return true;
  } catch (Exception $ex) {
    $this->error = $ex->getMessage();
    return false;
  }
}

// (F) FETCH SINGLE ROW
//  $sql : SQL query
//  $data : array of parameters
function fetch ($sql, $data=null) {
  if (!$this->exec($sql, $data)) { return false; }
  return $this->stmt->fetch();
}

// (G) 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) {
  // (G1) RUN SQL QUERY
  if (!$this->exec($sql, $data)) { return false; }

  // (G2) FETCH ALL AS-IT-IS
  if ($arrange===null) { return $this->stmt->fetchAll(); }

  // (G3) ARRANGE BY $DATA[$ARRANGE] => RESULTS
  else if (is_string($arrange)) {
    $data = [];
    while ($row = $this->stmt->fetch()) { $data[$row[$arrange]] = $row; }
    return $data;
  }

  // (G4) ARRANGE BY $DATA[$ARRANGE[0]] => $ARRANGE[1]
  else {
    $data = [];
    while ($row = $this->stmt->fetch()) { $data[$row[$arrange[0]]] = $row[$arrange[1]]; }
    return $data;
  }
}

Of course, the core will be worthless if it is just an empty shell. So here are some common database functions that can be “share used” by all future modules:

  • __construct() automatically connects to the database when a new Core() object is created.
  • __destruct() does the opposite to close the connection when the object is destroyed.
  • exec() runs an SQL query.
  • fetch() gets a single record from the database.
  • fetchAll() gets multiple rows of records from the database.

 

 

BUILDING MODULES

Now that we have a working core system, let us walk through developing a dummy user module on top of it.

 

STEP 5) USER TABLE

lib/users.sql
CREATE TABLE `users` (
  `user_id` int(11) NOT NULL,
  `user_name` varchar(255) NOT NULL,
  `user_email` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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` int(11) 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.

 

STEP 6) USER MODULE LIBRARY

lib/lib-Users.php
<?php
class User {
  // (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
    $check = $this->get($email);
    if (is_array($check)) {
      $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]
    );
  }
}

This may look complicated to some of the beginners, but keep calm and look carefully.

  • Remember from earlier that $_CORE->load("Module") will load lib/LIB-Module.php and create $this->Module = new Module()?
  • So calling $_CORE->load("User") will load lib/LIB-User.php and create $this->User = new User().
  • Remember that there is also a pointer back to the core object – $this->User->core =& $this?
  • So in this module, we are pretty much just using the core database functions exec(), fetch(), fetchAll() to do all the user database work.

 

 

STEP 7) DONE – USE IT!

demo.php
<?php
// (A) LOAD CORE LIBRARY
require "lib/core.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… This should be simple and Captain Obvious enough.

 

EXTRA) LINKED MODULES

lib/LIB-Newsletter.php
<?php
class Newsletter {
  function send () {
    // GET USERS/SUBSCRIBERS
    $this->core->load("User");
    $users = $this->core->User->getSubscribers();

    // SEND NEWSLETTER
    $this->core->load("Email");
    foreach ($users as $u) {
      $this->core->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.

 

USEFUL BITS & LINKS

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 – This is ultimately a sharing of how I create a plugin module system. There are no rules on “a plugin system must be done this way”. So yep, if there are parts that you don’t like, go ahead and discard them. Take the parts that work for you, 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!

Leave a Comment

Your email address will not be published. Required fields are marked *