2008/11/22
22 Nov, 2008

Introduction to PHP Data Services

  • Dimuthu LNAME
  • - WSO2

Applies to

WSO2 WSF/PHP 2.0.0 or higher
Environment Windows or Linux

Introduction

The database plays a big role in any day-to-day application. It is a major component in accounting, Web portals, CMS (Content Management Systems), and SaaS (Software as a Service) applications to search engines. In traditional MVC (Model-View-Controller) applications, we talk about the Model component which represents the database. In a 3-tier architectural pattern, it is the Data layer that represents the database and provides data to the Logic Layer per request.

As SOA (Service Oriented Architecture) evolves, the database is becoming just one part of the data layer or the model component. There are many data sources and data providers, which are mostly deployed as Web services, from which you can request data as you do with traditional databases. This gives you the advantages of interoperability, security and reliability of Web services and other WS-* (Web Service specification stack) support. It avoids the need to install different drivers for different databases, and most importantly it removes the tight binding of your application to the database. So with SOA adaption the Data Layer has been replaced by the Service Consuming Layer.

The story of the Data Provider also changed with the SOA adaption. New data sources are designed with SOA in mind. Legacy systems are wrapped by a service layer to make it easier to consume. We use the term Data Services for the data sources deployed as Web services.

There are many public data services available as REST (REpresentational State Transfer), which is a lighter way of providing services. The other method is to use WS-* features like WS-Security, and WS-Reliable Messaging to deploy the data services, which is mostly adopted by the enterprise.

PHP is a popular choice when it comes to developing database web applications. Today there are thousands of web servers powered by the LAMP (Linux + Apache + MySql + PHP) stacks. So it is clear that PHP is a great language to develop data services as it needs minimum effort and minimum changes to the existing infrastructure to make a database into a Web service.

The PHP Data Service library provides a solution to develop data services in PHP. It comes with the WSO2 WSF/PHP project, which is a WS-* middle-ware platform for PHP developers. With the PHP Data Services library,  you can deploy data services, by writing simple PHP scripts and deploying it in a web server.

In this tutorial, I will explain how to develop a data service with the WSO2 WSF/PHP, deploy it as both REST and SOAP services, and how to enable WS-* features to make your data service more secure and reliable.

Table of Contents

Configuring the Data Service Library

The data services library depends on the WSO2 WSF/PHP and the PHP PDO extensions. Download and install the latest WSO2 WSF/PHP package from https://wso2.com/products/web-services-framework/php/. You can find the installation guide for your platform at https://wso2.org/project/wsf/php/2.0.0/docs/install_guide.html

To install the PDO extension, refer to this installation guide: https://www.php.net/manual/en/pdo.installation.php. Remember you have to install the PDO extension relevant to your Database driver. For example, for mysql you should install pdo_mysql.

Designing Your Data Service

In Web services, we communicate by exchanging messages. So when designing the data service, we have to design the input message format that the service accepts and the output message format of the service reply. This will define the service interface of your data services.

The Data Service API allows you to define the message formats, and it tries to de-couple the names used in these messages with the names actually in the database. So you can later alter your database without affecting the service interface. You may have to update the mapping of these names in the PHP data service script.

For example, imagine you are a reporter for a basketball tournament. You have a very simple database table to keep the records of each game.

Games
gameId Venue Date

Here is the SQL statement to create the table. Note that all the necessary SQL scripts are attached.

 

CREATE TABLE `Games` (
`gameId` INT,
`Venue` VARCHAR( 32 ),
`Date` VARCHAR( 32 ),
PRIMARY KEY ( `gameId` )
) 

Teams
teamId Name Coach

 


CREATE TABLE `Teams` (
  `teamId` int(11),
  `Name` varchar(32),
  `Coach` varchar(32)L,
  PRIMARY KEY  (`teamId`)
) 

 

 

GamesTeams
gameId teamId Score

 

 

CREATE TABLE IF NOT EXISTS `GamesTeams` (
  `gameId` int(11),
  `teamId` int(11),
  `Score` int(11),
  PRIMARY KEY  (`teamId`,`gameId`),
  FOREIGN KEY (`gameId`) REFERENCES Games(`gameId`),
  FOREIGN KEY (`teamId`) REFERENCES Teams(`teamId`)
) 

 

This provides a service that gives the client information on the matches the particular team played. In the next sections, we will design the message formats and implement the data service for this scenario.

For example, to find out the games that Italy played, you as the designer of the operation, will be expecting a message similar to the following one.

 

<gamesOfTeam>
   <teamName>
       Italy
   </teamName>
</gamesOfTeam>

 

Here we name the operation as gamesOfTeam and the input parameter as the teamName, which in this case holds the value Italy.

You also want the venue, date, teams and the score of the game. Here is the message your service will reply with.

 

<gamesOfTeamResponse>
   <game>
      <venue>
         xxx
      </venue>
      <date>
         2008-08-09
      </date>
      <team1>
          Italy
      </team1>
      <team2>
          Spain

      </team2>
      <team1Score>
          40
      </team1Score>
      <team2Score>
          30
      </team2Score>
   </game>
   <game>
      <venue>
         yyy
      </venu>
      <date>
         2008-08-19
      </date>
      <team1>
          Italy
      </team1>
      <team2>
          Canada
      </team2>
      <team1Score>
          40
      </team1Score>
      <team2Score>
          60
      </team2Score>
   </game>
   <game>
      ...
   </game>
   ....
</gamesOfTeamResponse>

You can use this message format to provide an array of game elements which contain information about each game.

 

If you try to write the service from the general principle, you have to either write the WSDL with the above message schema if you follow the contract first approach, or write the code annotating the desired names and types for the code first approach. The data service library gives you an API that is specific to such scenarios. Therefore, you only have to give the names of the wrapper element, recurrent element and the parameters, and the data services library will take care of building the SOAP messages and generating the WSDL for your service.

Writing the Data Service

After you have decided on the operations and the message format, you can start coding your DataService. First you have to specify your database configurations. We keep the configuration data in an array so we can later feed it to the WSService class.


// database configurations
$db_config = array(
      "db" => "your_db_engine", //e.g. mysql
      "username" => "db_username",
      "password" => "db_password",
      "dbname" => "db_name",
      "dbhost" => "db_host");

Next, give the input and output message schemas as designed in the above section. For the input message format, you should give the input parameter information, including the parameters name and type. Here we only have one input parameter for the gamesOfTeam' operation, which is the team name.


// input message format
$inputFormat = array("teamName" => "STRING");

Write the database query that runs behind the data service.

// SQL to retrieve the result data set
$sql = "SELECT Games.Venue, ".
              "Games.Date, ".
              "Team1.Name AS Team1Name, ".
              "Team2.Name AS Team2Name, ".
              "GameTeam1.Score AS Score1, ".
              "GameTeam2.Score AS Score2 ".
         "FROM Teams Team1, ".
              "Teams Team2, ".
              "GamesTeams GameTeam1, ".
              "GamesTeams GameTeam2, ".
              "Games ".
        "WHERE GameTeam1.gameId = Games.gameId  AND ".
              "GameTeam2.gameId = Games.gameId AND ".
              "GameTeam1.teamId = Team1.teamId AND ".
              "GameTeam2.teamId = Team2.teamId AND ".
              "Team1.teamId <> Team2.teamId AND ".
              "Team1.name=? ".
     "GROUP BY Games.gameId";

Here we query for the two teams playing the game, where the name of one team is taken from the user parameter. This will return a result with the fields Venue, Date, Team1Name, Team2Name, Team1Score and Team2Score

After writing the query,  we have to write the output format. This is very easy since we already finalized a design for the output message, and we know the output result from the SQL query. For each game, we should provide the venue, date, team1, team2 and the score wrapped by the element named game. All these games will be wrapped inside the gamesOfTeamResponse' element. Here is how you provide that output message schema.

 

$outputFormat = array("resultElement" => "gamesOfTeamResponse",
                      "rowElement" => "game",  // this is the repeating wrapper element for each game
                      "elements" => array( "venue" => "Venue",
                                           "date" => "Date",
                                           "team1" => "Team1Name",
                                           "team2" => "Team2Name",
                                           "team1Score" => "Score1",
                                           "team2Score" => "Score2"));

 

In the elements section, we mapped the set of elements in our reply message, with the corresponding database table column names.

We just prepared the input message format, the SQL query and the output message format. This is all we need to define a Web service operation. We will name this operation  gamesOfTeam. We can define the operation map with this single operation.


// operation consists of an input format, sql query and output format
$operations = array("gamesOfTeam" => array("inputFormat" => $inputFormat,
                                            "sql" => $sql,
                                            "outputFormat" => $outputFormat));

Finally, create the WSService object by providing the database configuration and the operations map as constructor parameters, and finally by calling the service reply method.

$my_data_service = new DataService(array("config" => $db_config,
                                         "operations" => $operations));
$my_data_service->reply();

To import the DataService class to this script, perform a require("") statement at the beginning of the code. If you have already set the include_path to the /scripts directory, you only have to give the relative path to the DataService class as follows.

 

require_once("wso2/DataServices/DataService.php");

 

Deploying and Testing the Data Service

If you have already deployed a WSF/PHP Web service, deploying a data service will not be a new thing to you. You can just copy the script (say game_score_service.php) in your web root directory, so the service endpoint will be the URL of that script.

In order to test the service, you can send a sample request message to the endpoint with the WSClient.


<?php
// request xml message
$requestPayloadString = <<<XML
<gamesOfTeam>
   <teamName>Italy</teamName>
</gamesOfTeam>
XML;

try {
    // set your service real endpoint here
    $client = new WSClient(array( "to" => "https://localhost/game_score_service.php"));

    // invoking the request
    $responseMessage = $client->request( $requestPayloadString );

    printf("Response = %s <br>", htmlspecialchars($responseMessage->str));

} catch (Exception $e) {

    if ($e instanceof WSFault) {
        printf("Soap Fault: %s\n", $e->Reason);
    } else {
        printf("Message = %s\n",$e->getMessage());
    }
}
?>

Making the Service Available in both RESTful and SOAP Forms

We already created a data service for SOAP. To make it available as a RESTful service, you only have to make a small change. Give the give map of operation to the URL and feed it at the creation of the WSService. Here is the additional piece of code we add to our data service.


$restmap = array("gamesOfTeam" => array("HTTPMethod" => "GET",
                                        "RESTLocation" => "gamesOfTeam/{teamName}"));

//updated WSService

$my_data_service = new DataService(array("config" => $db_config,
                                         "operations" => $operations,
                                         "RESTMapping" => $restmap));
$my_data_service->reply();

That is all you have to do to make it a RESTful service. Now you can test the service by just typing the correct URL in a browser. Assuming the Data Service is deployed in https://localhost/game_score_service.php", a possible URL to test the service would be,

https://localhost/game_score_service.php/gamesOfTeam/Italy

Securing Your Data Service Using WS-Security Tokens

WSF/PHP has inbuilt support for WS-Security, which provides many functionalities to secure your Web service or data service. You can sign or encrypt your SOAP messages using a simple API. You can authenticate SOAP requests coming in to your service.

For this article, I selected to show the application of a username token in our data service. If we take the scenario of reporting a basketball tournament, we can restrict the access to the scores, so that only the authenticated users can access the Web service. You can use the username token defined in the WS-Security specification to send authentication information in SOAP messages.

For that we have to add some code to our service. Here is how we do it.

First you create a policy object defining your policy requirements.

// policy options
$security_options = array("useUsernameToken" => TRUE);
$policy = new WSPolicy(array("security"=>$security_options));

After you define the policy, you have to provide your security tokens related to the policy using the WSSecurityToken class. For the username token policy, you can give the username and password as hard coded values while constructing the WSSecurityToken object. I prefer to have a callback function that I can validate the request authentication information (i.e., the username and the password in the request message) against a database query.This is how you create the WSSecurityToken object.

// the security token with the username token parameters
$sec_token = new WSSecurityToken(array("passwordCallback" => "get_my_password_function",
                                       "passwordType" => "Digest"));

This is how you define the "get_my_password_function".


// username token password callback
function get_my_password_function($username) {
    global $db_config;

    $db_engine = $db_config["db"];
    $db_host = $db_config["dbhost"];
    $db_name = $db_config["dbname"];
    $db_username = $db_config["username"];
    $db_password = $db_config["password"];

    $db_dns = "{$db_engine}:host={$db_host};dbname={$db_name}";

    $dbh = new PDO($db_dns, $db_username, $db_password);

    $sql_query ="SELECT Password FROM `Users` WHERE `Name` = '{$username}'";

    $password = NULL;
    if($stmt= $dbh->query($sql_query)) {
        $results = $stmt->fetchAll();

        if(count($results) > 0) {
            $password = $results[0][0];
        }
    }

    return $password;
}

You need to add the following table to your database to store login information.

Users
Name Password

 

CREATE TABLE `Users` (
`name` VARCHAR( 32 ),
`Password` VARCHAR( 32 ),
PRIMARY KEY ( `name` )
)

 

I read the database login information from the $db_config and queried the database for the given username to retrieve its password. If the username exists, it will return the relevent password. In this callback function, we need to return the password to the caller and it will validate the authenticity of the request SOAP message.

To run security with WSF/PHP services, you need to provide either the SOAP action or the WS-Addressing action for each operation. You can give this in an action map as follows.

// the action => operation map
$actions = array("https://wso2.org/library/demo/gamesOfTeam" => "gamesOfTeam");

So we have the WSPolicy and WSSecurityToken instances and the WS-Addressing action map. We add this information at the creation of a DataService.

$my_data_service = new DataService(array("config" => $db_config,
                                         "operations" => $operations,
                                         "RESTMapping" => $restmap,
                                         "actions" => $actions,
                                         "useWSA" => TRUE,
                                         "policy" => $policy,
                                         "securityToken" => $sec_token));
$my_data_service->reply();

If you have used the WSService class with WS-Security, you will be familliar with the above options. Since the DataService class is just an inherited class of the WSService class, we can give all the options available in the WSService class to the DataService class as well.

We have successfully created a data service with the username token. Now we need to test this service with our client. Since we just enforced a policy in the service, we have to adhere to that in the client as well.  Here are the changes I made in the earlier client code (I've only updated the code within the try block).


    $reqMessage = new WSMessage($requestPayloadString,
                                array("to" => "https://localhost/game_score_service.php",
                                      "action" => "https://wso2.org/library/demo/gamesOfTeam"));

    $sec_array = array("useUsernameToken" => TRUE );
    $policy = new WSPolicy(array("security" => $sec_array));
    $sec_token = new WSSecurityToken(array("user" => "login_username",
                                           "password" => "login_password",
                                           "passwordType" => "Digest"));

    $client = new WSClient(array("useWSA" => TRUE,
                                 "policy" => $policy,
                                 "securityToken" => $sec_token));

    $resMessage = $client->request($reqMessage);

    printf("Response = %s <br>", htmlspecialchars($responseMessage->str));

Another security issue related to our scenario (reporting basketball scores) is whether the results sent to the clients are actually sent by their trusted service. There can be intruders that capture the SOAP messages and  change the results. This can lead to many problems.

To prevent such an attack, the service can sign their response digitally with the service private key. The clients can check whether the reply recieved is exactly what the service sent by verifying the message with its digital signarure. To provide this facility with a single predefined client, you can use the following policy object and the security token structure.


$sec_array = array("sign" => TRUE,
                    "algorithmSuite" => "Basic256Rsa15",
                    "securityTokenReference" => "KeyIdentifier");

$policy = new WSPolicy(array("security"=>$sec_array));

$cert = ws_get_cert_from_file("client_certificate.crt");
$pvt_key = ws_get_key_from_file("server_private_key.pem");

$sec_token = new WSSecurityToken(array("privateKey" => $pvt_key,
                                       "certificate" => $cert));


Conclusion

This article covers the basics on how to create a data service using the WSF/PHP Data Services library. It gave you a breif introduction to the data service concept and describes their applications in general. It described how to set up a WSF/PHP data service library with the necessary PHP extensions. We wrote a simple data service for reporting basketball scores to demonstrate the steps of developing a data service and deploying it as both SOAP and RESTful services. Finally it uncovered some WS-* support by showing how to use the WS-Security Username token to authenticate your service and the use of the digital signature to verify the validity of the message.

References

 

 

Author

Dimuthu Gamage is a senior software engineer at WSO2. He is a commiter of the Apache Web Services project and a developer of the WSF/PHP and WSF/Ruby projects. dimuthu at wso2 dot com.

 

About Author

  • Dimuthu LNAME