SOA with Symfony2 @ ConFoo 2014 in Montreal (CA)

219
SOA with Symfony2 Alessandro Nadalin - Montreal, February 2014

description

Symfony2 is one of the de-facto standards for developing enterprise-ready applications in PHP: being a very structured & decoupled framework, it becomes very handy and suitable for building Service Oriented architectures, which require loose coupling and a clean and tested structure: we will see hot to create a Service Oriented Architecture in Symfony2, taking advantage of messaging systems like RabbitMQ, HTTP APIs and Sf2's internals.

Transcript of SOA with Symfony2 @ ConFoo 2014 in Montreal (CA)

SOA with Symfony2Alessandro 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(A) WHAT?

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 PHPapp, hopefully built with Symfony2

which does everything on its own

Then you realize that to providea 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 ownin-memory persistence, to be faster

and you put it into another service

Then, as always...

SCALE.

And eventually, your lead architectwill come up and tell youthat your Java-based chat

sucks and should bereplaced 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) appis just a piece of

the puzzle

How does communication (usually) happen?

WEBSERVICES

Services can request data to other services,usually through WSs

POX

SOAP

PHP and SOAP in 2014http://www.whitewashing.de/2014/01/31/soap_and_php_in_2014.html

HTTP

REST

FosRESThttps://github.com/FriendsOfSymfony/FOSRestBundle

# app/config/routing.ymlusers: type: rest resource: My\Bundle\UsersController

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!

JMSSerializerBundle: 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

RabbitMQhttps://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: "My\\Mailer" 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 withanother DBMS, so FKs are virtually impossible

ABSTRACT THE DATA

You might think in "rows" but the architecturethinks 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 NEEDINTERFACES

ENTITIES NEEDINTERFACES

forget managers, youneed 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: “My\Namespace\StockStorage” arguments: … …

$this->get(‘stock’)->findActiveOnes();

stock: class: “My\Namespace\StockStorage” arguments: … …

$this->get(‘stock’)->findActiveOnes();

stock: class: “My\Namespace\StockStorage” arguments: … …

$this->get(‘stock’)->findActiveOnes();

No more FKs andthe ability of

JOINing to retrievesome related data

But you choosewhat perfectly fits

each service:your transactionsover a RDBMS andyour communityover a graph DB

So complicated!

Have fun returningserialized collections

over HTTPS in ~50ms with Doctrine!

3. Standardize

EVERY DEVELOPER NEEDSTHE ENTIRE ARCHITECTURE ON HIS MACHINE

The architecture needsto be installed in

~1 hour

Setting up VMsis an hassle and

they are so slow!

go #vagrant

But Vagrant isstill suboptimal:provisioning andsystem 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

1. The user enters the credentials once in your frontend

JS APP

AUTHSERVICE

2. The JS app will forward themto your Auth webservice

3. The Auth webservice will then generate the encryptedJWS and set a cookie withits value

JS APP

4. The JS app can now just execute calls usingthat cookie

1. The user enters the credentials once in your frontend

JS APP

AUTHSERVICE

2. The JS app will forward themto your Auth webservice

JS APP

AUTHSERVICE

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

JS APP

AUTHSERVICE

4. The JS app can now just execute calls using that cookie

1. The user enters the credentials once in your frontend

JS APP

AUTHSERVICE

2. The JS app will forward themto your Auth webservice

3. The Auth webservice will then generate the encryptedJWS and set a cookie withits value

JS APP

4. The JS app can now just execute calls usingthat cookie

setcookie($name, $jws,$ttl, $path, $domain, true);

setcookie($name, $jws,$ttl, $path, $domain, true);

HTTPS

JWS in PHP?

namshi/jose

use Namshi\JOSE\JWS;

$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 Namshi\JOSE\JWS;

$jws = JWS::load($_COOKIE['identity']);$public_key = openssl_pkey_get_public("/path/to/public.key");

if ($jws->verify($public_key)) { echo "EUREKA!;}

use Namshi\JOSE\JWS;

$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 Namshi\JOSE\JWS;

$jws = JWS::load($_COOKIE['identity']);$public_key = openssl_pkey_get_public("/path/to/public.key");

if ($jws->verify($public_key)) { echo "EUREKA!;}

use Namshi\JOSE\JWS;

$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 Namshi\JOSE\JWS;

$jws = JWS::load($_COOKIE['identity']);$public_key = openssl_pkey_get_public("/path/to/public.key");

if ($jws->verify($public_key)) { echo "EUREKA!;}

use Namshi\JOSE\JWS;

$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 Namshi\JOSE\JWS;

$jws = JWS::load($_COOKIE['identity']);$public_key = openssl_pkey_get_public("/path/to/public.key");

if ($jws->verify($public_key)) { echo "EUREKA!;}

use Namshi\JOSE\JWS;

$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 Namshi\JOSE\JWS;

$jws = JWS::load($_COOKIE['identity']);$public_key = openssl_pkey_get_public("/path/to/public.key");

if ($jws->verify($public_key)) { echo "EUREKA!;}

use Namshi\JOSE\JWS;

$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 Namshi\JOSE\JWS;

$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 Symfony\Component\Security\...\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 Symfony\Component\Security\...\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 Symfony\Component\Security\...\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 Symfony\Component\Security\...\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 Symfony\Component\Security\...\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 simplyuse 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

SUPERVISORhttp://supervisord.org/

SUPERVISEhttp://cr.yp.to/daemontools/supervise.html

use python ;-)

It doesn’t matter...

if you talk الحروف العربیة

Rabbit makes everyone talk the same language

chat

Batch processing

frontend

sync daemons

transcoding

agony

ERP

telcom

But I Symfony2

Tech monogamyis 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 supportingmultiple platforms

“...and Dart”

But hey, you say...

they really are not intosupporting the secondary platforms

“...and AngularJS”

5. Not alwayssunday

Monitor in real time

Native support for Symfony2

Logs are first-class citizens

https://github.com/Seldaek/monolog

Sharp, as muchas possible

I LIED A LOOOOOT

Symfony isn’t eventhe main point of this

SOA talk

You can build SOAswith anything

...or can you?

By being decoupled and HTTP-centricSymfony2 has turned into anideal 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

COMPLEXIS NOT

COMPLICATED

Loose coupling

every service is independent, not forced to theconstraints of a monolithic block

you have the freedom of changing or replacing serviceswithout 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 restof 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!

frontend engineer

We are looking for talented nerds!

frontend engineer

We are looking for talented nerds!

data engineer

lead frontend engineer

We are looking for talented nerds!

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-e48XUm-bu67nh-a7xaEr-8wLiNh-9aYU1k-9F4VUN-dYqzr1-9vosHb-8BtFuw-8P3h2e-9tqc6M-82qpt4-7UgkBJ-dgSnfS-aJiubZ-9Xji2U-9UVpkC-

7BSh7Y-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/