Skip to content

Plugins

Nicholas K. Dionysopoulos edited this page Jan 6, 2024 · 1 revision

Panopticon has a plugin system since version 1.1.0. This allows you to run custom code which hooks into events, providing additional features.

Each plugin can also have its own language files, allowing it to be translated separately from the core application.

🤵🏽 Pro Tip: Most customisations you will want to do are possible using plugins, template overrides, and custom CSS files (as opposed to overriding entire classes or, worse, modifying core code).

Plugin structure

Core plugins are placed under src/Plugin and cannot be disabled; they are forcibly loaded, always. You MUST NOT put your plugins there. This is a reserved area for core-provided plugins. You can, however, create a same-named plugin in your user_code folder, shading (overriding) the core plugin completely.

User plugins are placed under the folder user_core/Plugin and belong to the PHP namespace Akeeba\Panopticon\Plugin. Every plugin is in its own directory. There MUST be a class named Plugin in the PSR-4 namespace corresponding to the plugin's folder which extends the \Akeeba\Panopticon\Library\Plugin\PanopticonPlugin class; this is what is being loaded by Panopticon.

For example, a plugin named Example would be in the folder user_core/Plugin/Example. It needs a file user_core/Plugin/Example/Plugin.php which holds the class Akeeba\Panopticon\Plugin\Example\Plugin extending \Akeeba\Panopticon\Library\Plugin\PanopticonPlugin.

Plugins are discovered and loaded automatically.

By default, Panopticon uses PHP's Reflection to identify public methods whose name begins with on, registering them as event handlers. This is slow. It is best to override the method getObservableEvents, returning an array with your event handlers yourself. For example:

	public function getObservableEvents(): array
	{
		return [
		    'onExample',
        ];
	}

    public function onExample()
    {
        // Handles the `onExample` event
    }

If your plugin needs to load its own language files remember to override the constructor, setting the loadLanguage property to true:

	public function __construct(Observable &$subject, Container $container)
	{
		$this->loadLanguage = true;

		parent::__construct($subject, $container);
	}

The base PanopticonPlugin class implements the \Awf\Container\ContainerAwareInterface interface. You have access to the application's container using $this->getContainer(). By default, this returns an \Akeeba\Panopticon\Container object.

Plugin configuration

Currently, there is no per-plugin configuration.

Best practices

Remember that the plugins load very early into the boot process of the application. Not just the web application, but also the CLI application. You need to be mindful of the implications so as not to break, or unnecessarily slow down your Panopticon installation.

Deferred initialisation

You MUST NOT put any initialisation code of your plugin in the constructor! Instead, you should initialise your plugin as-needed, i.e. the first time one its event handlers is called.

There's a very simple coding pattern for this. First, create an initialisation method like so:

private function initialiseAsNeeded()
{
    static $isInitialised = false;
    
    if ($isInitialised)
    {
        return;
    }
    
    $isInitialised = true;
    
    // Your initialisation code here
}

Then just remember to call $this->initialiseAsNeeded() as the very first line of your event handler methods (the public methods whose names start with on).

Which application am I running under?

Depending on which Panopticon application you are running under, a different constant is defined:

  • AKEEBA_WEB when you are under the interactive web application.
  • AKEEBA_CLI when you are under the CLI application.

If your plugin behaves differently across applications, or only supports one or the other application, you MUST perform feature detection using these constants.

For example, if your plugin only supports the web application you should override the getObservableEvents method like so:

	public function getObservableEvents(): array
	{
	    if (!defined('AKEEBA_WEB'))
	    {
	        return [];
	    }
	
		return [
		    'onExample',
        ];
	}

DO NOT ASSUME that the above list of constants is exhaustive. In the future, for example, we might add an API application with its own constant, e.g. AKEEBA_API. Therefore, the following code is WRONG and MUST NOT be used:

    // DO NOT USE! THIS IS WRONG!
	public function getObservableEvents(): array
	{
	    // THIS DOES NOT DO WHAT YOU THINK IT DOES!
	    if (defined('AKEEBA_WEB'))
	    {
	        return [];
	    }
	
	    // Stuff for the CLI. OOPS! WRONG! THIS DOES NOT MEAN WHAT YOU THINK IT MEANS!
		return [
		    'onExample',
        ];
	}

The above example assumes that if it's not the web application then it has to be the CLI application. This is WRONG. It only means that it's not the web application. It might be the CLI application or any other application added in a future version of Panopticon. So, at some point, this code may run in an application context you did not expect. Upsy-daisy!

Don't use PHP superglobals

When you need to check the user input, or the server environment, DO NOT use the PHP superglobals, such as $_GET, $_POST, or $_SERVER. Instead, you should always go through the application input object. For example, to get the PATH environment variable:

$path = $this->getContainer()->input->server->getRaw('PATH');

Remember that the input object offers methods which apply input filtering. This should always be your first line of defense when reading data coming from external sources.

Security matters

Never trust blindly any data which is not hard-coded. This applies to the user input, but also the server environment and the application configuration.

When using the database, always use the quote method to escape the data you are inserting or querying against. Again, it does not matter where this data comes from.

When dealing with files, use the constants with the APATH_ prefix as your ground truth. You should not allow files to be read from or written to folders above APATH_ROOT, as they're not guaranteed to belong to the Panopticon installation.

If you are writing files with sensitive information always place them in a directory which has adequate access control (e.g. a .htaccess file preventing direct access), or use .php files whose first line is <?php die() ?>.

Be vigilant. When it comes to information security you're never paranoid; everyone is out to get you.

Loading plugins faster, and using other namespaces

By default, Panopticon uses PHP's DirectoryIterator against the core and user plugin directories to list their subdirectories, and use this information to create a list of potential plugin classes to load. This is slow.

You can instead create a file called plugins.php in your plugins directory (i.e. user_code/Plugin/plugins.php) which returns an array of the plugin classes to load. For example:

<?php
defined('AKEEBA') || die;

return [
	\Akeeba\Panopticon\Plugin\Uptime\Example::class
];

This is what we use for the core plugins, allowing them to load fast.

🤵🏽 Pro Tip: You can use the plugins.php file to tell Panopticon to load plugins from a namespace other than \Akeeba\Panopticon\Plugin. However, you will find that loading languages does not work. Fixing it is simple! Override the protected method getPluginPath to return the path where your plugin's language files are located in.

Clone this wiki locally