Giorno 13: L'utente
La giornata di ieri era piena di informazioni. Con pochissime linee di codice PHP, l'admin generator di symfony permette allo sviluppatore di creare interfacce di backend in pochi minuti.
Oggi scopriremo come symfony gestisce i dati persistenti tra le richieste HTTP. Come dovreste sapere, il protocollo HTTP è senza stato, il che significa che ogni richiesta è indipendente da quelle che la precedono o la seguono. I siti web moderni hanno bisogno di mantenere i dati persistenti tra le varie richieste, per migliorare la user experience dell'utente.
Una sessione utente può essere identificata usando un cookie. In symfony
lo sviluppatore non ha bisogno di manipolare la sessione in modo diretto,
piuttosto utilizza l'oggetto sfUser
, che rappresenta l'utente finale
dell'applicazione.
Flash utente
Abbiamo già visto l'oggetto utente in azione con i flash. Un flash è un messaggio salvato nella sessione dell'utente, che viene cancellato automaticamente dopo la prima richiesta successiva. È molto comodo quando si ha bisogno di mostrare un messaggio all'utente dopo un rinvio. L'admin generator fa un largo uso di messaggi flash per dare feedback all'utente ogni qual volta un'offerta di lavoro viene salvata, cancellata o rinnovata.
Un flash è impostato usando il metodo setFlash()
di sfUser
:
// apps/frontend/modules/job/actions/actions.class.php public function executeExtend(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $this->forward404Unless($job->extend()); $this->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', $job->getExpiresAt('m/d/Y'))); $this->redirect($this->generateUrl('job_show_user', $job)); }
Il primo parametro è l'identificatore del flash, mentre il secondo è il
messaggio da mostrare. Potete definire tutti i flash che volete, ma notice
e error
sono due dei più comuni (sono usati parecchio dall'admin generator).
È lasciato allo sviluppatore includere i messaggi flash nei template. Per
Jobeet vengono visualizzati dal layout.php
:
// apps/frontend/templates/layout.php <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div> <?php endif ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div> <?php endif ?>
Nei template la sessione utente è accessibile dalla variabile speciale $sf_user
.
note
Alcuni oggetti di symfony sono sempre accessibili nei template senza il
bisogno di passarli dall'action: $sf_request
, $sf_user
e $sf_response
.
Attributi utente
Sfortunatamente le user story di Jobeet non richiedono che qualcosa possa essere incluso nella sessione. Quindi aggiungiamo un nuovo requisito: per rendere più semplice la navigazione le ultime tre offerte visualizzate dall'utente dovranno essere mostrate nel menù con i link per tornare alla pagina dell'offerta.
Quando un utente accede alla pagina di un'offerta di lavoro l'oggetto stesso dell'offerta visualizzata deve essere aggiunto nella cronologia dell'utente e salvato nella sessione:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); // fetch jobs already stored in the job history $jobs = $this->getUser()->getAttribute('job_history', array()); // aggiunge il lavoro corrente all'inizio dell'array array_unshift($jobs, $this->job->getId()); // memorizza nuovamente in sessione la cronologia dei lavori $this->getUser()->setAttribute('job_history', $jobs); } // ... }
note
Avremmo potuto salvare gli oggetti JobeetJob
direttamente nella sessione.
Vi scoraggiamo fortemente di agire in questo modo perché le variabili di sessione
vengono serializzate tra le richieste. Quando la sessione viene caricata
gli oggetti JobeetJob
vengono de-serializzati e possono presentare dati
incoerenti se nel frattempo sono stati modificati o cancellati.
getAttribute()
, setAttribute()
Dato un identificatore il metodo sfUser::getAttribute()
prende i valori
dalla sessione utente. Al contrario il metodo setAttribute()
salva
qualsiasi variabile PHP nella sessione con l'identificatore passato.
Il metodo getAttribute()
prende un valore opzionale per segnalare se
l'identificatore passato non è ancora stato definito.
note
Il valore di default preso dal metodo getAttribute()
è una scorciatoia per:
if (!$value = $this->getAttribute('job_history')) { $value = array(); }
La classe myUser
Per rispettare meglio la separazione degli argomenti, spostiamo il codice
nella classe myUser
. La classe myUser
eredita dalla classe base predefinita
di symfony sfUser
e specifica dei comportamenti per l'applicazione:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); $this->getUser()->addJobToHistory($this->job); } // ... } // apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function addJobToHistory(JobeetJob $job) { $ids = $this->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $this->setAttribute('job_history', array_slice($ids, 0, 3)); } } }
Il codice è anche stato cambiato per tener conto di tutti i requisiti:
!in_array($job->getId(), $ids)
: Un lavoro non può essere memorizzatoarray_slice($ids, 0, 3)
: Solo gli ultimi tre lavori visti dall'utente sono visualizzati
Nel layout, aggiungiamo il codice seguente prima che la variabile
$sf_content
sia mostrata:
// apps/frontend/templates/layout.php <div id="job_history"> Recent viewed jobs: <ul> <?php foreach ($sf_user->getJobHistory() as $job): ?> <li> <?php echo link_to($job->getPosition().' - '.$job->getCompany(), 'job_show_user', $job) ?> </li> <?php endforeach ?> </ul> </div> <div class="content"> <?php echo $sf_content ?> </div>
Il layout usa un nuovo metodo getJobHistory()
per recuperare la cronologia
dei lavori:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function getJobHistory() { $ids = $this->getAttribute('job_history', array()); return JobeetJobPeer::retrieveByPKs($ids); } // ... }
Il metodo getJobHistory()
usa il metodo di Propel retrieveByPKs()
per
recuperare diversi oggetti JobeetJob
in una sola chiamata.
sfParameterHolder
Per completare le API della cronologia dei lavori, aggiungiamo un metodo per azzerare la cronologia:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function resetJobHistory() { $this->getAttributeHolder()->remove('job_history'); } // ... }
Gli attributi dell'utente sono gestiti da un oggetto della classe
sfParameterHolder
. I metodi getAttribute()
e setAttribute()
sono scorciatoie per getParameterHolder()->get()
e getParameterHolder()->set()
.
Siccome il metodo remove()
non ha scorciatoie in sfUser
, occorre
usare direttamente l'oggetto contenitore dei parametri.
note
La classe sfParameterHolder
è usata anche da sfRequest
per memorizzare i suoi parametri.
Sicurezza dell'applicazione
Autenticazione
Come molte altre feature di symfony, la sicurezza è gestita da un file YAML,
security.yml
. Ad esempio, si può trovare la configurazione di default per
l'applicazione di backend nella cartella config/
:
# apps/backend/config/security.yml default: is_secure: false
Se si cambia la voce is_secure
a true
, l'intera applicazione di backend
richiederà l'autenticazione dell'utente.
tip
In un file YAML, un booleano può essere espresso con la stringa true
e
false
.
Se si dà un'occhiata ai log nella web debug toolbar, si noterà che il metodo
executeLogin()
della classe defaultActions
è richiamato a ogni pagina
in cui si tenta di accedere.
Quando un utente non autenticato prova ad accedere a una
azione messa in sicurezza, symfony rimanda la richiesta all'azione
login
configurata in settings.yml
:
all: .actions: login_module: default login_action: login
note
Non è possibile mettere in sicurezza l'azione di login, per evitare una ricorsione infinita.
tip
Come abbiamo visto durante il giorno 5, lo stesso file di configurazione può
essere definito in diversi posti. Questo è anche il caso di security.yml
.
Per mettere in sicurezza (o togliere) una singola azione o un intero modulo,
basta creare un file security.yml
nella cartella config/
del modulo:
index: is_secure: false all: is_secure: true
Di default, la classe myUser
estende
sfBasicSecurityUser
,
e non sfUser
. sfBasicSecurityUser
fornisce metodi addizionali per gestire
l'autenticazione e l'autorizzazione degli utenti.
Per gestire l'autenticazione degli utenti, usare i metodi isAuthenticated()
e
setAuthenticated()
:
if (!$this->getUser()->isAuthenticated()) { $this->getUser()->setAuthenticated(true); }
Autorizzazione
Quando un utente è autenticato, l'accesso ad alcune azioni può essere ulteriormente ristretto definendo delle credenziali. Un utente deve avere le credenziali richieste per accedere alla pagina:
default: is_secure: false credentials: admin
Il sistema di credenziali di symfony è molto semplice e potente. Una credenziale può rappresentare qualsiasi cosa si abbia bisogno di descrivere nel modello di sicurezza dell'applicazione (come gruppi o permessi).
sidebar
Credenziali complesse
La voce credentials
di security.yml
supporta operazioni booleane per
descrivere requisiti di credenziali complesse.
Se un utente deve avere le credenziali A e B, includi le credenziali tra parentesi quadre:
index: credentials: [A, B]
Se un utente deve avere le credenziali A o B, includi le credenziali tra doppie parentesi quadre:
index: credentials: [[A, B]]
Si possono anche mescolare le parentesi quadre per descrivere ogni tipo di espressione booleana con qualsiasi numero di credenziali.
Per gestire le credenziali degli utenti, sfBasicSecurityUser
fornisce diversi
metodi:
// Aggiunge una o più credenziali $user->addCredential('foo'); $user->addCredentials('foo', 'bar'); // Verifica se l'utente ha una credenziale echo $user->hasCredential('foo'); => true // Verifica se l'utente ha entrambe le credenziali echo $user->hasCredential(array('foo', 'bar')); => true // Verifica se l'utente ha una delle credenziali echo $user->hasCredential(array('foo', 'bar'), false); => true // Rimuove una credenziale $user->removeCredential('foo'); echo $user->hasCredential('foo'); => false // Rimuove tutte le credenziali (utile per il logout) $user->clearCredentials(); echo $user->hasCredential('bar'); => false
Per il backend di Jobeet, non ci servono credenziali, perché abbiamo un solo profilo: l'amministratore,
Plugin
Siccome non ci piace reinventare la ruota, non vogliamo sviluppare un'azione di login da zero. Invece, installeremo un plugin di symfony.
Uno dei grandi punti di forza del framework symfony è l' ecosistema di plugin. Come vedremo nei prossimi giorni, è molto semplice creare un plugin. È anche molto potente, perché un plugin può contenere ogni cosa, dalla configurazione ai moduli, agli asset.
Oggi installeremo
sfGuardPlugin
per
rendere sicura l'applicazione di backend:
$ php symfony plugin:install sfGuardPlugin
Il task plugin:install
installa un plugin in base al nome. Tutti i plugin sono
memorizzati sotto la cartella plugins/
e ognuno ha la sua cartella, che prende
il nome dal plugin stesso.
note
Per poter usare il task plugin:install
occorre avere PEAR installato.
Quando si installa un plugin con il task plugin:install
, symfony installa
l'ultima versione stabile. Per installare una versione specifica di un plugin,
usare l'opzione --release
.
La pagina dei plugin elenca tutte le versioni disponibili, raggruppate per versione di symfony.
Essendo un plugin contenuto in una cartella, si può anche
scaricare il pacchetto
dal sito di symfony ed estrarlo, oppure creare un collegamento svn:externals
al
suo repository di Subversion.
Sicurezza nel backend
Ogni plugin ha un file README che spiega come configurarlo.
Vediamo come configurare un nuovo plugin. Siccome il plugin fornisce diverse nuove classi del modello per gestire utenti, gruppi e permessi, è necessario ricostruire il modello:
$ php symfony propel:build --all --and-load --no-confirmation
tip
Ricordate che il task propel:build --all --and-load
rimuove tutte le tabelle esistenti
prima di ricrearle. Per evitarlo, si può costruire il modello, i form e i
filtri, e poi creare le nuove tabelle eseguendo le istruzioni SQL generate
e memorizzate in data/sql/
.
Poiché sfGuardPlugin
aggiunge molti metodi alla classe utente, occorre cambiare
la classe base di myUser
in sfGuardSecurityUser
:
// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser { }
sfGuardPlugin
fornisce un'azione signin
nel modulo sfGuardAuth
per autenticare gli utenti:
Modifichiamo il file settings.yml
per cambiare l'azione di default
usata per la pagina di login:
# apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth] # ... .actions: login_module: sfGuardAuth login_action: signin # ...
Poiché i plugin sono condivisi da tutte le applicazioni di un progetto, occorre
abilitare esplicitamente i moduli che si vogliono usare, aggiungendoli
nell'impostazione enabled_modules
.
L'ultimo passo è creare un utente amministratore:
$ php symfony guard:create-user fabien SecretPaSS $ php symfony guard:promote fabien
tip
sfGuardPlugin
fornisce dei task per gestire gli utenti, i gruppi e i permessi
dalla linea di comando. Si può usare il task list
per elencare tutti i task
che appartengono al namespace guard
:
$ php symfony list guard
Quando l'utente non è autenticato, la barra del menù va nascosta:
// apps/backend/templates/layout.php <?php if ($sf_user->isAuthenticated()): ?> <div id="menu"> <ul> <li><?php echo link_to('Jobs', 'jobeet_job') ?></li> <li><?php echo link_to('Categories', 'jobeet_category') ?></li> </ul> </div> <?php endif ?>
E quando l'utente è autenticato, nel menù va aggiunto un link per uscire:
// apps/backend/templates/layout.php <li><?php echo link_to('Logout', 'sf_guard_signout') ?></li>
tip
Per elencare tutte le rotte fornite da sfGuardPlugin
, usare il task
app:routes
.
Per affinare ancora di più il backend, aggiungiamo un nuovo modulo per
gestire gli utenti amministratori. Fortunatamente, sfGuardPlugin
fornisce
un modulo del genere. Come per il modulo sfGuardAuth
, occorre abilitarlo
in settings.yml
:
// apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth, sfGuardUser]
Aggiungiamo un link nel menù:
// apps/backend/templates/layout.php <li><?php echo link_to('Users', 'sf_guard_user') ?></li>
Abbiamo finito!
Test degli utenti
Il tutorial di oggi non è finito, perché non abbiamo ancora parlato di test.
Poiché il browser di symfony simula i cookie, è facile testare i comportamenti
degli utenti, usando il tester incluso
sfTesterUser
.
Aggiorniamo i test funzionali per le feature del menù che abbiamo aggiunto
oggi. Aggiungiamo il codice seguente alla fine dei test funzionali
del modulo job
:
// test/functional/frontend/jobActionsTest.php $browser-> info('4 - User job history')-> loadData()-> restart()-> info(' 4.1 - When the user access a job, it is added to its history')-> get('/')-> click('Web Developer', array(), array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))-> end()-> info(' 4.2 - A job is not added twice in the history')-> click('Web Developer', array(), array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))-> end() ;
Per facilitare i test, prima ricarichiamo i dati delle fixture e facciamo ripartire il browser per iniziare una sessione pulita.
Il metodo isAttribute()
verifica l'attributo di un utente.
note
Il tester sfTesterUser
fornisce anche i metodi isAuthenticated()
e
hasCredential()
, per testare l'autenticazione e le autorizzazioni
dell'utente.
A domani
Le classi utente di symfony sono un bel modo per astrarre la gestione
delle sessioni di PHP. Insieme a un grande sistema di plugin
di symfony e al plugin sfGuardPlugin
, possiamo mettere in
sicurezza il backend di Jobeet in pochi minuti. Ed abbiamo anche
aggiunto un'interfaccia pulita per gestire gli utenti amministratori,
grazie ai moduli forniti dal plugin.
Domani sarà l'ultimo giorno della seconda settimana del tutorial Jobeet, e proveremo a renderlo di nuovo utile.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.