symfony2, backbone.js & socket.io - sflive paris 2k13 - wisembly
DESCRIPTION
Wisembly experience sharing on building one-page js app w/ Backbone.js over Symfony2 REST API and socket.io push server.TRANSCRIPT
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
MUST READ
http://fr.slideshare.net/nachomartin/symfony-javascript-combining-the-best-of-two-worlds
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
MUST READ 2
http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way
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
Great Ressources
http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way
http://fr.slideshare.net/nachomartin/symfony-javascript-combining-the-best-of-two-worlds
app.wisembly.com/sflive
http://wisembly.com/en/about#jobs
@guillaumepotier
app.wisembly.com/sflive
Any Questions ?
app.wisembly.com/sflive