Maintaining state

One of the biggest drawbacks of the earlier example is that it fetches the full set of results at one time, which is impractical for large result sets. It is more practical to display a full set of results across multiple pages and allow the user to move forward or backward through the pages.

This is simple in a conventional application where a connection to the separate server is maintained until the user terminates the application. In a web implementation however, this seemingly simple requirement involves a considerably higher level of complexity due to the stateless nature of web pages. One such complexity is that each time a new page of results is displayed, the initial search for the records must be re-executed. This is inconvenient for the web programmer and potentially slow for the user.

The IMu server provides a solution to this. When a handler object is created, a corresponding object is created on the server to service the handler's request: this server-side object is allocated a unique identifier by the IMu server. When making a request for more information, the unique identifier can be used to connect a new handler to the same server-side object, with its state intact.

The following example illustrates the connection of a second, independently created IMuModule object to the same server-side object:

// Create a module object as usual
$first = new IMuModule('eparties', $session);

// Run a search - this will create a server-side object
$keys = array(1, 2, 3, 4, 5, 42);
$first->findKeys($keys);

// Get a set of results
$result1 = $first->fetch('start', 0, 2, 'SummaryData');

// Create a second module object
$second = new IMuModule('eparties', $session);

/*
 * Attach it to the same server-side object as the first module.
 * This is the key step.
 */
$second->id = $first->id;

// Get a second set of results from the same search
$result2 = $second->fetch('current', 1, 2, 'SummaryData');

Although two completely separate IMuModule objects have been created, they are each connected to the same server-side object by virtue of having the same id property. This means that the second fetch call will access the same result set as the first fetch. Notice that a flag of current has been passed to the second call. The current state is maintained on the server-side object, so in this case the second call to Fetch will return the third and fourth records in the result set.

While this example illustrates the use of the id property, it is not particularly realistic as it is unlikely that two distinct objects which refer to the same server-side object would be required in the same piece of code. The need to re-connect to the same server-side object when generating another page of results is far more likely. This situation involves creating a server-side IMuModule object (to search the module and deliver the first set of results) in one request and then re-connecting to the same server-side object (to fetch a second set of results) in a second request. As before, this is achieved by assigning the same identifier to the id property of the object in the second page, but two other things need to be considered.

By default the IMu server destroys all server-side objects when a session finishes. This means that unless the server is explicitly instructed not to do so, the server-side object may be destroyed when the connection from the first page is closed. Telling the server to maintain the server-side object only requires that the destroy property on the object is set to false before calling any of its methods. In the example above, the server would be instructed not to destroy the object as follows:

$module = new IMuModule('eparties', $session);
$module->setDestroy(false);
$keys = array(1, 2, 3, 4, 5, 42);
$module->findKeys($keys);

The second point is quite subtle. When a connection is established to a server, it is necessary to specify the port to connect to. Depending on how the server has been configured, there may be more than one server process listening for connections on this port. Your program has no control over which of these processes will actually accept the connection and handle requests. Normally this makes no difference, but when trying to maintain state by re-connecting to a pre-existing server-side object, it is a problem.

For example, suppose there are three separate server processes listening for connections. When the first request is executed it connects, effectively at random, to the first process. This process responds to the request, creates a server-side object, searches the Parties module for the terms provided and returns the first set of results. The server is told not to destroy the object and passes the server-side identifier to another page which fetches the next set of results from the same search.

The problem comes when the next page connects to the server again. When the connection is established any one of the three server processes may accept the connection. However, only the first process is maintaining the relevant server-side object. If the second or third process accepts the connection, the object will not be found.

The solution to this problem is relatively straightforward. Before the first request closes the connection to its server, it must notify the server that subsequent requests need to connect explicitly to that process. This is achieved by setting the IMuSession object's suspend property to true prior to submitting any request to the server:

$session = new IMuSession('server.com', 12345);
$module = new IMuModule('eparties', $session);
$session->setSuspend(true);
$module->findKeys($keys);

The server handles a request to suspend a connection by starting to listen for connections on a second port. Unlike the primary port, this port is guaranteed to be used only by that particular server process. This means that a subsequent page can reconnect to a server on this second port and be guaranteed of connecting to the same server process. This in turn means that any saved server-side object will be accessible via its identifier. After the request has returned (in this example it was a call to findKeys), the IMuSession object's port property holds the port number to reconnect to:

$session->setSuspend(true);
$module->findKeys($keys);
$reconnect = $session->port;

Example

To illustrate we'll modify the very simple results page of the earlier section to display the list of matching names in blocks of five records per page. We'll provide simple Next and Prev links to allow the user to move through the results, and we will use some more GET parameters to pass the port we want to reconnect to, the identifier of the server-side object and the rownum of the first record to be displayed.

First build the search page, which is a plain HTML form:

<head>
  <title>Party Search</title>
</head>
<body>
  <form action="example.php">
    <p>Enter a last name to search for (e.g. S*):</p>
    <input type="text" name="name"/>
    <input type="submit" value="Search"/>
  </form>
</body>

Next build the results page, which runs the search and displays the results. The steps to build the search page are outlined in detail below.

  1. Create a IMuSession object. Then the port property is set to a standard value unless a port parameter has been passed in the URL.

    /*

    * Create new session object.

    */

    $session = new IMuSession();

    $session->setHost('imu.mel.kesoftware.com');

    /*

    * Work out what port to connect to

    */

    $port = 40136;

    if (isset($_GET['port']))

    $port = $_GET['port'];

    $session->setPort($port);

  2. Connect to the server and immediately set the suspend property to true to tell the server that we may want to connect again:

    /*

    * Establish connection and tell the server we may want to re-connect

    */

    $session->connect();

    $session->setSuspend(1);

    This ensures the server listens on a new, unique port.

  3. Create the client-side IMuModule object and set its destroy property to false:

    /*

    * Create module object and tell the server not to destroy it.

    */

    $module = new IMuModule('eparties', $session);

    $module->setDestroy(false);

    This ensures that the server will not destroy the corresponding server-side object when the session ends.

  4. If the URL included a name parameter, we need to do a new search. Alternatively, if it included an id parameter, we need to connect to an existing server-side object:

    /* If name is supplied, do new search. The search term is passed from

    ** search.html using GET

    */

    if (isset($_GET['name']))

    {

    $terms = new IMuTerms();

    $terms->add('NamLast', $_GET['name']);

    $module->findTerms($terms);

    }

    /*

    * Otherwise, if id is supplied reattach to existing server-side object

    */

    else if (isset($_GET['id']))

    {

    $module->id = $_GET['id'];

    }

    /*

    * Otherwise, we can't process

    */

    else

    {

    header('HTTP/1.1 400 Bad Request');

    print("missing 'name' or 'id' parameter\r\n");

    exit(1);

    }

  5. Build a list of columns to fetch:

    $columns = array

    (

    'NamFirst',

    'NamLast'

    );

  6. If the URL included a rownum parameter, fetch records starting from there. Otherwise start from record number 1:

    /*

    * Work out which block of records to fetch

    */

    $rownum = 1;

    if (isset($_GET['rownum']))

    $rownum = $_GET['rownum'];

  7. Build the main page:

    /*

    * Fetch next five records

    */

    $results = $module->fetch('start', $rownum - 1, 5, $columns);

    $hits = $results->hits;

    /*

    * Build the results page

    */

    ?>

    <!DOCTYPE html>

    <html>

    <head>

    <title>IMu PHP API - Maintaining State</title>

    </head>

    <body>

    <p>Number of matches: <?php echo $hits ?></p>

    <table>

    <?php

    // Display each match in a separate row in a table

    foreach ($results- rows as $row)

    {

    ?>

    <tr>

       <td><?php echo $row['rownum'] ?></td>

       <td><?php echo $row['NamFirst'], ' ', $row['NamLast'] ?></td>

    </tr>

    <?php

    }

    ?>

    </table>

    <?php

  8. Finally, add the Prev and Next links to allow the user to page backwards and forwards through the results. This is the most complicated part! First, to ensure that a connection is made to the same server and server-side object, add the appropriate port and id parameters to the link URL:

    /*

    * Add the Prev and Next links

    */

    $url = $_SERVER['PHP_SELF'];

    $url .= '?port=' . $session->port;

    $url .= '&id=' . $module->id;

  9. If the first record is not showing add a Prev link to allow the user to go back one page in the result set. Similarly, if the last record is not showing add a Next link to allow the user to go forward one page:

    $first = $results->rows[0];

    if ($first['rownum'] > 1)

    {

    $prev = $first['rownum'] - 5;

    if ($prev < 1)

       $prev = 1;

    $prev = $url . '&rownum=' . $prev;

    ?>

    <a href="<?php echo $prev ?>">Prev</a>

    <?php

    }

    $last = $results->rows[count($results->rows) - 1];

    if ($last['rownum'] < $results->hits)

    {

    $next = $last['rownum'] + 1;

    $next = $url . '&rownum=' . $next;

    ?>

    <a href="<?php echo $next ?>">Next</a>

    <?php

    }

    ?>

    </body>

    </html>