Web Services using PHP & nuSOAP - Part 2

Hi and welcome to part 2 of my tutorial on implementing web-services via nuSOAP and PHP! In the last article we set-up a small server and client app to test our connection to the web-services server. We invoked a method on the remote server via our client, passed a single parameter as input (wrapped in an array), and returned a complex-structure to the client application with data created from the server-method, and displayed that data in the browser screen.

That this was successful indicates that we're able to establish communication with our remote server and successfully execute remote functions, returning data from the remote server for our own processing and usage.

In this part of the tutorial, we're going to set-up the design and specifications for the server-side application by defining the mySQL data table we'll be using, the methods of the web-services server, and the input and output parameters expected for each of the methods. Additionally, if required, other methods may need to be written to support our primary design. These methods may not be exposed to web-services, but are usable by those methods which are.

In any programming project, begin with the end in-mind. Define the scope and range of your project fully - anticipate problems and extremes and factor these issues into your design so that your application is robust, scalable and maintainable. Nothing is worse than stopping forward progress on a coding project because you have to back-track and re-factor your design. Take the time now to define what you want to accomplish and how you will do it.

So, let's get started...

nuSOAP WSDL Review

Because we're using the nuSOAP library, the server application is self-documenting. When we access the server application directly, we can view the WSDL method, and the corresponding XML definitions for the server. This is beneficial to our client-developers as it provides them with functional spec on how-to use our web services.

Although nuSOAP generates this documentation auto-magically, remember the quality of the documentation is controlled by you - the server-side developer. Pay close attention to your comments when defining your methods in the server-side code as thoroughness pays off when it comes to the client-side developers using your code. Provide as much information as you can, in a lay-out that follows form and convention. The time you spend now, in documentation, will be saved 10-fold when it comes time to release your webservices on the world at-large!

Remember that, when you click on the WSDL link (in the purple box), if you're not using IE, you'll see a mish-mash of generated text in a new page. Right click the browser background and select "View Source" and you'll be presented with a formatted output of the XML generated for your server. If you're using IE, then you'll not need the intermediate step. The generated XML output will also be used by client developers for specification for input and output parameters as this data is defined explicitly by in XML. Specifically, the array structures (definitions) are masked in the top-level WSDL output - client-side developers will need to view the XML to see the atomic definitions of your complex structures either passed-into, or from, your web-services methods.

Let's continue by defining the database table schema for our application...

Doggie-Bath Database Table

In the first part of this tutorial, we proposed a fictitious business set-up involving a corporate doggie-daycare type of business. Our web-services developer has to create a web-services portal for corporate that will provide access to the customers database (table) via web-services. For sake of simplicity, yet still providing a working example including complex structure passing, we're going to provide three functions to corporate via web-services as methods that will allow remote clients to add users, edit users, and delete users from a mySQL database table.

Our customers table will be kept simple for the purposes of this tutorial - all customer data will be stored in a single table, and minimal information will be stored. If you're re-creating the tutorial on your own server, then you can use the following sql code to create and populate your own table that we'll be using in this example:

[ccne lang="sql" line_numbers="on" tab_size="4" width="100%"] -- phpMyAdmin SQL Dump -- version 2.11.6 -- http://www.phpmyadmin.net -- -- Host: localhost -- Generation Time: Apr 20, 2010 at 03:25 AM -- Server version: 5.0.67 -- PHP Version: 5.2.9

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

-- -------------------------------------------------------- -- -- Table structure for table `customers` --

CREATE TABLE IF NOT EXISTS `customers` ( `id` int(10) unsigned NOT NULL auto_increment COMMENT 'primary key', `customerFirstName` varchar(50) NOT NULL, `customerLastName` varchar(50) NOT NULL, `customerEmail` varchar(100) NOT NULL, `customerJoined` timestamp NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `customerEmail` (`customerEmail`), KEY `customerJoined` (`customerJoined`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='web services tutorial table' AUTO_INCREMENT=5 ;

-- -- Dumping data for table `customers` --

INSERT INTO `customers` (`id`, `customerFirstName`, `customerLastName`, `customerEmail`, `customerJoined`) VALUES (1, 'Marty', 'McFly', 'marty@boarders.com', '2010-04-20 03:24:22'), (2, 'Emmet', 'Brown', 'docbrown@mensa.org', '2010-04-20 03:24:22'), (3, 'Biff', 'Tannen', 'biff@butthead.com', '2010-04-20 03:25:22'), (4, 'Marvin', 'Berry', 'marvin@chucksbro.com', '2010-04-20 03:25:22'); [/ccne]

Next, we're going to set-up and define our methods for the application and provide some initial specification...

Server Methods Defined

Remember, too, from the first installment of this tutorial, that we're going to define the following methods for this table and managing our doggie-customer database. In addition to providing the names of the methods, we'll also start documenting some requirements and specifications for the methods:

  • createNewCustomer() -- function that will insert a new record into table: customers. Input passed as a complex structure and must contain the customers' first and last name, and email address.
  • getCustomerData() -- function will return data from the table in a complex structure containing all data from the table. Input parameter is optional - if no input parameter is specified, return all data. Otherwise integer input is expected corresponding to the customers.ID field of the table. If customer is located by ID, return a complex structure of all customer information for that id, otherwise return a NOTFOUND error message.
  • editCustomerData() -- function that will allow changing/updating of customer data in the table. Input will be an integer value with a complex structure containing three fields: the customers first and last name, and the email address , in associative indices. If any of the contents of the array are set to null, then that data element will not be changed. Otherwise, update the record (specified by the integer value input parameter) using the named values in the complex structure that are not-null. If the keyword "DELETE" appears as the data element in all three cells, then delete the record. Return a simple structure indicating operation success or failure.

I didn't specify this function in the first part of this tutorial. However, as in real-life, once you start designing your application, you'll discover the need to add additional functionality to your design. That's ok!

In our case, I think we'll need to add a search function and expose that function to web services. The reason being is that we're allowing edits to single records within the customer database that can be identified by the table's primary key. Since the pk is an arbitrary integer value, we can't logically expect applications outside our server to have access to the key values. Since email address are unique, we'll all the client applications to search our table by email address and we'll return the primary key of the record if the record is found.

Initially, I think you'd want to argue: "Hey, why not return the entire record?" Logically, that seems to make sense. However, I'd argue that if the client-side application already knows the customer's email address, then the chances are better than good that they'd also already know the customer's name. So, also in the interests of keeping it simple, we're going to make the business case to return only the pk id value of the record if found in our standard structure: aryReturn.

Also, I'd think that the client application would want to call the search function before invoking an insert function - just to ensure that you'll not be creating dups in the data table. That's if your client application is well-formed and well-behaved. Since we can't assume that the client application is going to be well-behaved, then we'll want to invoke our own search method prior to an insert operation to ensure that we're not creating duplicate records.

However, we did design the data table to prevent duplicate insertions based on the email address:

UNIQUE KEY `customerEmail` (`customerEmail`)

...so if a client application attempted to insert a new record into the table that had an email already in-use, mySQL will return an error preventing the insert operation from completing. Since we're going to handle this from the application server code, we won't be trapping and returning the mySQL-generated error.

So, add a new function to our specification:

  • searchCustomer() -- function to search for an existing record based on the email address of the customer. Input parameter is a single value: the customer's email address. function will search the table for email address - if found, return logical(true) in aryReturn['status'] and the record pk id in aryReturn['data']. If the record was not-found, return logical(false) in aryReturn['status']. If an error occurred during processing, return a logical(false) in aryReturn['status'] and the error message in aryReturn['data'].

Supporting Methods

In the course of designing the spec for our server methods, we've also identified the need for a function that will be used by the server-side methods, but will not be exposed to web-services. Think of this method as a private method - accessible only the web-services methods which are exposed to the client-side applications; the client-side applications will not be able to access methods that are not exposed to the WSDL, nor will these functions be visible on the nuSOAP server WSDL documentation.

The function we're going to need is a function to validate the email address as a well-formed address:

  • validateEmail() -- function not exposed to web-services that will validate the format of a user's email address. Takes a single string variable as input and returns a boolean indicating if the input was well-formed or not.

This is a fairly standard function - no need to write it from scratch - we should be able to google this function and use it within our server code.

We've defined a total of five functions that we'll need to write for the web-services server. The specification for each function is minimally defined but it's enough for us to get started.

The next point of concern involves accessing the database and how, exactly, this will be accomplished...

Database Library

How we access the database is of critical importance. We'll be using the mysqli extensions (improved mySQL interface) to take advantage of prepared statements. I'm going to provide you with a copy of the include file I use - this is a combination of a library provided by a user-comment from the documentation page on php.net's mysqli_stmt::prepare and some of my own modifications. I'll explain more about the library when we're invoking it from our server-side source code. For now, please take it on-faith that this library is functional - it's the one being called on the shallop.com site for this, and all other web-services tutorials:

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

/** * dbm.inc * * This object class serves as a mysqli extenstion to the database * for obtaining and returning prepared statements as associative * arrays. */

include_once($_SERVER['DOCUMENT_ROOT'] . '/classes/env_config.inc');

class DB { public $connection; public $errorstring = ""; public $errorstate = false;

#establish db connection public function __construct() { # constants for connection stored in env_config.inc $this->connection = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);

if(mysqli_connect_errno()) { $this->errorstring = "Database Connect Error : " . mysqli_connect_error($mysqli); $this->errorstate = true; return false; } }

#store mysqli object public function connect() { return $this->connection; }

#run a simple query and return via input param public function singleValueSimpleQuery($query, $connection, &$result) { $statement = $connection->stmt_init(); if (!$statement->prepare($query)) { $this->errorstate = true; $this->errorstring = printf("mysqli error: %s ", mysql_error()); return false; } else { $statement->execute(); $statement->bind_result($result); $statement->fetch(); $statement->close(); } return true; }

#run a prepared query public function runPreparedQuery($query, $params_r) { $stmt = $this->connection->prepare($query); $this->bindParameters($stmt, $params_r);

if ($stmt->execute()) { return $stmt; } else { $this->errorstring = ("Error in stmt: " . mysqli_error($this->connection)); $this->errorstate = true; return false; } }

# To run a select statement with bound parameters and bound results. # Returns an associative array two dimensional array which u can easily # manipulate with array functions.

public function preparedSelect($query, $bind_params_r, $doFetch="true") { $select = $this->runPreparedQuery($query, $bind_params_r); if ($doFetch) { $fields_r = $this->fetchFields($select);

foreach ($fields_r as $field) { $bind_result_r[] = &${$field}; }

$this->bindResult($select, $bind_result_r);

$result_r = array(); $i = 0; while ($select->fetch()) { foreach ($fields_r as $field) { $result_r[$i][$field] = $$field; } $i++; } $select->close(); return $result_r; } else { # if doFetch was called with a false value, assume this # was an insert or update operation and return the insert id return $select->insert_id; } }

#takes in array of bind parameters and binds them to result of #executed prepared stmt

private function bindParameters(&$obj, &$bind_params_r) { call_user_func_array(array($obj, "bind_param"), $bind_params_r); }

private function bindResult(&$obj, &$bind_result_r) { call_user_func_array(array($obj, "bind_result"), $bind_result_r); }

#returns a list of the selected field names

private function fetchFields($selectStmt) { $metadata = $selectStmt->result_metadata(); $fields_r = array(); while ($field = $metadata->fetch_field()) { # assign column names to array - convert column names to lower case. $fields_r[] = strtolower($field->name); } return $fields_r; } } #end of class

/*

# An example of the DB class in use

$DB = new DB(); $var = 5; $query = "SELECT abbr, name from books where id > ?" ; $bound_params_r = array("i", $var);

$result_r = $DB->preparedSelect($query, $bound_params_r);\ $class->array = $result_r[0];

# for multiple bound parameters in the query: # $result_r = $DB->preparedSelect($query, array("ss", "string1", "string2");

#loop thru result array and display result

foreach ($result_r as $result) { echo $result['abbr'] . " : " . $result['name'] . " " ; }

*/

?> [/ccne]

Copy-and-paste this above code and save it to your include directory as dbi.class.inc -- this how it will be referenced in the tutorial. If you change the name of the source file on your server, please make sure you identify the corresponding include statements in the code modules and make the corresponding name changes there as well.

This pretty much wraps things up for this phase of the tutorial so, let's recap...

Summary

Since we successfully developed proof-of-concept in part 1 of this tutorial series, we've moved-on to program design and specification in part 2.

We've defined our database table that we'll be using throughout the rest of the tutorial and we've defined the necessary methods/functions for our server-side code. We've spec'd out the functions in terms of scope, input and output parameters and error handling. We also identified the need for a support function that won't be exposed to web-services, but will be used by the web services functions.

Finally, I provided you with the code for the database object using the mySQL improved extension. This code will be invoked by our server function and will be the access portal to our database for the server-side method calls.

We're ready to start writing the server-side code methods and WSDL structures which we'll do in part 3 of this series.

Until then, thank-you for stopping by and I hope this helps you!