Table of Contents
- 10.1. Overview
- 10.2. Required packages
- 10.3. Constants in
Constants.php
- 10.4. XHTML-MP helper functions
- 10.5. Debugging PHP example scripts
- 10.6.
XHTMLMPPage
objects - 10.7.
AuthenticatedXHTMLMPPage
objects - 10.8.
BWAuthenticator
objects - 10.9.
Login.php
- 10.10.
Characters.php
- 10.11.
News.php
- 10.12.
Character.php
- 10.13.
Inventory.php
- 10.14.
PlayerAuctions.php
- 10.15.
SearchAuctions.php
- 10.16. PHP Error handling
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
.
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).
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
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.
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.
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.
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.
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(); ... }
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();
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'] );
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" ] );
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'];
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.
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 ) );
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.
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; } ...
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