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|Sessione~ utente può essere identificata usando un ~cookie|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|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|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->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', $job->getDateTimeObject('expires_at')->format('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
.
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|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); } // ... }
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 oggettiJobeetJob
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.
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 memorizzato -
array_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);
}
public function getJobHistory() { $ids = $this->getAttribute('job_history', array());
if (!empty($ids))
{
return Doctrine_Core::getTable('JobeetJob')
->createQuery('a')
->whereIn('a.id', $ids)
->execute()
;
}
return array();
}
// ...
}
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.
La classe
sfParameterHolder
è usata anche dasfRequest
per memorizzare i suoi parametri.
Sicurezza dell'applicazione
Autenticazione
Come molte altre feature di symfony, la ~sicurezza|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.
In un file YAML, un booleano può essere espresso con la stringa
true
efalse
.
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|Sicurezza~, symfony rimanda la richiesta all'azione login
configurata in settings.yml
:
---
all:
.actions:
login_module: default
login_action: login
Non è possibile mettere in sicurezza l'azione di login, per evitare una ricorsione infinita.
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|Restrizione di accesso~ o un intero modulo, basta creare un file ~security.yml
~ nella cartellaconfig/
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|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).
Credenziali complesse
La voce
credentials
disecurity.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|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
Oggi installeremo sfDoctrineGuardPlugin
per rendere sicura l'applicazione di backend:
$ php symfony plugin:install sfDoctrineGuardPlugin
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.
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. 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.
Assicurarsi che il plugin sia abilitato dopo la sua installazione, a meno di non usare il metodo
enableAllPluginsExcept()
nella classeconfig/ProjectConfiguration.class.php
.
Sicurezza nel backend
Ogni plugin ha un file README 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 $ php symfony doctrine:build --all --and-load --no-confirmation
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 indata/sql/
.
Poiché sfGuardPlugin
aggiunge molti metodi alla classe utente, occorre cambiare la classe base di myUser
in sfGuardSecurityUser
: Poiché sfDoctrineGuardPlugin
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: sfDoctrineGuardPlugin
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|Modulo~ 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
sfGuardPlugin
fornisce dei task per gestire gli utenti, i gruppi e i permessi dalla ~linea di comando|Linea di comando~. Si può usare il tasklist
per elencare tutti i task che appartengono al namespaceguard
:$ php symfony list guard
Quando l'utente non è ~autenticato|Autenticazione~, 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>
Per elencare tutte le rotte fornite da
sfGuardPlugin
, usare il taskapp: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
:
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|Cookie~, è facile testare i comportamenti degli utenti, usando il tester incluso ~sfTesterUser
~.
Aggiorniamo i ~test funzionali|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.
Il tester
sfTesterUser
fornisce anche i metodiisAuthenticated()
ehasCredential()
, 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.
ORM
インデックス
Document Index
関連ページリスト
Related Pages
- Giorno 1: Impostare il progetto
- Giorno 2: Il progetto
- Giorno 3: Il ~Modello dei dati~
- Giorno 4: Il controllore e la vista
- Giorno 5: Il routing
- Giorno 6: Di più sul Modello
- Giorno 7: Giocare con la pagina delle categorie
- Giorno 8: I test unitari
- Giorno 9: I test funzionali
- Giorno 10: Form
- Giorno 11: Testare i Form
- Giorno 12: Admin Generator
- Giorno 13: L'utente
- Giorno 14: Feed
- Giorno 15: Web Service
- Giorno 16: Inviare ~email|Email~
- Giorno 17: Ricerca
- Giorno 18: ~AJAX~
- Giorno 19: Internazionalizzazione e Localizzazione
- Giorno 20: I plugin
- Giorno 21: La Cache
- Giorno 22: Il rilascio
- Giorno 23: Un altro sguardo a symfony
- Appendice B - Licenza
- Riconoscimenti
日本語ドキュメント
Japanese Documents
- 2011/01/18 Chapter 17 - Extending Symfony
- 2011/01/18 The generator.yml Configuration File
- 2011/01/18 Les tâches
- 2011/01/18 Emails
- 2010/11/26 blogチュートリアル(8) ビューの作成