Yesterday was packed with a lot of information. With very few PHP lines of code, the symfony admin generator allows the developer to create backend interfaces in a matter of minutes.
Today, we will discover how symfony manages persistent data between HTTP requests. As you might know, the HTTP protocol is stateless, which means that each request is independent from its preceding or proceeding ones. Modern websites need a way to persist data between requests to enhance the user experience.
A user session can be identified using a cookie. In symfony,
the developer does not need to manipulate the session directly, but rather uses
the sfUser object, which represents the application end user.
User Flashes
We have already seen the user object in action with flashes. A flash is an ephemeral message stored in the user session that will be automatically deleted after the very next request. It is very useful when you need to display a message to the user after a redirect. The admin generator uses flashes a lot to display feedback to the user whenever a job is saved, deleted, or extended.

A flash is set by using the setFlash() method of 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)); }
The first argument is the identifier of the flash and the second one is the
message to display. You can define whatever flashes you want, but notice and
error are two of the more common ones (they are used extensively by the admin
generator).
It is up to the developer to include the flash message in the templates. For
Jobeet, they are output by the 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 ?>
In a template, the user is accessible via the special $sf_user variable.
note
  Some symfony objects are always accessible in the templates, without the need
  to explicitly pass them from the action: $sf_request, $sf_user, and
  $sf_response.
User Attributes
Unfortunately, the Jobeet user stories have no requirement that includes storing something in the user session. So let's add a new requirement: to ease job browsing, the last three jobs viewed by the user should be displayed in the menu with links to come back to the job page later on.
When a user access a job page, the displayed job object needs to be added in the user history and stored in the 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
  We could have feasibly stored the JobeetJob objects directly into the session.
  This is strongly discouraged because the session variables are serialized
  between requests. And when the session is loaded, the JobeetJob objects
  are de-serialized and can be "stalled" if they have been modified or
  deleted in the meantime.
getAttribute(), setAttribute()
Given an identifier, the sfUser::getAttribute() method fetches values from the
user session. Conversely, the setAttribute() method stores any PHP variable in
the session for a given identifier.
The getAttribute() method also takes an optional default value to return if
the identifier is not yet defined.
note
  The default value taken by the getAttribute() method is a shortcut for:
if (!$value = $this->getAttribute('job_history')) { $value = array(); }
The myUser class
To better respect the separation of concerns, let's move the code to the
myUser class. The myUser class overrides the default symfony base
sfUser class with application
specific behaviors:
// 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)); } } }
The code has also been changed to take into account all the requirements:
!in_array($job->getId(), $ids): A job cannot be stored twice in the historyarray_slice($ids, 0, 3): Only the latest three jobs viewed by the user are displayed
In the layout, add the following code before the $sf_content variable is
output:
// 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>
The layout uses a new getJobHistory() method to retrieve the current job
history:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function getJobHistory() { $ids = $this->getAttribute('job_history', array()); return JobeetJobPeer::retrieveByPKs($ids); } // ... }
The getJobHistory() method uses the Propel retrieveByPKs() method to
retrieve several JobeetJob objects in one call.

sfParameterHolder
To complete the job history API, let's add a method to reset the history:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function resetJobHistory() { $this->getAttributeHolder()->remove('job_history'); } // ... }
User's attributes are managed by an object of class sfParameterHolder. The
getAttribute() and setAttribute() methods are proxy methods for
getParameterHolder()->get() and getParameterHolder()->set(). As the
remove() method has no proxy method in sfUser, you need to use the parameter
holder object directly.
note
  The sfParameterHolder
  class is also used by sfRequest to store its parameters.
Application Security
Authentication
Like many other symfony features, security is managed by a YAML file,
security.yml. For instance, you can find the default configuration for the
backend application in the config/ directory:
# apps/backend/config/security.yml default: is_secure: false
If you switch the is_secure entry to true, the entire backend application
will require the user to be authenticated.

tip
  In a YAML file, a Boolean can be expressed with the strings true and
  false.
If you have a look at the logs in the web debug toolbar, you will notice that
the executeLogin() method of the defaultActions class is called for every
page you try to access.

When an un-authenticated user tries to access a secured action,
symfony forwards the request to the login action configured in settings.yml:
all:
  .actions:
    login_module: default
    login_action: login
    note
It is not possible to secure the login action. This is to avoid infinite recursion.
tip
  As we saw during day 4, the same configuration file can be defined in 
  several places. This is also the case for security.yml. To only 
  secure or un-secure a single action or a whole module, 
  create a security.yml in the config/ directory of the module:
index: is_secure: false all: is_secure: true
By default, the myUser class extends
sfBasicSecurityUser,
and not sfUser. sfBasicSecurityUser provides additional methods to manage
user authentication and authorization.
To manage user authentication, use the isAuthenticated() and
setAuthenticated() methods:
if (!$this->getUser()->isAuthenticated()) { $this->getUser()->setAuthenticated(true); }
Authorization
When a user is authenticated, the access to some actions can be even more restricted by defining credentials. A user must have the required credentials to access the page:
default: is_secure: false credentials: admin
The credential system of symfony is quite simple and powerful. A credential can represent anything you need to describe the application security model (like groups or permissions).
To manage the user credentials, sfBasicSecurityUser provides several methods:
// 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
For the Jobeet backend, we won't use any credentials as we only have one profile: the administrator.
Plugins
As we don't like to reinvent the wheel, we won't develop the login action from scratch. Instead, we will install a symfony plugin.
One of the great strengths of the symfony framework is the plugin ecosystem. As we will see in coming days, it is very easy to create a plugin. It is also quite powerful, as a plugin can contain anything from configuration to modules and assets.
Today, we will install
sfGuardPlugin to
secure the backend application:
$ php symfony plugin:install sfGuardPlugin
The plugin:install task installs a plugin by name. All plugins are stored
under the plugins/ directory and each one has its own directory named after
the plugin name.
note
  PEAR must be installed for the plugin:install task to work.
When you install a plugin with the plugin:install task, symfony installs the
latest stable version of it. To install a specific version of a plugin, pass the
--release option.
The plugin page lists all available version grouped by symfony versions.
As a plugin is self-contained into a directory, you can also download the package
from the symfony website and unarchive it, or alternatively make an
svn:externals link to its Subversion repository.
The plugin:install task automatically enables the plugin(s) it installs by
automatically updating the ProjectConfiguration.class.php file. But if you
install a plugin via Subversion or by downloading its archive, you need to
enable it by hand in ProjectConfiguration.class.php:
// config/ProjectConfiguration.class.php class ProjectConfiguration extends sfProjectConfiguration { public function setup() { $this->enablePlugins(array('sfPropelPlugin', 'sfGuardPlugin')); } }
Backend Security
Each plugin has a README file that explains how to configure it.
Let's see how to configure the new plugin. As the plugin provides several new model classes to manage users, groups, and permissions, you need to rebuild your model:
$ php symfony propel:build --all --and-load --no-confirmation
tip
  Remember that the propel:build --all --and-load task removes all existing tables
  before re-creating them. To avoid this, you can build the models, forms, and
  filters, and then, create the new tables by executing the generated SQL
  statements stored in data/sql/.
As sfGuardPlugin adds several methods to the user class, you need to change
the base class of myUser to sfGuardSecurityUser:
// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser { }
sfGuardPlugin provides a signin action in the sfGuardAuth module to
authenticate users.
Edit the settings.yml file to change the default action used for the login
page:
# apps/backend/config/settings.yml
all:
  .settings:
    enabled_modules: [default, sfGuardAuth]
 
    # ...
 
  .actions:
    login_module:    sfGuardAuth
    login_action:    signin
 
    # ...
    As plugins are shared amongst all applications of a project, you need to
explicitly enable the modules you want to use by adding them in the
enabled_modules setting|enabled_modules (Setting).

The last step is to create an administrator user:
$ php symfony guard:create-user fabien SecretPass $ php symfony guard:promote fabien
tip
  The sfGuardPlugin provides tasks to manage users, groups, and permissions
  from the command line. Use the list task to list all tasks 
  belonging to the guard namespace:
$ php symfony list guard
When the user is not authenticated, we need to hide the menu bar:
// 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 ?>
And when the user is authenticated, we need to add a logout link in the menu:
// apps/backend/templates/layout.php <li><?php echo link_to('Logout', 'sf_guard_signout') ?></li>
tip
  To list all routes provided by sfGuardPlugin, use the app:routes task.
To polish the Jobeet backend even more, let's add a new module to manage the
administrator users. Thankfully, the plugin provides such a module. As for the
sfGuardAuth module, you need to enable it in settings.yml:
// apps/backend/config/settings.yml
all:
  .settings:
    enabled_modules: [default, sfGuardAuth, sfGuardUser]
    Add a link in the menu:
// apps/backend/templates/layout.php <li><?php echo link_to('Users', 'sf_guard_user') ?></li>

We are done!
User Testing
Day 13 is not over as we have not yet talked about user testing. As the symfony
browser simulates cookies, it is quite easy to test user behaviors by
using the built-in
sfTesterUser tester.
Let's update the functional tests for the menu feature we
have added until now. Add the following code at the end of the job module
functional tests:
// 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() ;
To ease testing, we first reload the fixtures data and restart the browser to start with a clean session.
The isAttribute() method checks a given user attribute.
note
  The sfTesterUser tester also provides isAuthenticated() and
  hasCredential() methods to test user authentication and autorizations.
Final Thoughts
The symfony user classes are a nice way to abstract the PHP session management.
Coupled with the great symfony plugin system and the sfGuardPlugin plugin, we
have been able to secure the Jobeet backend in a matter of minutes. And we have
even added a clean interface to manage our administrator users for free, thanks
to the modules provided by the plugin.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.