Se avete portato a termine il giorno 4, dovreste avere preso confidenza con il pattern MVC e dovrebbe sembrarvi un modo più naturale di scrivere codice. Investite un po' di tempo in più per conoscerlo meglio e non tornerete più indietro. Parlando di cose pratiche, ieri abbiamo personalizzato le pagine di Jobeet e allo stesso tempo abbiamo rivisto alcuni concetti di symfony come il layout, gli helper e gli slot.
Oggi ci addentreremo nel magnifico mondo del framework del routing di symfony.
URL
Se cliccate su un'offerta di lavoro sulla homepage di Jobeet, l'URL apparirà
come il seguente: /job/show/id/1
. Se avete già sviluppato altri siti in PHP,
sarete abituati a URL del tipo /job.php?id=1
. Come fa symfony a farli funzionare?
Come fa symfony a decidere l'azione da chiamare basandosi su quest'URL?
Perché l'id
dell'offerta di lavoro viene recuperata con
$request->getParameter('id')
? Oggi risponderemo a tutte queste domande.
Prima però parliamo un po' degli URL e di cosa siano esattamente. In un contesto web, un URL è l'identificatore univoco di una risorsa. Quando visitate un URL, chiedete al browser di utilizzare una risorsa identificata da quell'URL. Perciò, siccome l'URL è l'interconnessione tra il sito web e l'utente, deve comunicare significative informazioni riguardo alla risorsa che identifica. Ma gli URL "tradizionali" non descrivono davvero una risorsa, essi mostrano la struttura interna di un'applicazione. All'utente non interessa che il sito sia realizzato in PHP o che una particolare offerta di lavoro abbia un certo identificatore sul database. Mostrare il funzionamento interno dell'applicazione è piuttosto dannoso in termini di sicurezza: cosa succederebbe se l'utente provasse ad accedere a risorse per cui non ha il permesso? Sicuramente lo sviluppatore dovrebbe metterle in sicurezza in modo appropriato, ma è sempre meglio nascondere informazioni sensibili.
Gli URL sono molto importanti in symfony, a tal punto da avere un intero framework dedicato alla loro gestione: il framework del routing . Il routing si occupa della gestione degli URI interni e URL esterni. Quando arriva una richiesta, il routing processa l'URL e lo converte in un URI interno.
Abbiamo già visto un URI interno della pagina di un'offerta di lavoro nel template
indexSuccess.php
:
'job/show?id='.$job->getId()
L'helper url_for()
|url_for()
converte l'URI interno in un vero e proprio URL:
/job/show/id/1
L'URI interno è costituito da molte parti: job
è il modulo, show
è l'azione
e la query string aggiunge i parametri da passare all'azione. Lo schema generico
per un URI interno è il seguente:
MODULO/AZIONE?chiave=valore&chiave_1=valore_1&...
Visto che il routing di symfony è un processo a due vie, si possono cambiare gli URL senza cambiarne l'implementazione tecnica. Questo è uno dei vantaggi principali del design pattern front-controller.
Configurazione del routing
La mappatura tra gli URI interni e gli URL esterni è stabilita nel file di
configurazione routing.yml
:
# apps/frontend/config/routing.yml homepage: url: / param: { module: default, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*
Il file routing.yml
descrive le rotte. Una rotta ha un nome (homepage
), uno
schema (/:module/:action/*
) e alcuni parametri (sotto la chiave param
).
Quando arriva una richiesta, il routing cerca uno schema corrispondente per l'URL
passato. La prima rotta corrispondente vince, perciò l'ordine nel file routing.yml
è importante. Diamo uno sguardo ad alcuni esempi per comprendere meglio come
funziona tutto questo.
Quando si richiede l'homepage di Jobeet, che contiene l'URL /job
, la prima rotta
corrispondente è default_index
. In uno schema, una parola preceduta dai
due punti (:
) è una variabile, quindi /:module
significa: individua una /
seguita da qualcosa. Nel nostro esempio, la variabile module
ha come valore
job
. Questo valore può essere recuperato con $request->getParameter('module')
.
Questa rotta definisce inoltre un valore di default per la variabile action
.
Quindi, per tutti gli URL corrispondenti a questa rotta, la richiesta avrà
anche un parametro action
con index
come valore.
Se si richiede la pagina /job/show/id/1
, symfony identificherà la corrispondenza
dell'ultimo schema: /:module/:action/*
. In uno schema, l'asterisco (*
) corrisponde
a un insieme di coppie variabile/valore separate da slash (/
):
Parametro richiesto | Valore |
---|---|
module | job |
action | show |
id | 1 |
note
Le viariabili module
e action
sono speciali, visto che sono utilizzate da
symfony per determinare quale azione eseguire.
L'URL /job/show/id/1
può essere creato da un template utilizzando la seguente
chiamata all'helper url_for()
:
url_for('job/show?id='.$job->getId())
Potete utilizzare anche il nome della rotta aggiungendo il prefisso @
:
url_for('@default?module=job&action=show&id='.$job->getId())
Le chiamate sono equivalenti, ma l'ultima è più efficiente, visto che il sistema di routing non deve analizzare tutte le rotte per trovare quella con la migliore corrispondenza ed è meno legata all'implementazione (i nomi del modulo e dell' azione non sono presenti nell'URI interno).
Personalizzazioni delle rotte
Per ora, quando si richiede l'URL /
nel browser, si ottiene la pagina
predefinita di congratulazioni di symfony. Questo perché l'URL corrisponde
alla rotta homepage
. Ma ha senso cambiarla per renderla l'homepage di
Jobeet. Per fare la modifica, cambiamo la variabile module
della rotta
homepage
in job
:
# apps/frontend/config/routing.yml homepage: url: / param: { module: job, action: index }
Possiamo ora cambiare il link del logo di Jobeet nel layout per fargli usare
la rotta homepage
:
<h1> <a href="<?php echo url_for('@homepage') ?>"> <img src="/legacy/images/jobeet.jpg" alt="Jobeet Job Board" /> </a> </h1>
È stato facile!
tip
Quando si aggiorna la configurazione delle rotte, le modifiche vengono
immediatamente prese in considerazione nell'ambiente di sviluppo. Ma per farle
funzionare anche nell'ambiente di produzione, occorre pulire la cache
utilizzando il task cache:clear
.
Per qualcosa di un po' più complesso, cambiamo l'URL della pagina dei lavori in qualcosa di più sensato:
/job/sensio-labs/paris-france/1/web-developer
Senza sapere niente su Jobeet e senza guardare la pagina, si può capire dall'URL che Sensio Labs sta cercando uno sviluppatore web per lavorare a Parigi, in Francia.
note
Gli URL carini sono importanti, perché portano informazioni all'utente. Sono anche utili quando si copia e incolla un URL in una email o per ottimizzare il sito per i motori di ricerca.
Il seguente schema corrisponde a questo URL:
/job/:company/:location/:id/:position
Modifichiamo il file routing.yml
e la rotta job
all'inizio del file:
job_show_user: url: /job/:company/:location/:id/:position param: { module: job, action: show }
Se si aggiorna l'homepage di Jobeet, i link ai lavori non sono cambiati. Questo
perché per rigenerare una rotta, bisogna passare le variabili richieste. Quindi
occorre cambiare la chiamata a url_for()
in indexSuccess.php
:
url_for('job/show?id='.$job->getId().'&company='.$job->getCompany(). '&location='.$job->getLocation().'&position='.$job->getPosition())
Un URI interno può anche essere espresso come array:
url_for(array( 'module' => 'job', 'action' => 'show', 'id' => $job->getId(), 'company' => $job->getCompany(), 'location' => $job->getLocation(), 'position' => $job->getPosition(), ))
Requisiti
Durante il primo giorno di tutorial, abbiamo parlato della validazione e
della gestione degli errori per buone ragioni. Il sistema delle rotte ha
una capacità di validazione intrinseca. Ogni schema di variabili può
essere validato da un'espressione regolare definita usando la voce
requirements
nella definizione della rotta:
job_show_user: url: /job/:company/:location/:id/:position param: { module: job, action: show } requirements: id: \d+
La voce requirements
forza id
a essere un valore numerico. Altrimenti,
la rotta non corrisponderà.
Classi di rotte
Ogni rotta definita in routing.yml
viene internamente convertita in un oggetto
della classe sfRoute
. Questa
classe può essere cambiata definendo una voce class
nella definizione della
rotta. Se avete familiarità col protocollo HTTP, forse saprete che definisce
diversi "metodi", come GET
, POST
, HEAD
, DELETE
e PUT
. I primi tre
sono supportati da tutti i browser, mentre gli altri due no.
Per restringere una rotta a corrispondere solo ad alcuni metodi di richiesta,
si può cambiare la classe della rotta in
sfRequestRoute
ed
aggiungere un requisito per la variabile virtuale sf_method
:
job_show_user: url: /job/:company/:location/:id/:position class: sfRequestRoute param: { module: job, action: show } requirements: id: \d+ sf_method: [get]
note
Richiedere a una rotta di corrispondere solo ad alcuni metodi HTTP non
è esattamente equivalente a usare sfWebRequest::isMethod()
nell'azione.
Questo perché il routing continuerà a cercare una rotta corrispondente
se il metodo non corrisponde a quello atteso.
Classi di oggetti rotta
Il nuovo URI interno per un lavoro è un po' lungo e difficile da scrivere
(url_for('job/show?id='.$job->getId().'&company='.$job->getCompany().'&location='.$job->getLocation().'&position='.$job->getPosition())
),
ma come abbiamo appena imparato nella sezione precedente, la classe della
rotta può essere cambiata. Per la rotta job_show_user
, è meglio usare
sfPropelRoute
,
essendo la classe ottimizzata per rotte che rappresentano oggetti Propel
o insiemi di oggetti Propel:
job_show_user: url: /job/:company/:location/:id/:position class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get]
La voce options
personalizza il comportamento della rotta. Qui, l'opzione
model
definisce la classe del modello di Propel (JobeetJob
) relativa
alla rotta e l'opzione type
impone che questa rotta sia legata a un
oggetto (si può anche usare list
, se una rotta rappresenta un insieme
di oggetti).
La rotta job_show_user
ora sa della sua relazione con JobeetJob
e
quindi possiamo semplificare la chiamata a url_for()
:
url_for(array('sf_route' => 'job_show_user', 'sf_subject' => $job))
oppure solo:
url_for('job_show_user', $job)
note
Il primo esempio è utile quando si devono passare più parametri oltre all'oggetto.
Questo funziona perché tutte le variabili nella rotta hanno un corrispondente
metodo di accesso nella classe JobeetJob
(ad esempio, la variabile di rotta
company
è sostituita con il valore digetCompany()
).
Guardando gli URL generati, non sono proprio come li avremmo voluti:
http://www.jobeet.com.localhost/frontend_dev.php/job/Sensio+Labs/Paris%2C+France/1/Web+Developer
Dobbiamo applicare uno "slug" ai valori delle colonne, sostituendo tutti i
caratteri non ASCII con un -
. Apriamo il file JobeetJob
e aggiungiamo i
seguenti metodi alla classe:
// lib/model/JobeetJob.php public function getCompanySlug() { return Jobeet::slugify($this->getCompany()); } public function getPositionSlug() { return Jobeet::slugify($this->getPosition()); } public function getLocationSlug() { return Jobeet::slugify($this->getLocation()); }
Quindi, creiamo il file lib/Jobeet.class.php
e vi inseriamo il metodo
slugify
:
// lib/Jobeet.class.php class Jobeet { static public function slugify($text) { // sostituisce tutto tranne lettere e punti con - $text = preg_replace('/\W+/', '-', $text); // cancella gli spazi e converte in minuscolo $text = strtolower(trim($text, '-')); return $text; } }
note
In questo tutorial, non mostriamo mai l'istruzione di apertura <?php
negli esempi che contengono solo codice PHP, in modo da ottimizzare
lo spazio e salvare un po' di alberi. Ovviamente dovete ricordare
sempre di aggiungerla quando create un nuovo file PHP.
Al contrario non va messa nei file dei template.
Abbiamo definito tre metodi di accesso "virtuali": getCompanySlug()
,
getPositionSlug()
e getLocationSlug()
. Essi restituiscono i valori
delle rispettive colonne, dopo aver applicato il metodo slugify()
.
Ora possiamo sostituire i veri nomi delle colonne con i loro
corrispettivi virtuali nella rotta job_show_user
:
job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get]
Ora abbiamo gli URL che ci aspettavamo:
http://www.jobeet.com.localhost/frontend_dev.php/job/sensio-labs/paris-france/1/web-developer
Ma siamo solo a metà strada. La rotta può generare un URL basato su un
oggetto, ma può anche trovare l'oggetto relativo a un URL dato. L'oggetto
relativo può essere recuperato col metodo getObject()
dell'oggetto rotta.
Quando una richiesta in arrivo viene esaminata, il routing memorizza
la rotta corrispondente in modo che possa essere usata nelle azioni.
Quindi, cambiamo il metodo executeShow()
per usare l'oggetto rotta per
recuperare l'oggetto Jobeet
:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); $this->forward404Unless($this->job); } // ... }
Se si prova a recuperare un lavoro per una id
sconosciuta, si otterrà
una pagina di errore 404, ma il messaggio di errore è cambiato:
Questo perché l'errore 404 è stato lanciato automaticamente dal metodo
getRoute()
. Quindi possiamo semplificare ulteriormente il metodo
executeShow
:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); } // ... }
tip
Se non si vuole che la rotta generi un errore 404, si può impostare l'opzione
di routing allow_empty
a true
.
note
L'oggetto collegato a una rotta è caricato in modo pigro. Viene
recuperato dal database solo se si richiama il metodo getRoute()
.
Il routing nelle azioni e nei template
In un template, l'helper url_for()
converte un URI interno in un URL esterno.
Alcuni altri helper di symfony accettano anche un URI interno come parametro, come
l'helper link_to()
, che genera un tag <a>
:
<?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>
Genera il seguente codice HTML:
<a href="/job/sensio-labs/paris-france/1/web-developer">Web Developer</a>
Sia url_for()
che link_to()
possono generare anche URL assolute:
url_for('job_show_user', $job, true); link_to($job->getPosition(), 'job_show_user', $job, true);
Se si vuole generare un URL da un'azione, si può usare il metodo generateUrl()
:
$this->redirect($this->generateUrl('job_show_user', $job));
Classi di raggruppamento delle rotte
Per il modulo job
, abbiamo già personalizzato la rotta dell'azione show
,
ma gli URL per gli altri metodi (index
, new
, edit
, create
, update
,
e delete
) sono ancora gestiti dalla rotta default
:
default: url: /:module/:action/*
La rotta default
è un ottimo modo per iniziare a scrivere codice senza definire
troppe rotta. Ma siccome tale rotta agisce come "acchiappa-tutto", non può essere
configurata per necessità specifiche.
Siccome tutte le azioni job
sono legate alla classe del modello JobeetJob
,
possiamo definire facilmente una rotta personalizzata sfPropelRoute
per ciascuno,
come abbiamo già fatto per l'azione show
. Ma siccome il modulo job
definisce
le classiche sette azioni possibili per un modello, possiamo anche usare la classe
sfPropelRouteCollection
.
Apriamo il file routing.yml
e modifichiamolo come segue:
// apps/frontend/config/routing.yml job: class: sfPropelRouteCollection options: { model: JobeetJob } job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get] # default rules homepage: url: / param: { module: job, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*
La rotta job
qui sopra è una scorciatoia che genera automaticamente le seguenti
sette rotte sfPropelRoute
:
job: url: /job.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: list } param: { module: job, action: index, sf_format: html } requirements: { sf_method: get } job_new: url: /job/new.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: new, sf_format: html } requirements: { sf_method: get } job_create: url: /job.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: create, sf_format: html } requirements: { sf_method: post } job_edit: url: /job/:id/edit.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: edit, sf_format: html } requirements: { sf_method: get } job_update: url: /job/:id.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: update, sf_format: html } requirements: { sf_method: put } job_delete: url: /job/:id.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: delete, sf_format: html } requirements: { sf_method: delete } job_show: url: /job/:id.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show, sf_format: html } requirements: { sf_method: get }
note
Alcune rotte generate da sfPropelRouteCollection
hanno lo stesso URL. Il
routing è comunque in grado di usarle perché hanno differenti metodi HTTP
come richiesta.
Le rotte job_delete
e job_update
richiedono metodi HTTP che non sono
supportati dai browser (rispettivamente DELETE
e PUT
). Ma funzionano perché
symfony li simula. Apriamo il template _form.php
per vedere un esempio:
// apps/frontend/modules/job/templates/_form.php <form action="..." ...> <?php if (!$form->getObject()->isNew()): ?> <input type="hidden" name="sf_method" value="PUT" /> <?php endif; ?> <?php echo link_to( 'Delete', 'job/delete?id='.$form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?') ) ?>
Tutti gli helper di symfony possono essere istruiti per simulare qualsiasi
metodo HTTP si vuole, passando il parametro speciale sf_method
.
note
symfony ha altri parametri speciali simili a sf_method
, tutti che iniziano
col prefisso sf_
. Nelle rotte generate viste sopra, se ne può vedere un altro:
sf_format
, che sarà spiegato in un giorno successivo.
Debug delle rotte
Quando si usa un insieme di rotte, a volte è utile elencare le rotte generate.
Il task app:routes
mostra tutte le rotte per una data applicazione:
$ php symfony app:routes frontend
Si possono ottenere anche molte informazioni di debug per una rotta passando il suo nome come parametro aggiuntivo:
$ php symfony app:routes frontend job_edit
Rotte di default
È una buona pratica definire rotte per tutti i proprio URL.
Siccome la rotta job
definisce tutte le rotte necessarie per
descrivere l'applicazione Jobeet, si possono rimuovere o
commentare le rotte di default nel file di configurazione routing.yml
:
// apps/frontend/config/routing.yml #default_index: # url: /:module # param: { action: index } # #default: # url: /:module/:action/*
L'applicazione Jobeet deve funzionare come prima.
A domani
Questo giorno è stato riempito di nuove informazioni. Abbiamo imparato come usare il framework del routing di syfmony e come disaccoppiare le nostre URL dall' implementazione tecnica.
Nel tutorial di domani non introdurremo nuovi concetti, ma spenderemo un po' di tempo per approfondire quello che abbiamo visto finora.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.