The following file is deployed (for example) to http://localhost/server.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();
}
?>
Given the above server, deployed to http://localhost/server.php and this (snippet of) client deployed to http://localhost/client.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>
The directory structure found in the zipped ScriptServer archive should be preserved. Given this structure;
./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;
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;
require_once ’ScriptServer.php’;
require_once SCRIPT_SERVER . ’Server/PostOffice.php’;
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;
//...
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.
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.
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;
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);
};
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.;
// 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);
}
You can set a timeout for asynchronous (only!) requests like;
// Timeout in milliseconds (1 second)
m.timeout = 1000;
If a request times out, a client error is generated (see 3.4).
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.
// 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;
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’);
}
}
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;
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;
m.clientErrorFunc = function(e) {
document.write(e.name+’: ’.e.message);
}
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;
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).
Application errors occur when your PHP server returns an instance of ScriptServer_Error (see 3.2.3). For example the following in PHP;
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.;
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;
m.applicationErrorFunc = function(e) {
document.write(e.name+’: ’.e.message);
}
The following error codes have been implemented in ScriptServer;
; 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.;
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).
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:
//...
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;
divideError: function(e) {
// Display the German error message
alert( ScriptServer_ErrorReader(e.code) );
}
}