Modifying the NuSOAP Server

OK, the last couple of posts have strayed from the NuSOAP topic. but I wanted to do a quick post before going into to testing the server on a .NET application. The whole point of this post is to modify the result set we are sending back for some of the items. And let’s also change around one of the inputs so it accepts an array of data as the input, instead of strings. Hopefully this example will help others build a NuSOAP server. Which is why I am doing some of these examples. I was tired of seeing example after example of NuSOAP servers with a “Hello World” type of structure. That does not help in the real world. So hopefully these examples will help others.

First, we need to recall our last example we had out there, it was the following files:
Data Used: http://www.hirdweb.com/webservice/items.txt
Server File: http://www.hirdweb.com/webservice/20100720_server.txt
Client File: http://www.hirdweb.com/webservice/20100720.txt

We will be modifying those files, so if you do not already have those, you can grab those there. The data file “items” is ok, and we do not need to change anything there. The server file we will need to change, so go ahead and save that file with a new name. After you do that, you will need to change the line in the server that sets the wsdl address. I named the new file 20100805_server.php.

$wsdl_addr = 'http://www.hirdweb.com/webservice/20100805_server.php';

The first thing to do is change the result set for the showNumbers functions. This is the function that accepts an identifier of 0 thru 5 and returns a list of numbers. It is a pretty simple example. In the real world, the identifier may be a book ISDN, or a product code, or a user id. Based on how you use this, it could return anything, like a list of authors (ISDN), or related products (product code), or friends (user ID). Or this can be used in conjunction with another function. But we need to format this a little better.

As of right now, here is the basic format that it returns:

"5"
Array (
  [status] => 
  [numbers] => Array
    (
      [0] => Array
        (
          [number] => 5
        )
      [1] => Array
        (
          [number] => 10
        )
      [2] => Array
        (
          [number] => 15
        )
      [3] => Array
        (
          [number] => 20
        )
      [4] => Array
        (
          [number] => 25
        )
    )
)

While this is not bad, we can format this a little better and make it appear more like this:

"5"
Array (
  [status] => 
  [numbers] => Array
    (
      [0] =>  5
      [1] =>  10
      [2] =>  15
      [3] =>  20
      [4] =>  25
    )
)

If all the results are the same no matter what, then we can group the results this way instead. This way the client who is using the data does not have to parse down to deeper levels of array for the same type of information. So the first thing is to plan. Since our data is set up in this way:

$various = array (
	array(
		array('number' => "01"),
		array('number' => "02"),
		array('number' => "03"),
		array('number' => "04"),
		array('number' => "05"),
		array('number' => "06"),
		array('number' => "07"),
		array('number' => "08"),
		array('number' => "09"),
	),
	array(
		array('number' => "121654898"),
		array('number' => "11151"),
		array('number' => "18852"),
		array('number' => "1114885111111"),
		array('number' => "11111111111111111111"),
	),	
	array(
		array('number' => "2654846354684654"),
	),
	array(
		array('number' => "303030303030303030"),
		array('number' => "32323232323232323232323232"),
	),
	array(
		array('number' => "44444444"),
		array('number' => "41414141"),
		array('number' => "42424242"),
		array('number' => "43434343"),
		array('number' => "40404040"),
	),
	array(
		array('number' => "5"),
		array('number' => "10"),
		array('number' => "15"),
		array('number' => "20"),
		array('number' => "25"),
	),
);

we will need to iterate through the results in order to get the correct array set up. So first we need to do a foreach loop. Remember to unit test the function changes before sending it out to the service. Here is what the function looked like before:

function showNumbers($id){
    require("items.php");
	
    // place all validation code here
    if ( ($id < 0) || ($id > 5)  ){
        // return an error
        return error("custom", "numbers", "You need to enter an ID that is 0, 1, 2, 3, 4, or 5.");
    }
    
    $numbers = $various[$id];

    $fin_data['head'] = array('status'=>'ok','message'=>'');
    $fin_data['numbers'] = $numbers;

    return $fin_data;
}

We need to change this around to iterate through the results and set them appropriately. This will change the part after the validation code to the following:

$numbers = $various[$id];

$final_set = array();
foreach ( $numbers as $number ){
    $final_set[] = $number['number']; 
}

$fin_data['head'] = array('status'=>'ok','message'=>'');
$fin_data['numbers'] = $final_set;

return $fin_data; 

This will give us our preferred format. Now we need to change the wsdl structure, as we have changed the way the result is formatted, so the WSDL needs to be updated to reflect this. If you do not change the format, it will result in something like the following:

Array
(
    [faultcode] => SOAP-ENV:Server
    [faultactor] => 
    [faultstring] => unable to serialize result
    [detail] => 
)

So lets change the WSDL. It currently shows the following for the numbers return formating (Minus the “showNumbers” declaration):

$server->wsdl->addComplexType(
  'number','complexType','struct','all','',
  array(
    'number' => array('name' => 'number', 'type' => 'xsd:string'),		
  )
);

$server->wsdl->addComplexType(
  'numbers','complexType','array','',
  'SOAP-ENC:Array',
  array(),
  array(array('ref'=>'SOAP-ENC:arrayType','wsdl:arrayType'=>'tns:number[]')), 'tns:number'
);

This is probably the best part, as it is very easy. We no longer need to define a complexType named “number”. So we can remove the “number” complexType. What we want is an array of strings (actually in this case it is integers, but just putting this as a string for the example, you really do want to declare the correct variable type when it is a production level set). The way to declare an array of strings is to just do this
array(array(‘ref’=>’SOAP-ENC:arrayType’,’wsdl:arrayType’=>’xsd:string[]’)), ‘xsd:string’
Since this is still an array, we need to keep the SOAP-ENC:Array declaration. And we change it to the following:

$server->wsdl->addComplexType(
  'numbers','complexType','array','',
  'SOAP-ENC:Array',
  array(),
  array(array('ref'=>'SOAP-ENC:arrayType','wsdl:arrayType'=>'xsd:string[]')), 'xsd:string'
);

And we now have the following sent to the client when they request this function:

"5"
Array
(
  [status] => 
  [numbers] => Array
    (
      [0] => 5
      [1] => 10
      [2] => 15
      [3] => 20
      [4] => 25
    )

)

Now, that is complete. We need to look at passing an array as in input instead of a string of data.

Passing an array of data into the service

For this one, we will look at the showTaxes function. This function takes 2 input paramters, a price and a state. We can combine that into one set, an array. As always, first plan the change, do not blindly go changing code. Plan it out. After it is planned, then attack the function. Here is a possible solution:

function showTaxes($in = array()){
	require('items.php');
	// Make sure the state is lowercase
	$state = strtolower($in['state']);
	$price = $in['price'];
	// Check the array keys to validate
	$validate = array_keys($tax_rates);
	
	// place all validation code here
	if ( strlen($state) > 2 ){
		return error('custom', '', 'The state that was passed is not in the correct format. It should only be 2 characters');
	}
	
	if ( !in_array($state, $validate) ){
		// return an error
		return error("not_found");
	}
	
	if ( $price < 0 ){
		return error('less_than_zero');
	}
	
	if ( $price == '' ){
		return error('empty');
	}
	
	$tax = $tax_rates[$state] * $price;
	$final_price = $tax + $price;
	
	// set the array
	$final = array(
		'price' => $price,
		'tax'	=> money_format('%i',$tax),
		'total' => money_format('%i',$final_price),
		'state' => $states[$state],
	);
	
	$fin_data['status'] = array('status'=>'ok','message'=>'');
	$fin_data['taxes'] = $final;
	
	return  $fin_data;
}

Now that is completed, make sure to test it so that you know the function works as expected. Since we did not change anything with the output result set, we do not need to worry about the WSDL for the output, but the input is changed, and we do need to address it. Right now it looks like:

$in = array(
	'price' => 'xsd:string',
	'state' => 'xsd:string',
);

This needs to change to a custom type. By doing this, we need to declare a new complexType, just like we would do for the output. A possible solution:

$in = array(
	'in' => 'tns:inTaxArray'
);
$out = array('return' => 'tns:showTaxes');
$namespace = 'uri:hirdwebservice';
$soapaction = 'uri:hirdwebservice/showTaxes';
$doc = 'Shows the taxes based on state taxes. Currently, the only states available in this are: AZ,AL,AK, CA, OR, WA, UT, ID, WY, Defaults to CA if no state is provided.';		
$server->register('showTaxes', $in, $out, $namespace, $soapaction, 'rpc', 'encoded', $doc);

And a possible declaration solution:

// the input declaration
$server->wsdl->addComplexType('inTaxArray','complexType','struct','all','',
  array(
    'price' => array('name' => 'price', 'type' => 'xsd:string'),
    'state' => array('name' => 'state', 'type' => 'xsd:string')
  )
);

Adding arrays as the input parameter can be this simple. It uses the same concept as the output. If it is an array that is coming in, it needs to be declared as custom complexType in order for the NuSOAP libraries to build the WSDL accordingly.

And if you need to accept a non-associative array of numbers, or strings or whatever, you can use the same idea as listed above when declaring the input array. For example, if your client wants to pass an array of numbers (like ISBNs, or product IDs, or customer IDs) to get multiple data sets, just declare the type with an array of strings, integers, etc.

Just remember that when dealing with data on the NuSOAP libraries, if it is a simple data type all you need to do is declare the type, ie string, float, integer, etc. If it is an array, or a more complex type, then make sure to declare that as an array. Associative arrays will need a name and structure for each element, which can be a xsd (simple type) or another level of complexity with a new “tns:” definition.

The links for this post:
Server: http://www.hirdweb.com/webservice/20100805_server.php
Client: http://www.hirdweb.com/webservice/20100805.php

The code for this post:
Data: http://www.hirdweb.com/webservice/items.txt
Server:http://www.hirdweb.com/webservice/20100805_server.txt
Client: http://www.hirdweb.com/webservice/20100805.txt

And next time lets look into how to test these to make sure they work with a .NET client.