DEV Community

Sebastian Schürmanns
Sebastian Schürmanns

Posted on

How I Created my First Plugin-System

Hi all,

I am pretty sure that most of you have already written a plugin for a CMS or any other software. But did you ever think about creating your own plugin system for your own software project, so that others can plug into your software and enhance it without touching the core code? It is a bit more tricky than crafting a simple plugin. But it is way easier than you might think. And for me, it changed the way I code new projects forever.

How I Coded Projects Before

I am NOT a professional developer, but a product owner and historian (originally). I code projects just for fun, for distraction and for therapeutic reasons. I started with PHP at the age of 35 and I learned it with the book "PHP for Kids". Growing up mentally, I learned how to write a tumblr-like application with dirty old spaghetti code. It was already organized in a MVC way, but I still ran into the usual mess of chaotic and unmaintainable code. To solve these problems, I switched to object orientation and namespacing, I started to use the PHP-microframework Slim and I familiarized myself with GitHub and Composer. Everything was organized in models, controllers, classes, methods, configuration files and external libraries, so there was a gleam of order in all that chaos. But this still does not help in certain situations ...

Asking the Right Questions

This situation popped up with my little side-project called Typemill, that should become a simple flat-file-cms for writers once it is grown up. For a real CMS I had to find a way to let other coders enhance my system with plugins, addons, extensions or whatever you want to call it. So I asked some search engines ...

... and I was pretty astonished, that all these search engines don't know much about "plugin systems" at all. Poor answers are usually a result of wrong questions. So I digged deeper and found out, that plugin systems follow special programming patterns (and to be honest, I didn't care much about programming patterns before). There are several patterns for plugin systems (read Anthony Ferraras article about patterns for plugins), but the most common pattern is the "mediator pattern". And the mediator pattern is also used for something called "event driven programming" and "event dispatching".

I am familiar with "events" in JavaScript, but I didn't know, that you can use "events" in server side code, too. In fact, not only content management systems use events for their plugin systems, but most of the frameworks like Zend, Laravel and others provide a build-in event system out of the box.

How I Tried to Understand Event Systems

I tried to wrap my head around "event systems" by comparing them with IFTTT: If this happens, than do that. In fact, you can describe all plugins with a simple and human readable IFTTT-logic like this:

If the application has generated a html-page, then add a newsletter subscription form to the page (oh nooo, don't do that, we all hate them!!!!)

You can even go a step further and describe the logic with a simple array like this:

['onHtmlLoaded' => 'addSubscriptionFormMethod']

Enter fullscreen mode Exit fullscreen mode

The key of the array (left side) is the name of the event and the value of the array (right side) is the name of your method. So if the event on the left side happens, then the methods of the right side should be called. If this than that. Pretty simple.

To get this simple logic to work, you need to create a special script often called "Event Dispatcher". But before you start coding, think about using one of the excellent libraries out there.

Working With The EventDispatcher

An event dispatcher is not too complicated and there are some very lightweight implementations with about 100 lines of code. What they do is always the same: They create and manage an array of event-names and event-listeners like shown above. The event-listeners are callables (method-names). So if you know the event-name, than it is no problem to call the subscribed methods with a handy php-function like call_user_func() or call_user_func_array(). And that is exactly what all these event dispatchers do.

The most widespread EventDispatcher-library is the Symfony Event Dispatcher. It provides something like this:

  • It manages the described array of event-names and callables.
  • It allows you to add new "listeners" or "subscribers" to this array. The "listeners" or "subsribers" are your plugins.
  • It allows you to "fire" (or "dispatch") events in your application, so that all the subscribers (plugin-method) for this event get called.

You can check the dispatcher-class of Symfony (just 240 lines) and you will be surprised how simple everything is.

Initiate the Dispatcher

We will create our little newsletter-plugin now to demonstrate the dispatcher with a real life example. Let's install the Symfony EventDispatcher with GitHub and Composer and then initiate it somewhere at the beginning of our code (for example in the beginning of an index.php file):

use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();

Enter fullscreen mode Exit fullscreen mode

Now we can work with the EventDispatcher. We will do it in the following steps:

  • We will define a new event called "onHtmlLoaded".
  • We will fire the event in your application.
  • We will write a newsletter-plugin that subscribes to this event.
  • We will get, manipulate and return data with the plugin.

Define An Event

Before you can use your own events in the Symfony EventDispatcher you have to define them. A new event can be defined with an empty class like this:

namespace Typemill\Events;

use Symfony\Component\EventDispatcher\Event;

class onHtmlLoaded extends Event
{

}

Enter fullscreen mode Exit fullscreen mode

All new events have to extend the base "Event" of Symfony. Later, we will define some helpful methods in this class, but for now an empty class is enough.

To keep your code organized, it is a good idea to create a new folder called "events" to collect all event definitions in a central place.

Fire The Event

Let's go back to our application and fire the new event. This can be done with a single line of code.

For our newsletter plugin, we want to fire the event right after the HTML-content is loaded. In Typemill, I use the parsedown-library to transform the markdown-text into html. So I add this simple line of code:

$contentHTML = $parsedown->text($contentMarkdown);

$dispatcher->dispatch('onHtmlLoaded', new Typemill\Events\onHtmlLoaded());

Enter fullscreen mode Exit fullscreen mode

The dispatch-method accepts two arguments: The name of the event and the event object. That is all. Now the event dispatcher uses the internal array and calls all plugin-methods, that have subscribed to this event.

But wait, there are no plugins yet. So let us create our newsletter-plugin now.

The Newsletter-Plugin: Subscribe to Events

I created a new "plugin"-folder for all plugins and in there I created a folder called "newsletter". The class for our newsletter-plugin looks like this:

namespace Plugins\Newsletter;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Typemill\Events\onHtmlReady;

class Newsletter
{
    public static function getSubscribedEvents()
    {
        return array('onHtmlLoaded' => 'myNewsletterMethod');
    }

    public function myNewsletterMethod()
    {
        die('I will add the newsletter to the html-page one day');
    }
}

Enter fullscreen mode Exit fullscreen mode

And finally there is the array, that we have mentioned at the beginning of this article. And this array looks exactly the same: The name of the event is on the left side and the name of the plugin-method is on the right side. If this than that.

The method, that returns the array, is always getSubscribedEvents() and it has to be a public static method. The Symfony EventDispatcher will simply check all classes for the existence of this method and add the array to its central array of events and listeners or subscribers. It is as simple as that.

Load Your Plugins

The last step is still missing: We have to initialize our plugin-class and add it tho the dispatcher, otherwise the dispatcher does not know about its existence. We should load all plugins right at the beginning of our application and add a plugin to the dispatcher like this:

$dispatcher = new EventDispatcher();

....
....

$dispatcher->addSubscriber(new \Plugins\Newsletter()); 

Enter fullscreen mode Exit fullscreen mode

Of course you don't want to add all plugins manually. For Typemill, I wrote a separate class, that scans all the plugin-folders, performs some checks and returns the fully qualified classnames, so that you can do something like this:

$dispatcher = new EventDispatcher();

...

$plugins = new Typemill\Plugins();
$pluginClassnames = $plugins->load();

foreach($pluginClassnames as $pluginClassname)
{
  $dispatcher->addSubscriber(new $pluginClassname());
} 

Enter fullscreen mode Exit fullscreen mode

Ok, I left out a lot of details, but this should be enough to get started.

Let us check what we have so far: If we visit a page in Typemill now, then the event "onHtmlReady" gets fired and the subscribed newsletter plugin get's called. Right now, the newsletter method simply kills the application and prints out the words "I will add the newsletter to the html-page one day".

It works. But it does not really feel like a cool plugin. So lets make it a bit more useful.

Manipulating Data With The Plugin

To create our newsletter-plugin, we want to get the html-content, add the newsletter subscription form and return everything to the application again.

To do this, we can simply add some getters and setters to the event class, that we have defined before. Let's do it this way:

namespace Typemill\Events;

use Symfony\Component\EventDispatcher\Event;

class onHtmlLoaded extends Event
{

    protected $data;

    public function __construct($data)
    {
        $this->data = $data;
    }

    public function getData()
    {
        return $this->data;
    }

    public function setData($data)
    {
        $this->data = $data;
    }
}

Enter fullscreen mode Exit fullscreen mode

When we fire an event in our application, we can simply pass some data like this now:

$contentHTML = $parsedown->text($contentMarkdown);

$dispatcher->dispatch('onHtmlLoaded', new Typemill\Events\onHtmlLoaded($contentHTML));

Enter fullscreen mode Exit fullscreen mode

And finally we can access and return the data in our newsletter plugin like this:

class Newsletter
{

    public static function getSubscribedEvents()

    {
        return array('onHtmlLoaded' => 'myNewsletterMethod');

    }

    public function myNewsletterMethod($data)
    {
        $content = $data->getData(); 

        $content .= '<p>I am the newsletter subscription form</p>';

        $data->setData($content);
    }

}
Enter fullscreen mode Exit fullscreen mode

We have added our newsletter form to the content and returned everything to the application. The final trick is, that our application uses the manipulated data from now on. And this can be done as simple as that:

$contentHTML = $parsedown->text($contentMarkdown);

$contentHTML = $dispatcher->dispatch('onHtmlLoaded', new Typemill\Events\onhtmlLoaded($contentHTML))->getData();

Enter fullscreen mode Exit fullscreen mode

Voila, the application will now send everything to the Twig template engine and display the page to the user. And we have added a newsletter-form to this page without touching any core file of the application. Our first plugin is done!

Events and Your Application's Life Cycle

In real life you want to provide a lot of entry points to your application, so that client coders can manipulate different data in a lot of steps. So your event-system will usually follow the life cycle of your application and fire events in each step of the life cicle. For Typemill, it looks similar to this:

  • The application loads all settings.
  • The application loads all plugins.
  • The application loads a template engine like Twig.
  • The application routes to a controller, that creates the page.
  • The controller loads the requested content file.
  • The controller transforms the markdown content into HTML.
  • The controller collects some other data (e.g. for a navigation).
  • The controller sends all the data to the template engine.
  • The template engine displays the page to the user.

The smaller the steps, the greater the flexibility.

You Just Created Your Own Plugin System

The code above is pretty rough and not much more than you can already read in the documentation of the symfony event dispatcher itself. And if you want to create a really useful plugin system, you have to add a lot of details: How should all these plugins get loaded? How can users configure your plugins? Which useful methods should you add to a basic plugin class? How can you add new routes to your application with a plugin?

A lot of open questions, but the basics are done. And for me it was really helpful to find all these ressources and examples. If you want to create your own plugin systems, you can check some real life implementations out there (I used Grav, Leafpub and many others to find my own way to integrate a plugin system into Typemill).

But even if you don't want to create your own plugin system, then this kind of event driven programming is still a great way to decouple your code and to bring a bit more order into the chaos of a new code-project.

Top comments (5)

Collapse
 
nexopos profile image
NexoPOS Solutions

Nice... I did something similar for one some of my projects... I'm currently looking for such solution on Angular... The solutions founds aren't that easy. I believe extensibility makes an app fit most customers requirements, without overloading the core.

Collapse
 
bn_geek profile image
Mohcin Bounouara

You do a great work.. really

Collapse
 
trendschau profile image
Sebastian Schürmanns

Thank you :)

Collapse
 
roseriyadh profile image
Rose Riyadh

amazing!! i will try it out for sure, thank you for making it as simple as possible

Collapse
 
pierre profile image
Pierre-Henry Soria ✨

Great article! Thanks for sharing Sebastian! 💫