Performing Remote Procedure Calls With PEAR XML_RPC

Learn to harness the power of XML-RPC in your PHP scripts.

Remote Control

If you're familiar with C or C++, you've probably already heard of Remote Procedure Calls - they're the framework by which procedures on a server are remotely executed by other clients on the network. RPCs are interesting, because they make it possible to design services that can be used independent of the operating environment of the client. So long as both client and server understand the RPC protocol, they can communicate with each other to execute commands and process return values.

Now, you might not know this, but your favourite language and mine, PHP, has been shipping with support for XML-based Remote Procedure Calls since PHP 4.1.0. This support makes PHP ideal for developers looking to design the next generation of cutting-edge Web services, and to create network-based applications using existing, widely-used protocols like XML and HTTP.

The only problem? Not too many people know how to do it.

That's where this tutorial comes in. Over the next few pages, I'll be demonstrating how you can use PHP, in combination with the ultra-cool PEAR XML_RPC package, to add powerful new capabilities to your Web applications. Keep reading.

Identity Crisis

Before getting into the code, a few words about XML-RPC. According to the primer available on the official XML-RPC Web site (http://www.xmlrpc.com/), XML-RPC is "a spec and a set of implementations that allow software running on disparate operating systems, running in different environments to make procedure calls over the Internet...using HTTP as the transport and XML as the encoding".

If that was a little too geeky for you, allow me to simplify things. XML-RPC is a client-server paradigm which builds on existing Internet technologies to simplify the task of invoking procedures and accessing objects across a network. It uses XML to encode procedure invocations (and decode procedure responses) into a package suitable for transmission across a network, and HTTP to actually perform the transmission.

At one end of the connection, an XML-RPC server receives XML-RPC requests containing procedure calls, decodes them, invokes the named functions and packages the responses into XML-RPC response packets suitable for retransmission back to the requesting client. The client can then decode the response packets and use the results of the procedure invocation in whatever manner it chooses. The entire process is fairly streamlined and, because of its reliance on existing standards, relatively easy to understand and use.

Here's a quick example of what a XML-RPC request for the remote procedure getFlavourOfTheDay() might look like:

<?xml version="1.0"?>
<methodCall>
<methodName>getFlavourOfTheDay</methodName>
<params>
<param>
<value><string>Tuesday</string></value>
</param>
</params>
</methodCall>

And here's what the response might look like:

<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>blueberry</string></value>
</param>
</params>
</methodResponse>

In this case, XML is used to format a request for the remote procedure getFlavourOfTheDay(), and HTTP is used to transfer the request from the client to the server. At the server, the XML-formatted request is decoded, the procedure is executed with the given parameter, and the return value is re-encoded back into XML and transmitted to the client. The client then decodes the XML, extracts the return value and uses it as needed.

If you're new to XML-RPC, the information above should be sufficient to explain the basic concepts and ensure that you can follow the material that comes next; however, before proceeding, I'd recommend that you read the official XML-RPC specification, available at http://www.xmlrpc.com/spec.

Value Add

The XML_RPC package is a PEAR package which provides an API for PHP developers to encode and decode XML-RPC requests and responses, and to build XML-RPC clients and servers. It's currently maintained by Stig Bakken, and is freely available from http://pear.php.net/package/XML_RPC

The XML_RPC package consists of a number of different objects, each serving a specific purpose. The most fundamental object, however, is the XML_RPC_Value() object, which is used to represent XML-RPC data values. This object supports all the simple and complex types outlined in the XML-RPC specification - to see how it works, type out and run the following script:

<?php
// include class
include('XML/RPC.php');

// string
$value = new XML_RPC_Value("gargantuan gobbling geese", "string");

echo $value->serialize();
?>

When you view the output of the script, you should see something like this:

<value>
<string>gargantuan gobbling geese</string>
</value>

The output above is the XML-RPC representation of the string "gargantuan gobbling geese". It has been created by instantiating an object of the XML_RPC_Value() class, and passing the class constructor two arguments: the value to be encoded, and the type of data it is to be encoded as.

The XML-RPC specification supports a number of different data types, both simple and complex. The following example demonstrates the simple (or "scalar") types:

<?php
// include class
include('XML/RPC.php');

// string
$value = new XML_RPC_Value("gargantuan gobbling geese", "string");

// int
$value = new XML_RPC_Value("2003", "int");

// float
$value = new XML_RPC_Value("12.67", "double");

// boolean
$value = new XML_RPC_Value("1", "boolean");

// datetime
$value = new XML_RPC_Value("20040829T22:12:00", "dateTime.iso8601");

// base64
$value = new XML_RPC_Value("1", "base64");
?>

The XML-RPC representation of each value can be obtained by calling the object's serialize() method, while the value itself can be retrieved with the scalarval() method. The following listing illustrates:

<?php
// include class
include('XML/RPC.php');

// float
$value = new XML_RPC_Value("12.67", "double");

// prints "<value><double>12.67</double></value>"
echo $value->serialize();

// print "12.67"
echo $value->scalarval();
?>

A Simple Request

Now, while all this futzing about with values is very amusing, it begs the questions: what use are these XML_RPC_Value() objects anyway?

To answer that question, you need to meet two other objects in the XML_RPC pantheon: the XML_RPC_Message() object, and the XML_RPC_Response() object. The XML_RPC_Message() object represents a procedure invocation by the client. When initializing this object, the object constructor must be passed two arguments: the name of the remote procedure, and the arguments to it (as an array of XML_RPC_Value() objects). Take a look at this next script, which illustrates by creating a message packet invoking the procedure setYear(2004):

<?php
// include class
include('XML/RPC.php');

// create value
$value = new XML_RPC_Value("2004", "int");

// create message packet
$msg = new XML_RPC_Message("setYear", array($value));

// encode into XML-RPC-compliant format
echo $msg->serialize();
?>

Here's the output:

<?xml version="1.0"?>
<methodCall>
<methodName>setYear</methodName>
<params>
<param>
<value><int>2004</int></value>
</param>
</params>
</methodCall>

Typically, the XML_RPC_Message() object is used at the client end of the connection, to create and encode an XML-RPC request. The corresponding server response is represented by a second object, the XML_RPC_Response() object, which is illustrated in the listing below:

<?php
// include class
include('XML/RPC.php');

// create value
$value = new XML_RPC_Value("-1", "int");

// create response packet
$resp = new XML_RPC_Response($value);

// encode into XML-RPC-compliant format
echo $resp->serialize();
?>

In this case, the object constructor is passed the return value of the procedure, again as an XML_RPC_Value() object. Here's the output:

<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><int>-1</int></value>
</param>
</params>
</methodResponse>

The answer to the question posed at the top of this page should now be self-evident. As the two examples above illustrate, XML_RPC_Value() objects are a crucial part of the client-server transaction, because procedure arguments and return values are encoded as XML_RPC_Value() objects by the client and server respectively. Encapsulating data (and data types) in XML help the agents at either end of the connection to transfer data back and forth without affecting its integrity.

Service With A Smile

With the basics out of the way, let's now turn our attention to the meat of the XML_RPC package - the Server and Client objects. Consider the following listing, which demonstrates the process of creating an XML-RPC server:

<?php
// include server class
include('XML/Server.php');

// create server
// set up dispatch map
$server = new XML_RPC_Server(array("whoAmI" => array("function" => "phpWhoAmI")));
?>

With the PEAR XML_RPC class, creating an XML-RPC server is pretty simple - all you need to do is initialize an object of the XML_RPC_Server() class, and pass the object constructor what geeks call a dispatch map. Essentially, a dispatch map maps the server's "public" XML-RPC procedures to "private" PHP functions; every time an XML-RPC procedure is called by a client, the server consults the dispatch map to see which PHP function it should invoke to handle the call.

In the example above, the XML-RPC server exposes a single procedure - whoAmI() - which is internally mapped to the phpWhoAmI() function. This PHP function is responsible for parsing the XML-RPC request packet, extracting the procedure arguments from it, performing its calculations, and encoding the return value into a response packet suitable for transmission to the server. Let's take a closer look, to see how all these tasks are accomplished:

<?php
// include server class
include('XML/Server.php');

// create server
// set up dispatch map
$server = new XML_RPC_Server(array("whoAmI" => array("function" => "phpWhoAmI")));

// function to parse arguments and return them to
// caller in different format
function phpWhoAmI($params)
{
    $name = $params->getParam(0);
    $species = $params->getParam(1);
    $age = $params->getParam(2);

    $response = "I am " . $name->scalarval() . ", a " . $age->scalarval() . "-year old " . $species->scalarval();

    // return response to client
    return new XML_RPC_Response(new XML_RPC_Value($response, "string"));
}
?>

From the above, it is clear that three arguments are required by the phpWhoAmI() function - a name, a species type, and an age. These three arguments are extracted as XML_RPC_Value() objects from the request packet via the getParam() method, and their values are retrieved via each object's scalarval() method. Once retrieved, the arguments are interpolated into a longer string, which is then repackaged as an XML_RPC_Response() object, and returned to the caller.

Meeting Bugs

So that takes care of the server - now how about the client? Well, there's a corresponding XML_RPC_Client() object, which takes care of sending an XML-RPC request to the server and decoding the response. Take a look:

<?php
// include base functions
include('XML/RPC.php');

// initialize client
$client = new XML_RPC_Client("/xmlrpc/server.php", "localhost", 80);

// turn on debugging
// $client->setDebug(1);

// generate XML-RPC message packet
// contains RPC method name and arguments
$msg = new XML_RPC_Message("whoAmI", array(
                new XML_RPC_Value("Bugs Bunny", "string"),
                new XML_RPC_Value("rabbit", "string"),
                new XML_RPC_Value("18", "int")));

// send request
$response = $client->send($msg);

// read response
if (!$response->faultCode()) {
    // no error
    // print response
    $value = $response->value();
    print $value->scalarval() . "\n";
} else {
    // error
    // print error message and code
    print "Error code: " . $response->faultCode() . " Message: " . $response->faultString() . "\n";
}
?>

When initializing an XML_RPC_Client() object, the object constructor must be passed three parameters: the path to the server script, the host name of the server, and the port number. These values are used to POST the XML-RPC request packet to the appropriate host.

Once the object has been initialized, an XML_RPC_Message() object is created, with the remote procedure name as the first argument and an array of parameters to be passed to it as the second. The XML_RPC_Client() object's send() method is then used to transmit the message packet to the server, and a response object is generated from the server's response. If an error occurred, the response object will contain a fault, which can be retrieved via the faultCode() and faultString() methods; if no error occurred, the response object will contain the return value of the remote procedure, which can be retrieved - again as an XML_RPC_Value() object - via its value() method.

Now, when you run the client above, the following XML-RPC message will be generated and transmitted to the server:

<?xml version="1.0"?>
<methodCall>
<methodName>whoAmI</methodName>
<params>
<param>
<value><string>Bugs Bunny</string></value>
</param>
<param>
<value><string>rabbit</string></value>
</param>
<param>
<value><int>18</int></value>
</param>
</params>
</methodCall>

And the server will respond with this message, which will be decoded and used by the client:

<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>I am Bugs Bunny, a 18-year old rabbit</string></value>
</param>
</params>
</methodResponse>

As the client and server above demonstrate, the procedure to construct the two ends of an XML-RPC connection is pretty straightforward - the only thing that really changes is the construction of the procedure call and the values passed back and forth through the message and response packets. It's therefore important to have a clear understanding of the XML-RPC data types, and of the internal structure of the message packets.

Not My Type

A few pages back, I introduced you to the simple data types supported by XML-RPC. However, those aren't the only game in town - the XML-RPC specification also describes two complex types, arrays and structs, which are conceptually similar to PHP's indexed and associative arrays respectively.

To see how an XML-RPC array works, consider the following script, which constructs one using XML_RPC_Value() objects:

<?php
// include class
include('XML/RPC.php');

// array
$value = new XML_RPC_Value(array(
             new XML_RPC_Value("huey", "string"),
             new XML_RPC_Value("dewey", "string"),
             new XML_RPC_Value("louie", "string"),
            ), "array");
?>

Here's what the XML-encoded value looks like:

<value>
<array>
<data>
<value><string>huey</string></value>
<value><string>dewey</string></value>
<value><string>louie</string></value>
</data>
</array>
</value>

And here's the code to construct a struct:

<?php
// include class
include('XML/RPC.php');

// hash
$value = new XML_RPC_Value(array(
            "desc" => new XML_RPC_Value("power drill", "string"),
            "price" => new XML_RPC_Value("12.99", "double")
            ), "struct");
?>

And here's the XML version:

<value>
<struct>
<member>
<name>desc</name>
<value><string>power drill</string></value>
</member>
<member>
<name>price</name>
<value><double>12.99</double></value>
</member>
</struct>
</value>

Arrays and structs can be used to pass related values to and from a remote procedure. Consider the example of an XML-RPC server which exposes a getAverage() method and expects an array of numbers as input argument. Here's the client, which invokes the procedure with an array as argument,

<?php
// include base functions
include('XML/RPC.php');

// initialize client
$client = new XML_RPC_Client("/xmlrpc/server.php", "localhost", 80);

// turn on debugging
// $client->setDebug(1);

// generate XML-RPC message packet
// contains RPC method name and arguments
$numberSet = new XML_RPC_Value(array(
                new XML_RPC_Value("12", "int"),
                new XML_RPC_Value("13", "int"),
                new XML_RPC_Value("14", "int")
                ), "array");

$msg = new XML_RPC_Message("getAverage", array($numberSet));

// send request
$response = $client->send($msg);

// read response
if (!$response->faultCode()) {
    // no error
    // print response
    $value = $response->value();
    print "Average is " . $value->scalarval() . "\n";
} else {
    // error
    // print error message and code
    print "Error code: " . $response->faultCode() . " Message: " . $response->faultString() . "\n";
}
?>

and here's the server, which parses the input array and performs calculations on it:

<?php
// include server class
include('XML/Server.php');

// create server
// set up dispatch map
$server = new XML_RPC_Server(array("getAverage" => array("function" => "phpCalcAverage")));

// function to calculate average
function phpCalcAverage($params)
{
    // initialize sum
    $sum = 0;

    // get first argument (array)
    $arr = $params->getParam(0);

    // iterate and calculate sum of array elements
    for ($x=0; $x<$arr->arraysize(); $x++) {
        $elem = $arr->arraymem($x);
        $sum += $elem->scalarval();
    }

    // calculate average
    $avg = $sum / $arr->arraysize();

    // return response to client
    return new XML_RPC_Response(new XML_RPC_Value($avg, "double"));
}
?>

Pay special attention to the internals of the phpCalcAverage() function - this function uses the arraysize() and arraymem() methods of the array object to calculate the total number of elements in the array and to retrieve individual elements. These methods are unique to the complex data types in the XML_RPC package (structs come with a structmem() method).

Fault-y Towers

In the XML-RPC world, errors are referred to as "faults". These faults are generated by the XML-RPC server in the event that an error occurs in processing the XML-RPC request. Every fault has two attributes: a numeric fault code, and a human-readable fault string. Here's an example:

<?xml version="1.0" ?>
<methodResponse>
<fault>
  <value>
    <struct>
      <member>
        <name>faultCode</name>
        <value><int>19</int></value>
      </member>
      <member>
        <name>faultString</name>
        <value><string>Could not find file</string></value>
      </member>
    </struct>
  </value>
</fault>
</methodResponse>

With the PEAR XML_RPC class, faults can be generated using an XML_RPC_Response() object, as in the example below:

<?php
// include class
include('XML/RPC.php');

// create response packet containing fault
$resp = new XML_RPC_Response(0, 19, "Could not find file");
?>

Faults can be retrieved using the faultCode() and faultString() methods of the XML_RPC_Response() object - you've already seen this in previous examples.

When deciding what numeric codes to assign to your faults, it is wise to follow the XML_RPC class' recommendation that "fault codes for your servers should start at the value indicated by the global $XML_RPC_erruser + 1".

Access Granted

Now that you have some inkling of how XML-RPC works, let's put everything you've learned into a real-world example. The next example shows you how to build an authentication server, which exposes a simple isValidLogin() procedure. This procedure accepts a username and password, consults a MySQL database to see if they match, and returns a "yay" or "nay" to the caller. By exposing this procedure through XML-RPC, it becomes possible for remote clients to check the validity of user credentials, without needing access to the user database themselves. This system is thus particularly suitable to distributed-computing environments, which have multiple clients running on different platforms and operating systems.

First, a sample of the user database:

mysql> SELECT * FROM users;
+----+----------+------------------------------------------+
| id | username | password                                 |
+----+----------+------------------------------------------+
|  1 | john     | a51dda7c7ff50b61eaea0444371f4a6a9301e501 |
|  2 | joe      | 16a9a54ddf4259952e3c118c763138e83693d7fd |
+----+----------+------------------------------------------+
2 rows in set (0.00 sec)

Here, each username is unique, and each password is encrypted with the MySQL SHA1() function.

Next, the server, which exposes the isValidLogin() procedure.

<?php
// include server class
include('XML/Server.php');

// create server
// set up dispatch map
$server = new XML_RPC_Server(array("isValidLogin" => array("function" => "auth")));

// authentication function
// connects to MySQL database and verifies user credentials
function auth($params)
{
    // import starting value for user fault codes
    global $XML_RPC_erruser;

    // get first argument (struct)
    $struct = $params->getParam(0);

    // get values of struct elements
    $value = $struct->structmem('username');
    $username = $value->scalarval();

    $value = $struct->structmem('password');
    $password = $value->scalarval();

    // open connection to MySQL server
    // if connection not possible, return fault
    if (!$connection = @mysql_connect("localhost", "user", "pass")) {
        return new XML_RPC_Response(0, $XML_RPC_erruser+1, "Unable to connect to database server");
    }

    // select database for use
    // if database cannot be selected, generate fault
    if (!mysql_select_db("db1717")) {
        return new XML_RPC_Response(0, $XML_RPC_erruser+2, "Unable to select database");
    }

    // create and execute query
    // if query cannot be executed, generate fault
    $query = "SELECT * FROM users WHERE username = '$username' AND password = SHA1('$password')";
    if (!$result = mysql_query($query)) {
        return new XML_RPC_Response(0, $XML_RPC_erruser+3, "Unable to execute query");
    }

    // check if a record was returned
    // if yes, then username/password is correct
    // if no, then username/password is wrong
    // return appropriate Boolean value
    if (mysql_num_rows($result) == 1) {
        return new XML_RPC_Response(new XML_RPC_Value(1, "boolean"));
    } else {
        return new XML_RPC_Response(new XML_RPC_Value(0, "boolean"));
    }
}
?>

It's clear from the above that the isValidLogin() procedure accepts a hash of two elements, the supplied user name and the corresponding plain-text password, and checks these values against the encrypted values in the database. Depending on the result of the test, a Boolean true or false value is returned to the caller.

Finally, the client, which presents a form asking for the user name and password, and submits the same to the server using XML-RPC. The return value of the procedure call is then used to display an appropriate message to the user.

<html>
<head></head>
<body>

<?php
// form not submitted
if (!$_POST['submit']) {
    ?>
    <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
    Username: <input type="text" name="name">
    <br />
    Password: <input type="password" name="pass">
    <br />
    <input type="submit" name="submit">
    </form>
<?php
}
// form submitted
else {
    // include base functions
    include('XML/RPC.php');

    // initialize client
    $client = new XML_RPC_Client("/xmlrpc/server.php", "localhost", 80);

    // turn on debugging
    // $client->setDebug(1);

    // build XML-RPC <struct> containing username and password
    $struct = new XML_RPC_Value(array(
                "username" => new XML_RPC_Value($_POST['name'], "string"),
                "password" => new XML_RPC_Value($_POST['pass'], "string")
                ), "struct");

    // generate XML-RPC message packet
    // contains RPC method name and arguments
    $msg = new XML_RPC_Message("isValidLogin", array($struct));

    // send request
    $response = $client->send($msg);

    // read response
    if (!$response->faultCode()) {
        // no error
        // check response and print message
        $value = $response->value();
        if ($value->scalarval() == 1) {
            echo "Access granted";
        } else {
            echo "Access denied";
        }
    } else {
        // error
        // print error message and code
        print "Error code: " . $response->faultCode() . " Message: " . $response->faultString() . "\n";
    }
}
?>

</body>
</html>

The cool thing? No matter what platform or operating system your client is running on, so long as it can do XML-RPC, it can use this server for authentication. This makes it possible to build a centralized user database that many different agents can access, independent of local constraints.

And that's about all I have for you. Over the course of the last few pages, I introduced you to one of the coolest packages in PEAR, the XML_RPC package. As you've seen, this package provides a simple API to create XML-RPC clients and servers, and encode and decode message packets and data values. Play with it a little, and read more about what it can do at http://pear.php.net/manual/en/package.webservices.xml-rpc.php.

Note: Examples are illustrative only, and are not meant for a production environment. Melonfire provides no warranties or support for the source code described in this article. YMMV!

This article was first published on30 Sep 2006.