Modular Application
Architecture

 

Building Plugin-based Architectures

Asmir Mustafic

PHP Yorkshire 2019 - York - UK

Me

Asmir Mustafic

@goetas

Open source

  • jms/serializer (contributor/maintainer)
  • masterminds/html5 (contributor/maintainer)
  • hautelook/templated-uri-bundle (contributor/maintainer)
  • goetas-webservices/xsd2php (author)
  • goetas-webservices/xsd-reader (author)
  • goetas-webservices/soap-client (author)
  • goetas/twital (author)
  • many others...

Application

Component 1

Component 3

Component 2

Application

Problems

  • Too many features, even more than what a whole company can handle
    • Even more feature requests! all perfectly legit

Development does not scale

Application

Component 1

Component 3

Component 2

Application

Component 1

Component 3

Component 2

Application

Component 2

Component 3

Component 1

Application core

Component 2

Component 1

Component 3

Multiple applications

Plugins

Plugins ≈ Components

 

built to run into different copies of the original application

  • Independent release cycles
  • Developed externally
  • Clear dependencies
  • Tend to be small and focused
  • Potentially different technologies
  • Core application is more maintainable
  • Adding features is scalable
    • ​No coordination needed
       
  • Can create community/partnerships

Why plugins?

Use cases

Symfony

Laravel

Wordpress

Magento

Drupal

more and more...

 Extensibility

is a key factor
in software development

My use case

immobinet.it

property management system

  • PHP
  • ~500 K lines of code
  • ~600 sql tables
  • countless features
  • few hundreds of different instances/installations

immobinet.it

176 plugins

immobinet.it

  • ~30 K lines of code in core
    (~7 % of the total)
     
  • 39 sql tables
    (~6 % of the total)

Core

How to build a plugin-based software?

  1. Traditional Extensible Software 

  2. Plugin-Based Software 

Traditional Extensible Software

Application

Plugin 2

Plugin 3

Plugin 1

Plugin 4

Application

Plugin 2

Plugin 3

Plugin 1

New features require new entry-points

Complexity grows

  • Intentionally limit extensibility

  • Acceptable solution when refactoring legacy software

When to use?

Plugin-Based Software

Application

application core

Plugin

Plugin

Plugin

Plugin

Plugin

  • The way to go when extensibility is the main goal

When to use?

Challenges

  1. Plugin registration
    • Configuration
       
  2. Exposing features
    • Communication
      with application and between plugins
       
  3. Exposing resources
    • Static and dynamic assets

Plugin Registration

Challenges

// code
// code
// code

$plugins; // <-- array

plugin registration

foreach ($plugins as $plugin) {
    // $plugin do something
}
  • Which code to load?

  • Where/How to find the code?

Plugin Registration

Convention-based

aka Discovery by the application

Focus on:

Where/How to find that code?

Plugin Registration

Wordpress

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

.....

Laravel

"extra": {
    "laravel": {
        "providers": [
            "Goetas\\Debugbar\\ServiceProvider"
        ],
        "aliases": {
            "Debugbar": "Goetas\\Debugbar\\Facade"
        }
    }
},

Look into composer.json

Pros

  • Simple
  • Easy for beginners 
  • Most used

Cons

  •  Needs good documentation
  • Tends to grow in complexity
  • Difficult to handle new cases
  • Not easy to exclude some plugins
  • Imposes limitations on directory structure
  • Less secure

Convention-based

Configuration-based

Focus on:

Which code to load?

Plugin Registration

Symfony

<?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

CakePHP

<?php

class Application extends BaseApplication
{
    public function bootstrap()
    {
        // more plugins here

        $this->addPlugin(\Cake\ElasticSearch\Plugin::class);
    }
}

EDIT Application.php

Pros

  • Focus on the plugins to load, not on technology details
  • Easy to document
  • Generally only one way to load plugins

Cons

  • Needs more human intervention
  • Difficult to automate
  • Difficult for not experienced devs

Configuration-based

Plugin manager

Registration

Drupal

Wordpress

Magento

...

installation is done by 

End User

Plugin manager

Plugin manager

Convenient GUI

Application core

Application

Plugin
loader
Plugin

Plugin

Plugin

Plugin

Plugin

Configuration

Registration

Application core

Component 2

Component 1

Application

Other Application

Component 3

Additional information provided
to plugins to make them fit into the application where they are running

Component 4

Component 5

Configuration storage

  • files
  • database
  • environment variables
  • plugin-manager
  • ...

depends on your

End User

Expose plugin functionalities

Challenges

Entry-points

How many?

Which?

How?

Dependency Injection

and

Dependency Injection Container

Dependency Injection

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

Dependency Injection Container

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 

Dependency Injection Container


$computer = $container->getComputer();

Dependency Injection Container

  • Symfony
  • Laravel
  • PHP-DI
  • Zend
  • Pimple
  • League
  • ...

Combine PHP, XML, YAML, Annotations and many other techniques to achieve magic!

Dependency Injection Container


$container = new Container();

// configure with some magic the $container 

$computer = $container->getComputer();

Perfect entrypoint for a plugin system

Almost unlimited entry points

Only limitation is the Computer class itself

Dependency Injection Container


$container = new Container();

$plugins; // registered somehow

foreach ($plugins as $plugin) {
   $plugin($container);
}

$computer = $container->get('computer');

Dependency Injection Container


// load from somewhere a configuration array
$configurations; 

foreach ($plugins as $name => $plugin) {
   $plugin($container, $configurations[$name]);
}

$computer = $container->get('computer');

Expose features

By allowing plugins to configure
the
Dependency Injection Container

Good software design

in the application and plugins


(don't hard-code anything!)

Good software design?

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

Mediator Pattern

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 

Implementations

  • symfony/event-dispatcher
  • doctrine/event-manager
  • illuminate/events
  • league/event
  • PSR-14 event manager
  • ...

Symfony event dispatcher


$dispatcher = new EventDispatcher();

// plugin 1
$dispatcher->addListener('menu', function() {
    echo "- Users\n"; 
});

// plugin 2
$dispatcher->addListener('menu', function() {
    echo "- Users\n";
});

$dispatcher->dispatch('menu'); 

Event data


$dispatcher->addListener('menu', function(Event $event) {
    $user = $event->getSubject();

    if (has_credentials($user)) {
       echo " - Settings\n";
    }
});

$event = new GenericEvent($user);

$dispatcher->dispatch('menu', $event); 

Custom event types


$dispatcher->addListener('menu', function(MenuEvent $event) {
    $user = $event->getSubject();

    $event->addItem("Settings");

});

$event = new MenuEvent($user);

$dispatcher->dispatch('menu', $event); 


echo $event->getItems();

Priorities


// plugin 1
$dispatcher->addListener('menu', function(Event $event) {

    // code

});

// plugin 2
$dispatcher->addListener('menu', function(Event $event) {

   // code

}, 100);

Stoping events


$dispatcher->addListener('menu', function(Event $event) {
    $user = $event->getSubject();


    if ($user) {
        $event->addItem("Dashboard");
    } else {
        $event->addItem("Log in");
        $event->stopPropagation();
    }

}, 100);

Use cases

  • Symfony Events
  • Drupal Hooks
  • Wordpress Hooks
  • Magento Events
  • Doctrine Events
  • ...

The most common pattern in plugin architectures 

Application core

Plugin

Plugin

e

e

e

e

e

e

e

Application

Exposing Resources

(used via code)

translations, templates, configuration files

Challenges

Use the
dependency injection container
and
override object configurations

Exposing Resources

Exposing Resources

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

Add extra locations for the PDF files?

Exposing Resources

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

Exposing Resources

$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');

Exposing Resources

(static)

Images, CSS, JS, PDF... files...

Challenges

application/
├── src/ 
└── public/
    ├── plugins/
    └── index.php

Naive solutions

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

Symlink files in public folder

  • simple

  • development friendly

  • requires "install" step 

  • no permissions control

Pros

Cons

server {
   ...

   location ~ ^/plugins/([a-z]+) {
      root /application/plugins/$1/public
   } 
}

Server configuration

nginx.conf

  • clean public dir
  • highly configurable
  • development friendly
  • requires server configs
  • difficult permissions control

Pros

Cons

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

Serve via application

Pros

  • easy to control permissions
  • clean public dir
  • development friendly

Cons

  • bad performance
  • PHP has to handle HTTP

improved performance

if ($hasPermissions) {
  header("Content-Type: text/plain");
  header("X-Sendfile: $pluginDir/plugin-1/public/file.txt");
} else {
  header('HTTP/1.0 401 Unauthorized');
}

Serve via application assisted by server

Recap

  • Plugin architectures
    • Traditional Extensible software
    • Plugin-Based software
  • Plugin registration
  • Exposing features
    • DI & DIC
    • Software patterns
  • Exposing resources

Extra challenges

Performance issues?

Mediator pattern tends to become a bottleneck

Complexity?

Too many events can lead to chaos

Use good naming conventions and have good documentation

Plugin versioning?

Plugin dependencies?

semver

+

existing tools

(as composer)

Thank you!

Made with Slides.com