Applicazioni con architetture distribuite in Symfony2

10 ottobre 2014, Symfony Day, Milano

Chi sono?

Matteo Galli

Software Engineer @ Facile.it (Utilities)

 

...ma anche fotografo e viaggiatore

www.facile.it

matteo.galli@facile.it

twitter.com/thinkindie

instagram.com/thinkindie

Chi siamo?

Facile.it è un comparatore di tariffe

  • assicurative
  • energetiche (luce e gas)
  • telefoniche (ADSL, fisso e mobile)
  • mutui, conti bancari e prestiti

 

I nostri uffici sono a Milano, ma soprattutto...

WE ARE HIRING!

Cosa utilizziamo?

  • Stack LAMP / LEMP
  • Symfony / Zend
  • ElasticSearch
  • Node.js
  • RabbitMQ
  • Redis
  • Ember.js / AngularJS / Backbone.js
  • Vagrant / Ansible / Puppet

 

Siamo presenti anche su GitHub github.com/facile-it

Cosa vi raccontiamo oggi

In una parola...

Bunny

Bunny: i requisiti

  • Separazione tra frontend e backend
  • Multibusiness
  • Ricerca globale
  • ACL, anche nella ricerca
  • Gestione di processi in background

Separazione tra frontend e backend

+

=

<3

Separazione tra frontend e backend

WebService RESTful basato sull'esistenza di risorse (ad es. le entità di Doctrine) di cui si ha accesso alle loro rappresentazioni tramite un identificatore globale (URI).​

Separazione tra frontend e backend

GET /api/users/1 HTTP/1.1
Host: ws.bunny.facile.it
Accept: application/vnd.api+json
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
date: Thu, 19 Oct 2014 14:38:16 GMT

{  
    "id":1,
    "timestamp":"2014-06-10T15:42:37+02:00",
    "lastUpdateTimestamp":"2014-06-10T15:42:37+02:00",
    "username":"BunnyWs",
    "nome":"Bunny",
    "cognome":"WS",
    "email":"bunnyws@facile.it",
    "isActive":true
}

Separazione tra frontend e backend

  • FOSRestBundle
  • FOSOAuthServerBundle
  • FOSUserBundle
  • JSONAPI (Ember.js Driven)

FOSRestBundle

Semplifica e velocizza lo sviluppo di un WebService RESTful

  • gestione dei codici HTTP di risposta
  • differenti view handlers per differenti valori di Accept
  • URL REST-friendly

FOSRestBundle

/**
 * @RouteResource("Compagnia")
 */
class CompagnieController extends BunnyController 
{
    /**
     * @View()
     */
    public function cgetAction() {}
    /**
     * @View()
     */
    public function getAction(Request $request) {}
    /**
     * @View()
     */
    public function postAction(Request $request) {}
    /**
     * @View()
     */
    public function patchAction(Request $request) {}
    /**
     * @View()
     */
    public function deleteAction(Request $request) {}
}

Multibusiness aka Bundlize the world

  • Un bundle di base per il WS
  • Un bundle per ogni business (ADSL, Luce&Gas, Mobile), differenti al più per un numero ristretto di entità o di comandi in background
  • Un bundle per ciascuna altra attività cross-business (es.: invio di SMS) 

Ricerca Globale or AMA

  • Posso rapidamente ricercare un qualsiasi documento contenente un qualsiasi termine
  • FOSElasticaBundle: le entità di Doctrine vengono persiste anche su ElasticSearch

SmartReindexing: la modifica di una entità richiede l'aggiornamento di tutti i documenti in cui è contenuta

$inheritanceTree = $this->getInheritanceTree($rootEntity);
$entities = $this->em->getConfiguration()->getMetadataDriverImpl()->getAllClassNames();
foreach ($entities as $entity) {
     $class = $this->cmf->getMetadataFor($entity);
     $associationMappings = $class->getAssociationMappings();
     if (empty($associationMappings)) {
         continue;
     } else {
         foreach ($associationMappings as $nestedEntity) {
             if (in_array($nestedEntity['targetEntity'], $inheritanceTree)) {
                 ...
             }
         }
     }
}

​DoctrineMetadata: ho già una mappa (ricorsiva) delle relazioni tra entità

ACL, anche nella ricerca

ACL = Access Control List

Permettono di definire regole (ACE) capillari per accedere a risorse.

Es.: permessi dei files su piattaforme Unix, regole del firewall.

ACL, anche nella ricerca

La gestione base delle ACL viene affidata al provider di default di Symfony e vengono persistite su MySQL

Le ACE vengono definite in ciascuna entità tramite custom annotations

namespace Facile\Ws\BunnyBundle\Entity;
use Facile\Ws\BunnyBundle\Annotation\EntityAce as ACE;
/**
 * @ACE("ROLE", name="ROLE_ADMIN", mask="MASK_MASTER");
 * @ACE("ROLE", name="ROLE_USER", mask="MASK_VIEW");
 */
class Compagnia
{
    /**
     * @ACE("ROLE", name="ROLE_USER", mask="MASK_VIEW")
     * @ACE("ROLE", name="ROLE_ADMIN", mask="MASK_MASTER")
     */
    protected $nome;
}

(Un comando si occupa poi di inizializzare tutte le entità)

ACL, anche nella ricerca

Utilizzare il provider di default di Symfony per filtrare una collezione di entità non è performante né soddisfacente.

(Non provatelo a casa)

ACL, anche nella ricerca

You know, for search!

Quando una entità viene salvata su ElasticSearch il documento viene diviso in due parti

  • meta: contenente le ACL "espanse"
  • data: l'entità vera e propria
{
   "data" : {
      "lastUpdateTimestamp" : "2014-10-08T09:28:45+02:00",
      "id" : 5,
      "timestamp" : "2014-06-10T15:42:33+02:00",
      "venduto" : true,
      "nome" : "Facile.it",
      "type" : "energia"
   },
   "meta" : {
      "acl" : [
         {
            "mask" : 1,
            "name" : "role_user",
            "type" : "role"
         },
         {
            "name" : "role_admin",
            "mask" : 64,
            "type" : "role"
         }
      ]
   }
}

La ricerca di documenti viene eseguita con \Elastica\Query e filtrata con i ruoli ricoperti dall'utente attivo

$elasticaFilterOr = new \Elastica\Filter\BoolOr();
foreach ($this->getUserExpandedRoles() as $role) {
    $elasticaFilterRolesName = new \Elastica\Filter\Term();
    $elasticaFilterRolesName->setTerm('meta.acl.name', strtolower($role));
    $elasticaFilterRolesType = new \Elastica\Filter\Term();
    $elasticaFilterRolesType->setTerm('meta.acl.type', 'role');

    $elasticaFilterAndRole = new \Elastica\Filter\BoolAnd();
    $elasticaFilterAndRole->addFilter($elasticaFilterRolesName);
    $elasticaFilterAndRole->addFilter($elasticaFilterRolesType);

    $elasticaFilterOr->addFilter($elasticaFilterAndRole);
}

$elasticaQuery = new \Elastica\Query(new \Elastica\Query\MatchAll());
$elasticaQuery->setFilter($elasticaFilterOr);

Utilizzando i filtri (dalla documentazione di ES):

  • non viene calcolato lo _score per ogni documento, ma viene applicata una logica booleana
  • i risultati di molti filtri possono essere salvati in cache
  • restringo la query sui documenti effettivamente ricercabili, ottimizzando la query stessa

Redis è un database di tipo chiave-valore, dove la chiave, come il valore, possono essere una qualsiasi sequenza di bytes.

http://redis.io

Redis è utilizzabile per:

  • Caching avanzato
  • Rankings in tempo reale
  • Contatori 
  • Pub/Sub
  • Code

Bunny utilizza Redis per la cache di

  • Metadati di Doctrine
  • Query (DQL SQL)
  • Risultati delle query
  • Entità
doctrine:
    orm:
        entity_managers:
            bunny:
                metadata_cache_driver:
                    type: redis
                    host: localhost
                    port: 6380
                query_cache_driver:
                    type: redis
                    host: localhost
                    port: 6380
                result_cache_driver:
                    type: redis
                    host: localhost
                    port: 6380

Redis viene utilizzato anche per la gestione di code con Resque tramite php-resque.

Qualsiasi operazione che particolarmente complessa viene demandata quindi ad un processo asincrono o programmato.

Esempi di processi asincroni:

  • SmartReindexing
  • Generazione di Report
  • Caricamento su account FTP esterni
  • Caricamenti batch su gestionali di terzi

Domande?

Grazie

https://joind.in/talk/view/12215

Applicazioni Distribuite con Symfony2

By Matteo Galli

Applicazioni Distribuite con Symfony2

  • 3,128