SOA with Symfony2
Alessandro Nadalin - Montreal, February 2014
This talk is for those...
Stuck with the legacy
dealing with CRONs
in the need of a solid foundation
rely on web services
need a pluggable software architecture
who love @fabpot
SOA
1. SO WHAT?
(A)
A software design based on discrete software
components, "services", that collectively
provide the functionalities of the larger
software application
You typically start with the infamous PHP
app, hopefully built with Symfony2
which does everything on its own
Then you realize that to provide
a chat system to your users
PHP might not be the best...
And soon you also decide,
to improve performances,
that your frontend should have its own
in-memory persistence, to be faster
and you put it into another service
Then, as always...
SCALE.
And eventually, your lead architect
will come up and tell you
that your Java-based chat
sucks and should be
replaced with...
NODEJS
In human-understandable words, SOA is a software design which
embraces splitting a monolithic, totalitarian software
architecture into smaller pieces, thus making them independent,
loosely coupled and more maintainable
Ok, but in the real world?
2. Your (Symfony2) app
is just a piece of
the puzzle
How does communication (usually) happen?
WEBSERVICES
Services can request data to other services,
usually through WSs
POX
https://github.com/kriswallsmith/buzz
https://github.com/fabpot/Goutte
https://github.com/guzzle/guzzle
SOAP
PHP and SOAP in 2014
http://www.whitewashing.de/2014/01/31/soap_and_php_in_2014.html
HTTP
REST
FosREST
https://github.com/FriendsOfSymfony/FOSRestBundle
# app/config/routing.yml
users:
type:
rest
resource: MyBundleUsersController
GET /users
class UsersController
{
public function getUsersAction()
{
return $this->get(‘storage’)->getUsers();
}
}
class UsersController
{
public function getUsersAction()
{
return $this->get(‘storage’)->getUsers();
}
}
class UsersController
{
public function getUsersAction()
{
return $this->get(‘storage’)->getUsers();
}
}
View?
SERIALIZE ALL
THE THINGS!
JMSSerializer
Bundle: https://github.com/schmittjoh/JMSSerializerBundle
/**
* @ExclusionPolicy("all")
*/
class Customer implements UserInterface, EquatableInterface
{
/**
* @Expose
* @SerializedName("addresses")
* @Groups({"Customer:depth:1"})
*/
protected $addresses;
/**
* @Expose
* @Groups({
* "Customer:list",
* "Customer:detail",
* })
*/
private $email;
/**
* @ExclusionPolicy("all")
*/
class Customer implements UserInterface, EquatableInterface
{
/**
* @Expose
* @SerializedName("addresses")
* @Groups({"Customer:depth:1"})
*/
protected $addresses;
/**
* @Expose
* @Groups({
* "Customer:list",
* "Customer:detail",
* })
*/
private $email;
/**
* @ExclusionPolicy("all")
*/
class Customer implements UserInterface, EquatableInterface
{
/**
* @Expose
* @SerializedName("addresses")
* @Groups({"Customer:depth:1"})
*/
protected $addresses;
/**
* @Expose
* @Groups({
* "Customer:list",
* "Customer:detail",
* })
*/
private $email;
/**
* @ExclusionPolicy("all")
*/
class Customer implements UserInterface, EquatableInterface
{
/**
* @Expose
* @SerializedName("addresses")
* @Groups({"Customer:depth:1"})
*/
protected $addresses;
/**
* @Expose
* @Groups({
* "Customer:list",
* "Customer:detail",
* })
*/
private $email;
/**
* @ExclusionPolicy("all")
*/
class Customer implements UserInterface, EquatableInterface
{
/**
* @Expose
* @SerializedName("addresses")
* @Groups({"Customer:depth:1"})
*/
protected $addresses;
/**
* @Expose
* @Groups({
* "Customer:list",
* "Customer:detail",
* })
*/
private $email;
/**
* @ExclusionPolicy("all")
*/
class Customer implements UserInterface, EquatableInterface
{
/**
* @Expose
* @SerializedName("addresses")
* @Groups({"Customer:depth:1"})
*/
protected $addresses;
/**
* @Expose
* @Groups({
* "Customer:list",
* "Customer:detail",
* })
*/
private $email;
EVENTS
services notify the architecture that an event has happened
asynchronous messaging queues
RabbitMQ
https://github.com/videlalvaro/rabbitmqbundle
old_sound_rabbit_mq:
connections:
default:
host:
'localhost'
port:
5672
user:
'guest'
password: 'guest'
vhost:
'/'
lazy:
false
producers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
consumers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
queue_options:
{name: 'userreg'}
callback:
mailer
old_sound_rabbit_mq:
connections:
default:
host:
'localhost'
port:
5672
user:
'guest'
password: 'guest'
vhost:
'/'
lazy:
false
producers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
consumers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
queue_options:
{name: 'userreg'}
callback:
mailer
old_sound_rabbit_mq:
connections:
default:
host:
'localhost'
port:
5672
user:
'guest'
password: 'guest'
vhost:
'/'
lazy:
false
producers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
consumers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
queue_options:
{name: 'userreg'}
callback:
mailer
old_sound_rabbit_mq:
connections:
default:
host:
'localhost'
port:
5672
user:
'guest'
password: 'guest'
vhost:
'/'
lazy:
false
producers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
consumers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
queue_options:
{name: 'userreg'}
callback:
mailer
old_sound_rabbit_mq:
connections:
default:
host:
'localhost'
port:
5672
user:
'guest'
password: 'guest'
vhost:
'/'
lazy:
false
producers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
consumers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
queue_options:
{name: 'userreg'}
callback:
mailer
mailer:
class: "MyMailer"
arguments: [..., ...]
$this->get('old_sound_rabbit_mq.user_registration_producer')
->publish(serialize($msg));
php app/console rabbitmq:consumer user_registration
old_sound_rabbit_mq:
connections:
default:
host:
'localhost'
port:
5672
user:
'guest'
password: 'guest'
vhost:
'/'
lazy:
false
producers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
consumers:
user_registration:
connection:
default
exchange_options: {name: 'userreg', type: direct}
queue_options:
{name: 'userreg'}
callback:
mailer
class Mailer
{
…
public function execute(AMQPMessage $message)
{
// do stuff
}
}
class Mailer
{
…
public function execute(AMQPMessage $message)
{
// do stuff
}
}
2. Free data
CONSIDER ELIMINATING FK CONSTRAINTS
A service might need to handle data with
another DBMS, so FKs are virtually impossible
ABSTRACT THE DATA
You might think in "rows" but the architecture
thinks in "resources"
$this->get(‘doctrine’)
->getRepository(‘My:Entity’)
->findActiveOnes()
$this->get(‘doctrine’)
->getRepository(‘My:Entity’)
->findActiveOnes()
$this->get(‘storage’)
->getRepository(‘My:Entity’)
->findActiveOnes()
class RedisStorage
{
public function getRepository($name)
{
$this->hash = $name;
return $this;
}
public function findActiveOnes()
{
$results = $this->redis->hget($this->hash);
return array_filter($results, function($r){
return $r[‘active’] == true;
});
}
}
class RedisStorage
{
public function getRepository($name)
{
$this->hash = $name;
return $this;
}
public function findActiveOnes()
{
$results = $this->redis->hget($this->hash);
return array_filter($results, function($r){
return $r[‘active’] == true;
});
}
}
class RedisStorage
{
public function getRepository($name)
{
$this->hash = $name;
return $this;
}
public function findActiveOnes()
{
$results = $this->redis->hget($this->hash);
return array_filter($results, function($r){
return $r[‘active’] == true;
});
}
}
class RedisStorage
{
public function getRepository($name)
{
$this->hash = $name;
return $this;
}
public function findActiveOnes()
{
$results = $this->redis->hget($this->hash);
return array_filter($results, function($r){
return $r[‘active’] == true;
});
}
}
class RedisStorage
{
public function getRepository($name)
{
$this->hash = $name;
return $this;
}
public function findActiveOnes()
{
$results = $this->redis->hget($this->hash);
return array_filter($results, function($r){
return $r[‘active’] == true;
});
}
}
class RedisStorage
{
public function getRepository($name)
{
$this->hash = $name;
return $this;
}
public function findActiveOnes()
{
$results = $this->redis->hget($this->hash);
return array_filter($results, function($r){
return $r[‘active’] == true;
});
}
}
REPOSITORIES NEED
INTERFACES
ENTITIES NEED
INTERFACES
forget managers, you
need collections
implements StockStorageInterface
use StockStorageInterface as Storage;
class RedisStorage implements Storage
{
...
use StockStorageInterface as Storage;
class RedisStorage implements Storage
{
...
use StockStorageInterface as Storage;
class StockStorage implements Storage
{
...
collections as a service
stock:
class: “MyNamespaceStockStorage”
arguments:
…
…

$this->get(‘stock’)->findActiveOnes();
stock:
class: “MyNamespaceStockStorage”
arguments:
…
…

$this->get(‘stock’)->findActiveOnes();
stock:
class: “MyNamespaceStockStorage”
arguments:
…
…

$this->get(‘stock’)->findActiveOnes();
No more FKs and
the ability of
JOINing to retrieve
some related data
But you choose
what perfectly fits
each service:
your transactions
over a RDBMS and
your community
over a graph DB
So complicated!
Have fun returning
serialized collections
over HTTPS in
~50ms with Doctrine!
3. Standardize
EVERY DEVELOPER NEEDS
THE ENTIRE ARCHITECTURE ON HIS MACHINE
The architecture needs
to be installed in
~1 hour
Setting up VMs
is an hassle and
they are so slow!
go #vagrant
But Vagrant is
still suboptimal:
provisioning and
system resources
are still a pain!
4. Identity
Centralized authentication = identity service
OAuth
OpenID
JWS
JSON WEB SIGNATURE
JSON WEB TOKEN
JSON WEB SIGNATURE
JAVASCRIPT OBJECT
SIGNING & ENCRYPTION
JOSE
http://www.thread-safe.com/2012/03/json-object-signing-and-encryption-jose.html
1. The user enters the
credentials once in your
frontend

2. The JS app will forward them
to your Auth webservice
JS APP

AUTH
SERVICE

3. The Auth webservice will
then generate the encrypted
JWS and set a cookie with
its value

JS APP

4. The JS app can now just
execute calls using
that cookie
1. The user enters the credentials
once in your frontend

JS APP
2. The JS app will forward them
to your Auth webservice

JS APP
AUTH
SERVICE
AUTH
SERVICE
3. The Auth webservice will then generate the
encrypted JWS and set a cookie with its value
AUTH
SERVICE

JS APP

4. The JS app can now just execute
calls using that cookie
1. The user enters the
credentials once in your
frontend

2. The JS app will forward them
to your Auth webservice
JS APP

AUTH
SERVICE

3. The Auth webservice will
then generate the encrypted
JWS and set a cookie with
its value

JS APP

4. The JS app can now just
execute calls using
that cookie
setcookie($name, $jws,$ttl, $path, $domain, true);
setcookie($name, $jws,$ttl, $path, $domain, true);

HTTPS
JWS in PHP?
namshi/jose
use NamshiJOSEJWS;
$jws = new JWS('RS256');
$jws->setPayload(array(
'uid' => $user->getid(),
));
$privateKey = openssl_get_privatekey("file://path/to/private.
key");
$jws->sign($privateKey);
setcookie('identity', $jws->getTokenString());

use NamshiJOSEJWS;
$jws
= JWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");
if ($jws->verify($public_key)) {
echo "EUREKA!;
}
use NamshiJOSEJWS;
$jws = new JWS('RS256');
$jws->setPayload(array(
'uid' => $user->getid(),
));
$privateKey = openssl_get_privatekey("file://path/to/private.
key");
$jws->sign($privateKey);
setcookie('identity', $jws->getTokenString());

use NamshiJOSEJWS;
$jws
= JWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");
if ($jws->verify($public_key)) {
echo "EUREKA!;
}
use NamshiJOSEJWS;
$jws = new JWS('RS256');
$jws->setPayload(array(
'uid' => $user->getid(),
));
$privateKey = openssl_get_privatekey("file://path/to/private.
key");
$jws->sign($privateKey);
setcookie('identity', $jws->getTokenString());

use NamshiJOSEJWS;
$jws
= JWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");
if ($jws->verify($public_key)) {
echo "EUREKA!;
}
use NamshiJOSEJWS;
$jws = new JWS('RS256');
$jws->setPayload(array(
'uid' => $user->getid(),
));
$privateKey = openssl_get_privatekey("file://path/to/private.
key");
$jws->sign($privateKey);
setcookie('identity', $jws->getTokenString(), ...);

use NamshiJOSEJWS;
$jws
= JWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");
if ($jws->verify($public_key)) {
echo "EUREKA!;
}
use NamshiJOSEJWS;
$jws = new JWS('RS256');
$jws->setPayload(array(
'uid' => $user->getid(),
));
$privateKey = openssl_get_privatekey("file://path/to/private.
key");
$jws->sign($privateKey);
setcookie('identity', $jws->getTokenString());

use NamshiJOSEJWS;
$jws
= JWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");
if ($jws->verify($public_key)) {
echo "EUREKA!;
}
use NamshiJOSEJWS;
$jws = new JWS('RS256');
$jws->setPayload(array(
'uid' => $user->getid(),
));
$privateKey = openssl_get_privatekey("file://path/to/private.
key");
$jws->sign($privateKey);
setcookie('identity', $jws->getTokenString());

use NamshiJOSEJWS;
$jws
= JWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");
if ($jws->verify($public_key)) {
echo "EUREKA!;
}
...what about Symfony2?
use SymfonyComponentSecurity...AuthenticationProviderInterface;
class JwsProvider implements AuthenticationProviderInterface
{
...
public function authenticate(TokenInterface $token)
{
$key = openssl_pkey_get_public($this->publicKeyPath);
$jws = $token->getJws();
if ($key && $jws->isValid($key)) {
$token->setUser(User::fromArray($jws->getPayload()));
return $token;
}
throw new AuthenticationException('authentication failed.');
}
...
}
use SymfonyComponentSecurity...AuthenticationProviderInterface;
class JwsProvider implements AuthenticationProviderInterface
{
...
public function authenticate(TokenInterface $token)
{
$key = openssl_pkey_get_public($this->publicKeyPath);
$jws = $token->getJws();
if ($key && $jws->isValid($key)) {
$token->setUser(User::fromArray($jws->getPayload()));
return $token;
}
throw new AuthenticationException('authentication failed.');
}
...
}
use SymfonyComponentSecurity...AuthenticationProviderInterface;
class JwsProvider implements AuthenticationProviderInterface
{
...
public function authenticate(TokenInterface $token)
{
$key = openssl_pkey_get_public($this->publicKeyPath);
$jws = $token->getJws();
if ($key && $jws->isValid($key)) {
$token->setUser(User::fromArray($jws->getPayload()));
return $token;
}
throw new AuthenticationException('authentication failed.');
}
...
}
use SymfonyComponentSecurity...AuthenticationProviderInterface;
class JwsProvider implements AuthenticationProviderInterface
{
...
public function authenticate(TokenInterface $token)
{
$key = openssl_pkey_get_public($this->publicKeyPath);
$jws = $token->getJws();
if ($key && $jws->isValid($key)) {
$token->setUser(User::fromArray($jws->getPayload()));
return $token;
}
throw new AuthenticationException('authentication failed.');
}
...
}
use SymfonyComponentSecurity...AuthenticationProviderInterface;
class JwsProvider implements AuthenticationProviderInterface
{
...
public function authenticate(TokenInterface $token)
{
$key = openssl_pkey_get_public($this->publicKeyPath);
$jws = $token->getJws();
if ($key && $jws->isValid($key)) {
$token->setUser(User::fromArray($jws->getPayload()));
return $token;
}
throw new AuthenticationException('authentication failed.');
}
...
}
I can't simply
use the HTTP
basic authentication,
it was so
convenient!
...and flawed.
Modern apps,
modern tech.
4. Embrace
messaging
Don't wait, notify instead
Different services can intercept an even, separately
If one is down, the others keep working
Who cares about milliseconds for notifications?
The human body is the bottleneck
Email?
SMS?
Be reliable
“Daemons are great”
“Daemons are great”
- No PHP developer ever
SUPERVISOR
http://supervisord.org/
SUPERVISE
http://cr.yp.to/daemontools/supervise.html
use python ;-)
It doesn’t matter...
‫اﻟﺤﺮوف اﻟﻌﺮﺑﯿﺔ ‪if you talk‬‬
Rabbit makes everyone talk the same language
chat

sync daemons
Batch processing
frontend

ERP
agony
transcoding

telcom
But I

Symfony2
Tech monogamy
is so ‘90
“given a hammer,
everything
becomes a nail”
One size doesn’t fit all
“But look at Google,
they basically use
python for everything”
“...and C”
“...and C++”
“...and Java”
“...and JavaScript”
“...and Go”
But hey, you say...
they really dislike supporting
multiple platforms
“...and Dart”
But hey, you say...
they really are not into
supporting the secondary platforms
“...and AngularJS”
5. Not always
sunday
Monitor in real time
Native support for Symfony2
Logs are first-class citizens
https://github.com/Seldaek/monolog
Sharp, as much
as possible
I LIED A LOOOOOT
Symfony isn’t even
the main point of this
SOA talk
You can build SOAs
with anything
...or can you?
http://odino.org/why-we-choose-symfony2-over-any-other-php-framework/
By being decoupled and HTTP-centric
Symfony2 has turned into an
ideal application framework
that can take (part of)
the stage in a SOA
Full-stack is dead
PHP developers are dead
LONG LIVE API ENGINEERS!
All in all...
SOA is complex
like Symfony2
A puzzle with more pieces
like Symfony2
More things to keep in mind
like Symfony2
COMPLEX
IS NOT
COMPLICATED
Loose coupling
every service is independent, not forced to the
constraints of a monolithic block
you have the freedom of changing or replacing services
without the hassle of touching an entire system
State-of-the-art defense against outages
Fault tolerance
if one of the services has an outage, the rest
of the architecture still works
if a service, listening for messages, is down,
the publisher doesn't get stuck
Cleaner architecture
SoC happens at architectural, not application, level and you can perform large-scale
refactorings without the fear of destroying the entire system
...yawn...
Alessandro Nadalin
Alessandro Nadalin
@_odino_
Alessandro Nadalin
@_odino_
Namshi | Rocket Internet
Alessandro Nadalin
@_odino_
Namshi | Rocket Internet
VP Technology
Alessandro Nadalin
@_odino_
Namshi | Rocket Internet
VP Technology
odino.org
Thanks!
Alessandro Nadalin
@_odino_
Namshi | Rocket Internet
VP Technology
odino.org
By the way
Wanna join?
We are looking for talented nerds!
We are looking for talented nerds!

frontend engineer
We are looking for talented nerds!

frontend engineer
data engineer
We are looking for talented nerds!

lead frontend engineer
data engineer
Thanks!
Alessandro Nadalin
@_odino_
Namshi | Rocket Internet
VP Technology
odino.org
Image credits
http://www.flickr.com/photos/randystiefer/6998037429/sizes/h/in/photostream/
http://www.flickr.com/photos/55432818@N02/5500963965/
http://www.flickr.com/photos/pamhule/4503305775/
http://www.flickr.com/photos/wili/1427890704/
http://www.flickr.com/photos/nickpiggott/5212959770/sizes/l/in/photostream/
http://www.flickr.com/photos/nomad9491/2549965427/sizes/l/in/photostream/
http://www.flickr.com/photos/amyvdh/95764607/sizes/l/in/photostream/
http://www.flickr.com/photos/matthoult/4524176654/
http://www.flickr.com/photos/kittyeden/2416355396/sizes/l/in/photostream/
http://www.flickr.com/photos/jpverkamp/3078094381/
http://www.flickr.com/photos/madpoet_one/5554416836/
http://www.flickr.com/photos/87792096@N00/2732978107/
http://www.flickr.com/photos/petriv/4787037035/
http://www.flickr.com/photos/51035796522@N01/111091247/sizes/l/in/photostream/
http://www.flickr.com/photos/m-i-k-e/6366787693/sizes/l/in/photostream/
http://www.flickr.com/photos/39065466@N04/9111005211/
http://www.flickr.com/photos/marchorowitz/5449945176/sizes/l/in/photolist-9iAoQ1-8s4ueH-bCWef9-bCWdPh-e48XUmbu67nh-a7xaEr-8wLiNh-9aYU1k-9F4VUN-dYqzr1-9vosHb-8BtFuw-8P3h2e-9tqc6M-82qpt4-7UgkBJ-dgSnfS-aJiubZ-9Xji2U-9UVpkC7BSh7Y-8GE54k-91GHtB-8VMHJ2-8wiwvo-aCmPCg-925Tg8-bcBv9T-dGUseY/
http://www.flickr.com/photos/blegg/745322703/sizes/l/in/photostream/
http://www.flickr.com/photos/centralasian/4649550142/sizes/l/in/photostream/
http://www.flickr.com/photos/pennstatelive/4947279459/sizes/l/in/photostream/
http://www.flickr.com/photos/tjblackwell/7819341478/
http://www.flickr.com/photos/brainbitch/6066375386/
http://www.flickr.com/photos/nnova/4215594009/
http://www.flickr.com/photos/publicenergy/2246574379/
http://www.flickr.com/photos/andrewteman/4592833017/sizes/o/in/photostream/
http://www.flickr.com/photos/beautifulrevelry/8548004964/sizes/o/in/photostream/
http://www.flickr.com/photos/denaldo/5066810104/sizes/l/in/photostream/
http://www.flickr.com/photos/picturewendy/8365723674/sizes/l/in/photostream/
http://www.flickr.com/photos/danielygo/6644679037/sizes/l/in/photostream/
http://www.flickr.com/photos/ross/7614352/sizes/l/in/photostream/
http://www.flickr.com/photos/75932013@N02/6874087329/sizes/l/in/photostream/
http://crucifixjel.deviantart.com/art/300-Wallpaper-03-66516887
https://www.flickr.com/photos/acidsaturation/6635987033/sizes/l/

SOA with Symfony2 @ ConFoo 2014 in Montreal (CA)