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.

Flash

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 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.

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.

Cronologia dei lavori

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 da sfRequest 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.

Login

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.

Web debug

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 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|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).

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 classe config/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 in data/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~.

login con sfGuardPlugin

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 task list per elencare tutti i task che appartengono al namespace guard:

$ 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 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:

/

Aggiungiamo un link nel menù:

// apps/backend/templates/layout.php
<li><?php echo link_to('Users', 'sf_guard_user') ?></li>
 

menù del backend

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 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.

ORM