Web Services using PHP & nuSoap - Part 3

Welcome to Part III of creating Web-Services with nuSOAP and PHP - a tutorial of what's turning out to be epic proportions.... In this installation we're going to create a WSDL method for inserting a new record into our table: customers. (Please refer to part II for information on creating our mySQL table and it's schema.) We're going to jump right into the deep-end of the code pool as we're going to create a web-services method that requires complex structures as both it's input and output parameters. Additionally, we're going to require a support function that will be invoked from the method that's registered with the WSDL. Finally, we'll test the web-services method by invoking it from a remote client and we'll evaluate the results returned from the server. Quick review: we've got a successful dog-washing business that provides it's customers with access to a calendar scheduling system via the web. The inference is that customers register with the business by providing their first and last names, and an email address. We store this information in the table: customers. Corporate has asked us to develop a web-services interface into the customer's data table as this table will become the authoritative repository for the entire organization.

In today's installment, we're going to create a web-services function called: createNewCustomers and we'll register the function with the WSDL so that it may be invoked by remote clients. The function will be relatively robust, providing rudimentary error checking and parsing of selected data fields. The method will interface into the mySQL table via the mySQLi (improved) extension. We provided the source code for our db-interface library in part II so, please, refer to that article to obtain the source code.

After the jump, we'll talk briefly about data table security and authentication before revealing the source code for our new method...

If you've ever installed a piece of open-source code that utilized the mySQL (or postgres) database, then you've had to customize the configuration file that provides the connection-authorization data for hooking into your local mySQL table. You have to specify the db user name, the db host, the password, and the name of the database that will be accessed.

Web-Services requires the same configuration but only on the server-side part of the code base. Clients that connect to your web-services need not know your connection information to your local tables. A client may be required to provide login/password data is when the remote web-services server has enabled .htaccess-level security. (We talked about this in part II.) If your using my web-services off of shallop.com, there is no .htaccess authentication. The database user information, the database name, even the database host, is masked from the client because access is handled by the server-side code.

Ok. So, let's move on to the first function: createNewCustomer().

createNewCustomer() creates a new customer tuple in the customers table if:

  • customer's first name is not null or blank
  • customer's first name is less than 50 characters
  • customer's last name is not null or blank
  • customer's last name is less than 50 characters
  • customer has a valid email address
  • customer's email is less than 100 characters
  • customer's email address does not exist in the data table

We're going to require that the client's data pass these seven conditions before we allow the client to add a new row of data to our table.

The method itself will require a single input parameter: $newCustomerData - which is a complex structure, registered with the WSDL, and consists of an associative array with three data elements:

  • strFirstName -- a string containing between 1 and 50 characters
  • strLastName -- a string containing between 1 and 50 characters
  • strEmail -- a string containing between 1 and 50 characters

The method requires a single output paramater, also an associative array: $aryReturn, also registered with the WSDL, and consists of:

  • status -- a boolean value
  • data -- contains either data from a successful operation, or an error message

The client has the responsibility of testing for a successful operation when the structure is returned from the server.

So, let's talk about testing...

In testing the integrity of the data passed to us, from the client, for insertion into our table, we're going to focus mainly on the integrity of the data and not the quality. In other words, I can't test that "B0b" is a valid first name any more than I can test that "Glerble" is a valid first name. All I really care about are my requirements: that data exists and that the data not exceed the column constraints of the table.

So, for all of the elements that I'm passing to the server-side method, within the complex structure, all I care about is that (a) data exists and (b) data won't overflow the column in which the data is being stored.

As far as the email address is concerned, I am going for two additional criteria against the data passed to us from the client: the email address must valid-formed (meeting the constraints and requirements of a valid email address), and secondly: because email addresses are unique, the email address may not pre-exist in the table. The inference here is that our code will prevent web-services from duplicating customer accounts in the data table.

We should also require that names contain only alpha-characters - no numbers or special characters allowed. Additionally, we should sanitize our input with the php function call: htmlspecialchars(). Using the mySQLi prepared statements means that the mySQL engine will process special characters for us while reducing the risk of injection-type attacks.

Our email validation routine is contained in a support function that we'll store in a separate file: dw_server_functions.inc. I obtained this function from LinuxJournal.com by Douglas Lovell, as it's the most versatile function I've found that enforces standards compliance on email addresses; this function is way past simple preg matches for email address validation and if you've never played with string validation as it applies to email, then I strongly encourage you to carefully read this article.

And that's pretty much it for testing our input data. If we pass all data validation checks, then we're going to insert the record into the table and return a boolean(true) in the $aryReturn['status'] element and the table insert id in the $aryReturn['data'] element.

Here's the completed (so far!) server-side code. Copy/Paste this code into a file named: dw_server.php. For details on registering methods and structures with the WSDL, please see the second article in this series. You can view the WSDL online from my server.

[ccne lang="php" line_numbers="on" tab_size="4" width="100%"] /** * dw_server.php * * PHP server-side source code for the DogWash web-services tutorial. */

$cdir = $_SERVER['DOCUMENT_ROOT'];

# include dbi library require($cdir . "/webservices/dbi.class.inc"); # include support functions library (functions not registered w/WSDL) require($cdir . "/webservices/dw_server_functions.inc"); # include the nuSOAP extension require($cdir . "/webservices/lib/nusoap.php");

# create and initialize a new server object $server = new soap_server(); $server->configureWSDL("shallopWebServices", "urn:shallopWebServices");

# define WSDL structures

# aryReturn is our standard return-data data structure. # it's a two-element associative arrary: # ['status'] (bool) indicating success or fail # ['data'] (string) data returned or error message $server->wsdl->addComplexType( 'aryReturn', 'complexType', 'struct', 'all', '', array ( 'status' => array('name' => 'status', 'type' => 'xsd:boolean'), 'data' => array('name' =>'data', 'type' => 'xsd:string') ) );

# aryCustomerData is the input structure for creating a new customer node. # associative array with three elements: # ['strFirstName'] (string) - customer's first name # ['strLastName'] (string) - customer's last name # ['strEmail'] (string) - customer's email address $server->wsdl->addComplexType( 'aryCustomerData', 'complexType', 'struct', 'all', '', array( 'strFirstName' => array('name' => 'strFirstName', 'type' => 'xsd:string'), 'strLastName' => array('name' => 'strLastName', 'type' => 'xsd:string'), 'strEmail' => array('name' => 'strEmail', 'type' => 'xsd:string') ) );

# register server methods with the WSDL server object $server->register('createNewCustomer', array('newCustomerData' => 'tns:aryCustomerData'), array('return' => 'tns:aryReturn'), 'shallopWebServices', 'urn: shallopWebServices', 'rpc', 'encoded', ' Summary: creates a new customer in the customers table based on data passed-in by complex structure defined by aryCustomerData.

Input: $newCustomerData (type: complex structure) - associative array containing the following elements:

  • strFirstName - string(50) containing customer's first name.
  • strLastName - string(50) containing the customer's last name.
  • strEmail - string(100) containing the customer's email address.

Output: Function returns an array - aryReturn['status'] will be true if the insert operation succeeded, otherwise boolean(false) will be returned. If the operation is successful, then the insert-id of the newly-created record is returned in aryReturn['data']. Otherwise, the relevant error message is returned in aryReturn['data'].' );

/** * createNewCustomer - nuSOAP web services function * * createNewCustomer function takes a complex structure ($newCustomerData) and * validates the data elements: * -- data must exist * -- email address must be a valid-formed address * -- email address may not pre-exist in the customers table * * if all these conditions are satisfied, then allow the new record insertion * and return the insert id to the client. * * otherwise, return an error condition to the client with a meaningful * error message to assist the client in remote diagnostics. * * @param complex structure $newCustomerData * @return complex structure $aryReturn */ function createNewCustomer($newCustomerData) { # validate input and return error on bad data detection if (is_null($newCustomerData)) { return (array('status' => false, 'data' => 'Error - cannot pass null structure to ws-function: createNewCustomer()')); } if (empty($newCustomerData['strFirstName'])) { return (array('status' => false, 'data' => 'Error - customer first name cannot be blank - please provide first name for customer.')); } if (strlen($newCustomerData['strFirstName'] > 50)) { return (array('status' => false, 'data' => 'Error - first name cannot be longer than 50 characters.')); } if (empty($newCustomerData['strLastName'])) { return (array('status' => false, 'data' => 'Error - customer last name cannot be blank - please provide last name for customer.')); } if (strlen($newCustomerData['strLastName'] > 50)) { return (array('status' => false, 'data' => 'Error - last name cannot be longer than 50 characters.')); } if (empty($newCustomerData['strEmail'])) { return (array('status' => false, 'data' => 'Error - customer email address cannot be blank - please provide email address for customer.')); } if (strlen($newCustomerData['strEmail'] > 100)) { return (array('status' => false, 'data' => 'Error - email address cannot be longer than 100 characters.')); } if (!validEmail($newCustomerData['strEmail'])) { return (array('status' => false, 'data' => 'Error - customer email address: ' . $newCustomerData['strEmail'] . ' is not a valid email address.')); } # check database to see if email already exists - if so, return an error $dbc = new DB(); $query = "SELECT COUNT(id) AS count FROM customers WHERE customerEmail = ?"; $results_r = $dbc->preparedSelect($query, array("s", $newCustomerData['strEmail'])); if ($dbc->errorstate) { return (array('status' => false, 'data' => $dbc->errorstring)); } if ($results_r[0]['count']) { return (array('status' => false, 'data' => 'Error - customer with email address: ' . $newCustomerData['strEmail'] . ' already exists in customers table.')); } # checkpoint: data exists, email valid and does not pre-exist in table...insert new record into table $query = "INSERT INTO customers (customerFirstName, customerLastName, customerEmail) VALUES (?, ?, ?)"; $iid = $dbc->preparedSelect($query, array("sss", htmlspecialchars($newCustomerData['strFirstName']), htmlspecialchars($newCustomerData['strLastName']), htmlspecialchars($newCustomerData['strEmail'])), false); if ($dbc->errorstate) { return (array('status' => false, 'data' => $dbc->errorstring)); } return(array('status' => true, 'data' => $iid)); }

# create HTTP listener: $request = isSet($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : ''; $server->service($request); exit (); ?> [/ccne]

Next, let's tackle the client-side code to insert a new record...

The client-side code is pretty simple. All we need to do is include the nuSOAP extension, create a nuSOAP client, create our input structure, invoke the remote method, and display the results.

What's important in this example is how we've prepared our input parameter to the server-side function. Since we're passing in a complex structure, an associative array, we're required to wrap our array within another array so that it's individual elements can be referenced correctly by the server-side method. And that's really the only trick into passing complex structures into a nuSOAP method.

Here's the entire source code listing for the client-side. Save this file as: dw_client.php. Note that, like dw_server.php, these files are in-transition. As we continue this tutorial, we're going to constantly add to the source code files.

[ccne lang="php" line_numbers="on" tab_size="4" width="100%"]

$cdir = $_SERVER['DOCUMENT_ROOT'];

# include the nuSOAP extension require($cdir . "/webservices/lib/nusoap.php");

$client = new nusoap_client('http://www.shallop.com/webservices/dw_server.php?wsdl', true);

$param = array('newCustomerData' => array('strFirstName' => 'George', 'strLastName' => 'McFly', 'strEmail' => 'george_mcfly@bookworms.com' ) );

$response = $client->call('createNewCustomer', $param); echo $response['data']; echo " \n"; $foo = var_dump($response, true); echo $foo; echo " \n"; ?> [/ccne]

When we execute the client-side source code from a browser, we get the following output:

5

array(2) { ["status"]=> bool(true) ["data"]=> string(1) "5" } bool(true)

(Your mileage may vary.)

The "5" in the first line is the insert-id of the new record. The second line is a var_dump() of the data structure returned from the server method. When I browse my table, I can see that the data was successfully inserted.

The next question to ask is: did we handle error processing correctly? What happens if I run the client again by clicking on reload in my browser?

Well, in the source code we check for duplicate emails before allowing an insert to occur. If our code is correct, the server-side method should prevent a duplicate record from being inserted when we reload the form. Reloading the form causes the client-side code to execute again - since we've not changed the source code, the data to be inserted, the server-side method should reject the request because it fails the duplicate email test.

And, indeed, we see:

Error - customer with email address: george_mcfly@bookworms.com already exists in customers table. array(2) { ["status"]=> bool(false) ["data"]=> string(98) "Error - customer with email address: george_mcfly@bookworms.com already exists in customers table." } bool(true)

Next: the support function....

In the course of writing our server-side methods, we'll probably identify the need to develop additional functions. For sake of clarity let's call a function that's registered with the WSDL our server-side methods, and any function not registered with the WSDL (but also on the server-side), our server-side functions. To further distinguish the two sets of functions, we're going to store all our server-side functions in a separate file: dw_server_functions.inc. You could, if you wanted, include these functions within the source code of dw_server.php. For a function to accessible via web-services, it has to be registered with the WSDL - we covered this point in the second installment of this series.

As it stands currently, our source-code file for our server-side functions contains only a single function. Here is the listing thus far:

[ccne lang="php" line_numbers="on" tab_size="4" width="100%"] /** Validate an email address. Provide email address (raw input) Returns true if the email address has the email address format and the domain exists.

obtained from: http://www.linuxjournal.com/article/9585 */ function validEmail($email) { $isValid = true; $atIndex = strrpos($email, "@"); if (is_bool($atIndex) && !$atIndex) { $isValid = false; } else { $domain = substr($email, $atIndex+1); $local = substr($email, 0, $atIndex); $localLen = strlen($local); $domainLen = strlen($domain); if ($localLen < 1 || $localLen > 64) { // local part length exceeded $isValid = false; } else if ($domainLen < 1 || $domainLen > 255) { // domain part length exceeded $isValid = false; } else if ($local[0] == '.' || $local[$localLen-1] == '.') { // local part starts or ends with '.' $isValid = false; } else if (preg_match('/\\.\\./', $local)) { // local part has two consecutive dots $isValid = false; } else if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) { // character not valid in domain part $isValid = false; } else if (preg_match('/\\.\\./', $domain)) { // domain part has two consecutive dots $isValid = false; } else if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\","",$local))) { // character not valid in local part unless // local part is quoted if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\","",$local))) { $isValid = false; } } if ($isValid && !(checkdnsrr($domain,"MX") || checkdnsrr($domain,"A"))) { // domain not found in DNS $isValid = false; } } return $isValid; } ?> [/ccne]

A quick comment about the file: env_vars.inc:

This source code file contains some pre-defined constants, in PHP, that are grabbed from the mySQLi extension for connecting to your local server. If you're going to reproduce this tutorial on your server, you'll need to define these constants in accordance with your particular database environment. Here's the template for the file (env_vars.inc) for you to change:

[ccne lang="php" line_numbers="on" tab_size="4" width="100%"] define ("DB_NAME", "YOUR DB NAME"); define ("DB_USER", "YOUR DB USER"); define ("DB_HOST", "localhost"); define ("DB_PASS", "YOUR DB PASSWORD"); ?> [/php]

These five files:

  • dw_server.php
  • dw_client.php
  • dw_server_functions.inc
  • dbi.class.inc
  • env_vars.inc

are all you need locally, on your own server, to establish this working demo of web-services.

That wraps up this tutorial segment. Please leave any questions (or comments) as comments. I hope this information helps you!