Introduction
Do you ever wish that you could simply define a class in PHP and have some magic process turn it into asynchronous function calls in JavaScript on the client-side? Microsoft did it in ASP.NET with their ScriptService
and ScriptMethod
attributes for Web Services, but PHP appears to lack similar functionality.
Until today.
Welcome to the deceptively small, deliciously complex, and deceivingly easy-to-use DigifiScriptService
class.
Background
For the past 8 months, my day job has involved me working with ScriptServices and ScriptMethods in ASP.NET. I've have found them a joy to work with, as the ease of invoking ScriptMethods from the dynamically-generated JavaScript allows me to quickly make cool-looking web apps without any postbacks.
After my day job, however, the business I run on the side deals almost exclusively in LAMP, with PHP as my weapon of choice. Once I realized the power of the ScriptMethods in making lightweight web pages in ASP.NET, I hungered for the same functionality in PHP. It all boiled down to a simple desire: I wanted to write a class in PHP and have it magically available in JavaScript on the client side, just like ASP.NET's ScriptServices. In my mind, the ideal solution would be one that requires minimal configuration or altering of existing page structures.
Searching on the web yielded very few results, the best one I found being one that required embedding the remote method code in the same file as the page being served to the user, as well as a lot of initialization required by the programmer. It also used a hidden <IFRAME>
to perform the posting. It just didn't cut the mustard.
After reviewing what I could find, I decided that my ideal solution needed to meet the following requirements:
- It has to work like ASP.NET's ScriptServices and ScriptMethods as best as possible
- The client-side JavaScript to invoke the ScriptMethods has to be dynamically generated
- The JavaScript methods that invoke the ScriptMethods should be static
- There should be no initialization required to generate a ScriptService class other than to define the class and its methods (i.e., the programmer shouldn't know/care how it all works)
I figured that converting a PHP class to a magic JavaScript object would be the hardest part, so I started with that (turns out I was wrong). Ideally, I wanted to simply write a class and, without having to call any initialization or declare any methods, have it magically turn itself into client-side JavaScript on demand. I read through all the chapters on the PHP manual regarding classes, and was delighted to discover that PHP supported Reflection. I now had what I needed.
Since PHP lacks support for decorative attributes like .NET, I decided to create a base class, DigifiScriptService
. All you have to do is inherit from that class, and any public function will automatically be turned into ScriptMethods.
Of course, making it do that is the interesting part.
The first step was to override the constructor and check to see if the request has a query string consisting of "js". That is what I decided will trigger the JavaScript generation:
class DigifiScriptService
{
function __construct()
{
$class = get_class($this);
if(getenv(QUERY_STRING) == 'js')
{
}
}
}
The function get_class
returns the name of the actual class being instantiated, so if I have a class called MyScriptService
that inherits from DigifiScriptService
and created a new instance of it, get_class
will return "MyScriptService". Using the class name, I can use the ReflectionClass
class in PHP to extract the list of all the methods in the class.
$classBody = '';
$r = new ReflectionClass($class);
foreach($r->getMethods()as $m => $method)
{
Now, my rule is that only public functions will be exposed in the resulting JavaScript proxy class, so I need to filter for that (and the constructor, as it is also a public method):
if($method->isPublic() && $method->name != '__construct')
{
$args = '';
$argSetter = '';
foreach($method->getParameters() as $i => $parameter)
{
$args .= $parameter->name . ',';
$argSetter .= "__drm_args[$i]=" . $parameter->name . ';';
}
echo 'function ' . $class . "_" . $method->name .
"(${args}onSuccess, onFailure, context)" . "{var __drm_args=[];
${argSetter}__digifiss__remoteMethodCall('" . $_SERVER['SCRIPT_NAME'].
"', '$method->name',__drm_args,onSuccess,onFailure,context);};";
if($classBody != '') $classBody .= ',';
$classBody .= $method->name .
":function(${args}onSuccess, onFailure, context) {" .
$class . "_". $method->name .
"(${args}onSuccess, onFailure, context)" . ";}";
}
}
echo "var $class = { $classBody }";
The end result? A JavaScript "class" where your methods can be invoked in a seemingly static fashion:
<script type="text/javascript">
MyScriptService.MyScriptMethod(param, onSuccess, onFailure, 'some contextual data');
</script>
Now, how do we get the JavaScript proxy class into our page? Simply add the following call to your page (preferably between the <HEAD></HEAD>
tags, although it can go just about anywhere in your page body):
<head>
<title>DigifiScriptService Sample Page</title>
<? DigifiScriptService::add_service('MyScriptService.php'); ?>
</head>
What does that call do? Well, the static method on DigifiScriptService
is simply this:
public static function add_service($path)
{
echo '<script type="text/javascript" src="' .
$path . '?js"></script>';
}
It adds a script
tag pointing to our ScriptService file, passing that magic ?js in the query string, resulting in the JavaScript proxy being returned. For ASP.NET developers, this is analogous to adding a <ScriptService>
tag to your ScriptManager
object.
If you examine the JavaScript proxy class, you'll notice that every ScriptMethod simply makes a call to the same function: __digifiss__remoteMethodCall
. This method is housed in digifiss.js, and is the only JavaScript file you need to explicitly add when using this code. I won't go into the nuts and bolts of it here (it took a lot of visiting various sites to put it all together), but here is an overview:
- It only allows a configurable maximum number of concurrent requests
- It builds each method call into a request to the PHP file housing the ScriptService using an
XMLHttpRequest
object - It automatically receives the result from the call, and passes on the result to the method specified to the
onSuccess
or onFailure
parameters appropriately
Using the Code
All you need on your web server are the files digifiss.php and digifiss.js. The rest you write yourself.
Defining your ScriptService
<?
include_once 'digifiss.php';
class MyScriptService extends DigifiScriptService
{
public function HelloWorld()
{
return "Hello World";
}
public function Add($x, $y)
{
return $x + $y;
}
private function DoSomethingPrivate()
{
return "Not Exposed";
}
}
new MyScriptService();
?>
Using your ScriptService
In your actual page, there's just a tiny bit of overhead; one include_once
, one call to a static method on DigifiScriptService
, and one reference to digifiss.js:
<?
include_once 'digifiss.php';
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type"
content="text/html; charset=windows-1250">
<title>DigifiScriptService Sample Page</title>
<script type="text/javascript" src="digifiss.js"></script>
<? DigifiScriptService::add_service('MyScriptService.php'); ?>
</head>
<body>
<a href="javascript:DoHelloWorld();">Click for Hello World</a>
<div id="HelloWorld"></div>
<br />
<br />
<a href="javascript:DoAddition();">Add 2 + 2</a>
<div id="Addition"></div>
<br />
<br />
<script type="text/javascript">
function DoHelloWorld()
{
document.getElementById('HelloWorld').innerHTML = 'Loading...';
MyScriptService.HelloWorld(onSuccess, onFailure, 'HelloWorld');
}
function DoAddition()
{
document.getElementById('Addition').innerHTML = 'Loading...';
MyScriptService.Add(2, 2, onSuccess, onFailure, 'Addition');
}
function onSuccess(result, context, method)
{
document.getElementById(context).innerHTML = result;
}
function onFailure(result, context, method)
{
document.getElementById(context).innerHTML = result;
}
</script>
</body>
</html>
Bonus Code
A great number of my ScriptMethod calls in my day job involve connecting to a database, extracting the data as XML, applying an XSL transform on it, and sending the resulting HTML back to the client to be inserted as some <div>
tag's innerHTML
value. To provide this functionality in PHP, I have added two additional functions in the DigifiScriptService
class that you're free to use:
mysql_query_to_xml
- takes a MySQL connection link ID, the SQL to run, and the names of the parent and child nodes, and returns an XML DOMDocument
object which contains each row of your SQL result as a child of the parent node. mysql_query_to_xsl
- takes a MySQL connection link ID, the SQL to run, the names of the parent and child nodes, and the path to an XSL stylesheet file, and returns the result of converting the MySQL result to XML and transforming it using the XSL file.
For an idea of how you could use these, here's an example:
<?
include_once 'digifiss.php';
class DatabaseExample extends DigifiScriptService
{
public function FindCustomers($searchString)
{
$connection = mysql_connect("localhost", "dbuser", "password");
@mysql_select_db("customers", $connection);
$sql = "SELECT customerId, firstName, lastName FROM customers" .
" WHERE CONCAT(firstName, ' ', lastName) " .
"LIKE '%$searchString%' ORDER BY lastName";
$result = $this->mysql_query_to_xsl($connection, $sql, 'Customers',
'Customer', 'searchresults.xsl');
mysql_close($connection);
return $result;
}
}
new DatabaseExample();
?>
Important Notes
- At present, you cannot upload files with this method. The way JavaScript security works, it is unlikely that this will ever change.
- I have tested this on Internet Explorer 7 and FireFox 3.0. I assume it'll work in IE 6, but I don't have a copy available. I have no idea if this will work on Safari or any other platform.
- For those who are interested in the guts, I tried to comment as much as possible.
Points of Interest
I started this at midnight and worked until 4:30am, and the first version used an <IFRAME>
for the posting of the data, which worked quite well.
When I woke up 6 hours later, I decided that the "clicking" sound IE makes while posting to the IFrame was unacceptable, and spent a few more hours figuring out the XMLHttpRequest
solution.
History
- 2009/02/21 - Initial version.