Programming is a wonderful mix of art and science; source code is both a poem and a math problem. It should be as simple and elegant as it is functional and fast. This blog is about that (along with whatever else I feel like writing about).

Friday, May 19, 2006

My Own JSON/PHP Web Services: SSWebService

Web Services. It's a new buzzword. Everyone needs them, because they'll make everything better, right?

Well, nuts to that.

What web services really amount to is nothing more than remote procedure calls that you can make from a web browser. That's cool enough. You make a request, and get a response. Instead of sending HTML that you render, the web service just returns the data. Most web services today use XML to package that data, though a few places -- like Yahoo -- are starting to use JSON instead.

JSON has a few major advantages over XML. First, it is smaller: less bandwidth is wasted by 1.0 crapola. Second, it is very easy to parse it in Javascript. In fact, JSON stands for "Javascript Object Notation," and is native to Javascript. Parsing a JSON-encoded object takes very little time. I've written Javascript applications that need to parse an XML file, and let me tell you, parsing XML in Javascript is not trivial and is a major performance bottleneck.

For an upcoming project, I wanted to develop a web service. There will be PHP interacting with the database, and I want to make simple calls from the browser. The frontend will be a Javascript application, and it is not page-based at all. So I needed a way to pass data back and forth between the browser and the server simply and quickly.

I investigated using SOAP, but was underwhelmed. I decided to design my own web service in PHP, which would use JSON for all its data passing.

The first thing I needed was to be able to register functions with the service, such that only functions that are actively made public are available. I quickly put together a PHP class with a "methods" instance variable and a "register" instance method.

Each function will take a single parameter, but this parameter is expected to be an arbitrary JSON object, which can hold any amount of data. So rather than calling a function like sum(1,2), you would call the function sum($par), where $par = {one:1, two:2}. Then you parse that parameter object in the sum() function and handle it appropriately. It's an extra step and a bit more to remember, but I feel that it adds plenty of flexibility. For example, I wrote a sum function using this brand of parameter passing, and it was utterly trivial to make it work for an arbitrary number of values.

function sum($par) {
    $sum = 0;
    foreach ($par as $key=>$val) {
        $sum += $val;
    }
    return array("sum"=>$sum);
}


Oh yes. I forgot to mention how the return values work. Rather than simply returning a single value, I will always be returning an associative array/object (the PHP associative array type is most closely related to the Javascript object type). This way I know what the return value is (it's called the "sum"), and for more complicated functions I can return as much data as I want.

In order to serve a request, which should be a POST with two parameters ("method" and "param"), I need to decode the param parameter from JSON into a PHP-native object, check that the method is registered, and then call the method with the parameter. I then take the function's return value, JSON-encode it, and return it to the browser.

Here is the code for the web service (sswebservice.php):
<?php

require_once("JSON.php");

class SSWebService {
    var $methods;
    var $json;
   
    function initialize() {
        $this->json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
    }
   
    /*
    add a function to the server, so it can be accessed
    */
    function register($name) {
        $this->methods[$name] = true;
    }
   
    /*
    set a registered function to be inaccessible
    */
    function deregister($name) {
        $this->methods[$name] = false;
    }
   
    /*
    execute the given method, passing its single parameter
    JSON-encodes the return value, which should be an object or associative array
    */
    function call($name, $param) {
        if ($this->methods[$name] == true) {
            $evalstring = $name."(\$param);";
            eval("\$rval=".$evalstring.";");
            return $this->json->encode($rval);
        }
    }
   
    /*
    decode the JSON param into a native object, and call the given method
    return the JSON-encoded object to the browser via echo
    */
    function serve($method, $param) {
        $obj = $this->json->decode(stripslashes($param));
       
        if ($this->methods[$method] == true) {
            $res = $this->call($method, $obj);
        } else {
            $res = $this->json->encode("Not a registered function.");
        }
       
        echo $res;
    }
}

?>


And here is a demonstration web service that uses it (jsontest.php):
<?php

require_once('sswebservice.php');

$server = new SSWebService;

$server->initialize();

function helloWorld($par) {
    return array("string"=>"Hello, world!");
}

function hello($par) {
    return array("string"=>"Hello, ".$par["name"]."!");
}

function sum($par) {
    $sum = 0;
    foreach($par as $key=>$val) {
        $sum += $val;
    }
    return array("sum"=>$sum);
}

$server->register("sum");
$server->register("helloWorld");
$server->register("hello");

$method = $_POST["method"];
$param = $_POST["param"];

$server->serve($method, $param);

?>


As you can see, this web service has three available functions: sum, hello, and helloWorld. Now I just need a way to call them from the browser.

I tend to use the Prototype/Scriptaculous libraries for my web development, so I first whipped something up using Prototype's Ajax.Request object, but that turns out to be quite a bit of code every time I want to make a call to my web service. So I created a very simple class with nothing in it but a class method (ssclient.js):

function SSClient() {

}

SSClient.call = function(url, method, obj, callback) {
    var param = "method=" + method + "&param=" + obj.toJSONString();
    new Ajax.Request(url, {
        parameters: param,
        onSuccess: function(req) {
            var rval = req.responseText.parseJSON();
            callback(rval);
        },
        onFailure: function(req) {
            alert("Call failed.");
        }
    });
}


So when I want to make a call to my web service, I simply need to supply the URL, the method I want to call, the Javascript object I want to send, and a callback function to execute when it returns. The callback function should expect to receive a JS-native object, since I parse the JSON-encoded object before calling the callback function.

Here is an example of the code to call the web service:

function button1() {
    SSClient.call("jsontest.php", "sum", {
        one: 1,
        two: 6,
        three: 9
    }, sum_callback);
}

function sum_callback(r) {
    alert(r.sum);
}


As you can see, the SSClient.call() function is very easy to use, and I can create an anonymous object to pass along. In this case, the callback function simply pops up the return value. Note that it is accessed by r.sum, which is because of how I set up my return values in jsontest.php. Neat.

This requires Prototype, json.js from http://www.json.org/json.js, and JSON.php from http://mike.teczno.com/JSON/JSON.phps.

And now I'm off to build an application with my new web service!

11 comments:

Paul Trippett said...

Great article!

I was having several problems with the JSON.js file from json.org and using the protypejs.org scripts using the json.js file below fixes these problems as the below library does not extend the object.prototype

http://trimpath.com/project/wiki/JsonLibrary

Matthew Story said...

Just reading through this to touch up. As of PHP5 you can use the magic __construct() function in your PHP class, as opposed to creating an initialize function and calling it after you create an instance. Only saves a line . . . but whatever.

Unknown said...

Hi Sean,
Great example. I tried to use this but I have an error.

" Error: obj.toJSONString is not a function
Source File: http://localhost:9090/json/pear-json%20extension/ssclient.js
Line: 8 "

Do you know what is the problem ?

tnahs a lot.

Marco Ortega from Santiago de Chile

Unknown said...

Hi Sean again, Now I have the follow error. The old error was corrected.

Error: Ajax is not defined
Source File: http://localhost:9090/json/pear-json%20extension/ssclient.js
Line: 10

Unknown said...

Please tell me how to create Ajax class ? as I am still getting same error as above.

Erik said...

Isn't using eval slow?

Isn't it beter to use:
$rval = $name($param);

Instead of:
$evalstring = $name."(\$param);";
eval("\$rval=".$evalstring.";");"

Greets,
Erik

Faerae said...

would you mind to share source codes in zip file?

I'm a Newbie...

thanks..

eric said...

is there something missing here?? you write, "I tend to use the Prototype/Scriptaculous libraries for my web development, so I first whipped something up using Prototype's Ajax.Request object, but that turns out to be quite a bit of code every time I want to make a call to my web service. So I created a very simple class with nothing in it but a class method" but then you use Ajax.Request - i am confused. is there something missing here?

eric said...

better yet...can i do something like: http://localhost/webservice/jsontest.php?method='helloWorld'&param='nothing'

when i do I get an error message: Notice: Undefined index: 'helloWorld' in C:\Program Files (x86)\Apache Software Foundation\Apache2.2\htdocs\webservice\sswebservice.php on line 47
"Not a registered function."

eric said...

can i do, http://localhost/webservice/jsontest.php?method='helloWorld'&param='nothing'

when i do, i get an error message: Notice: Undefined index: 'helloWorld' in C:\Program Files (x86)\Apache Software Foundation\Apache2.2\htdocs\webservice\sswebservice.php on line 47
"Not a registered function."

Anonymous said...

where is the html file or index code? sorry im a newbie on web service.