Symfony2, Backbone.js & socket.io - SfLive Paris 2k13 - Wisembly

Post on 27-Jan-2015

122 views 0 download

Tags:

description

Wisembly experience sharing on building one-page js app w/ Backbone.js over Symfony2 REST API and socket.io push server.

Transcript of Symfony2, Backbone.js & socket.io - SfLive Paris 2k13 - Wisembly

Symfony2, Backbone.js & socket.io

Symfony live Paris 2013

@guillaumepotierguillaume@wisembly.com

app.wisembly.com/sflive

app.wisembly.com/sflive

app.wisembly.com/sflive

2011, September

app.wisembly.com/sflive

PHP

MySQL

app.wisembly.com/sflive

PHP

MySQL

Doctrine 2

Symfony 2

Twig

app.wisembly.com/sflive

PHP

MySQL

Doctrine 2

Symfony 2

Twig

jQuery

Twig js

Assetic

app.wisembly.com/sflive

PHP

MySQL

Doctrine 2

Symfony 2

Twig

jQuery

Twig js

Assetic

Underscore.js

Backbone.js

app.wisembly.com/sflive

PHP

MySQL

Doctrine 2

Symfony 2

Twig

jQuery

Twig js

Assetic

Underscore.js

Backbone.js

TOO MUCH!

app.wisembly.com/sflive

Client

Server

app.wisembly.com/sflive

Client

Server

PHP

MySQL

Doctrine 2

Symfony 2

Twig REST

app.wisembly.com/sflive

Client

Server

PHP

MySQL

Doctrine 2

Symfony 2

Twig

HTML

jQuery

Underscore.js

Backbone.js

REST

app.wisembly.com/sflive

Client

Server

PHP

MySQL

Doctrine 2

Symfony 2

Twig

HTML

jQuery

Underscore.js

Backbone.js

RESTLONG POLLING!

app.wisembly.com/sflive

Users want fast & smooth SaaS apps

app.wisembly.com/sflive

Users want fast & smooth SaaS appsUsers want multiplateform SaaS apps

app.wisembly.com/sflive

Users want fast & smooth SaaS appsUsers want multiplateform SaaS apps

Users want dynamic & interactive SaaS apps

app.wisembly.com/sflive

Users want fast & smooth SaaS appsUsers want multiplateform SaaS apps

Users want dynamic & interactive SaaS apps

You should do so!

app.wisembly.com/sflive

Users want fast & smooth SaaS appsUsers want multiplateform SaaS apps

Users want dynamic & interactive SaaS apps

You should do so!

BUT HOW?

app.wisembly.com/sflive

app.wisembly.com/sflive

?

app.wisembly.com/sflive

HELL

NO!?

app.wisembly.com/sflive

app.wisembly.com/sflive

Nowadays

app.wisembly.com/sflive

app.wisembly.com/sflive

small / lightweight / stable

app.wisembly.com/sflive

small / lightweight / stable

easy to learn, easy to extend

app.wisembly.com/sflive

small / lightweight / stable

easy to learn, easy to extend

great resources:

layoutManager

relational

app.wisembly.com/sflive

Models Models

app.wisembly.com/sflive

Models Models

Collections Repositories

app.wisembly.com/sflive

Models Models

Collections Repositories

Views Controllers

app.wisembly.com/sflive

Models Models

Collections Repositories

Views Controllers

Templates Views

app.wisembly.com/sflive

Models Models

Collections Repositories

Views Controllers

Templates Views

Routing Routing

app.wisembly.com/sflive

Models Models

Collections Repositories

Views Controllers

Templates Views

Routing RoutingREST + PUSH

app.wisembly.com/sflive

BazingaExposeTranslationFOSJsRouting

FOSRestBundleJMSSerializer

app.wisembly.com/sflive

BazingaExposeTranslationFOSJsRouting

FOSRestBundleJMSSerializer

app.wisembly.com/sflive

1 - MAKE AN API

app.wisembly.com/sflive

Books = new Backbone.collection();Books.url = ‘/books’;

@nacmarti

n

app.wisembly.com/sflive

Books = new Backbone.collection();Books.url = ‘/books’;

GET /books

Books.fetch();

@nacmarti

n

app.wisembly.com/sflive

events: { ‘click .mybutton’:‘doStuffAndSave’ }

doStuffAndSave: function() {var book = Books.get(3);book.stuff();book.save();

}

@nacmarti

n

app.wisembly.com/sflive

events: { ‘click .mybutton’:‘doStuffAndSave’ }

doStuffAndSave: function() {var book = Books.get(3);book.stuff();book.save();

}

PUT /books/3

@nacmarti

n

app.wisembly.com/sflive

[ {“id”:1,

“name”:”guillaume”, “phone”: “0611010011”

...

}, {...},]

/** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id;

/** * @var string $name * * @ORM\Column(name="name", type="string", length=50, nullable=true) */ private $name;

/** * @var string $phone * * @ORM\Column(name="phone", type="string", length=20, nullable=true) */ private $phone;

...

app.wisembly.com/sflive

II - MAKE A GOOD REST API

app.wisembly.com/sflive

JMSSerializer

or

app.wisembly.com/sflive

class User implements UserInterface, EquatableInterface, ApiAbleInterface{

}

JMSSerializer

or

app.wisembly.com/sflive

public function toArray(){ return [ 'id' => $this->getId(), 'name' => $this->getName(), 'email' => $this->getEmail(), ];}

class User implements UserInterface, EquatableInterface, ApiAbleInterface{

}

JMSSerializer

or

app.wisembly.com/sflive

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

/** * @Route("{keyword}/quote/{id}", name="api3_quote_get", options={"expose"=true}) * @Method({"GET", "OPTIONS"}) */ public function getQuote(EventInterface $event, $id) { try { $quote = $this->get('api3.quote')->get($id); return $this->container->get('api3.response')->newSuccessResponse($quote->toArray(), 200); } catch (NoResultException $e) { return $this->container->get('api3.response')->newErrorResponse('No quote found', ErrorCode::NO_QUOTE, 404); } }

FOSRestBundle

or

app.wisembly.com/sflive

Entity EntityRepository

Entity Service

Controller

API Service

app.wisembly.com/sflive

<?php

// ...

private function processForm(User $user) {

$form = $this->createForm(new UserType(), $user); $form->bind($this->getRequest());

if ($form->isValid()) {

$this->doYourStuff(); return $user; }

// ...

}

Use Validator and Form

@couac

app.wisembly.com/sflive

/** * @Route("event/{keyword}/poll/{id}", name="api3_poll_edit", requirements={"id" = "\d+"}, options={"expose"=true}) * @Method({"POST", "PUT", "OPTIONS"}) */ public function editPollAction(EventInterface $event, Request $request, $id) { try {

$poll = $this->get('api3.poll')->get($event, $id);

$poll = $this->get('api3.poll')->edit($poll, $request);

return $this->container->get('api3.response')->newSuccessResponse($poll->toArray(), 201);

} catch (NoResultException $e) {// ... } catch (AccessDeniedException $e) {// ... } catch (\Exception $e) {// ... } }

app.wisembly.com/sflive

/** * @Route("event/{keyword}/poll/{id}", name="api3_poll_edit", requirements={"id" = "\d+"}, options={"expose"=true}) * @Method({"POST", "PUT", "OPTIONS"}) */ public function editPollAction(EventInterface $event, Request $request, $id) { try {

$poll = $this->get('api3.poll')->get($event, $id);

$poll = $this->get('api3.poll')->edit($poll, $request);

return $this->container->get('api3.response')->newSuccessResponse($poll->toArray(), 201);

} catch (NoResultException $e) {// ... } catch (AccessDeniedException $e) {// ... } catch (\Exception $e) {// ... } }

app.wisembly.com/sflive

/** * @Route("event/{keyword}/poll/{id}", name="api3_poll_edit", requirements={"id" = "\d+"}, options={"expose"=true}) * @Method({"POST", "PUT", "OPTIONS"}) */ public function editPollAction(EventInterface $event, Request $request, $id) { try {

$poll = $this->get('api3.poll')->get($event, $id);

$poll = $this->get('api3.poll')->edit($poll, $request);

return $this->container->get('api3.response')->newSuccessResponse($poll->toArray(), 201);

} catch (NoResultException $e) {// ... } catch (AccessDeniedException $e) {// ... } catch (\Exception $e) {// ... } }

app.wisembly.com/sflive

/** * @Route("event/{keyword}/poll/{id}", name="api3_poll_edit", requirements={"id" = "\d+"}, options={"expose"=true}) * @Method({"POST", "PUT", "OPTIONS"}) */ public function editPollAction(EventInterface $event, Request $request, $id) { try {

$poll = $this->get('api3.poll')->get($event, $id);

$poll = $this->get('api3.poll')->edit($poll, $request);

return $this->container->get('api3.response')->newSuccessResponse($poll->toArray(), 201);

} catch (NoResultException $e) {// ... } catch (AccessDeniedException $e) {// ... } catch (\Exception $e) {// ... } }

app.wisembly.com/sflive

/** * @Route("event/{keyword}/poll/{id}", name="api3_poll_edit", requirements={"id" = "\d+"}, options={"expose"=true}) * @Method({"POST", "PUT", "OPTIONS"}) */ public function editPollAction(EventInterface $event, Request $request, $id) { try {

$poll = $this->get('api3.poll')->get($event, $id);

$poll = $this->get('api3.poll')->edit($poll, $request);

return $this->container->get('api3.response')->newSuccessResponse($poll->toArray(), 201);

} catch (NoResultException $e) {// ... } catch (AccessDeniedException $e) {// ... } catch (\Exception $e) {// ... } }

app.wisembly.com/sflive

app.wisembly.com/sflive

app.wisembly.com/sflive

BazingaExposeTranslationFOSJsRouting

FOSRestBundleJMSSerializer

app.wisembly.com/sflive

events: {‘bookUpdated’:‘update’,‘bookCreated’: ‘create’,‘bookDeleted’:‘delete’,

}

update: function(websocketData) {doStuff(websocketData);

},create: function(websocketData) {

doOtherStuff(websocketData);},delete: function(websocketData) {

stillDoOtherStuff(websocketData);}

@nacmarti

n

app.wisembly.com/sflive

events: {‘bookUpdated’:‘update’,‘bookCreated’: ‘create’,‘bookDeleted’:‘delete’,

}

update: function(websocketData) {doStuff(websocketData);

},create: function(websocketData) {

doOtherStuff(websocketData);},delete: function(websocketData) {

stillDoOtherStuff(websocketData);}

FULL EVENT BASEDREAL TIME

@nacmarti

n

app.wisembly.com/sflive

websocketData?

Who sends what?

Which port, which protocol?

app.wisembly.com/sflive

app.wisembly.com/sflive

BazingaExposeTranslationFOSJsRouting

FOSRestBundleJMSSerializer

app.wisembly.com/sflive

Authenticate user againstPUSH server

app.wisembly.com/sflive

sessionTokendomain

Authenticate user againstPUSH server

app.wisembly.com/sflive

sessionTokendomain

RESTsessionToken

domain

Authenticate user againstPUSH server

app.wisembly.com/sflive

sessionTokendomain

REST

rights

sessionTokendomain

Authenticate user againstPUSH server

app.wisembly.com/sflive

REST

rights

sessionTokendomain

Authenticated!

Authenticate user againstPUSH server

app.wisembly.com/sflive

PUSH: The «Classic» way

rights

app.wisembly.com/sflive

PUSH: The «Classic» way

RESTsessionToken

domain

rights

app.wisembly.com/sflive

PUSH: The «Classic» way

RESTsessionToken

domaindata

rights

app.wisembly.com/sflive

PUSH: The «Classic» way

RESTsessionToken

domaindata

rights

app.wisembly.com/sflive

PUSH: The «Classic» way

RESTsessionToken

domaindata

websocketData

rights

app.wisembly.com/sflive

• Slow: HTTP ajax round-trip

• !DRY: Double front processing (Ajax / Push)

• Push server complexity: authorizations

app.wisembly.com/sflive

PUSH: The «Wisembly» way

RESTsessionToken

domaindata

websocketData

rights

app.wisembly.com/sflive

PUSH: The «Wisembly» way

RESTsessionToken

domain

websocketData

websocketData

rightssecret

app.wisembly.com/sflive

PUSH: The «Wisembly» way

RESTsessionToken

domain

websocketData

websocketData

websocketData

rightssecret

app.wisembly.com/sflive

PUSH: The «Wisembly» way

RESTsessionToken

domaindata

websocketData

websocketData

websocketData

rightssecret

app.wisembly.com/sflive

Push «surprises»

app.wisembly.com/sflive

Push «surprises»

• Must find always opened port

app.wisembly.com/sflive

Push «surprises»

• Must find always opened port

• Websocket protocol must go through firewalls

app.wisembly.com/sflive

Push «surprises»

• Must find always opened port

• Websocket protocol must go through firewalls

• Push may disconnect (very!) frequently and loose events (duh!)

app.wisembly.com/sflive

app.wisembly.com/sflive

• 80 always opened, but websocket very often blocked -> FAIL -> goto 443 w/ https

app.wisembly.com/sflive

• 80 always opened, but websocket very often blocked -> FAIL -> goto 443 w/ https

• Implement disconnection mechanism and lost events in case of socket.io «degraded» protocol (xhr polling, jsonp polling)

app.wisembly.com/sflive

The «Wisembly» way

hashN: { eventName, args }

app.wisembly.com/sflive

The «Wisembly» way

hashN: { eventName, args }

app.wisembly.com/sflive

The «Wisembly» way

hash1: { eventName, args }hash2: { eventName, args }

...hashN: { eventName, args }

hashN: { eventName, args }

hashN: { eventName, args }

hashN: { eventName, args }

hashN

hashN hashN

app.wisembly.com/sflive

The «Wisembly» way

hashN: { eventName, args }...

hashN+M: { eventName, args }

hashN

hashM hashM

app.wisembly.com/sflive

The «Wisembly» way

hashN: { eventName, args }...

hashN+M: { eventName, args }

hashNRESTonReconect()since hashN

hashM hashM

app.wisembly.com/sflive

The «Wisembly» way

hashN: { eventName, args }...

hashN+M: { eventName, args }

hashNRESTonReconect()since hashN

hashN+1: { eventName, args }...

hashN+M: { eventName, args }

hashM hashM

app.wisembly.com/sflive

The «Wisembly» way

hashN: { eventName, args }...

hashN+M: { eventName, args }

RESTonReconect()since hashN

hashN+1: { eventName, args }...

hashN+M: { eventName, args }

hashM hashM

hashM

app.wisembly.com/sflive

Any Questions ?

app.wisembly.com/sflive