How to build customizable multitenant web applications
Stephan Hochdörfer, bitExpert AG
"Building an application so customizable it's the last
application you'll ever need to build"
Harrie Verveer
About me
Stephan Hochdörfer, bitExpert AG
Department Manager Research Labs
enjoying PHP since 1999
@shochdoerfer
Single Tenancy
Developer vs. Businessman
Single Tenancy – more customers
Single Tenancy – even more customers
Where will this lead to?
Maintenance nightmare!
Single Tenancy
Tenant 1
Application
Database
Hardware
Single Tenancy
Tenant 2
Application
Database
Hardware
Tenant 1
Application
Database
Hardware
Tenant 3
Application
Database
Hardware
Multi Tenancy
Tenant 2Tenant 1
Application
Database
Hardware
Tenant 3
What should be customizable?
What should be customizable?
Tenant 2Tenant 1
Application
Database
Hardware
Tenant 3
What should be customizable?
Tenant 2Tenant 1
Application
Database
Hardware
Tenant 3
How to skin an application?
Frontend | Branding
How to skin an application?
Application | Frontend
Remember:It`s a web application!
How to skin an application?
Application | Frontend
HTML
How to skin an application?
Application | Frontend
HTML + CSS
Application | Frontend
Application | Frontend
Application | Frontend
How to customize?
Application | Frontend
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title>My App</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" type="text/css" href="css/styles/myapp.css" /></head><body></body></html>
How to customize?
Application | Frontend
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title>My App</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" type="text/css" href="css/styles/<?php echo$tenant ?>.css" /></head><body></body></html>
How to customize?
Application | Frontend
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title>My App</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" type="text/css" href="css/styles/myapp.css" /> <link rel="stylesheet" type="text/css" href="css/styles/<?php echo $tenant ?>.css" /></head><body></body></html>
Feature driven CSS
Application | Frontend
Wait, there`s more...
Feature driven CSS
Application | Frontend
display: none
Application | Frontend
Next level...
Menubar generation
Application | Backend
<?php
if($user->hasEnabled(Module::ORDERMANAGEMENT)){ if($user->canAccess(OrderManagement::LIST_ORDERS)) {
$this->renderLink(OrderManagement::LIST_ORDERS); }
if($user->canAccess(OrderManagement::ADD_ORDER)) {
$this->renderLink(OrderManagement::ADD_ORDER); }
if($user->canAccess(OrderManagement::CANCEL_ORDER)) {
$this->renderLink(OrderManagement::CANCEL_ORDER); }}
Menubar generation
Application | Backend
<?php
if($tenant->hasModule(Module::ORDERMANAGEMENT){ if($user->hasEnabled(Module::ORDERMANAGEMENT)) {
if($user->canAccess(OrderManagement::LIST_ORDERS)){ $this->renderLink(OrderManagement::LIST_ORDERS);}
if($user->canAccess(OrderManagement::ADD_ORDER)){ $this->renderLink(OrderManagement::ADD_ORDER);}
}}
Menubar generation
Application | Backend
Modularize!
Menubar generation
Application | Backend
Module 2Module 1
Application core
Module 3
register at start up
Menubar generation
Application | Backend
Module 2Module 1
Application core
Module 3
register at start up
Configuration for tenant 1
Menubar generation
Application | Backend
Module 2Module 1
Application core
Module 3
register at start up
Configuration for tenant 2
Optimize workflows
Application | Backend
<?php
if('CC' == $paymentType){ // handle credit card payment}else if('COD' == $paymentType){ // handle cash on delivery payment}
Optimize workflows
Application | Backend
<?php
if('CC' == $paymentType){ // handle credit card payment for some tenants! if(in_array($tenant->getName(), array('tenant1', 'tenant2')) {
// insert logic here... }}else if('COD' == $paymentType){ // handle cash on delivery payment for some tenants! if(in_array($tenant->getName(), array('tenant3')) {
// insert logic here... }}
Optimize workflows
Application | Backend
Decouple functionality!
Optimize workflows
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType);
$payment->execute($order);
Optimize workflows
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType, $tenant);
$payment->execute($order);
Optimize workflows
Application | Backend
How to add custom logic?
Custom logic - Subclassing?
Application | Backend
AbstractPayment
CCPayment
CCPaymentTenant 1
CCPaymentTenant 2
Custom logic
Application | Backend
Any alternatives?
Custom logic
Application | Backend
Let`s add hooks...
Custom logic - Hooks
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType, $tenant);
if($this->paymentPreProcessor instanceof IPaymentPreProcessor) { $this->paymentPreProcessor->run($payment, $tenant, $order);}
$payment->execute($order);
if($this->paymentPostProcessor instanceof IPaymentPostProcessor) { $this->paymentPostProcessor->run($payment, $tenant, $order);}
Custom logic
Application | Backend
How to set the dependencies?
Custom logic
Application | Backend
Inject the dependencies!
Custom logic – Dependency Injection
Application | Backend
<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.bitexpert.de/schema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.bitexpert.de/schema/
http://www.bitexpert.de/schema/bitFramework-beans.xsd">
<bean id="MyApp.Service.Order" class="MyApp\Service\Order.php"></bean>
<bean id="Tenant1.Service.Order" class="MyApp\Service\Order.php"><property name="paymentPreProcessor"
ref="Tentant1.Payment.PaymentValidation" /></bean>
<bean id="Tenant2.Service.Order" class="MyApp\Service\Order.php"><property name="paymentPreProcessor"
ref="Tentant2.Payment.StrictValidation" /><property name="paymentPostProcessor"
ref="Tentant2.Payment.PushOrderToSAP" /></bean>
</beans>
Custom logic
Application | Backend
Any improvements?
Custom logic
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType, $tenant);
if($this->paymentPreProcessor instanceof IPaymentPreProcessor) { $this->paymentPreProcessor->run($payment, $tenant, $order);}
$payment->execute($order);
if($this->paymentPostProcessor instanceof IPaymentPostProcessor) { $this->paymentPostProcessor->run($payment, $tenant, $order);}
Custom logic
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType, $tenant);
if($this->paymentPreProcessor instanceof IPaymentPreProcessor) { $this->paymentPreProcessor->run($payment, $tenant, $order);}
$payment->execute($order);
if($this->paymentPostProcessor instanceof IPaymentPostProcessor) { $this->paymentPostProcessor->run($payment, $tenant, $order);}
Custom logic
Application | Backend
Aspect-oriented programming
Custom logic – Aspects for the masses!
Application | Backend
/** * @aspect */class CustomPaymentProcessingAspect {
/** * @around MyApp\Service\Order->processPayment */public function customFilter(\AOP\JoinPointInterface $joinPoint) {
// @TODO: implement pre-processing logic// ...
$result = $joinPoint->getAdviceChain()->proceed($joinPoint);
// @TODO: implement post-processing logic// ...
return $result;}
}
Custom logic - Result
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType, $tenant);
$payment->execute($order);
Application | Backend
Next level...
Database – Where to store the data?
Database
Database – Where to store the data?
Database
We need to store data for a tenant!
Database – Where to store the data?
Database
Database per Tenant?
Database – Where to store the data?
Database
Database per Tenant?
Schema per Tenant?
Database – Where to store the data?
Database
Database per Tenant?
Schema per Tenant?
Tenant Id per Row?
Database – How to access the data?
Database
ORM dynamic statements
vs.
What`s beyond?
Generalize you should!
Multi Tenancy
What`s beyond?
Software system family
No single solution!
What`s beyond?
A factory for mass production!
What`s beyond?
Multi Tenancy – Single Instance
Tenant 2Tenant 1
Application
Database
Hardware
Tenant 3
What`s beyond?
Multi Tenancy – Multi Instance
Tenant 2Tenant 1
Application
Database
Hardware
Tenant 3
What`s beyond?
Generative Programming
ConfigurationConfiguration
Implementationcomponents
Implementationcomponents
Generatorapplication
Generatorapplication
ProductProductGenerator
Generator
1 ... n
What`s beyond?
Generative Programming
ConfigurationConfiguration
Implementationcomponents
Implementationcomponents
Generatorapplication
Generatorapplication
Tenant 1Tenant 1
GeneratorGenerator
Tenant xTenant x
What`s beyond?
Generative Programming - Goal
What`s beyond?
Create an optimized application!
Generative Programming - Goal
What`s beyond?
Create an optimized applicationfor one tenant!
Generative Programming – Bonus points
What`s beyond?
Reduce application complexity
Generative Programming – Bonus points
What`s beyond?
FileFrm FILEOrderService_php5 { private String PreProcessor = ""; private String PostProcessor = "";
public FILEOrderService_php5() {setFilename("Order.php5");setRelativePath("/classes/MyApp/Service");
}
private void assign() {BEGINCONTENT()<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType, $tenant);
<!{PreProcessor}!>$payment->execute($order);<!{PostProcessor}!>ENDCONTENT() }}
Generative Programming – Bonus points
What`s beyond?
FileFrm FILEOrderService_php5 { [...]
private void configure() {if(this.getConfiguration().hasFeature('PreProcessor')) { PreProcessor = this.getPreProcessorContent(
this.getConfiguration.getTenant() );}
if(this.getConfiguration().hasFeature('PostProcessor')) { PostProcessor = this.getPostProcessorContent(
this.getConfiguration.getTenant() );}
}}
Generative Programming – Bonus points
Application | Backend
Example:Preprocessor:Postprocessor:
Output:
Generative Programming – Bonus points
Application | Backend
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType);
$payment->execute($order);
Example:Preprocessor:Postprocessor:
Output:
Generative Programming – Bonus points
Application | Backend
Example:Preprocessor: $this->paymentPreProcessor->run($payment, $tenant, $order);
Postprocessor:
Output:
Generative Programming – Bonus points
Application | Backend
Example:Preprocessor: $this->paymentPreProcessor->run($payment, $tenant, $order);
Postprocessor:
Output:
<?php
$paymentType = 'CC'; // set via request$payment = PaymentFactory::create($paymentType);
$this->paymentPreProcessor->run($payment, $tenant, $order);
$payment->execute($order);
Generative Programming – Bonus points
What`s beyond?
Reduce maintenance support
Generative Programming – Bonus points
What`s beyond?
FeatureImplementation
component
Generative Programming – Bonus points
What`s beyond?
Feature
Implementationcomponent
Generative Programming – Bonus points
What`s beyond?
Feature
Implementationcomponent
Customer
Generative Programming – The book
What`s beyond?
http://joind.in/2497
Flickr Creditshttp://www.flickr.com/photos/andresrueda/3452940751/
http://www.flickr.com/photos/andresrueda/3455410635/