ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Managing Sessions and State with PHP

by Brian Fioca
05/18/2006

MVC Frameworks, such as WASP, provide a solid jumping-off point for entry-level programmers to produce strong, well-formed code. The framework simplifies such things as creating a page (using a kind of page object) and creating or searching for data (using database table objects).

However, probably the most difficult concept for novice PHP programmers to grasp is handling their application's state and session data. This article will address that issue by providing a simple strategy for state management using the WASP framework.

WASP is an MVC framework written for PHP and based on concepts outlined in previous articles on this site. "Simplify PHP Development with WASP" introduced WASP. This article uses WASP version 1.2.

Page State vs. Session State

To understand state and session management in an MVC framework, you must understand the difference between the two, and when to use one instead of the other.

Session State

A web application session is a single start-to-finish interaction between a user and the application. When the user begins the application, the session saves certain variables to keep track of his progress through the application. For example, when a user visits an online store such as Amazon.com, his session begins when he first loads the page. The session then keeps track of the stores he browses to and which items, if any, he adds to his shopping cart. Finally, it allows him to check out and complete his purchase of any items stored in the session at the end.

The application server handles session management. Typically, it assigns a unique identifier to a user by using a browser cookie or by adding it to the request using a hidden entry on the page. A well-designed application or framework can track the user's session on every page where it is appropriate to know the state of her progress through the application. The session lives through the end of the user's interaction with the application, and possibly persists in the user's browser so that the visit can continue when she returns to the site later.

Page State

Page state is much smaller in scope than session state. Page state, by definition, only applies to the state of the current page or possibly a group of pages, such as a wizard. Page state stores such things as form validation errors, progression through a series of data collection forms, or page data-display toggles or switches based on user input. For example, when an Amazon.com user logs in to the site, the pages "recognize" him and tailor the displayed items for sale based on his information. The pages themselves are the same, but the state has changed.

Page state management usually works through the user's browser, maintaining query string parameters or hidden entry form data. Page state data should not be stored in the session, because subsequent page visits will retain their previous state unless you clear the session when you leave the page. With an application comprised of many complicated pages, it can be troublesome to clear the session; thus, it is a good practice not to store page state data in the session.

Session State Example

The article "Simplify PHP Development with WASP" showed how to build a simple Task List application to create and view a list of tasks. To illustrate session management, I'll add features to that application. I recommend working through the tutorial in that article to get to the point where you can follow along with this example.

This example will show how to add simple login capability to your todo application. Everyone will be able to see the list, but only certain people can create entries. Once logged in, you will be able to create as many entries as you like without having to provide your credentials.

First, to modify the controller (the C in MVC) for the Entry page to enforce login restrictions, edit the file Todo/Entry/EntryIndexPage.php:

        //Check for Add button presses
        if (Request::getParameter('Add') != null)
        {
                $oTask = new TaskWrapper();
                $oTask->fillFromRequest();
                $oTask->save();
                $this->redirect('../');
         }

Remember from the previous tutorial that this is where the application saves entries submitted when a user clicks the Add button. To ensure that only properly authenticated users can actually add entries, protect the code:

        //Check for Add button presses
        if (Request::getParameter('Add') != null)
        {
            //Make sure the user has been logged in, and the
            //appropriate session variable has been set
            if (SessionManager::getParameter('user') != null)
            {
                $oTask = new TaskWrapper();
                $oTask->fillFromRequest();
                $oTask->save();
                $this->redirect('../');
            }
            else  //Present an error to the user.
            {
                $this->reportError("You must log in first.");
            }
        }

The line

            if (SessionManager::getParameter('user') != null)

checks the WASP SessionManager for a variable called user, and if it is set, lets the user save her entry. I haven't discussed the code to set this parameter yet, so for now you won't be able to save any entries.

In the case where the user has not logged in, the code

        $this->reportError("Invalid Username or Password.");

tells the WASP framework to present the user with an error message on the page. This happens in the View (the V in MVC). Open the file Todo/Entry/templates/index.chunk to make the change:

<html>
  <body>
    <form name="entry" method="post">
      <h4>Create Entry</h4>
      <div flexy:foreach="arErrors,key,value" class="error">
        {value}</div>
      <p>
        You must log in to create an entry.<br/>
        Username: <input type="text" name="user"/>
        Password: <input type="password" name="password"/>
        <input type="submit" name="Login" value="Login"/>
      </p>
      <p>
        Name:<br/>
        <input type="text" name="Name"/>
      </p>
      <p>
        Date Due (format mm/dd/yyyy):<br/>
        <input type="text" name="Due"/>
      </p>
      <input type="submit" name="Add" value="Add"/>
    </form>
  </body>
 </html>

The lines

      <div flexy:foreach="arErrors,key,value" class="error">
        {value}</div>

ensure that all error messages appear at the top of the page. You don't have to set the placeholder for arErrors because the framework handles it when you call reportError().

The rest of the changes allow you to submit login credentials to the application. You must handle the Login button press back in the controller code. Go back to EntryIndexPage.php and add some code to the handleEvents() method right under the code for handling the Add button press:

        //Check for Login button presses
        if (Request::getParameter('Login') != null)
        {
            //Check user and password
            if (Request::getParameter('user') == 'login' &&
                Request::getParameter('password') == 'password')
            {
                //Save the session state
                SessionManager::setParameter('user', 
                                             Request::getParameter('user'));
            }
            else
            {
                $this->reportError("Invalid Username or Password.");
            }
        }

When a user clicks the Login button, her username and password are checked to ensure they're valid. Note that in a real application, you wouldn't want to hardcode the username and password here, but I've done this to keep this example simple. More typical code would load a UserWrapper database object with the passed-in username and, if it exists, check that the password matches.

After this code has verified the credentials, these lines of code

                SessionManager::setParameter('user',
                                             Request::getParameter('user'));

store a variable called user in the session set to the value submitted in the form. From now on, anywhere in the application, you can discover the username of the logged-in user by calling

        SessionManager::getParameter('user');

which, if you remember, is the same technique used to protect the Add button from unauthenticated users.

Now when you visit the page you won't be able to add an entry unless you've filled out the user and password information and clicked Login first. Notice, however, that even though you can successfully log in and then create entries (try it; it works!), the login form entries are always present. You can fix this by using page state management.

Page State Example

Whether or not you know it, you've already written some code that changes the page state: the reportError() method. When the application detects a failed login, reportError() sets a variable telling the page to display an error message. It isn't that much more difficult to make major changes to the page.

To make the entry page smart enough to know how to draw itself based on whether or not a user has logged in, define two page states: one for when the user has logged in and one for when he has not. My preferred way to do this is by defining page state constants in the controller for the page.

Edit EntryIndexPage.php and make some additions:

...
class EntryIndexPage extends Chunk
{
    const TEMPLATE_NAME = './Entry/templates/index.chunk';

    //Two versions of the page: logged in or not yet logged in
    const STATE_NOT_LOGGED_IN = 'not_logged_in';
    const STATE_LOGGED_IN = 'logged_in';

    //Holds Page State
    protected $_stState;

    function __construct()
...

Now you've defined two constants corresponding to the two states of the page and a member variable to hold the current state of the page. The reason to define them as constants is twofold. The first is because if you store page state in a query string or in an entry on a page, the value of the constant will determine the state. Defining a constant ensures you'll always use the same value (you have to, or the code won't execute!). It also makes it easy to change the constant if the need ever arises. Suppose your manager decides he wants you to alter the state of the page in some way. You only need to change it in one place.

The second reason to define your page state as constants at the top of the controller class is because it lets other developers see instantly all of the ways to draw this particular page in the application. If another developer comes onto your project later and has to debug your code, she can easily determine which code goes to which pages based on the states defined at the top of the class.

Now it is time to use the page state constants to determine how to draw the page. Use the init() method to set the state of the page based on the value of the parameter stored in the session. Add this code to the class's init() method:

    protected function init()
    {
        //Check for logged in user and set the page state
        if (SessionManager::getParameter('user') == null)
            $this->_stState = self::STATE_NOT_LOGGED_IN;
        else
            $this->_stState = self::STATE_LOGGED_IN;

Notice you've now set the $_stState variable to the appropriate constant based on whether the user is logged in. You can use this throughout the rest of the page to determine which parts of the page to draw.

Now modify the draw() method to tell the view which version of the page to draw. Make your version of the draw() method look like:

    public function draw()
    {
        $this->setPlaceholder('TITLE', 'Entry Index Page');

        //Tell the page what its state is
        $boLogin = $this->_stState == self::STATE_NOT_LOGGED_IN;
        $this->setPlaceholder('boLogin', $boLogin);

        parent::draw();
    }

This sets a placeholder called boLogin, which is a Boolean value that will tell the view to draw the login section of the page if the user has not logged in.

Edit the index.chunk file again. In the <p> tag containing the login form elements, add a flexy directive telling the view to only draw the paragraph if the boLogin placeholder is true:

<html>
  <body>
    <form name="entry" method="post">
      <h4>Create Entry</h4>
      <div flexy:foreach="arErrors,key,value" class="error">
        {value}</div>
      <p flexy:if="boLogin">
        You must log in to create an entry.<br/>
        Username: <input type="text" name="user"/>
        Password: <input type="password" name="password"/>
        <input type="submit" name="Login" value="Login"/>
      </p>
      <p>
        Name:<br/>
        <input type="text" name="Name"/>
      </p>
      <p>
        Date Due (format mm/dd/yyyy):<br/>
        <input type="text" name="Due"/>
      </p>
      <input type="submit" name="Add" value="Add"/>
    </form>
  </body>
 </html>

Now, once the user has logged in, the page will no longer display the login interface.

Author's Note: WASP actually comes with its own way of protecting pages and entire modules from unauthenticated users. There is a method in UIModule called setLoginRequired() that takes a Boolean argument, which if true will check to see if there is a variable (UIModule::USER, to be precise) set in the session. Otherwise, it will redirect to a URL specified in the application's configuration file. For the purposes of illustrating a simple session state example, this is a custom login handler tied to a specific page. This is arguably better for small applications such as this one.

Conclusion

In this article, I've discussed the differences between session state and page state management. I've also given you examples of how to do both, and discussed best practices for ensuring your code will be extendable and maintainable.

MVC Frameworks make it easy to maintain and keep separate page and session states. By containing state information in its logical contextual grouping, you can ensure your application can scale in a more organic way, without having to maintain hundreds of global session variables. Proper handling of state variables also allows developers unfamiliar with the application to easily understand how individual modules and pages work on their own, without having to understand or change the way the application works as a whole.

Brian Fioca has over ten years of experience in architecture, design, and development of enterprise solutions in Java, and has recently (somewhat to his surprise) become a champion for dynamic language technologies in the LAMP world.


Return to the PHP DevCenter.

Copyright © 2009 O'Reilly Media, Inc.