Hier a été emballé avec beaucoup d'informations. Avec très peu de lignes de code PHP, l'admin generator de symfony permet au développeur de créer des interfaces backend en quelques minutes.
Aujourd'hui, nous allons découvrir comment symfony gère les données persistantes entre les requêtes HTTP. Comme vous le savez, le protocole HTTP est apatride, ce qui signifie que chaque requête est indépendante de ses précédentes ou suivantes. Les sites web modernes ont besoin d'un moyen de maintenir les données entre les requêtes pour améliorer l'expérience utilisateur.
Une session d'utilisateur peut être identifiée à l'aide d'un cookie.
Dans Symfony, le développeur n'a pas besoin de manipuler la session directement, mais
utilise plutôt l'objet sfUser
, qui représente l'utilisateur final de l'application.
Flashes d'utilisateur
Nous avons déjà vu l'objet user dans l'action avec des flashes. Un flash est un message éphémère stocké dans la session de l'utilisateur qui sera automatiquement supprimé après la requête suivante. Il est très utile lorsque vous avez besoin d'afficher un message à l'utilisateur après une redirection. L'admin generator utilise beaucoup les flashes pour afficher les informations à l'utilisateur chaque fois qu'un emploi est enregistré, effacé ou prolongé.
Un flash est défini en utilisant la méthode setFlash()
de 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)); }
Le premier argument est l'identifiant du flash et le second est le message
à afficher. Vous pouvez définir les flashes que vous voulez, mais notice
et
error
sont les deux des algorithmes les plus utilisés (ils sont largement utilisés par
l'admin generator).
Il appartient au développeur d'inclure le message flash dans
les Templates. Pour Jobeet, ils sont affichés par le 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; ?>
Dans un Template, l'utilisateur est accessible via la variable spéciale sf_user
.
note
Certains objets de symfony sont toujours accessibles dans les Templates, sans qu'il
soit nécessaire de les passer explicitement à l'action : sf_request
, sf_user
et
sf_response
.
Les attributs d'utilisateur
Malheureusement, les histoires d'utilisateurs de Jobeet n'ont aucune exigence qui incluent le stockage de quelque chose dans la session utilisateur. Ajoutons donc une nouvelle exigence : pour faciliter la navigation dans l'emploi, les trois derniers emplois vus par l'utilisateur doivent être affiché dans le menu avec des liens pour revenir à la page des emplois par la suite.
Quand un utilisateur accède à une page d'emploi, l'objet affiché emploi doit être ajoutée dans l'historique de l'utilisateur et stocké dans la session :
// 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()); // add the current job at the beginning of the array array_unshift($jobs, $this->job->getId()); // store the new job history back into the session $this->getUser()->setAttribute('job_history', $jobs); } // ... }
note
Nous aurions pu en pratique stocker les objets JobeetJob
directement dans la session.
Cela est fortement déconseillée, car les variables de session sont sérialisés entre les
requêtes. Et lorsque la session est chargée, les objets JobeetJob
sont
dé-sérialisés et peuvent être "bloqués", s'ils ont été modifiés ou supprimés
entre-temps.
getAttribute()
, setAttribute()
Pour un identifiant donné, la méthode sfUser::getAttribute()
récupère les valeurs de
la session de l'utilisateur. Inversement, la méthode setAttribute()
stocke toutes
les variables PHP dans la session pour un identifiant donné.
La méthode getAttribute()
prend également une valeur facultative par défaut qu'elle
retourne si l'identifiant n'est pas encore défini.
note
La valeur par défaut prise par la méthode getAttribute()
est un raccourci pour :
if (!$value = $this->getAttribute('job_history')) { $value = array(); }
La classe myUser
Afin de mieux respecter la séparation des préoccupations, nous allons déplacer le code
dans la classe myUser
. La classe myUser
substitue la classe de base de symfony par défaut
sfUser
avec les comportements spécifiques
de l'application :
// 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)); } } }
Le code a également été modifiées afin de prendre en compte toutes les exigences :
!in_array($job->getId(), $ids)
: Un emploi ne peut être stocké deux fois dans l'historiquearray_slice($ids, 0, 3)
: Seuls les trois derniers emplois vus par l'utilisateur sont affichés
Dans le layout, ajoutez le code suivant avant que la variable $sf_content
soit
affichée :
// 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>
La mise en page utilise une nouvelle méthode getJobHistory()
pour récupérer l'historique des emplois actuels :
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function getJobHistory() { $ids = $this->getAttribute('job_history', array()); return JobeetJobPeer::retrieveByPKs($ids); } // ... }
La méthode getJobHistory()
utilise la méthode Propel retrieveByPKs()
pour
récupérer plusieurs objets JobeetJob
dans un seul appel.
sfParameterHolder
Pour compléter l'API de l'historique des emplois, nous allons ajouter une méthode pour réinitialiser l'historique :
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function resetJobHistory() { $this->getAttributeHolder()->remove('job_history'); } // ... }
Les attributs de l'utilisateur sont gérés par un objet de la classe sfParameterHolder
.
Les méthodes getAttribute()
et setAttribute()
sont des méthodes proxy pour
getParameterHolder()->get()
et getParameterHolder()->set()
. Comme la méthode
remove()
n'a pas de méthode de proxy dans sfUser
, vous devez utiliser l'objet
titulaire du paramètre directement.
note
La classe sfParameterHolder
est aussi utilisé par sfRequest
pour stocker ses paramètres.
Sécurité de l'application
Authentification
Comme beaucoup d'autres fonctionnalités symfony, la sécurité est gérée par
un fichier YAML, ~security.yml~
. Par exemple, vous pouvez trouver la configuration par
défaut pour l'application backend dans le répertoire config/
:
# apps/backend/config/security.yml default: is_secure: off
Si vous passez l'entrée is_secure
à on
, l'ensemble de l'application backend
nécessitera que l'utilisateur soit authentifié.
tip
Dans un fichier YAML, un booléen peut être exprimée avec les chaines true
et
false
, ou on
et off
..
Si vous jetez un oeil sur les journaux dans le web debug toolbar, vous verrez que
la méthode executeLogin()
de la classe defaultActions
est appelée à chaque page
que vous essayez d'accéder.
Quand un utilisateur non-authentifié tente d'accéder à une action sécurisé,
symfony transmet la requête à l'action login
configuré dans settings.yml
:
all: .actions: login_module: default login_action: login
note
Il n'est pas possible de sécuriser l'action de connexion afin d'éviter une récursion infinie.
tip
Comme nous l'avons vu pendant le jour 4, le même fichier de configuration peut être
défini dans plusieurs endroits. C'est également le cas pour security.yml
. Pour
sécuriser ou dé-sécuriser seulement une seule action ou un module
complet, créer un security.yml
dans le répertoire config/
du module :
index: is_secure: off all: is_secure: on
Par défaut, la classe myUser
étend
sfBasicSecurityUser
,
et non sfUser
. sfBasicSecurityUser
fournit des méthodes supplémentaires pour gérer
l'authentification des utilisateurs et l'autorisation.
Pour gérer l'authentification utilisateur, utilisez les méthodes
isAuthenticated()
et setAuthenticated()
:
if (!$this->getUser()->isAuthenticated()) { $this->getUser()->setAuthenticated(true); }
Autorisation
Lorsqu'un utilisateur est authentifié, l'accès à certaines actions peuvent être encore plus limité en définissant les credentials. Un utilisateur doit disposer des droits nécessaires pour accéder à la page :
default: is_secure: off credentials: admin
Le système de droits de symfony est assez simple et puissant. Un credential peut représenter tout ce que vous devez décrire au modèle de sécurité de l'application (comme des groupes ou des autorisations).
Pour gérer les credentials d'utilisateur, sfBasicSecurityUser
fournit plusieurs
méthodes :
// Add one or more credentials $user->addCredential('foo'); $user->addCredentials('foo', 'bar'); // Check if the user has a credential echo $user->hasCredential('foo'); => true // Check if the user has both credentials echo $user->hasCredential(array('foo', 'bar')); => true // Check if the user has one of the credentials echo $user->hasCredential(array('foo', 'bar'), false); => true // Remove a credential $user->removeCredential('foo'); echo $user->hasCredential('foo'); => false // Remove all credentials (useful in the logout process) $user->clearCredentials(); echo $user->hasCredential('bar'); => false
Pour le backend de Jobeet, nous n'utiliserons pas les credentials car nous avons qu'un seul profil: l'administrateur.
Plugins
Comme nous n'aimons pas à réinventer la roue, nous ne développerons pas l'action de connexion à partir de zéro. Au lieu de cela, nous allons installer un plugin symfony.
Une des grandes forces du framework symfony est l'écosystème de plugin. Comme nous le verrons dans les prochains jours, il est très facile de créer un plugin. Il est aussi assez puissant, car un plugin peut contenir n'importe quoi, de la configuration aux modules et aux ressources.
Aujourd'hui, nous allons installer
sfGuardPlugin
pour
sécuriser l'application backend :
$ php symfony plugin:install sfGuardPlugin
La tâche plugin:install
installe un plugin par son nom. Tous les plugins sont
stockés dans le répertoire plugins/
et chacun a son propre répertoire nommé d'après
le nom du plugin.
note
PEAR doit être installé pour que la tâche plugin:install
fonctionne.
Lorsque vous installez un plugin avec la tâche plugin:install
, symfony installe sa
dernière version stable. Pour installer une version spécifique d'un plugin, passer
l'option --release
.
La page du plugin liste toutes les versions disponibles regroupés selon la version de symfony.
Comme un plugin est autonome dans un répertoire, vous pouvez également
télécharger le package
à partir du site web de symfony et le décompressez, ou bien faire un
lien svn:externals
, grâce à son
dépôt Subversion.
tip
N'oubliez pas de vous assurer que le plugin est activé après que vous l'ayez installé si vous n'avez
pas utilisé la méthode enableAllPluginsExcept()
dans votre classe config/ProjectConfiguration.class.php
.
Sécurité du backend
Chaque plugin a un fichier README qui explique comment le configurer.
Voyons comment configurer le nouveau plugin. Comme le plugin fournit plusieurs nouvelles classes du modèle pour gérer les utilisateurs, les groupes et les autorisations, vous devez reconstruire votre modèle :
$ php symfony propel:build-all-load --no-confirmation
tip
N'oubliez pas que la tâche propel:build-all-load
supprime toutes les tables existantes
avant de les re-créer. Pour éviter cela, vous pouvez construire les modèles, les
formulaires, et les filtres, puis, créer les nouvelles tables en exécutant les instructions
SQL générées stockées dans data/sql/
.
Comme toujours, lorsque de nouvelles classes sont créées, vous devez vider le cache symfony :
$ php symfony cc
Comme sfGuardPlugin
ajoute plusieurs méthodes à la classe user, vous avez besoin de
changer la classe de base de myUser
en sfGuardSecurityUser
:
// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser { }
sfGuardPlugin
fournit une action signin
dans le module sfGuardAuth
pour
authentifier les utilisateurs.
Editez le fichier settings.yml
pour changer l'action par défaut utilisée pour
la page du login :
# apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth] # ... .actions: login_module: sfGuardAuth login_action: signin # ...
Comme les plugins sont partagées par toutes les applications d'un projet, vous devez
explicitement activer les modules que vous souhaitez utiliser en les ajoutant
dans le paramètre enabled_modules
|enabled_modules
(Paramètre).
La dernière étape est de créer un utilisateur administrateur :
$ php symfony guard:create-user fabien SecretPass $ php symfony guard:promote fabien
tip
Le sfGuardPlugin
fournit des tâches pour gérer les utilisateurs, les groupes et les
autorisations par la ligne de commande. Utilisez la tâche list
pour
lister toutes les tâches qui appartiennent à l'espace de nom de guard
:
$ php symfony list guard
Lorsque l'utilisateur n'est pas authentifié, nous avons besoin de masquer la barre de menu :
// 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; ?>
Et lorsque l'utilisateur est authentifié, nous avons besoin d'ajouter un lien de déconnexion dans le menu :
// apps/backend/templates/layout.php <li><?php echo link_to('Logout', '@sf_guard_signout') ?></li>
tip
Pour lister toutes les routes fournies par sfGuardPlugin
, utiliser la tâche app:routes
.
Pour améliorer, encore plus, le backend de Jobeet, nous allons ajouter un nouveau module
pour gérer les utilisateurs administrateurs. Heureusement, sfGuardPlugin
fournit un tel
module. Comme pour le module sfGuardAuth
, vous devez l'activer dans settings.yml
:
// apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth, sfGuardUser]
Ajoutez un lien dans le menu :
// apps/backend/templates/layout.php <li><?php echo link_to('Users', '@sf_guard_user') ?></li>
Nous avons finis !
Tester l'utilisateur
Le tutoriel d'aujourd'hui n'est pas terminée tant que nous n'avons pas encore parlé
des tests utilisateurs. Comme le navigateur symfony simule les cookies, il est
assez facile de tester les comportements des utilisateurs en utilisant le testeur intégré sfTesterUser
.
Actualisons les tests fonctionnels pour le menu des fonctions que
nous avons ajouté aujourd'hui. Ajoutez le code suivant à la fin de la tâche des tests
fonctionnels du module 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() ;
Pour faciliter le test, nous avons d'abord recharger les données des jeux de test et redémarrer le navigateur pour commencer par une session propre.
La méthode isAttribute()
vérifie un attribut utilisateur donné.
note
Le testeur sfTesterUser
fournit également les méthodes isAuthenticated()
et
hasCredential()
pour tester les authentifications et les autorizations de l'utilisateur.
À demain
Les classes d'utilisateurs de symfony sont un moyen agréable pour la
gestion des sessions PHP. Couplé avec l'excellent système de plugin de symfony et
le plugin sfGuardPlugin
, nous avons été en mesure de sécuriser le backend de
Jobeet en quelques minutes. Et nous avons même ajouté une interface propre pour gérer
nos utilisateurs administrateurs gratuitement, grâce aux modules fournis par le plugin.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.