Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / PHP

Simulating .NET's ScriptService in PHP

Rate me:
Please Sign up or sign in to vote.
5.00/5 (18 votes)
22 Feb 2009CPOL6 min read 54K   392   30   12
An easy-to-use class that allows us to create PHP classes whose methods are automatically exposed in client-side JavaScript.

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:

PHP
class DigifiScriptService
{
  function __construct()
  {
    //Get the real (descendant) class' name:
    $class = get_class($this);
    
    //If they are requesting the javascript file, produce the javascript:
    if(getenv(QUERY_STRING) == 'js')
    {
      // JavaScript generation here
      // ...
    }
    
    // ... //
  }
}

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.

PHP
$classBody = '';
$r = new ReflectionClass($class);

//Build the javascript methods that map to the PHP ones:
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):

PHP
  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);};";
    
    //Class body contains what appear to be redundant function definitions,
    //but this is to simulate the "static" method interface that .NET 
    //uses; now we don't have to instantiate anything in JavaScript to
    //call our functions:
    if($classBody != '') $classBody .= ',';
    $classBody .= $method->name . 
                  ":function(${args}onSuccess, onFailure, context) {" . 
                  $class . "_". $method->name . 
                  "(${args}onSuccess, onFailure, context)" . ";}";
  }
}

//Dump out the actual javascript fake class definition:
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):

HTML
<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

PHP
<?
// MyScriptService.php

include_once 'digifiss.php';

class MyScriptService extends DigifiScriptService
{
  //This is a public function which will be turned into a ScriptMethod
  public function HelloWorld()
  {
    return "Hello World";
  }
  
  //A ScriptMethod with arguments:
  public function Add($x, $y)
  {
    return $x + $y;
  }
  
  //A private method, will be ignored by the JavaScript generator:
  private function DoSomethingPrivate()
  {
    return "Not Exposed";
  }
}

//This is the only line of initialization required:
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:

HTML
<?
//SamplePage.php

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:

PHP
<?
// DatabaseExample.php

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";
    
    //Produces an HTML <table> containing the search results:
    $result = $this->mysql_query_to_xsl($connection, $sql, 'Customers', 
                                           'Customer', 'searchresults.xsl');

    mysql_close($connection);

    return $result;
  }
}

//This is the only line of initialization required:
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) digifi inc.
Canada Canada
Clayton Rumley is web developer for hire from Winnipeg, Manitoba, Canada.

Comments and Discussions

 
GeneralMessage Closed Pin
25-Feb-09 5:11
Jose Maria Estrade25-Feb-09 5:11 
GeneralRe: Awesome Pin
Clayton Rumley25-Feb-09 5:28
Clayton Rumley25-Feb-09 5:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.