Implement an autodiscover plugin system in PHP

Jan 20, 2026 by Thibault Debatty | 218 views

PHP

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.

php-plugin.png

To create the plugin system, we will need:

  1. an interface that all plugins must implement
  2. a discovery mechanism so the main application can list and call the plugins

Plugin Interface

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;
}

Plugin Discovery

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;
    }
}

Usage

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

This website uses cookies. More information about the use of cookies is available in the cookies policy.
Accept