start page | rating of books | rating of authors | reviews | copyrights

Programming PHPProgramming PHPSearch this book

7.6. Maintaining State

HTTP is a stateless protocol, which means that once a web server completes a client's request for a web page, the connection between the two goes away. In other words, there is no way for a server to recognize that a sequence of requests all originate from the same client.

State is useful, though. You can't build a shopping-cart application, for example, if you can't keep track of a sequence of requests from a single user. You need to know when a user puts a item in his cart, when he adds items, when he removes them, and what's in the cart when he decides to check out.

To get around the Web's lack of state, programmers have come up with many tricks to keep track of state information between requests (also known as session tracking ). One such technique is to use hidden form fields to pass around information. PHP treats hidden form fields just like normal form fields, so the values are available in the $_GET and $_POST arrays. Using hidden form fields, you can pass around the entire contents of a shopping cart. However, a more common technique is to assign each user a unique identifier and pass the ID around using a single hidden form field. While hidden form fields work in all browsers, they work only for a sequence of dynamically generated forms, so they aren't as generally useful as some other techniques.

Another technique is URL rewriting, where every local URL on which the user might click is dynamically modified to include extra information. This extra information is often specified as a parameter in the URL. For example, if you assign every user a unique ID, you might include that ID in all URLs, as follows:

http://www.example.com/catalog.php?userid=123

If you make sure to dynamically modify all local links to include a user ID, you can now keep track of individual users in your application. URL rewriting works for all dynamically generated documents, not just forms, but actually performing the rewriting can be tedious.

A third technique for maintaining state is to use cookies. A cookie is a bit of information that the server can give to a client. On every subsequent request the client will give that information back to the server, thus identifying itself. Cookies are useful for retaining information through repeated visits by a browser, but they're not without their own problems. The main problem is that some browsers don't support cookies, and even with browsers that do, the user can disable cookies. So any application that uses cookies for state maintenance needs to use another technique as a fallback mechanism. We'll discuss cookies in more detail shortly.

The best way to maintain state with PHP is to use the built-in session-tracking system. This system lets you create persistent variables that are accessible from different pages of your application, as well as in different visits to the site by the same user. Behind the scenes, PHP's session-tracking mechanism uses cookies (or URLs) to elegantly solve most problems that require state, taking care of all the details for you. We'll cover PHP's session-tracking system in detail later in this chapter.

7.6.1. Cookies

A cookie is basically a string that contains several fields. A server can send one or more cookies to a browser in the headers of a response. Some of the cookie's fields indicate the pages for which the browser should send the cookie as part of the request. The value field of the cookie is the payload—servers can store any data they like there (within limits), such as a unique code identifying the user, preferences, etc.

Use the setcookie( ) function to send a cookie to the browser:

setcookie(name [, value [, expire [, path [, domain [, secure ]]]]]);

This function creates the cookie string from the given arguments and creates a Cookie header with that string as its value. Because cookies are sent as headers in the response, setcookie( ) must be called before any of the body of the document is sent. The parameters of setcookie( ) are:

name
A unique name for a particular cookie. You can have multiple cookies with different names and attributes. The name must not contain whitespace or semicolons.

value
The arbitrary string value attached to this cookie. The original Netscape specification limited the total size of a cookie (including name, expiration date, and other information) to 4 KB, so while there's no specific limit on the size of a cookie value, it probably can't be much larger than 3.5 KB.

expire
The expiration date for this cookie. If no expiration date is specified, the browser saves the cookie in memory and not on disk. When the browser exits, the cookie disappears. The expiration date is specified as the number of seconds since midnight, January 1, 1970, GMT. For example, pass time( )+60*60*2 to expire the cookie in two hours' time.

path
The browser will return the cookie only for URLs below this path. The default is the directory in which the current page resides. For example, if /store/front/cart.php sets a cookie and doesn't specify a path, the cookie will be sent back to the server for all pages whose URL path starts with /store/front/.

domain
The browser will return the cookie only for URLs within this domain. The default is the server hostname.

secure
The browser will transmit the cookie only over https connections. The default is false, meaning that it's okay to send the cookie over insecure connections.

When a browser sends a cookie back to the server, you can access that cookie through the $_COOKIE array. The key is the cookie name, and the value is the cookie's value field. For instance, the following code at the top of a page keeps track of the number of times the page has been accessed by this client:

<?php
 $page_accesses = $_COOKIE['accesses'];
 setcookie('accesses', ++$page_accesses);
?>

When decoding cookies, any periods (.) in a cookie's name are turned into underscores. For instance, a cookie named tip.top is accessible as $_COOKIE['tip_top'].

Example 7-10 shows an HTML page that gives a range of options for background and foreground colors.

Example 7-10. Preference selection

<html>
<head><title>Set Your Preferences</title></head>
<body>
<form action="prefs.php" method="post">
  
Background:
<select name="background">
<option value="black">Black</option>
<option value="white">White</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select><br />
  
Foreground:
<select name="foreground">
<option value="black">Black</option>
<option value="white">White</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select><p />
  
<input type="submit" value="Change Preferences">
</form>
</body>
</html>

The form in Example 7-10 submits to the PHP script prefs.php, which is shown in Example 7-11. This script sets cookies for the color preferences specified in the form. Note that the calls to setcookie( ) are made before the HTML page is started.

Example 7-11. Setting preferences with cookies

<?php
 $colors = array('black' => '#000000',
                 'white' => '#ffffff',
                 'red'   => '#ff0000',
                 'blue'  => '#0000ff');
  
 $bg_name = $_POST['background'];
 $fg_name = $_POST['foreground'];
  
 setcookie('bg', $colors[$bg_name]);
 setcookie('fg', $colors[$fg_name]);
?>
<html>
<head><title>Preferences Set</title></head>
<body>
  
Thank you. Your preferences have been changed to:<br />
Background: <?= $bg_name ?><br />
Foreground: <?= $fg_name ?><br />
  
Click <a href="prefs-demo.php">here</a> to see the preferences
in action.
  
</body>
</html>

The page created by Example 7-11 contains a link to another page, shown in Example 7-12, that uses the color preferences by accessing the $_COOKIE array.

Example 7-12. Using the color preferences with cookies

<html>
<head><title>Front Door</title></head>
<?php
 $bg = $_COOKIE['bg'];
 $fg = $_COOKIE['fg'];
?>
<body bgcolor="<?= $bg ?>" text="<?= $fg ?>">
<h1>Welcome to the Store</h1>
  
We have many fine products for you to view.  Please feel free to browse
the aisles and stop an assistant at any time.  But remember, you break it
you bought it!<p>
  
Would you like to <a href="prefs.html">change your preferences?</a>
  
</body>
</html>

There are plenty of caveats about the use of cookies. Not all clients support or accept cookies, and even if the client does support cookies, the user may have turned them off. Furthermore, the cookie specification says that no cookie can exceed 4 KB in size, only 20 cookies are allowed per domain, and a total of 300 cookies can be stored on the client side. Some browsers may have higher limits, but you can't rely on that. Finally, you have no control over when browsers actually expire cookies—if they are at capacity and need to add a new cookie, they may discard a cookie that has not yet expired. You should also be careful of setting cookies to expire quickly. Expiration times rely on the client's clock being as accurate as yours. Many people do not have their system clocks set accurately, so you can't rely on rapid expirations.

Despite these limitations, cookies are very useful for retaining information through repeated visits by a browser.

7.6.2. Sessions

PHP has built-in support for sessions, handling all the cookie manipulation for you to provide persistent variables that are accessible from different pages and across multiple visits to the site. Sessions allow you to easily create multipage forms (such as shopping carts), save user authentication information from page to page, and store persistent user preferences on a site.

Each first-time visitor is issued a unique session ID. By default, the session ID is stored in a cookie called PHPSESSID. If the user's browser does not support cookies or has cookies turned off, the session ID is propagated in URLs within the web site.

Every session has a data store associated with it. You can register variables to be loaded from the data store when each page starts and saved back to the data store when the page ends. Registered variables persist between pages, and changes to variables made on one page are visible from others. For example, an "add this to your shopping cart" link can take the user to a page that adds an item to a registered array of items in the cart. This registered array can then be used on another page to display the contents of the cart.

7.6.2.1. Session basics

To enable sessions for a page, call session_start( ) before any of the document has been generated:

<?php session_start( ) ?>
<html> 
...
</html>

This assigns a new session ID if it has to, possibly creating a cookie to be sent to the browser, and loads any persistent variables from the store.

If you have registered objects, the class definitions for those objects must be loaded before the call to session_start( ). See Chapter 6 for discussion and an example.

You can register a variable with the session by passing the name of the variable to session_register( ) . For example, here is a basic hit counter:

<?php 
 session_start( );
 session_register('hits');
 ++$hits;
?>
This page has been viewed <?= $hits ?> times.

The session_start( ) function loads registered variables into the associative array $HTTP_SESSION_VARS. The keys are the variables' names (e.g., $HTTP_SESSION_VARS['hits']). If register_globals is enabled in the php.ini file, the variables are also set directly. Because the array and the variable both reference the same value, setting the value of one also changes the value of the other.

You can unregister a variable from a session, which removes it from the data store, by calling session_unregister( ). The session_is_registered( ) function returns true if the given variable is registered. If you're curious, the session_id( ) function returns the current session ID.

To end a session, call session_destroy( ). This removes the data store for the current session, but it doesn't remove the cookie from the browser cache. This means that, on subsequent visits to sessions-enabled pages, the user will have the same session ID she had before the call to session_destroy( ), but none of the data.

Example 7-13 shows the first code block from Example 7-11 rewritten to use sessions instead of manually setting cookies.

Example 7-13. Setting preferences with sessions

<?php
 $colors = array('black' => '#000000',
                 'white' => '#ffffff',
                 'red'   => '#ff0000',
                 'blue'  => '#0000ff');
 session_start( );
 session_register('bg');
 session_register('fg');
  
 $bg_name = $_POST['background'];
 $fg_name = $_POST['foreground'];
  
 $bg = $colors[$bg_name];
 $fg = $colors[$fg_name];
?>

Example 7-14 shows Example 7-12 rewritten to use sessions. Once the session is started, the $bg and $fg variables are created, and all the script has to do is use them.

Example 7-14. Using preferences from sessions

<?php session_start( ) ?>
<html>
<head><title>Front Door</title></head>
<body bgcolor="<?= $bg ?>" text="<?= $fg ?>">
<h1>Welcome to the Store</h1>
  
We have many fine products for you to view.  Please feel free to browse
the aisles and stop an assistant at any time.  But remember, you break it
you bought it!<p>
  
Would you like to <a href="prefs.html">change your preferences?</a>
  
</body>
</html>

By default, PHP session ID cookies expire when the browser closes. That is, sessions don't persist after the browser exits. To change this, you'll need to set the session.cookie_lifetime option in php.ini to the lifetime of the cookie, in seconds.

7.6.2.2. Alternatives to cookies

By default, the session ID is passed from page to page in the PHPSESSID cookie. However, PHP's session system supports two alternatives: form fields and URLs. Passing the session ID via hidden fields is extremely awkward, as it forces you to make every link between pages be a form's submit button. We will not discuss this method further here.

The URL system for passing around the session ID, however, is very elegant. PHP can rewrite your HTML files, adding the session ID to every relative link. For this to work, though, PHP must be configured with the -enable-trans-id option when compiled (see Chapter 1). There is a performance penalty for this, as PHP must parse and rewrite every page. Busy sites may wish to stick with cookies, as they do not incur the slowdown caused by page rewriting.

7.6.2.3. Custom storage

By default, PHP stores session information in files in your server's temporary directory. Each session's variables are stored in a separate file. Every variable is serialized into the file in a proprietary format. You can change all of these things in the php.ini file.

You can change the location of the session files by setting the session.save_path value in php.ini. If you are on a shared server with your own installation of PHP, set the directory to somewhere in your own directory tree, so other users on the same machine cannot access your session files.

PHP can store session information in one of two formats in the current session store—either PHP's built-in format, or WDDX (http://www.openwddx.org/). You can change the format by setting the session.serialize_handler value in your php.ini file to either php for the default behavior, or wddx for WDDX format.

You can write your own functions for reading and writing the registered variables. In this section, we'll develop an example that stores session data in a database, which lets you share sessions between multiple sites. It's easy to install your custom session store. First, set session.save_handler to user in your php.ini file. Next, write functions for opening a new session, closing a session, reading session information, writing session information, destroying a session, and cleaning up after a session. Then register them with the session_set_save_handler( ) function:

session_set_save_handler(open_fn, close_fn, read_fn, write_fn, destroy_fn, gc_fn);

To make all the PHP files within a directory use your custom session store, set the following options in your httpd.conf file:

<Directory "/var/html/test"> 
    php_value session.save_handler user
    php_value session.save_path mydb
    php_value session.name session_store
</Directory>

The mydb value should be replaced with the name of the database containing the table. It is used by the custom session store to find the database.

The following sample code uses a MySQL database for a session store (databases are discussed in full in Chapter 8). The table used in the example has the following structure:

CREATE TABLE session_store (
  session_id char(32) not null PRIMARY KEY,
  expiration timestamp,
  value text not null
);

The first function you must provide is the open handler, which takes care of opening a new session. It is called with the current value of session.save_path (from your php.ini file) and the name of the variable containing the PHP session ID (which defaults to PHPSESSID and can be changed in the php.ini file by setting session.name). Our open handler simply connects to the database and sets the global variable $table to the name of the database table that holds the session information:

function open ($save_path,$session_name) { 
  global $table; 
  
  mysql_connect('localhost'); 
  mysql_select_db($save_path);  
  
  $table = $session_name; 
  
  return true; 
} 

Once a session has been opened, the read and write handlers are called as necessary to get the current state information and to store that state in a persistent manner. The read handler is given the session ID, and the write handler is called with the session's ID and the data for the session. Our database read and write handlers query and update the database table:

function read($session_id) { 
    global $table; 
    $result = mysql_query("SELECT value FROM $table
                           WHERE session_id='$session_id'"); 
    if($result && mysql_num_rows($result)) { 
        return mysql_result($result,0); 
    } else { 
        error_log("read: ".mysql_error( )."\n",3,"/tmp/errors.log"); 
        return ""; 
    } 
} 
  
function write($session_id, $data) { 
    global $table; 
    $data = addslashes($data); 
    mysql_query("REPLACE INTO $table (session_id,value)
                 VALUES('$session_id','$data')")
    or error_log("write: ".mysql_error( )."\n",3,"/tmp/errors.log"); 
    return true; 
} 

Complementing the open handler is the close handler, which is called after each page's script is done executing. It performs any cleanup necessary when closing a session (usually very minimal). Our database close handler simply closes the database connection:

function close( ) {
  mysql_close( );
  
  return true;
}

When a session is completed, the destroy handler is called. It is responsible for cleaning up anything created during the open handler's call. In the case of the database storage system, we must remove that session's entry in the table:

function destroy($session_id) {
  global $table;
  
  mysql_query( "DELETE FROM $table WHERE session_id = '$session_id'";
  
  return true;
}

The final handler, the garbage-collection handler, is called at intervals to clean up expired session data. The function should check for data that has not been used in longer than the lifetime given by the call to the handler. Our database garbage-collection handler removes entries from the table whose last-modified timestamp exceeds the maximum time:

function gc($max_time) {
    global $table;
    mysql_query( 
      "DELETE FROM $table WHERE UNIX_TIMESTAMP(expiration)
       < UNIX_TIMESTAMP( )-$max_time")
      or error_log("gc: ".mysql_error( )."\n",3,"/tmp/errors.log");
    return true; 
}

After creating all the handler functions, install them by calling session_set_save_handler( ) with the appropriate function names. With the preceding examples, call:

session_set_save_handler('open', 'close', 'read', 'write', 'destroy', 'gc');

You must call session_set_save_handler( ) before starting a session with session_start( ). This is normally accomplished by putting the store functions and call to session_set_save_handler( ) in a file that's included in every page that needs the custom session handler. For example:

<?php require_once 'database_store.inc';
 session_start( );
?>

Because the handlers are called after output for the script is sent, no function that generates output can be called. If errors occur, log them into a file using error_log( ) , as we did earlier.

7.6.3. Combining Cookies and Sessions

Using a combination of cookies and your own session handler, you can preserve state across visits. Any state that should be forgotten when a user leaves the site, such as which page the user is on, can be left up to PHP's built-in sessions. Any state that should persist between user visits, such as a unique user ID, can be stored in a cookie. With the user's ID, you can retrieve the user's more permanent state, such as display preferences, mailing address, and so on, from a permanent store, such as a database.

Example 7-15 allows the user to select text and background colors and stores those values in a cookie. Any visits to the page within the next week send the color values in the cookie.

Example 7-15. Saving state across visits

<?php
 if($_POST['bgcolor']) {
   setcookie('bgcolor', $_POST['bgcolor'], time( ) + (60 * 60 * 24 * 7));
 }
  
 $bgcolor = empty($bgcolor) ? 'gray' : $bgcolor;
?>
  
<body bgcolor="<?= $bgcolor ?>">
  
<form action="<?= $PHP_SELF ?>" method="POST">
  <select name="bgcolor">
    <option value="gray">Gray</option>
    <option value="white">White</option>
    <option value="black">Black</option>
    <option value="blue">Blue</option>
    <option value="green">Green</option>
    <option value="red">Red</option>
  </select>
  
  <input type="submit" />
</form>
</body>


Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.