Build a plugin system for your Symfony app

What is a plugin system and why would you need one?

As a web developer, you're no doubt already used to designing and writing modular code.

As a Symfony developer, you almost certainly follow a set of conventions for organizing your code - a directory structure that follows PSR-4 autoloading conventions for arranging classes into namespaces. Controllers, services, entities, and the rest.

When you're building an in-house application, you have complete control over the codebase. You can refactor, reorganize, and extend it as you see fit.

But what if you want to allow other developers to extend your application with their own code? Or what if you want to deploy the same application to multiple clients, but with different functionality enabled for each?

This is where a plugin system comes in. A plugin system allows you to define a set of extension points in your application, and to allow developers to write code that hooks into those extension points.

Plugins can offer a way to enable specific functionality for users while keeping the main codebase streamlined and more maintainable. Common use cases for plugins in Symfony apps include integrating with third-party services, adding new features or content types, supporting multiple service gateways, or creating custom themes and templates.

A plugin system is especially valuable when building a scalable application, where user needs or business requirements might change. Compared to embedding all features directly in an application, plugins enable you to toggle features on or off, implement new functionality faster, and manage dependencies independently.

This approach improves flexibility, supports a cleaner architecture and promotes better long-term code health - perfect for apps that need to evolve with a user base or market trends.

Plugins versus bundles

If you're familiar with Symfony, you might be wondering how plugins differ from bundles.

Bundles are a way to organize and distribute Symfony code that is reusable across different projects and code bases. They are a collection of files and directories that provide a specific set of functionality, such as a bundle for user authentication, a bundle for sending emails, or a bundle for integrating with a third-party API.

So bundles are a core part of the Symfony ecosystem, but they extend at the framework level rather than the application level.

Plugins, on the other hand, are a way to extend an application with custom functionality. While bundles are typically designed to be reusable across multiple projects, plugins are specific to a single application.

So if you're building an application that needs to be extended by third-party developers, such as a Content Management System (CMS) or an e-commerce platform, you might want to use a plugin system to allow developers to easily write custom code that hooks into your application - for example, to add new content types, to send an email when a page is published, or to integrate with a payment gateway.

What should a plugin system do?

There are a number of things we probably want when we design a plugin system for a Symfony application:

  • We want to be able to define extension points in our application, where plugins can hook in.
  • We want to be able to load plugins dynamically, without having to modify the main application code.
  • We want to be able to enable or disable plugins at runtime.
  • We want to be able to configure plugins, and to pass configuration to them.
  • We want plugins to be able to interact with the main application code.
  • We want plugins to be able to add routes, services and database entities to the application.

In this article, we'll look at how to build a simple but robust plugin system for a Symfony application. We'll define a plugin entity to keep track of plugins in the database, a plugin manager service to load and manage plugins, and a plugin interface that plugins can implement to hook into the application, as well as an AbstractPlugin class that provides some common functionality for plugins to get started.

How to build a plugin system in Symfony

Step 1: Define a plugin entity

We'll start with our Plugin entity. This entity will represent a plugin in the database, and will store information about the plugin such as its name, description, author, version, and settings, as well as whether the plugin is installed and enabled.

<?php

namespace App\Entity;

use App\Repository\PluginRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: PluginRepository::class)]
class Plugin
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $name = '';

    #[ORM\Column(length: 255)]
    private string $version = '1.0.0';

    #[ORM\Column(length: 10000)]
    private string $description = '';

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    private bool $installed = false;

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    private bool $enabled = false;

    #[ORM\Column(length: 255)]
    private ?string $handle = null;

    private bool $upgradable = false;

    private string $latestVersion = '';

    #[ORM\Column(type: 'json', nullable: true, options: ['default' => '[]'])]
    private ?array $settings = null;

    /**
        getters and setters...
     * 
     */
}

Step 2: Define a plugin interface

All our plugins will implement a common interface, PluginInterface, which defines the minimum set of methods that a plugin must implement in order to be loaded and managed by the plugin system.

We're also going to add Symfony's #[AutoconfigureTag] annotation to the interface, so that any services that implement the interface will be automatically tagged with the app.plugin tag. This will allow us to load and manage plugins using Symfony's service container later on. I've also added #[ControllerServiceArguments] to the interface, so that plugins can provide new endpoints to the application which can receive arguments from the container.

<?php

namespace App\Interface;

use App\Entity\Plugin;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

#[AutoconfigureTag('app.plugin')]
#[AutoconfigureTag('controller.service_arguments')]
interface PluginInterface
{
    public static function getName(): string;

    public static function getVersion(): string;

    public static function getAuthor(): string;

    public static function getDescription(): string;

    public function install(): void;

    public function uninstall(): void;

    public function enable(): void;

    public function disable(): void;

    public function upgrade(): void;

    public function manage(Request $request, Plugin $plugin): ?Response;

    public function boot(): void;
}

The static methods on this class will be used the plugin manager to get information about the plugin, which will be used to automatically create new Plugin entities in the database.

Step 3: Give all plugins a place to start

We'll create an AbstractPlugin class that provides some common functionality for plugins to get started. Although this class is not strictly necessary, it can be useful to provide a common base class for all plugins to extend, so that they don't need to implement all the methods of the PluginInterface themselves and can instead override only the aspects of the plugin that are specific to them.

Another benefit of the AbstractPlugin class is that it can provide some common functionality that all plugins can take advantage of, including some other Symfony and application services. We can leave each individual plugin to define its own constructor and dependencies, but we can use Symfony's #[Required] annotation to ensure that the required services are injected into the plugin via setter methods.

<?php

namespace App\Service;

use App\Entity\Plugin;
use App\Event\System\PluginBootEvent;
use App\Event\SystemEvents;
use App\Interface\PluginInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use ReflectionObject;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Service\Attribute\Required;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

abstract class AbstractPlugin implements PluginInterface
{
    protected EntityManagerInterface $entityManager;
    protected LoggerInterface $logger;
    protected Environment $twig;
    protected FormFactoryInterface $formFactory;
    protected EventDispatcherInterface $eventDispatcher;
    protected Security $security;
    protected PluginManager $pluginManager;
    protected ?Plugin $plugin = null;
    protected Content $contentService;
    protected CacheInterface $cache;

    #[Required]
    public function setCache(CacheInterface $cache): void
    {
        $this->cache = $cache;
    }

    #[Required]
    public function setSecurity(Security $security): void
    {
        $this->security = $security;
    }

    #[Required]
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
    {
        $this->eventDispatcher = $eventDispatcher;
    }

    #[Required]
    public function setPluginManager(PluginManager $pluginManager): void
    {
        $this->pluginManager = $pluginManager;
    }

    #[Required]
    public function setTwig(Environment $twig): void
    {
        $this->twig = $twig;
        $object = new ReflectionObject($this);
        $dir = dirname($object->getFileName());
        $className = $object->getShortName();
        $dir .= '/' . $className . '/templates';
        if (is_dir($dir)) {
            /** @var FilesystemLoader $loader */
            $loader = $this->twig->getLoader();
            $loader->addPath($dir, $className);
        }
    }

    #[Required]
    public function setFormFactory(FormFactoryInterface $formFactory): void
    {
        $this->formFactory = $formFactory;
    }

    #[Required]
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    #[Required]
    public function setEntityManager(EntityManagerInterface $entityManager): void
    {
        $this->entityManager = $entityManager;
    }

    protected function addEventListener(string $event, string|callable $method, int $priority = 0): void
    {
        if (is_string($method)) {
            $method = [$this, $method];
        }
        $this->eventDispatcher->addListener($event, $method, $priority);
    }

    public function install(): void
    {
        return;
    }

    public function uninstall(): void
    {
        return;
    }

    public function enable(): void
    {
        return;
    }

    public function disable(): void
    {
        return;
    }

    public function upgrade(): void
    {
        return;
    }

    public function boot(): void
    {
        return;
    }

    public function manage(Request $request, Plugin $plugin): ?Response
    {
        return null;
    }

    // this event will be dispatched by the plugin manager 
    #[AsEventListener(SystemEvents::PLUGIN_BOOT)]
    public function onPluginBoot(PluginBootEvent $event): void
    {
        /** @var Plugin $plugin */
        $plugin = $event->getPlugin();
        $object = new ReflectionObject($this);
        $className = $object->getShortName();
        if ($plugin->getHandle() === $className) {
            $this->plugin = $plugin;
            $this->boot();
        }
    }

    public static function getVersion(): string
    {
        return '1.0.0';
    }

    abstract public static function getName(): string;

    abstract public static function getAuthor(): string;

    abstract public static function getDescription(): string;
}

Great. Any plugin which chooses to extend this class need only implement the getName, getAuthor, and getDescription methods, and can override any of the other methods as needed. These plugins will have access to a range of Symfony services, including the entity manager, logger, Twig templating engine, form factory, event dispatcher, and security.

We've even set up Twig to automatically load templates from a directory named after the plugin class, if it exists.

Step 4: Create a plugin manager

The plugin manager is responsible for loading, enabling, disabling, and upgrading plugins. It will also dispatch events to allow plugins to hook into the application at various points.

It is the plugin manager which will scan the application's src/Plugin directory for plugin classes and register them as Plugin entities in the database. It will also listen for an event dispatched in a request listener for when the system boots, and will dispatch a PluginBootEvent to allow plugins to hook into the application at startup.

Key to gluing all of this together is Symfony's service locators which will allow us to use an annotation to access a mini service container containing all services tagged with app.plugin.

In the example here, we use #[TaggedLocator] - note that in Symfony 7, this has been deprecated in favour of #[AutowireLocator].

How the plugin manager works

  1. The plugin manager listens for a system boot event (fired from a kernel listener), and when it receives this event, it loads all the enabled plugins from the database and dispatches a PluginBootEvent for each one.
  2. Plugin implementations, including the abstract class above, listen for this event and run whatever code they need to run at startup.
  3. The plugin manager also has methods for some controller (say in an admin dashboard) to install, uninstall, upgrade, enable, and disable plugins. The corresponding methods on the plugins themselves are called when these actions are initiated. We use the service locator to get all services tagged with app.plugin and call the relevant method on each one.
  4. There is also a method which can be invoked to scan the src/Plugin directory for new plugins and register them in the database. This would also typically be called by a separate controller in a dashboard.

The plugin manager code

Here is example code for the plugin manager:

<?php

namespace App\Service;

use App\Entity\Plugin;
use App\Event\System\PluginBootEvent;
use App\Event\System\PluginEvent;
use App\Event\SystemEvents;
use App\Repository\PluginRepository;
use App\Interface\PluginInterface;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class PluginManager
{
    private array $enabledPlugins = [];

    public function __construct(
        private readonly PluginRepository $repository,
        #[TaggedLocator('app.plugin')]
        private readonly ServiceLocator $pluginLocator,
        private readonly LoggerInterface $logger,
        private readonly EventDispatcherInterface $eventDispatcher,
    ) {
    }

    /**
     * @return Plugin[]
     */
    public function getEnabledPlugins(): array
    {
        return $this->repository->getEnabledPlugins();
    }

    #[AsEventListener(SystemEvents::BOOT)]
    public function onBoot(): void
    {
        $plugins = $this->getEnabledPlugins();
        foreach ($plugins as $plugin) {
            $this->enabledPlugins[] = $plugin;
            $event = new PluginBootEvent($plugin);
            $this->eventDispatcher->dispatch($event, SystemEvents::PLUGIN_BOOT);
        }
    }

    public function enable(string|Plugin $plugin): void
    {
        $plugin = is_string($plugin) ? $this->getPluginByHandle($plugin) : $plugin;
        if (!$plugin) {
            return;
        }
        $pluginInstance = $this->getPluginInstance($plugin);
        $pluginInstance->enable();
        $plugin->setEnabled(true);
        $this->eventDispatcher->dispatch(
            new PluginEvent('enable', $plugin),
            SystemEvents::PLUGIN_ENABLE
        );
        $this->repository->save($plugin, true);
    }

    public function disable(string|Plugin $plugin): void
    {
        $plugin = is_string($plugin) ? $this->getPluginByHandle($plugin) : $plugin;
        if (!$plugin) {
            return;
        }
        $pluginInstance = $this->getPluginInstance($plugin);
        $pluginInstance->disable();
        $plugin->setEnabled(false);
        $this->eventDispatcher->dispatch(
            new PluginEvent('disable', $plugin),
            SystemEvents::PLUGIN_DISABLE
        );
        $this->repository->save($plugin, true);
    }

    public function upgrade(Plugin $plugin): void
    {
        $pluginInstance = $this->getPluginInstance($plugin);
        if (!$pluginInstance) {
            return;
        }
        try {
            $pluginInstance->upgrade();
        } catch (Exception $e) {
            $this->logger->error('Plugin upgrade failed: ' . $plugin->getHandle(), ['exception' => [
                'code' => $e->getCode(),
                'message' => $e->getMessage(),
            ]]);
            return;
        }
        $plugin->setVersion($plugin->getLatestVersion());
        $plugin->setUpgradable(false);
        $this->eventDispatcher->dispatch(
            new PluginEvent('upgrade', $plugin),
            SystemEvents::PLUGIN_UPGRADE
        );
        $this->repository->save($plugin, true);
    }

    public function getPluginByHandle(string $handle): ?Plugin
    {
        return $this->repository->findOneByHandle($handle);
    }

    public function install(Plugin $plugin): void
    {
        $pluginInstance = $this->getPluginInstance($plugin);
        if (!$pluginInstance) {
            return;
        }
        try {
            $pluginInstance->install();
        } catch (Exception $e) {
            $this->logger->error('Plugin installation failed: ' . $plugin->getHandle(), ['exception' => [
                'code' => $e->getCode(),
                'message' => $e->getMessage(),
            ]]);
            return;
        }
        $plugin->setInstalled(true);
        $this->eventDispatcher->dispatch(
            new PluginEvent('install', $plugin),
            SystemEvents::PLUGIN_INSTALL
        );
        $this->repository->save($plugin, true);
    }

    public function uninstall(Plugin $plugin): void
    {
        $pluginInstance = $this->getPluginInstance($plugin);
        if (!$pluginInstance) {
            return;
        }
        try {
            $pluginInstance->uninstall();
        } catch (Exception $e) {
            $this->logger->error('Plugin uninstallation failed: ' . $plugin->getHandle(), ['exception' => [
                'code' => $e->getCode(),
                'message' => $e->getMessage(),
            ]]);
            return;
        }
        $plugin->setInstalled(false);
        $plugin->setEnabled(false);
        $plugin->setSettings([]);
        $this->eventDispatcher->dispatch(
            new PluginEvent('uninstall', $plugin),
            SystemEvents::PLUGIN_UNINSTALL
        );
        $this->repository->save($plugin, true);
    }

    public function getPlugins(): array
    {
        $plugins = [];
        $pluginFiles = $this->getPluginFiles();

        foreach ($pluginFiles as $pluginFile) {
            $className = $this->getClassName($pluginFile);

            if ($this->isValidPluginClass($className)) {
                $pluginData = $this->getPluginData($className);
                $pluginEntity = $this->getPluginEntity($pluginData);
                $plugins[] = $pluginEntity;
            }
        }

        return $plugins;
    }

    public function getPluginInstance(string|Plugin $plugin): ?PluginInterface
    {
        $pluginHandle = is_string($plugin) ? $plugin : $plugin->getHandle();
        $pluginClass = 'App\\Plugin\\' . $pluginHandle;
        try {
            return $this->pluginLocator->get($pluginClass);
        } catch (ContainerExceptionInterface | NotFoundExceptionInterface) {
            $this->logger->error('Plugin not found: ' . $pluginClass);
            return null;
        }
    }

    private function getPluginFiles(): array
    {
        return glob(__DIR__ . '/../Plugin/*.php');
    }

    private function getClassName(string $pluginFile): string
    {
        return 'App\\Plugin\\' . basename($pluginFile, '.php');
    }

    private function isValidPluginClass(string $className): bool
    {
        return class_exists($className) && in_array(PluginInterface::class, class_implements($className));
    }

    private function getPluginData(string $className): array
    {
        $reflectionClass = new ReflectionClass($className);
        $methods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC);
        $pluginData = [
            'name' => '',
            'version' => '0.0.1',
            'description' => '',
        ];

        foreach ($methods as $method) {
            if ($this->isPluginDataMethod($method)) {
                $methodName = $method->getName();
                try {
                    $field = strtolower(str_replace('get', '', $methodName));
                    $pluginData[$field] = $reflectionClass->getMethod($methodName)->invoke(null);
                } catch (ReflectionException) {
                    continue;
                }
            }
        }

        $pluginData['handle'] = $reflectionClass->getShortName();

        return $pluginData;
    }

    private function isPluginDataMethod(ReflectionMethod $method): bool
    {
        return $method->isStatic() &&
            $method->isPublic() &&
            $method->getNumberOfRequiredParameters() == 0 &&
            in_array($method->getName(), ['getName', 'getVersion', 'getDescription']);
    }

    private function getPluginEntity(array $pluginData): Plugin
    {
        $pluginEntity = $this->repository->findOneBy(['handle' => $pluginData['handle']]);

        if (!$pluginEntity) {
            $pluginEntity = new Plugin();
            $pluginEntity->setHandle($pluginData['handle']);
            $pluginEntity->setName($pluginData['name'] ?? $pluginData['handle']);
            $pluginEntity->setVersion($pluginData['version'] ?? '1.0.0');
            $pluginEntity->setDescription($pluginData['description'] ?? '');
            $pluginEntity->setInstalled(false);
            $pluginEntity->setEnabled(false);
            $this->repository->save($pluginEntity, true);
            $this->eventDispatcher->dispatch(
                new PluginEvent('register', $pluginEntity),
                SystemEvents::PLUGIN_REGISTER
            );
        }

        $pluginEntity->setUpgradable(
            version_compare($pluginData['version'], $pluginEntity->getVersion(), '>')
        );
        $pluginEntity->setLatestVersion($pluginData['version']);

        return $pluginEntity;
    }

    public function manage(Plugin $plugin, Request $request): ?Response
    {
        $pluginInstance = $this->getPluginInstance($plugin);
        return $pluginInstance?->manage($request, $plugin);
    }
}

Notice the manage function at the end. The idea here is that with our imaginary admin dashboard, a plugin can provide a form which can be used to update whatever settings the plugin needs. Consider a plugin which implements a manage method like this:

    public function manage(Request $request, Plugin $plugin): Response
    {
        $settings = $plugin->getSettings();
        $form = $this->formFactory->create(Settings::class, $settings);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $plugin->setSettings($form->getData());
            $this->entityManager->flush();
            /** @var FlashBagAwareSessionInterface $session */
            $session = $request->getSession();
            $session->getFlashBag()->add('success', 'Settings saved.');
        }
        $content = $this->twig->render('@SamplePlugin/settings.html.twig', [
            'plugin' => $plugin,
            'form' => $form->createView(),
        ]);
        return new Response($content);
    }

This method would be called when the plugin is managed in the dashboard, and would render a form using Symfony's form factory, which would be used to update the plugin's settings. And because this plugin extends the AbstractPlugin, it already has access to all these services. So in our src/Plugin directory, we have the SamplePlugin class, and a SamplePlugin subdirectory containing a templates directory with a settings.html.twig file. Our AbstractPlugin has already configured Twig to correctly load this template.

Step 5: Create a loader for a plugin's routes

We want plugins to be able to add routes to the application. We can do this by creating a PluginLoader class which scans for enabled plugins at compile time and adds their routes to the application.

Here is an example of how this might work:

<?php

namespace App\Routing;

use App\Service\PluginManager;
use ReflectionClass;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\RouteCollection;

class PluginLoader extends Loader
{
    private bool $isLoaded = false;
    private PluginManager $pluginManager;

    public function __construct(PluginManager $pluginManager, ?string $env = null)
    {
        $this->pluginManager = $pluginManager;
        parent::__construct($env);
    }

    /**
     * @inheritDoc
     */
    public function load(mixed $resource, ?string $type = null): mixed
    {
        if ($this->isLoaded) {
            throw new RuntimeException('Do not add the "plugin" loader twice');
        }

        $routes = new RouteCollection();
        $plugins = $this->pluginManager->getEnabledPlugins();
        $loader = new AttributeRouteControllerLoader();

        foreach ($plugins as $plugin) {
            $pluginInstance = $this->pluginManager->getPluginInstance($plugin);
            $reflectionClass = new ReflectionClass($pluginInstance);

            $classRoutes = $loader->load($reflectionClass->getName());

            foreach ($classRoutes as $routeName => $route) {
                $routes->add($routeName, $route);
            }
        }

        $this->isLoaded = true;
        return $routes;
    }

    /**
     * @inheritDoc
     */
    public function supports(mixed $resource, ?string $type = null): bool
    {
        return $type === 'plugin';
    }
}

To register and enable this loader, we need to update both our services config and our routing config.

In config/services.yaml:

services:
    App\Routing\PluginLoader:
        tags: [routing.loader]

And in config/routes.yaml:

app_plugin:
    resource: .
    type: plugin

Step 6: Create a plugin controller

Finally, we need a controller to manage plugins in the dashboard. This controller will allow us to install, uninstall, enable, disable, and upgrade plugins, as well as manage their settings.

Here is an example of how this might work:

<?php

namespace App\Controller\Dashboard;

use App\Entity\Plugin;
use App\Service\PluginManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/admin/plugins')]
class PluginController extends AbstractController
{
    #[Route('', name: 'index', methods: ['GET'])]
    public function index(): Response
    {
        /** @var Plugin[] $plugins */
        $plugins = $this->pluginManager->getPlugins();
        return $this->render('admin/plugins/index.html.twig', [
            'plugins' => $plugins,
        ]);
    }

    #[Route('/{plugin}', name: 'manage', requirements: ['plugin' => '\d+'], methods: ['GET', 'POST'])]
    public function manage(Plugin $plugin, Request $request): Response
    {
        $response = $this->pluginManager->manage($plugin, $request);
        if ($response !== null) {
            return $response;
        }
        $this->addFlash('warning', 'Plugin management not implemented for ' . $plugin->getName());
        return $this->redirectToRoute('plugin_index');
    }

    #[Route('/{plugin}/{action}', name: 'action', requirements: ['plugin' => '\d+'], methods: ['GET'])]
    public function action(Plugin $plugin, string $action, Request $request): Response
    {
        switch ($action) {
            case 'install':
                if ($plugin->isInstalled()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is already installed.');
                    break;
                }
                $this->pluginManager->install($plugin);
                $this->addFlash('success', 'Plugin "' . $plugin->getName() . '" installed successfully.');
                break;
            case 'uninstall':
                if (!$plugin->isInstalled()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is not installed.');
                    break;
                }
                if ($plugin->isEnabled()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is enabled. Disable it first.');
                    break;
                }
                $this->pluginManager->uninstall($plugin);
                $this->addFlash('success', 'Plugin "' . $plugin->getName() . '" uninstalled successfully.');
                break;
            case 'enable':
                if ($plugin->isEnabled()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is already enabled.');
                    break;
                }
                $this->pluginManager->enable($plugin);
                $this->addFlash('success', 'Plugin "' . $plugin->getName() . '" enabled successfully.');
                break;
            case 'disable':
                if (!$plugin->isEnabled()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is not enabled.');
                    break;
                }
                $this->pluginManager->disable($plugin);
                $this->addFlash('success', 'Plugin "' . $plugin->getName() . '" disabled successfully.');
                break;
            case 'upgrade':
                if (!$plugin->isInstalled()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is not installed.');
                    break;
                }
                if (!$plugin->isUpgradable()) {
                    $this->addFlash('warning', 'Plugin "' . $plugin->getName() . '" is already up to date.');
                    break;
                }
                $this->pluginManager->upgrade($plugin);
                $this->addFlash('success', 'Plugin "' . $plugin->getName() . '" upgraded successfully.');
                break;
            case 'test':
                $this->addFlash('info', 'Test action not implemented for ' . $plugin->getName());
                break;
            default:
                $this->addFlash('warning', 'Invalid action.');
        }
        return $this->redirectToRoute('plugin_index');
    }
}

Conclusion

That's it! We've built a simple but robust plugin system for a Symfony application. Our plugins can leverage either the AbstractPlugin class or implement the PluginInterface directly, and can hook into the application at startup, add routes, and manage their settings. They can also add event listeners via the AbstractPlugin addEventListener method, inside the plugin's own boot() method, which will ensure any event listeners added by the plugin are only active when the plugin is enabled.

Of course there are many ways to extend this system. You could add support for themes, widgets, or content types, or you could add more granular control over which parts of the application a plugin can hook into, via a permissions system and the event system. You could add support for plugins to provide their own services, or to interact with other plugins.

You could also add support for dependencies between plugins, or for plugins to provide services to other plugins.

The possibilities are endless. But hopefully this article has given you a good starting point for building a plugin system for your Symfony application. Good luck!


Comments

Add a comment

All comments are pre-moderated and will not be published until approval.
Moderation policy: no abuse, no spam, no problem.

You can write in _italics_ or **bold** like this.

Recent posts


Sunday 17 November 2024, 22:53

Keep your database data secure by selectively encrypting fields using this free bundle.

php

Sunday 27 October 2024, 19:02

Learn how to build an extensible plugin system for a Symfony application

php

SPONSORED AD

Buy this advertising space. Your product, your logo, your promotional text, your call to action, visible on every page. Space available for 3, 6 or 12 months.

Get in touch

Saturday 10 February 2024, 17:18

The difference between failure and success isn't whether you make mistakes, it's whether you learn from them.

musings coding

Monday 22 January 2024, 20:15

Recalling the time I turned down a job offer because the company's interview technique sucked.

musings

Friday 19 January 2024, 18:50

Recalling the time I was rejected on the basis of a tech test...for the strangest reason!

musings