3 Developing with ScriptServer

3.1 In a Hurry

3.1.1 Server side

The following file is deployed (for example) to http://localhost/server.php

<?php

// Including this sets up the SCRIPT_SERVER constant

require_once ’/path/to/ScriptServer.php’;

// Load the PostOffice server

require_once SCRIPT_SERVER . ’Server/PostOffice.php’;

// Some class you’ve written...

class HelloWorld {

    function sayHello($name) {

        return ’Hello ’.$name;

    }

}

// Create the PostOffice server

$S = & new ScriptServer_Server_PostOffice();

// Register your class with it...

$S->addHandler(new HelloWorld());

// This allows the JavaScript to be seen by

// just adding ?client to the end of the

// server’s URL

if (isset($_SERVER[’QUERY_STRING’]) &&

        strcasecmp($_SERVER[’QUERY_STRING’], ’client’)==0) {

    

    // Compress the output Javascript (strip whitespace)

    define(’SCRIPT_SERVER_RENDER_FORMATTING’,FALSE);

    

    // Utility to help with displaying ScriptServer

    // JavaScript files and generated code

    require_once SCRIPT_SERVER . ’Renderer.php’;

    $R = & ScriptServer_Renderer::load();

    // Add serializer.js

    $R->addSerializer();

    // Add xmlhttp.js

    $R->addXmlHttp();

    // Add post.js

    $R->addPost();

    // Get the client code generator from the server

    $G = & $S->getGenerator();

    // Add the generated code for display

    $R->addScript($G->getClient());

    // Display the JavaScript client

    $script = $R->toString();

    header(’Content-Length: ’.strlen($script));

    header(’Content-Type: text/javascript’);

    echo $script;

} else {

    // This is where the real serving happens...

    // Include error handler

    // PHP errors, warnings and notices serialized to JS

    require_once SCRIPT_SERVER . ’ErrorHandler.php’;

    // IE’s XMLHttpRequest caches unless told not to...

    header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); 

    header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . "GMT" ); 

    header( "Cache-Control: no-cache, must-revalidate" ); 

    header( "Pragma: no-cache" );

    // Start serving requests...

    $S->serve();

}

?>

3.1.2 The Client Side

Given the above server, deployed to http://localhost/server.php and this (snippet of) client deployed to http://localhost/client.html;

<html>

<head>

<!-- Load the generated client side code... -->

<script type=’text/javascript’ src=’http://localhost/server.php?client’></script>

<script type=’text/javascript’>

<!--

// Some function perhaps called onclick of a button

function doSayHello(name) {

    // Create the client object, passing the async

    // handler

    var h = new helloworld(HelloWorldHandler);

    // Call the remote method

    H.sayHello(name);

}

// A handler is required to accept the response

// of asychronous calls...

var HelloWorldHandler = {

    // Function must have same name as remote method

    sayHello: function(result) {

        alert(result);

    }

}

-->

</script>

3.2 Library Walk Through

3.2.1 Requirements

3.2.2 Loading the Library in PHP

The directory structure found in the zipped ScriptServer archive should be preserved. Given this structure;

./ScriptServer.php

./ScriptServer/[files here]

It is possible to load the library by including the file ScriptServer.php, which sets up the PHP constant SCRIPT_SERVER that points at the full path to the ScriptServer subdirectory. This allows you to load ScriptServer classes like;

<?php

require_once ’/full/path/to/ScriptServer.php’;

require_once SCRIPT_SERVER . ’Server/PostOffice.php’;

If you place ScriptServer somewhere in your include_path, ScriptServer.php being in the root of one of the identified include_path directories, you should be able to use it like;

<?php

require_once ’ScriptServer.php’;

require_once SCRIPT_SERVER . ’Server/PostOffice.php’;

3.2.3 Overview of Files in ScriptServer subdirectory

3.2.4 PostOffice Server

The class ScriptServer_Server_PostOffice (require_once SCRIPT_SERVER . ’Server/PostOffice.php’;) provides a server implementation the defines how clients should interact with it. Currently it is the only implementation ScriptServer provides - others may develop later to take into account notions like REST.

It is primarily RPC based, each class and method registered with it being available via the URL while arguments to methods are provided as URL encoded POST data.

For example if you register a class like the following with it;

<?php

//...

class Math {

    function add($x, $y) {

        return $x + $y;

    }

    function subtract($x, $y) {

        return $x - $y;

    }

}

//...

?>

If the server is deployed to http://localhost/server.php, the method are exposed like;

The arguments to these methods are provided ”invisibly” as HTTP POST data. When using a generated client you should not have to worry about how this happens but, behind the scenes, the JavaScript PostRequest class is used, which adds parameters to a url encoded POST request as a list. Although the parameters can be named (so it is in fact a hash not a list), the PostOffice server is only interested in the order in which they appear.

3.3 Client Side Issues

3.3.1 Case Sensitivity

PHP classes are, registered with a server like the PostOffice server automatically have their name and methods registered in lower case. The reason for this is the way PHP reflection functions like get_class_methods() work. With PHP4 these return all the method names in lower case. With PHP5 the case it preserved. Meanwhile, in all PHP versions, when you call a method (or create an object) it’s case-insensitive while Javascript is case-sensitive. Forcing everything to lower case allows ScriptServer to work without problems under PHP4 and 5.

It also has the advantage that it reduces the chances of conflicting with native Javascript classes, which always use studly caps naming. For example registering a PHP class Math results in a generated JavaScript class called ’math’ - if case sensitivity was preserved it would clash with the native Math class in Javascript.

Addressing via the URL is also done in lower case.

3.3.2 Synchronous vs. Asynchronous Requests

XmlHttpRequest allows you to make synchronous and asynchronous requests. You have the option to use either with a generated client but you should generally avoid synchronous requests - they lock the client’s browser until the request completes and there is no mechanism for controlling their timeout. The client needs to have it’s state altered to switch between sync and async modes.

Some JavaScript examples, given the Math class above;

// Defaults to sync mode

var m = new math();

// sync call

var result = m.add(1,1);

// Pass a handler to the constructor - now in async mode

var m = new math(MathHandler);

// Result passed to MathHandler

m.add(1,1);

// Default sync mode

var m = new math();

// Switch to async mode, passing the handler

m.Async(MathHandler);

m.add(1,1);

// Start with async mode

var m = new math(MathHandler);

// Switch to sync mode

m.Sync();

m.add(1,1);

// Possible definition of the MathHandler used above

var MathHandler = {

    add: function(result) {

        alert(result);

    }

};

// Another way to do it

var MathHandler = new Object();

MathHandler.add = function(result) {

   alert(result);

};

3.3.3 Communicating the Context with Async Calls

To allow the context of a call to be available to the handler of the call, it’s currently recommended to attach information to the handler itself, before placing the call e.g.;

function doAdd(x,y) {

    // Define a local handler

    var MathHandler = new Object();

    MathHandler.x = x;

    MathHandler.y = y;

    MathHandler.add = function(result) {

       document.write(this.x+’ plus ’+this.y+’ is ’+result);

    }

    var m = new math(MathHandler);

    m.add(x,y);

}

3.3.4 Timeouts

You can set a timeout for asynchronous (only!) requests like;

var m = new math();

// Timeout in milliseconds (1 second)

m.timeout = 1000;

If a request times out, a client error is generated (see 3.4).

3.3.5 Multiple Requests using Same Client

A single generated client (like the math class in these examples) using a single instance of XmlHttpRequest (via XmlHttpClient defined in xmlhttp.js). A single client cannot be used to make further calls until existing calls have been completed. The simple way to keep this fact away from users it to create a new instance of the client each time it’s needed e.g.

function doAdd(x,y) {

    // Create a new instance

    var m = new math(MathHandler);

    m.add(x,y);

}

This may result is excessive numbers of objects (plus I’ve heard stories of wierd bugs that start showing up when more than a certain number of XmlHttpRequest objects are in use). A smart approach might be to use a of pool clients - right now the API doesn’t help you much with this unless you access ”private” members but that should change in future.

Each generated client has a __client property which refers an instance of XmlHttpClient (defined in xmlhttp.js). It need initializing first using the __initClient() function. A simple example implementation of pooling might be;

// Global for simple example

var clientPool = null;

// Builds five math clients

function buildPool() {

   clientPool = new Array();

   for(var i=0;i<5;i++) {

       var m = new math();

       m.__initClient();

       clientPool.push(m);

   }

}

function getMath() {

    // Initialize if needed

    if (!clientPool) {

        buildPool();

    }

    // Find an available client

    for(var i = 0; i < clientPool.length; i++) {

       if ( !clientPool[i].__client.callInProgress() ) {

           return clientPool[i];

       }

    }

    return false;

}

function doAdd() {

   var m = getMath();

   if ( m ) {

       m.Async(MathHandler);

       m.add(1,1);

   } else {

       alert(’Busy! Try again later’);

   }

}

3.4 Error Handling

By default generated clients will report all errors directly to the end user, using JavaScript alerts - in other words generated clients announce all errors loudly unless you specifically tell them not to.

Errors are seperated into three types;

3.4.1 Client Errors

These are errors which occur before a request is made (e.g. the browser does not support XmlHttpRequest or the client already has a call in progress) or related to basic HTTP connectivity (e.g. the server failed to return a HTTP 200 OK status code or the request timed out).

You can override client error handling like;

var m = new math();

m.clientErrorFunc = function(e) {

    document.write(e.name+’: ’.e.message);

}

3.4.2 Server Errors

Server errors occur when either the response returned by the server is not valid JavaScript syntax (cannot be eval()ed) or when a PHP error occurred, when the server has ErrorHandler loaded (see 3.2.3). This also means if you use PHP’s trigger_error() function, it will generate a Server Error on the client side.

Handling of server errors can be overridden like;

var m = new math();

m.serverErrorFunc = function(e) {

    document.write(e.name+’: ’.e.message);

}

If a Server error occurs, it is generated immediately, halting all further execution (so the response from the called method is discarded).

3.4.3 Application Errors

Application errors occur when your PHP server returns an instance of ScriptServer_Error (see 3.2.3). For example the following in PHP;

class Math {

   

    function divide($x,$y) {

        if ( $y == 0 ) {

            require_once SCRIPT_SERVER . ’Types.php’;

            $Error = & new ScriptServer_Error();

            $Error->setError(

                ’ZeroDivisionError’,

                ’Cannot divide by zero’

            );

            return $Error;

        } else {

            return $x / $y;

        }

    }

}

If an application error is returned, all other returned data is discarded; so if the error is nested inside a complex PHP data structure, only the error is returned - the rest of the data structure is discarded.

Application errors are handled in two different ways. If the handler you provide for an async call provides a function with the same name as the called method + ’Error’, this will be called, e.g.;

var MathHandler = {

    divide: function(result) {

        document.write(’The result is: ’+result);

    }

    // Application errors for math.divide sent here

    divideError: function(e) {

       document.write(e.name+’: ’+e.message);

    }

}

If a correctly named error handling function is not defined in the handler, the error is handled by the .applicationErrorFunc property of the generated client. This can be overridden like;

var m = new math();

m.applicationErrorFunc = function(e) {

    document.write(e.name+’: ’.e.message);

}

3.4.4 Error Codes

The following error codes have been implemented in ScriptServer;

[Client_Error]

; Client Errors (range 1000 - 1999)

; Range 1000 - 1199 is reserved - avoid using them in your code!

1000 = "Unable to create XmlHttpRequest: not supported"

1001 = "Call in progress. XmlHttpClient cannot be until current request is completed"

1002 = "Invalid HTTP status code in server response: must by 200 OK"

1003 = "Request timed out: asychronous requests only"

1004 = "Invalid request parameter name: contains non-word characters - must match [^A-Za-z0-9_]"

1005 = "Call type not defined: request.type must have value = ’async’ || ’sync’"

1006 = "Javascript to PHP serialization error e.g. recursive references in data structure"

1007 = "Problem calling XmlHttpRequest.open() e.g. permission denied to access URL in different domain"

[Server_Error]

; Server Errors (range 2000 - 2999): see ErrorHandler.php

; Range 2000 - 2199 is reserved - avoid using them in your code!

2000 = "Internal Server Error" ; Covers PHP E_NOTICE and E_WARNING messages

2001 = "Server Notice" ; Results from trigger_error() with E_USER_NOTICE

2002 = "Server Warning" ; Results from trigger_error() with E_USER_WARNING

2003 = "Server Error" ; Results from trigger_error() with E_USER_ERROR

2004 = "Internal Server Alert" ; From E_STRICT errors in PHP5

2005 = "Internal Server Exception" ; Uncaught PHP5 exception

2006 = "Junk from server. Response not well formed"; Javascript eval() failed - note e.response contains what was evaled

[Application_Error]

;Application Errors (range 3000+) - nothing reserved

These can be found in the file ScriptServer/errors/errors.en.ini. The codes are ”attached” to exceptions using the property Error.code e.g.;

var m = new math(MathHandler);

m.timeout = 200;

m.clientErrorFunc = function(e) {

    if ( e.code == 1003 ) {

        // Ignore this because it’s a timeout

    }

}

For some errors, further properties are used to return useful information for troubleshooting. For example error code 1002 (invalid HTTP status) adds the property .headers to the exception, which contains all the HTTP response headers received from the server. Also error code 2006 adds a property .response which contains the text response body from the server.

If you wish to implement your own errors codes, typically these will be in the 3000+ range (application errors). It’s a good idea to use error codes as it allows the client side to respond accurately to different problems it may encounter plus allows delivery of localised error messages in languages other than English (see below). The ranges 1000-1199 and 2000-2199 are reserved as possible future ScriptServer error codes (avoid defining them in your own code).

Localised Error Messages

As a basic mechanism to allow localised error messages to be displayed to users, the ScriptServer_Renderer class provides the method addErrorReader() which generates a client side Javascript function called ScriptServer_ErrorReader, containing a dictionary of error codes and their messages. The error codes are read, server side, from a file parallel to the errors.en.ini file (currently there are no translations). In principle this could be used like;

Server Side:

<?php

//...

require_once SCRIPT_SERVER . ’Renderer.php’;

$R = & new ScriptServer_Renderer();

// Note this function accepts a second argument; a hash

// of application errors - see the source

$R->addErrorReader(’de’); // Load the German error messages

$R->render();

?>

On the client side the generated ScriptServer_ErrorReader function could be used like;

var MathHandler = {

   divideError: function(e) {

       // Display the German error message

       alert( ScriptServer_ErrorReader(e.code) );

   }

}