bw logo

Chapter 10. PHP Presentation Layer

The presentation layer is written in PHP. It handles requests from web user agents (such as mobile phones and PC browsers) and presents information from the game. The example code PHP sources are found at fantasydemo/src/web/php.

10.1. Overview

PHP pages in the example code are represented as PHP objects, and the PHP class definition for <class> is located in <class>.php. Generally, after the class definition an instance of the page object is created and asked to render itself.

We do not recommend this way of structuring a web interface. The purpose of this PHP construction is to illustrate, as clearly as possible, solutions to common problems encountered when implementing a web interface to a BigWorld game instance. It is not meant to illustrate best practices in web interface implementation.

Developers can use any frameworks that they wish to implement a web interface in PHP or Python. BigWorld does not limit the use of third-party frameworks, from complex systems such as Zope to simple templating engines such as PHP Smarty.

The examples adhere to the XHTML-MP standard (XHTML Mobile Profile).

10.2. Required packages

The Web Integration example requires some packages to be installed on the machine running Apache:

  • JSON: TwistedWeb queries return data as JSON objects. Install this package by running the following as root:

    # yum install php-pecl-json
  • Image processing: The FantasyDemo PHP scripts make use of the GraphicsDraw library of image processing functions. To install this package, run the following as root:

    # yum install php-gd

10.3. Constants in Constants.php

Configuration constants and static data are defined in Constants.php. Among other things, it contains:

  • The static item type data (such as URLs to image icons, image statistics, etc.) that are used when displaying player inventory.

  • The URL of the login page.

  • The URL of the welcome page after authentication.

10.4. XHTML-MP helper functions

XHTML-MP-functions.php contains functions for commonly used XHTML-MP (XHTML Mobile Profile) element constructs. The simple base ones are listed below:

  • xhtmlMpSingleTag( $name, $className='', $attrString='' )

    Returns the element source of a single unenclosed XHTML element with the given name, class and attribute string.

  • xhtmlMpTag( $name, $contents, $className='', $attrString='' )

    Returns the element source of a single enclosed XHTML element with the given name, contents, class and attribute string.

  • xhtmlMpAddAttribute( $attrString='', $key, $value )

    Adds a key value attribute to an attribute string and returns it.

From these, the other common XHTML elements are built. Here are some examples:

  • xhtmlMpHeading( $contents, $level=1, $className='', $attrString='' )

    Returns the element source of a single unenclosed XHTML element with the given name, class and attribute string.

  • xhtmlMpDiv( $contents, $className='', $attrString='' )

    Returns the element source for a XHTML DIV element with the given optional class and attribute string.

  • xhtmlMpPara( $contents, $className='' $attrString )

    Returns the element source for a XHTML paragraph.

There is also a XHTMLMPForm class for creating XHTML MP forms.

10.5. Debugging PHP example scripts

There is a debug library implemented in Debug.php that is used throughout the code example. You can use this to trace the flow of the example scripts using the various debugging output options.

Generally, debug output is displayed in a page as a XHTML comment.

class SomePage extends AuthenticatedXHTMLMPPage
{
...
   function renderBody() 
   {
      ...
      debug( "this is a test" );
      ...
   }
...
}

Example PHP using the debug function

The code above will generate HTTP output like this:

<!--
this is a test
-->

Example HTTP output

Additionally, you may use this in an overridden XHTMLMPPage::initialise method. Because initialise does not write output except for HTTP headers in order to perform actions such as HTTP redirects, debug output is deferred until the rendering stage of the page, where you will see debug output as:

<!-- deferred error output follows
debug output instance 1
debug output instance 2
debug output instance 3
deferred error output above -->

Example HTTP output

There are also some helpful debugging functions for getting representations of more complex PHP objects such as Arrays and class instance objects:

  • debugStringObj

    Returns the string output.

  • debugObj

    Sends the string output through debug().

Both these functions generate debug strings representing the objects. This is useful for PHP Arrays and PHP class instance objects.

There is also a registered error handler that prints errors through debug(), including information such as stack trace, function line numbers, and passed parameter values.

10.6. XHTMLMPPage objects

These are abstractions of a page, and are the basis for all viewable pages on the web interface. Class definitions can be found in Page.php.

There are methods designed to be overridden for the processing stage and the output stage.

The XHTMLMPPage::initialise() method is called by XHTMLMPPage::render() for processing before any page source is output. Its purpose is to usually set up the page and provide a processing hook for processing HTTP GET/POST request parameters, and initialise page instance variables so they can be easily rendered in the output stage.

XHTMLMPPage::initialise() allows you to set redirections from a page to another URL — XHTMLMPPage::setRedirect() takes a parameter $url for this purpose. After calling initialise(), if a redirection has been set, then the browser redirects via the HTTP header Location.

XHTMLMPPage::renderBody() (called from XHTMLMPPage::render()) renders the page, and outputs the XHTML element for the page content.

10.7. AuthenticatedXHTMLMPPage objects

Authenticated XHTML Page inherited objects (class AuthenticatedXHTMLMPPage in AuthenticatedPage.php) are pages that only authenticated users can view. Authentication is performed by an instance of Authenticator, with the name of the Authenticator class used to do this (which is configured in Constants.php). For FantasyDemo scripts, it is the BWAuthenticator class.

Authenticator objects also provide a means of storing key-value pairs as server-side session variables.

The absence of authentication token variables set in the session indicates that the user is not logged in, which instructs the client browser to redirect to the login page configured in Constants.php. This login page must process requests for logging in so that an authenticator object be created that authenticates the user and their password and creates the necessary authentication token variables.

There is also a timeout for an authenticated session; if no access has been made for a configured amount of time (for details, see Constants.php), then the session is invalidated, and browsers that have timed out are redirected back to the configured login page with an error message stating that their session has expired.

Authenticators are used by authenticated pages to check the presence of a valid authentication token:

if ($this->auth->doesAuthTokenExist()) 
{
   $authErr = $this->auth->authenticateSessionToken();
   ...
}

10.8. BWAuthenticator objects

This class provides an example of how to perform authentication with the BigWorld system. It involves invoking the db/LogOn method with the user's name and password:

$db = new RemoteEntity( "db" );
$result = $db->logOn( array( "username" => $username, "password" => $pass ) );

The result returned is a PHP object containing the entity's type and database id. These details are then stored for later.

$this->setEntityDetails( $result[ "type" ], $result[ "id" ] );

This stores the string that is required to create a RemoteEntity.

function setEntityDetails( $type, $id )
{
    ...
    $this->setVariable( BW_AUTHENTICATOR_TOKEN_KEY_ENTITY_PATH,
            "entities_by_id/" . $entityType . '/' . $id );
}

The mailbox can then be later retrieved with the entity() function.

function entity()
{
    $entityPath =
        $this->getVariable( BW_AUTHENTICATOR_TOKEN_KEY_ENTITY_PATH );
    ...

    return new RemoteEntity( $entityPath );
}

Authenticated pages can access this mailbox by their authentication object:

$playerMailbox = $this->auth->entity();

Methods can then be called with:

$playerMailbox.someMethod();

10.9. Login.php

This page is responsible for collecting the user name and password to be authenticated against the BigWorld server. Thus, any user name and password that is valid when logging in with the FantasyDemo client is also valid here, so that the bw.xml configuration options dbMgr/createUnknown and dbMgr/rememberUnknown become relevant (for details on these configuration options, see the document Server Operations Guide's section Server Configuration with bw.xml DBMgr Configuration Options).

Authentication is performed by making a request to the authenticator object, for example:

$this->auth->authenticateUserPass( $_REQUEST['username'], $_REQUEST['password'] );

10.10. Characters.php

Once the user authenticates using a username and password, the Account mailbox is queried for its list of associated Avatar characters. This is done as follows:

$account = $this->auth->entity();
$res = $account->webGetCharacterList();

This returns the list of character descriptors in $res['characters']. Each character descriptor is a dictionary with keys name and type (of entity, usually Avatar).

You can also create characters via this page:

$res = $account->webCreateCharacter( array( "name" => $_GET['new_character_name'] ) );

You choose a character to progress. Once chosen, the session player Account mailbox is replaced by a mailbox to the player Avatar entity and the keep-alive period is set on the newly made character mailbox.

$res = $account->webChooseCharacter( array(
            "name" => $_GET[ 'character' ],
            "type" => "Avatar" ) );
...
$this->auth->setEntityDetails( $res[ "type" ], $res[ "id" ] );

10.11. News.php

This page is the entry point after a user has logged in and chosen a character. Currently, this is a static PHP page, but one possible extension to this is to have a News entity in the world, which is queried by this page each time it loads up.

A hook for doing this is present in the NewsPage::initialise() method:

// the articles could also come from an entity
// e.g.
// $newsagent = new RemoteEntity( "global_entities/NewsAgent" );
// $res = $newsagent->getNewsArticles();
// $this->articles = $res['articles'];

10.12. Character.php

This page queries the player Avatar mailbox in real-time via the Avatar.webGetPlayerInfo() web method for the current statistics of the player for display:

$player = $this->auth->entity();
$res = $player->webGetPlayerInfo();

The position and the direction the player is currently facing is also reported back through this page if the player is online.

10.13. Inventory.php

This page queries the player character mailbox via the Avatar.webGetGoldAndInventory() web method. This method is defined in the entity definitions file for the Avatar entity type, in fantasydemo/res/scripts/entity_defs/Avatar.def:

<webGetGoldAndInventory>
   <ReturnValues>
      <!-- The Avatar's available gold pieces. -->
      <goldPieces>        GOLDPIECES  </goldPieces>

      <!-- List of item descriptions as dictionaries with keys:
           'serial': the serial number of the item
           'itemType': the item type
           'lockHandle' : lock handle associated with this item
      -->
      <inventoryItems>    PYTHON      </inventoryItems>

      <!-- List of dictionary with keys:
          'serials': a list of serial numbers of locked items
          'goldPieces': the gold pieces locked
      -->
      <lockedItems>       PYTHON      </lockedItems>
    </ReturnValues>
</webGetGoldAndInventory>

The excerpt above shows that that the return value to the PHP scripting layer is an Array with keys goldPieces, inventoryItems and lockedItems. We store them in the class instance variable $this->inventory:

$entity =& $this->auth->entity();
...
$this->inventory = $entity->webGetGoldAndInventory();

The gold pieces are accessible from this instance variable when displaying its value to the user:

echo(
   xhtmlMpDiv(
      'Gold: '. $this->inventory['goldPieces'],
         'goldRow'
   )
);

This page is also responsible for handling requests for creating auctions from the player. The player nominates an item in their inventory, based on its serial number, then sets the initial auction parameters through the form and on submission, and creating the auction is a case of invoking the Avatar.webCreateAuction:

$res = $entity->webCreateAuction( array(
    "itemSerial" => $itemSerialToAuction,
    "expiry" => $expiry,
    "startBid" => $bidPrice,
    "buyout" => $buyout ) );

10.14. PlayerAuctions.php

This page enables players to see the state of auctions they have created. The search criteria used is an instance of SellerCriteria with the current player's database ID. This is retrieved from Avatar.webGetPlayerInfo():

$res = $this->player->webGetPlayerInfo();
$this->playerDBID = $res['databaseID'];

The result is used in constructing and applying the SellerCriteria for getting search results to present to the user.

10.15. SearchAuctions.php

This page enables players to search for auctions by using the singleton AuctionHouse entity, and allows for bidding of searched auctions.

Search criteria objects are built up using various methods in AuctionHouse, for example:

$res = $this->auctionHouse->webCreateItemTypeCriteria( array(
    "itemTypes" => $itemTypesList ) );
...
$searchCriteria = $res['criteria'];
...
$res = $this->auctionHouse->webCreateBidRangeCriteria( array(
    "minBid" => $searchMinBid,
    "maxBid" => $searchMaxBid ) );
$bidRangeCriteria = $res['criteria'];
$res = $this->auctionHouse->webCombineAnd( array(
                    "criteria1" => $searchCriteria,
                    "criteria2" => $bidRangeCriteria ) );
$searchCriteria = $res['criteria'];

The $searchCriteria object contains the combined search criteria. It can be applied to a search via the AuctionHouse.webSearchAuctions() method. This method returns a list of auction IDs that match the criteria.

To retrieve the auction descriptors (which contains information such as the seller player, the current bid amount, the item type), we use the AuctionHouse.webGetAuctionInfo() which takes a list of auction IDs and returns a list of auction descriptors.

$res = $this->auctionHouse->webSearchAuctions( array(
            "criteria" => $searchCriteria ) );
...
$res = $this->auctionHouse->webGetAuctionInfo( array(
            "auctions" => $res["searchedAuctions"] ) );
...
// store the auctions in an associative array by auction ID
$this->searchResults = Array();
foreach ($res['auctionInfo'] ) as $auctionInfo)
{
    $this->searchResults[$auctionInfo['auctionID'] = $auctionInfo;
}
...

10.16. PHP Error handling

As described in section BigWorld.php Error handling, all mailbox queries can fail, resulting in an error object being returned. In addition to errors coming from the server binaries, errors can be returned from the remote methods themselves. These are custom exception types that will be raised by the auction house scripts, the authentication scripts, and the other methods called remotely by the web client. For example, in the AuctionHouse methods called by the FantasyDemo web client PHP code, there are such error classes as InsufficientGoldError and SearchCriteriaError, allowing error messages that are displayed to the player to be as helpful as possible.

The file fantasydemo/res/scripts/server_common/CustomErrors.py contains the declarations of these custom Python exception classes. They are as follows:

  • AuctionHouseError

    Attempt to access an AuctionHouse entity that is not allowed or non-existent

  • BidError

    Bid for an auction with an invalid amount

  • BuyoutError

    Set an invalid buyout price for an auction

  • CreateEntityError

    An entity can't be created

  • DBError

    A database query or modification fails

  • InsufficientGoldError

    The player doesn't have enough gold for the desired transaction

  • InvalidAuctionError

    Attempt to access an invalid or non-existent auction

  • InvalidDamageAmountError

    Attempt to deal an invalid amount of damage to an entity

  • InvalidItemError

    An item can't be accessed

  • ItemLockError

    An item can't be locked

  • PriceError

    Set an invalid price for an auction

  • SearchCriteriaError

    Search criteria for an auction are invalid

The TwistedWeb service will catch these exception objects, convert them to JSON objects and return them instead of the queried response object, as explained in TwistedWeb Error handling.

Additional error types can be used by declaring them in CustomErrors.py. They must be derived from BWTwoWay.BWCustomError. They should also be declared in CustomErrors.php, to allow them to be handled individually. For example:

# CustomErrors.py

from BWTwoWay import BWCustomError

class MyCustomGameError( BWCustomError ):
    pass
// CustomErrors.php

require_once( "BWError.php" );

class BWCustomError extends BWError {}

class MyCustomGameError( BWCustomError ):
    pass

After BigWorld.php receives a response, it decodes the returned JSON object. If the object is found to be an error, it will be raised as a PHP exception. The caller of the mailbox query must therefore handle any potential exceptions that may be raised. For example:

try
{
    $result = $mailbox->someMethod();
}
catch( Exception $e )
{
    addExceptionMsg( $e );
    return;
}

// Use $result
...

All built-in BigWorld errors and custom errors extend a common base class, BWError. This class implements a message method, which creates a string containing both the underlying error type, and the exception message. It also returns a well-formatted string for BWGenericError objects, described in BigWorld.php Error handling. This method can be used to generate error messages in an exception handler. For example:

function getExceptionMsg( $exception )
{
    if ($exception instanceof BWError)
    {
        return $exception->message();
    }
    else
    {
        // uses the built-in getMessage method
        return $exception->getMessage();
    }
}

function addExceptionMsg( $exception )
{
    $msg = $this->getExceptionMsg( $exception )

    // Create an error message from $msg
    ...
}

For example:

throw new NoConnectionError( "Unable to contact game server" );

If the above exception is caught and the object sent as the argument to addExceptionMsg, the player will receive the following error message:

NoConnectionError: Unable to contact game server

Alternatively, if an instance of a BWGenericError or a non-BWError object is thrown with the same message, only that message would be emitted, and not the exception type:

Unable to contact game server

The complete error handling mechanism for the FantasyDemo web client is implemented in Page.php.

CustomErrors.php also defines an error class for errors specific to the PHP, called BWPHPError. Errors of this type can be used to take advantage of the formatting of error messages using the message method provided by BWError. This allows PHP errors to be handled and reported in a manner consistent with the error objects encountered by BigWorld.php:

// CustomErrors.php

class BWPHPError extends BWError {}

class InvalidFieldError extends BWPHPError {}
// BWAuthenticator.php

...
function authenticateUserPass( $username, $pass )
{
    if ($username == '')
    {
        throw new InvalidFieldError( "Username is empty" );
    }
    if ($pass == '')
    {
        throw new InvalidFieldError( "Password is empty" );
    }
    ...
}
...
// Login.php

...
    try
    {
        $auth->authenticateUserPass( $username, $pass );
    }
    catch( InvalidFieldError $e )
    {
        addExceptionMsg( $e );
        return;
    }
...

Attempting to log in with an empty username will result in the following error message being displayed:

InvalidFieldError: Username is empty