Building Plugin-based Architectures
Asmir Mustafic
PHP Yorkshire 2019 - York - UK
Component 1
Component 3
Component 2
Component 1
Component 3
Component 2
Component 1
Component 3
Component 2
Component 2
Component 3
Component 1
Component 2
Component 1
Component 3
built to run into different copies of the original application
Symfony
Laravel
Wordpress
Magento
Drupal
more and more...
Plugin 2
Plugin 3
Plugin 1
Plugin 4
Plugin 2
Plugin 3
Plugin 1
Intentionally limit extensibility
Acceptable solution when refactoring legacy software
Plugin
Plugin
Plugin
Plugin
Plugin
The way to go when extensibility is the main goal
// code
// code
// code
$plugins; // <-- array
foreach ($plugins as $plugin) {
// $plugin do something
}
aka Discovery by the application
Focus on:
Where/How to find that code?
Look into file location convention
wp-content/plugins/myplugin/myplugin.php
wp-content/themes/mytheme/style.css wp-content/themes/mytheme/functions.php
wp-content/themes/mytheme/header.php wp-content/themes/mytheme/footer.php
.....
"extra": {
"laravel": {
"providers": [
"Goetas\\Debugbar\\ServiceProvider"
],
"aliases": {
"Debugbar": "Goetas\\Debugbar\\Facade"
}
}
},
Look into composer.json
Focus on:
Which code to load?
<?php
return [
// ...
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
// ...
Goetas\DebugBundle\DebugGoetasBundle::class => ['all' => true],
// ...
];
EDIT bundles.php
<?php
class Application extends BaseApplication
{
public function bootstrap()
{
// more plugins here
$this->addPlugin(\Cake\ElasticSearch\Plugin::class);
}
}
EDIT Application.php
...
Plugin
loader
Plugin
Plugin
Plugin
Plugin
Plugin
Component 2
Component 1
Component 3
Additional information provided
to plugins to make them fit into the application where they are running
Component 4
Component 5
How many?
Which?
How?
class Computer
{
protected $hardDrive;
public function __construct()
{
$this->hardDrive = new HardDrive();
}
}
$computer = new Computer();
Without Dependency Injection
class ComputerWithSSD extends Computer
{
public function __construct()
{
patent::__construct();
$this->hardDrive = new SSDDrive();
}
}
$computer = new ComputerWithSSD();
Without Dependency Injection
class Computer
{
private $hardDrive;
public function __construct($hardDrive)
{
$this->hardDrive = $hardDrive;
}
}
$hardDrive = new HardDrive();
$computer = new Computer($hardDrive);
class Computer
{
private $hardDrive;
public function __construct($hardDrive)
{
$this->hardDrive = $hardDrive;
}
}
$hardDrive = new SSDDrive();
$computer = new Computer($hardDrive);
class Computer
{
private $hardDrive;
private $videoCard;
private $usb = [];
private $cdRom;
// more
public function __construct(
$hardDrive,
$videoCard,
array $usb
// more
) {
}
public function setCDROM($cdRom)
{
$this->cdRom = $cdRom;
}
// more
}
$hardDrive = new HardDrive($size);
if (have_money) {
$videoCard = new NvidiaGPU();
} else {
$videoCard = new IntegratedVideo();
}
$usb[] = new USB2();
$usb[] = new USB2();
$usb[] = new USB3();
$computer = new Computer(
$hardDrive,
$videoCard
$usb
);
if (!laptop) {
$computer->setCDROM(new CDBurner());
}
Without Dependency Injection Container
$container = new Container();
// configure with some magic the $container
$computer = $container->getComputer();
Combine PHP, XML, YAML, Annotations and many other techniques to achieve magic!
$container = new Container();
// configure with some magic the $container
$computer = $container->getComputer();
Almost unlimited entry points
Only limitation is the Computer class itself
$container = new Container();
$plugins; // registered somehow
foreach ($plugins as $plugin) {
$plugin($container);
}
$computer = $container->get('computer');
// load from somewhere a configuration array
$configurations;
foreach ($plugins as $name => $plugin) {
$plugin($container, $configurations[$name]);
}
$computer = $container->get('computer');
Good software design
in the application and plugins
(don't hard-code anything!)
Abstract Factory Builder Factory Method Object Pool Prototype Singleton Adapter Bridge Composite Decorator Facade Flyweight Private Class Data Proxy Chain of responsibility Command Interpreter Iterator Mediator Memento Null Object Observer State Strategy Template method Visitor
Abstract Factory Builder Factory Method Object Pool Prototype Singleton Adapter Bridge Composite Decorator Facade Flyweight Private Class Data Proxy Chain of responsibility Command Interpreter Iterator Mediator Memento Null Object Observer State Strategy Template method Visitor
class Mediator
{
private $events = [];
public function addListener($eventName, $callback)
{
if (!isset($this->events[$eventName])) {
$this->events[$eventName] = [];
}
$this->events[$eventName][] = $callback;
}
public function trigger($eventName, $data = null)
{
foreach ($this->events[$eventName] as $callback) {
$callback($data, $eventName);
}
}
}
$mediator = new Mediator();
// plugin 1
$mediator->addListener('menu', function(User $user) {
echo "- Users\n";
});
// plugin 2
$mediator->addListener('menu', function(User $user) {
echo "- Settings\n";
});
// plugin n ...
$mediator->trigger('menu', $user);
- Users
- Settings
$dispatcher = new EventDispatcher();
// plugin 1
$dispatcher->addListener('menu', function() {
echo "- Users\n";
});
// plugin 2
$dispatcher->addListener('menu', function() {
echo "- Users\n";
});
$dispatcher->dispatch('menu');
$dispatcher->addListener('menu', function(Event $event) {
$user = $event->getSubject();
if (has_credentials($user)) {
echo " - Settings\n";
}
});
$event = new GenericEvent($user);
$dispatcher->dispatch('menu', $event);
$dispatcher->addListener('menu', function(MenuEvent $event) {
$user = $event->getSubject();
$event->addItem("Settings");
});
$event = new MenuEvent($user);
$dispatcher->dispatch('menu', $event);
echo $event->getItems();
// plugin 1
$dispatcher->addListener('menu', function(Event $event) {
// code
});
// plugin 2
$dispatcher->addListener('menu', function(Event $event) {
// code
}, 100);
$dispatcher->addListener('menu', function(Event $event) {
$user = $event->getSubject();
if ($user) {
$event->addItem("Dashboard");
} else {
$event->addItem("Log in");
$event->stopPropagation();
}
}, 100);
The most common pattern in plugin architectures
Plugin
Plugin
e
e
e
e
e
e
e
translations, templates, configuration files
class PdfFinder
{
private $locations = [
'/dev/system/drive'
];
public function find(string $name): string
{
foreach ($this->locations as $path) {
if (is_file("$path/$name")) {
return "$path/$name";
}
}
throw new Exception("PDF not found");
}
}
class PdfFinder
{
private $locations = [
'/dev/system/drive'
];
public function find(string $name): string
{
foreach ($this->locations as $path) {
if (is_file("$path/$name")) {
return "$path/$name";
}
}
throw new Exception("PDF not found");
}
public function addLocation(string $path): void
{
$this->locations[] = $path;
}
}
$container
->getDefinition('pdf.finder')
->addMethodCall('addLocation', ['/home/me/pdf']);
A plugin can edit DI service definition
$finder = $container->get('pdf.finder');
// find a PDF
$pdf = $finder->find('test.pdf');
Images, CSS, JS, PDF... files...
application/
├── src/
└── public/
├── plugins/
└── index.php
application/
├── src/
├── plugins/
└── public/
├── plugins-public/
│ ├── plugin-1
│ └── plugin-n*
└── index.php
Install plugins
in public dir
Copy public assets
in public dir
app/
├── src/
├── plugins/
│ └─── plugin-1/
│ ├── src/
│ └── public
│
└── public/
├── plugins/
│ └── plugin-1*
└── index.php
simple
development friendly
requires "install" step
no permissions control
server {
...
location ~ ^/plugins/([a-z]+) {
root /application/plugins/$1/public
}
}
nginx.conf
if ($hasPermissions) {
header("Content-Type: text/plain");
$fp = fopen("$pluginDir/plugin-1/public/file.txt", "r");
fpassthru($fp);
fclose($fp);
} else {
header('HTTP/1.0 401 Unauthorized');
}
if ($hasPermissions) {
header("Content-Type: text/plain");
header("X-Sendfile: $pluginDir/plugin-1/public/file.txt");
} else {
header('HTTP/1.0 401 Unauthorized');
}
Mediator pattern tends to become a bottleneck
Too many events can lead to chaos
Use good naming conventions and have good documentation
semver
+
existing tools
(as composer)