Jan 20, 2026 by Thibault Debatty | 218 views
https://cylab.be/blog/474/implement-an-autodiscover-plugin-system-in-php
When working on a large project, it can be useful to split the code into multiple plugins or extensions. It makes it easy to add functionalities by simply adding a file to the project. Here is a simple example.
To create the plugin system, we will need:
So, let’s first create the interface that plugins must implement. Here is an example that you should modify to fit your application:
<?php
namespace App;
interface PluginInterface
{
public function init() : void;
}
We can now implement the discovery mechanism. For this one I’ll use the finder component from symfony:
composer require symfony/finder
And the code:
<?php
namespace App;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
class PluginSystem
{
private $plugins;
public function __construct()
{
$this->plugins = $this->discover();
}
public function discover() : array
{
$root = __DIR__;
$interface = PluginInterface::class;
$finder = new Finder();
$files = $finder->files()->in($root)->name('*.php');
$plugins = [];
foreach ($files as $file) {
/** @var SplFileInfo $file */
$class = $this->getClassFullNameFromFile($file->getPathname());
// this file/class does not implement to correct interface
if (! is_a($class, $interface, true)) {
continue;
}
// this is an abstract class
$reflection = new \ReflectionClass($class);
if ($reflection->isAbstract()) {
continue;
}
// add to the list of plugins
$plugins[] = $class;
}
return $plugins;
}
/**
* Get class name from file path.
*
* https://gist.github.com/cwhite92/f0aaf008e1679b27768fbb8c884df6f7
*
* @param string $path
* @return string
*/
private function getClassFullNameFromFile(string $path)
{
$namespace = $class = $buffer = '';
$handle = fopen($path, 'r');
while (!feof($handle)) {
$buffer .= fread($handle, 512);
// Suppress warnings for cases where `$buffer` ends in the middle of a PHP comment.
$tokens = @token_get_all($buffer);
// Filter out whitespace and comments from the tokens, as they are irrelevant.
$tokens = array_filter($tokens, fn($token) => $token[0] !== T_WHITESPACE && $token[0] !== T_COMMENT);
// Reset array indexes after filtering.
$tokens = array_values($tokens);
foreach ($tokens as $index => $token) {
// The namespace is a `T_NAME_QUALIFIED` that is immediately preceded by a `T_NAMESPACE`.
if ($token[0] === T_NAMESPACE && isset($tokens[$index + 1]) && $tokens[$index + 1][0] === T_NAME_QUALIFIED) {
$namespace = $tokens[$index + 1][1];
continue;
}
// The class name is a `T_STRING` which makes it unreliable to match against, so check if we have a
// `T_CLASS` token with a `T_STRING` token ahead of it.
if ($token[0] === T_CLASS && isset($tokens[$index + 1]) && $tokens[$index + 1][0] === T_STRING) {
$class = $tokens[$index + 1][1];
}
}
if ($namespace && $class) {
// We've found both the namespace and the class, we can now stop reading and parsing the file.
break;
}
}
fclose($handle);
return $namespace . '\\' . $class;
}
public function plugins() : array
{
return $this->plugins;
}
}
We can now use the plugins with something like:
$p = new PluginSystem();
foreach ($p->plugins() as $plugin)
{
$instance = new $plugin;
// plugins implement the interface, so we can call:
$instance->init();
}
This blog post is licensed under
CC BY-SA 4.0