Developing a Betfair application with bflib

chris (2010-09-08 03:52:36)
5659 views
2 replies

In my last tutorial, I showed the basics of how to get set up with bflib and your first betfair application. The application simply demonstrated installing the bflib classes and hooking into them to get authenticated and pull some data from the betfair exchange. In this tutorial, we are going to illustrate how you might add a couple more key features to this simple app:

1) Using some other methods to provide basic navigation of the markets on offer.
2) Creating custom methods for specific/combination behaviours.
3) Putting a rudimentary web front-end onto your betfair app.

Getting Started.

To get started, you should run through the steps shown in the previous tutorial. This will take you through downloading the bflib source code, setting up your own configuration and creating your first application classes. Once you are up and running, and are able to get a list of all the event types available on Betfair, you can proceed with the following process.

Our Application.

The purpose of this application is to allow the user to navigate through the various event types and events down to individual markets. On arriving at a market, the application will list the runners, their current best 'offers to back' (back prices) and the volumes available at each price.

My first file is the index.php, which will sit in the document root of the application web site. The role of this file is to look incoming URLs and to capture an 'action' and an 'item'. The action represents the verb, or method to be executed. The item is the id of the object we are acting upon. This initially imples that there is always a one-to-one relationship between the two. Sometimes this won't be the case, but the bflib framework provides a mechanism for creating bespoke actions, which wrap multiple operations under the hood. You could, of course, provide further extensions in your application class if necessary - if the complexity of your app exceeds what can be achieved with bflib standalone. In this example, it won't be necesary to do that.

So let's start with the code for the index.php file:

<?php
/**
*    Copyright Christopher Lacy-Hulbert 2009
*
*/

/* set up an object to pass to the view */
$dataSet = new stdClass();
$dataSet->data = new stdClass();
$dataSet->description = 'activeEventTypes';

/* action and id are empty unless passed in the app url */
$action='';
$item='';

/**
* Define autoload classpath and require it in.
* @param $class The class to be loaded
*/
function __autoload( $class ){
        $classpath = '../classes/'.$class.'.class.php';
        if (file_exists( $classpath )){
                require_once($classpath);
                return;
        }
        $classpath = '/export/bflib/classes/'.$class.'.class.php';
        if (file_exists( $classpath )){
                require_once($classpath);
                return;
        }
}

/* check to see if 'a' action is passed in */
if(false === empty( $_GET['a'])){
        $action = filter_input(INPUT_GET,'a');
        $dataSet->description = $action;
}

/* capture an itemid if it's set */
if(false === empty( $_GET['item'])){
        $item = filter_input(INPUT_GET,'item');
}

$testApp = new testApp();
$testApp->setActionString($action);
$testApp->setItemId($item);
$returnedSoapResult = $testApp->run();

/* stick these results into the dataSet->data */
$dataSet->data = $returnedSoapResult;

/* if no method passed in with URI, just render all event types */
$testAppView = new testAppView();
$testAppView->render( $dataSet );
?>

There are a couple of key elements to this. Firstly, we create an object which will capture all the data which comes back from our test application class (and ultimately from the bflib internals). At the bottom of the file, this object gets passed into the 'render' method of the testAppView class. That's where all the web page generation happens. You will also see the classpath definitions being set up near the start. If your application is going to use classes which you have written to perform specific tasks (based on your own models), you should declare additional classpath entries here.

The first bit of work happens when the 'a' and 'item' variables in the URL are filtered and assigned to $action and $item. The $action is assigned to the 'description' member variable in the dataSet object. We'll use that as a means of telling the view what the current action is, as passed in from the last click on the page. Nothing fancy so far.

In the last few lines, we simply create an instance of the testApp class, pass in the action and the item and then call the 'run' method. This class does very little other than serve as a broker between the web page and the bflib controller. You could even get away without using the testApp class, but as you will see in a second, I am using it to perform a couple of important roles.

The returned soap response is then assigned to the 'data' member of the dataSet object and then passed up to the view.

So now let's look at the testApp class itself. Here is the source code:

<?php
/**
* Copyright Christopher Lacy-Hulbert 2009
*
* Simple application using bflib
*/

class testApp {
        private $context = '';
        private $itemId;

        /**
        * construct testApp object
        *
        */
        public function __construct(){ }

        /**
        * convert the method passed in the GET params into an API method name. Hard-wired to provide
        * a means of controlling what methods are supported.  Couple be moved to a simple mapping.
        *
        */
        public function setActionString($actionString){
                switch($actionString){
                        case 'getCompleteMarketPricesCompressed':
                                $this->context = 'getCompleteMarketPricesCompressed';
                                break;

                        case 'getMarket':
                                $this->context = 'getMarket';
                                break;

                        case 'getEvents':
                                $this->context = 'getEvents';
                                break;

                        case 'getAllMarkets':
                                $this->context = 'getAllMarkets';
                                break;

                        case 'getRunnersAndTopPrices':
                                $this->context = 'getRunnersAndTopPrices';
                                break;

                        case 'getRunnersAndPrices':
                                $this->context = 'getRunnersAndPrices';
                                break;

                        default:
                                $this->context = 'getActiveEventTypes';
                                break;
                }
        }

        public function setItemId($item){
                $this->itemId = $item;
                return(true);
        }

        /**
        * Instantiate a betfairController object, passing through the 'context' 
        * and then grab the soapresponse after the soap call is made.
        *
        */
        public function run(){
                $this->bflib = new betfairController();
                $soapResult = array();
                $soapResult = $this->scheduleRequest();
                $soapResult = $this->prepareSoapResult($soapResult);
                return($soapResult);
        }

        /* 
        * Perform application-specific reforming of the soap result 
        *
        * @param object $soapResult
        * @return object $soapResult
        */
        public function prepareSoapResult($soapResult){
                switch($this->context){
                        case 'getRunnersAndTopPrices':
                                return($soapResult);
                                break;

                        default:
                                // do nothing
                                return($soapResult);
                                break;
                }
        }

        /**
        *  Tell the betfairController class which API verb/method we are going to use and ask it to set up the request object
        *  Then run the controller and  capture the soapResponse object that comes back
        *
        */
        public function scheduleRequest(){
                /* set the bflib context as this context */
                if(true === isset($this->context) && false === empty($this->context)){
                        $this->bflib->setContext ( $this->context );
                }

                if(true === isset($this->itemId) && false === empty($this->itemId)){
                        $this->bflib->setItemId ( $this->itemId );
                }

                /* call the bflib->constructRequestData method to set up the soap data */
                $requestObject = $this->bflib->constructRequestData($this->context);

                /* run the bflib->execute and bflib->prepareResponseData requests and capture the soapResult which comes back */
                $soapResponse = $this->bflib->run();

                return($soapResponse);
        }
}
?>                 

This is almost identical to the testApp class used in the first tutorial. There are, however a couple of important behaviours to point out.

First, you'll notice that the setActionString method simply interprets the 'a' (action) from the URL into a 'context' variable. The action remains unchanged in most cases. The purpose of this is to provide support only for those methods which I want to expose through the site. Clearly, it would be dangerous to allow any method to be called without specific controls in place (eg anything that exposes payment details, or places a bet). This mapping also allows me to choose what action labels I use for specific methods. You will notice that I have added support for 'getRunnersAndTopPrices'. This method doesn't exist within the Betfair WSDL's, but I am acting as if it does. That's because bflib provides support for this method as a bespoke combination method, which actually wraps two calls and does some filtering of the returning results. That will save us having to blend the results from calls to getMarket and getCompleteMarketPricesCompressed.

Also worth noting is the prepareSoapResult() method. I'm not doing anything with it here, but if you decide to further modify the data that comes back from bflib, this is one place you could do that - ensuring that you end up with a single fully-ready data object for your betfair-test to pass into the view.

There is very little more to say about what I'm doing here. The local 'run' method sets up the bflib instance and the scheduleRequest method simply passes the context and item id into bflib to be called against the Betfair exchange APIs once I call bflib's 'run' method.

At this point, all the work is done, but I haven't yet explained how all this appears on a web page. So let's visit the testAppView class. Remember, the soapResponse from bflib gets returned from testApp.class.php into the betfair-test.php. At that point, the View is instantiated, the soapResponse gets loaded into a dataSet object and given a 'description', and then it presumably gets displayed by the 'render' method. Here's what happens:

<?php
class testAppView {

        // change this to reflect the host name of your web site.
        private $sitename = 'http://localhost/';

        /**
        * construct testAppView object
        *
        */
        public function __construct(){ }

        /**
        * render
        *
        * @param dataSet
        * @return bool
        */
        public function render($dataSet){
                switch($dataSet->description){
                        case 'activeEventTypes':
                                echo '<ul>';
                                foreach($dataSet->data->Result->eventTypeItems->EventType as $eventTypeItem){
                                        echo '<li><a href="'.$this->sitename.'betfair-test/betfair-test.php?a=getEvents&item='.$eventTypeItem->id.'">'.$eventTypeItem->name.'</a></li>';
                                }
                                echo '</ul>';
                                break;

                        case 'getEvents':
                                $displayUnit = '';
                                /* either this is another list of event nodes, or it is a market summary */
                                if(isset($dataSet->data->Result->marketItems->MarketSummary)){
                                        echo '<ul>';
                                        if(is_array($dataSet->data->Result->marketItems->MarketSummary)){
                                                foreach($dataSet->data->Result->marketItems->MarketSummary as $market){
                                                        echo '<li><p><a href="'.$this->sitename.'betfair-test/betfair-test.php?a=getRunnersAndTopPrices&item='.$market->marketId.'">'.$market->marketName.'</a></p></li>';
                                                }
                                        }else{
                                                $market = $dataSet->data->Result->marketItems->MarketSummary;
                                                        echo '<li><p><a href="http://'.$this->sitename.'betfair-test/betfair-test.php?a=getRunnersAndTopPrices&item='.$market->marketId.'">'.$market->marketName.'</a></p></li>';
                                        }
                                        echo '</ul>';
                                }if(isset($dataSet->data->Result->eventItems->BFEvent)){
                                        echo '<ul>';
                                        /*  if it's an array, there are more than one events, otherwise there is just one */
                                        if(is_array($dataSet->data->Result->eventItems->BFEvent)){
                                                foreach($dataSet->data->Result->eventItems->BFEvent as $event){
                                                        echo '<li><p><a href="'.$this->sitename.'betfair-test/betfair-test.php?a=getEvents&item='.$event->eventId.'">'.$event->eventName.'</a></p></li>';
                                                }
                                        }else{
                                                $event = $dataSet->data->Result->eventItems->BFEvent;
                                                echo '<li><p><a href="'.$this->sitename.'betfair-test/betfair-test.php?a=getEvents&item='.$event->eventId.'">'.$event->eventName.'</a></p></li>';
                                        }
                                        echo '</ul>';
                                }
                                echo $displayUnit;
                                break;

                        case 'getRunnersAndTopPrices':
                                if(betfairConstants::ERROR_OK === $dataSet->data->Result->header->errorCode){
                                        $name = $dataSet->data->Result->marketDataItems[0]->marketName;
                                        $display =<<<EOT
                                                  <h1>${name}</h1><table summary="Runner data">
                                                        <tr><th>id</th><th>Runner name</th><th>Top Price</th><th>Volume</th></tr>
EOT;
                                        foreach($dataSet->data->allRunnerData as $runner){
                                                $id = $runner->selectionId;
                                                $name = $runner->name;
                                                $topPrice = $runner->topPrice;
                                                $topPriceVol = $runner->topPriceVol;
                                                $display.=<<<EOT
                                                        <tr><td>${id}</td><td>${name}</td><td>${topPrice}</td><td>${topPriceVol}</td></tr>
EOT;
                                        }
                                        $display.=<<<EOT
                                                </table>
EOT;
                                        echo($display);

                                }
                                break;

                        default:
                                break;

                }
                return(true);
        }
}
?>

I'll make no excuses - it's not the prettiest code. Nor does it generate the prettiest web page. The purpose here is simply to illustrate how the results returned from the testApp class are passed into the view and rendered. Generally this involves iterating through a set of results, rendering the returned names and id's and interpolating them into links for the next click-call cycle. Note: don't forget to change the $sitename to point to your own website's host name.

This view really relies on a big switch statement to test the 'description' of the dataSet coming in (most likely this is the action string passed in from the previous web click). As you can see, the links rendered within each case generally refer to a method which is naturally downstream from the action being handled, so if the user has just requested 'getEvents', the View will either provide them with further links to getEvents (in cases where there are sub-events to the item just viewed), or the link will call through to getRunnersAndTopPrices. Of course, the methods called would depend entirely on your own application.

The outcome of all this should be a navigation through the Betfair Events hierarchy, eventually ending up on a very simple market view, showing runners, their id's and their top back prices and volumes. Keep hitting refresh on this last screen and you will see the prices updating in real time.

The next tutorial will delve into some more advanced (and as-yet less complete) aspects of bflib, including debugging methods, error handling, session management, caching and more. Stay tuned!

Digg it! Submit to Slashdot Add to Blinklist Del.icio.us Add to Newsvine Add to Technorati Add it to Google Bookmarks
comment
Ashley Rollinson
2011-06-07 19:42:11

error message

I get

*Fatal error: Call to undefined function apc_fetch() in /*****/*******/*****/classes/betfairCache.class.php on line 56

When calling the index.php

Have I missed something?

Thanks!
reply icon
chris
2011-06-08 01:55:54

I get

*Fatal error: Call to undefined function apc_fetch() in /*****/*******/*****/classes/betfairCache.class.php on line 56

Thanks!


Have you installed the APC extension? If not, you can get the APC extension details from the APC home on PECL : http://pecl.php.net/package/APC. Come to think about it, the cache manager should check for APC support and degrade to the array-based caching instead. ISTR putting this in a few weeks ago. I'll check that in the meantime, but would suggest you check that APC is correctly installed and enabled in your php.ini too.

christo
reply icon