4. Examples
4.1. Taxes Application: Introduction
Here we introduce the TAX application, which will be used many times later on. This application calculates a taxpayer’s tax liability. We consider the simplified case of a taxpayer who has only a single salary to report:
- we calculate the number of tax brackets for the employee as nbParts = nbEnfants / 2 + 1 if they are unmarried, and nbEnfants / 2 + 2 if they are married, where nbEnfants is the number of children.
- if they have at least three children, they receive an additional half-share
- We calculate their taxable income R = 0.72 * S, where S is their annual salary
- We calculate their family coefficient QF = R / nbParts
- we calculate his tax I. Consider the following table:
12620.0 | 0 | 0 |
13,190 | 0.05 | 631 |
15,640 | 0.1 | 1,290.5 |
24,740 | 0.15 | 2,072.5 |
31,810 | 0.2 | 3,309.5 |
39,970 | 0.25 | 4900 |
48,360 | 0.3 | 6,898.5 |
55,790 | 0.35 | 9,316.5 |
92,970 | 0.4 | 12,106 |
127,860 | 0.45 | 16,754.5 |
151,250 | 0.50 | 23,147.5 |
172,040 | 0.55 | 30,710 |
195,000 | 0.60 | 39,312 |
0 | 0.65 | 49,062 |
Each row has 3 fields. To calculate tax I, find the first row where QF <= field1. For example, if QF = 23,000, the row found will be
Tax I is then equal to 0.15*R - 2072.5*nbParts. If QF is such that the condition QF<=field1 is never met, then the coefficients from the last row are used. Here:
which gives tax I = 0.65*R - 49062*nbParts.
The data defining the different tax brackets is stored in an ODBC-MySQL database. MySQL is a public-domain DBMS that can be used on various platforms, including Windows and Linux. Using this DBMS, a database named dbimpots was created, containing a single table called impots. Access to the database is controlled by a username/password, in this case admimpots/mdpimpots. The following screenshot shows how to use the dbimpots database with MySQL:
C:\Program Files\EasyPHP\mysql\bin>mysql -u admimpots -p
Enter password: *********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 18 to server version: 3.23.49-max-nt
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
mysql> use dbimpots;
Database changed
mysql> show tables;
+--------------------+
| Tables_in_dbimpots |
+--------------------+
| impots |
+--------------------+
1 row in set (0.00 sec)
mysql> describe taxes;
+---------+--------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------+------+-----+---------+-------+
| limits | double | YES | | NULL | |
| coeffR | double | YES | | NULL | |
| coeffN | double | YES | | NULL | |
+---------+--------+------+-----+---------+-------+
3 rows in set (0.02 sec)
mysql> select * from impots;
+---------+--------+---------+
| limits | coeffR | coeffN |
+---------+--------+---------+
| 12620 | 0 | 0 |
| 13190 | 0.05 | 631 |
| 15640 | 0.1 | 1290.5 |
| 24,740 | 0.15 | 2,072.5 |
| 31810 | 0.2 | 3309.5 |
| 39970 | 0.25 | 4900 |
| 48360 | 0.3 | 6898 |
| 55,790 | 0.35 | 9,316.5 |
| 92,970 | 0.4 | 12,106 |
| 127,860 | 0.45 | 16,754 |
| 151,250 | 0.5 | 23,147.5 |
| 172,040 | 0.55 | 30,710 |
| 195000 | 0.6 | 39312 |
| 0 | 0.65 | 49062 |
+---------+--------+---------+
14 rows in set (0.00 sec)
mysql>quit
The dbimpots database is converted into an ODBC data source as follows:
- Launch the 32-bit ODBC Data Source Administrator

- Use the [Add] button to add a new ODBC data source

- Select the MySQL driver and click [Finish]
![]() |
- The MySQL driver requests some information:
1 | The DSN name to give the ODBC data source—it can be anything |
2 | the machine on which the MySQL DBMS is running—here, localhost. It is worth noting that the database could be a remote database. Local applications using the ODBC data source would not notice this. This would be the case, in particular, for our PHP application. |
3 | the MySQL database to use. MySQL is a DBMS that manages relational databases, which are sets of tables linked together by relationships. Here, we specify the name of the database being managed. |
4 | The name of a user with access rights to this database |
5 | their password |
4.2. Tax Application: the ImpotsDSN class
Our application will rely on the PHP class ImpotsDSN, which will have the following attributes, constructor, and method:
<?php
...
// attributes
var $limits; // array of limits
var $coeffR; // array of coeffR
var $coeffN; // array of coeffN
var $errors; // array of errors
- We saw in the problem statement that we needed three sets of data
12620.0 | 0 | 0 |
13190 | 0.05 | 631 |
15,640 | 0.1 | 1,290.5 |
24,740 | 0.15 | 2,072.5 |
31,810 | 0.2 | 3,309.5 |
39,970 | 0.25 | 4900 |
48,360 | 0.3 | 6,898.5 |
55,790 | 0.35 | 9,316.5 |
92,970 | 0.4 | 12,106 |
127,860 | 0.45 | 16,754.5 |
151,250 | 0.50 | 23,147.5 |
172,040 | 0.55 | 30,710 |
195,000 | 0.60 | 39,312 |
0 | 0.65 | 49062 |
The first column will be placed in the $limites attribute of the ImotsDSN class, the second in the $coeffR attribute, and the third in $coeffN. These three attributes are initialized by the class constructor. The constructor retrieves the data from an ODBC data source. Various errors may occur during object construction. These errors are reported in the $erreurs attribute as an array of error messages.
- The constructor receives a $impots dictionary as a parameter with the following fields:
// dsn: DSN name of the ODBC data source containing the values of the limit, coeffR, and coeffN arrays
// user: name of a user with read access to the ODBC source
// pwd: their password
// table: name of the table containing the values (limits, coeffR, coeffN)
// limits: name of the column containing the limit values
// coeffR: name of the column containing the coeffR values
// coeffN: name of the column containing the coeffN values
The constructor parameter provides all the information needed to read data from the ODBC data source
- Once the ImpotsDSN object is created, users of the class can call the object’s calculateTaxes method. This method takes a $person dictionary as a parameter:
// $person["married"]: yes, no
// $person["children"]: number of children
// $person["salary"]: annual salary
The complete class is as follows:
<?php
// definition of a class objImpots
class TaxDSN{
// attributes: the 3 data arrays
var $limits; // limits array
var $coeffR; // array of coeffR
var $coeffN; // array of coeffN
var $errors; // array of errors
// constructor
function ImpotsDSN($impots){
// $taxes: dictionary containing the following fields
// dsn: DSN name of the ODBC data source containing the values of the limit arrays, coeffR, and coeffN
// user: name of a user with read access to the ODBC source
// pwd: their password
// table: name of the table containing the values (limits, coeffR, coeffN)
// limits: name of the column containing the limit values
// coeffR: name of the column containing the coeffR values
// coeffN: name of the column containing the coeffN values
// initially no errors
$this->errors = array();
// Checking the call
if (!isset($impots[dsn]) || !isset($impots[user]) || !isset($impots[pwd]) || !isset($impots[table]) ||
! isset($impots[limits]) || ! isset($impots[coeffR]) || ! isset($impots[coeffN])){
// error
$this->errors[] = "Invalid call";
// end
return;
}//if
// Open the DSN database
$connection = odbc_connect($taxes[dsn], $taxes[user], $taxes[pwd]);
// error?
if(! $connection){
// error
$this->errors[] = "Unable to open the DSN database [$impots[dsn]] (".odbc_error().")";
// end
return;
}//if
// execute a query on the database
$query = odbc_prepare($connection, "select $taxes[limits], $taxes[coeffR], $taxes[coeffN] from $taxes[table]");
if(! odbc_execute($query)){
// error
$this->errors[] = "Unable to access the DSN database [$impots[dsn]] (".odbc_error().")";
// end
odbc_close($connection);
return;
}//if
// process query results
$this->limits = array();
$this->coeffR = array();
$this->coeffN = array();
while(odbc_fetch_row($query)){
// one more row
$this->limits[] = odbc_result($query, $taxes[limits]);
$this->coeffR[] = odbc_result($query, $impots[coeffR]);
$this->coeffN[] = odbc_result($query, $impots[coeffN]);
}//while
// Close database
odbc_close($connection);
}//constructor
// --------------------------------------------------------------------------
function calculate($person){
// $person["married"]: yes, no
// $person["children"]: number of children
// $person["salary"]: annual salary
// Is the object in a valid state?
if (! is_array($this->errors) || count($this->errors) != 0) return -1;
// Check the call
if (! isset($person[married]) || ! isset($person[children]) || ! isset($person[salary])){
// error
$this->errors[] = "Invalid call";
// end
return -1;
}//if
// Are the parameters correct?
$person[married] = strtolower($person[married]);
if($person[married] != 'yes' && $person[married] != 'no') {
// error
$this->errors[] = "Marital status [$person[married]] is incorrect";
}//if
if(! preg_match("/^\s*\d{1,3}\s*$/",$person[children])){
// error
$this->errors[] = "Incorrect number of children [$person[children]]";
}//if
if(! preg_match("/^\s*\d+\s*$/",$person[salary])){
// error
$this->errors[] = "Invalid salary [$person[salary]]";
}//if
// any errors?
if(count($this->errors)!=0) return -1;
// number of shares
if($person[married]==true) $numberOfShares=$person[children]/2+2;
else $nbParts=$person[children]/2+1;
// add 1/2 share if there are at least 3 children
if($person[children] >= 3) $numberOfShares += 0.5;
// taxable income
$taxableIncome = 0.72 * $person[salary];
// family quotient
$quotient = $taxableIncome / $numberOfShares;
// is placed at the end of the limits array to stop the following loop
$this->limits[$this->limitCount] = $quota;
// tax calculation
$i=0;
while($quotient > $this->limits[$i]) $i++;
// Since we placed $quotient at the end of the $limits array, the previous loop
// cannot go beyond the $limits array
// now we can calculate the tax
return floor($taxableIncome * $this->coeffR[$i] - $nbParts * $this->coeffN[$i]);
}//taxCalculation
}//taxClass
?>
A test program could look like this:
<?php
// libraries
include "ImpotsDSN.php";
// Create a tax object
$conf = array(dsn => "mysql-dbimpots", user => "admimpots", pwd => "mdpimpots",
table=>impots,limits=>limits,coeffR=>coeffR,coeffN=>coeffN);
$objImpots = new ImpotsDSN($conf);
// Any errors?
if(count($objImpots->errors)!=0){
// msg
echo "The following errors occurred:\n";
$errors = $objTaxes->errors;
for($i=0;$i<count($errors);$i++){
echo "$errors[$i]\n";
}//for
// end
exit(1);
}//if
// test
calculateTaxes($taxObject, yes, 2, 200000);
calculateTaxes($taxObj, false, 2, 200000);
calculateTaxes($taxObj, yes, 3, 200000);
calculateTaxes($taxObj, false, 3, 200000);
calculateTaxes($taxObj,array(),array(),array());
// end
exit(0);
// -----------------------------------------------------------
function calculateTaxes($taxObject, $married, $children, $salary){
// echo
echo "taxes($spouse,$children,$salary)\n";
// calculate tax
$person = array(spouse => $spouse, children => $children, salary => $salary);
$amount = $taxObject->calculate($person);
// any errors?
if(count($objTaxes->errors)!=0){
// message
echo "The following errors occurred:\n";
$errors = $objTaxes->errors;
for($i=0;$i<count($errors);$i++){
echo "$errors[$i]\n";
}//for
}else echo "amount=$amount\n";
}//calculateImp
- The test program makes sure to "include" the file containing the ImpotsDSN class
- then creates an objImpots object of the ImpotsDSN class by passing the necessary information to the object's constructor
- Once this object is created, its calculate method will be called five times
The results obtained are as follows:
dos>e:\php43\php.exe test.php
taxes(yes,2,200000)
amount=22504
taxes(no,2,200000)
amount=33388
taxes(yes,3,200000)
amount=16,400
taxes(no,3,200000)
amount=22504
taxes(Array,Array,Array)
The following errors occurred:
Incorrect marital status [array]
Number of children [Array] incorrect
Income [Array] incorrect
We will now use the ImpotsDSN class without redefining it.
4.3. Tax Application: Version 1
We are now presenting version 1 of the IMPOTS application. This is a web application that presents an HTML interface to the user to obtain the three parameters needed to calculate the tax:
- marital status (married or single)
- number of children
- annual income

The form is displayed using the following PHP page:
<?php //tax form ?>
<html>
<head>
<title>taxes</title>
<script language="JavaScript" type="text/javascript">
function clear(){
// Clear the form
with(document.frmImpots){
optMarie[0].checked=false;
optMarie[1].checked=true;
txtChildren.value = "";
txtSalary.value="";
txtTaxes.value="";
}//with
}//clear
</script>
</head>
<body background="/poly/impots/images/standard.jpg">
<center>
Tax Calculation
<hr>
<form name="frmImpots" method="POST">
<table>
<tr>
<td>Are you married?</td>
<td>
<input type="radio" name="optMarie" value="yes" <?php echo $request->chkoui ?>>yes
<input type="radio" name="optMarie" value="no" <?php echo $request->chkno ?>>no
</td>
</tr>
<tr>
<td>Number of children</td>
<td><input type="text" size="5" name="txtEnfants" value="<?php echo $request->children ?>"></td>
</tr>
<tr>
<td>Annual salary</td>
<td><input type="text" size="10" name="txtSalary" value="<?php echo $query->salary ?>"></td>
</tr>
<tr>
<td><font color="green">Tax</font></td>
<td><input type="text" size="10" name="txtTaxes" value="<?php echo $request->taxes ?>" readonly></td>
</tr>
<tr></tr>
<tr>
<td><input type="submit" value="Calculate"></td>
<td><input type="button" value="Clear" onclick="clear()"></td>
</tr>
</table>
</form>
</center>
<?php
// Are there any errors?
if(count($request->errors)!=0){
// Display errors
echo "<hr>\n<font color=\"red\">\n";
echo "The following errors occurred<br>";
echo "<ul>";
for($i=0;$i<count($query->errors);$i++){
echo "<li>".$request->errors[$i]."</li>\n";
}
echo "</ul>\n</font>\n";
}//if
?>
</body>
</html>
The PHP page simply displays information passed to it by the application's main program in the $request variable, which is an object with the following fields:
attributes of the "yes" and "no" radio buttons—their possible values are "checked" or "" to enable or disable the corresponding radio button | |
the taxpayer's number of children | |
their annual salary | |
the amount of tax due | |
a list of any errors—may be empty. |
The page sent to the client contains a JavaScript script with a clear function associated with the "Clear" button, whose purpose is to reset the form to its initial state: unchecked button, empty input fields. Note that this result could not be achieved with an HTML "reset" button. Indeed, when this type of button is used, the browser resets the form to the state in which it received it. However, in our application, the browser receives forms that may not be empty.
The application that processes the previous form is as follows:
<?php
// processes the tax form
// libraries
include "ImpotsDSN.php";
// application configuration
ini_set("register_globals", "off");
ini_set("display_errors", "off");
$taxForm="impots_form.php";
$taxErrors = "tax_errors.php";
$taxDB = array(dsn => "mysql-dbimpots", user => "admimpots", pwd => "mdpimpots",
table=>taxes,limits=>limits,coeffR=>coeffR,coeffN=>coeffN);
// retrieve the parameters
$query->married=$_POST["optMarried"];
$query->children = $_POST["txtChildren"];
$query->salary=$_POST["txtSalary"];
// Do we have all the parameters?
if(!isset($request->spouse) || !isset($request->children) || !isset($request->salary)){
// prepare empty form
$request->chkoui = "";
$request->chkno = "checked";
$request->children="";
$request->salary = "";
$request->taxes="";
$query->errors = array();
// display form
include $taxForm;
// end
exit(0);
}//if
// parameter validation
$request = check($request);
// any errors?
if(count($request->errors) != 0) {
// display form
include "$taxForm";
// end
exit(0);
}//if
// calculate the response
$request = calculateTaxes($dbTaxes, $request);
// any errors?
if(count($request->errors) != 0) {
// display error form
include "$taxErrors";
// end
exit(0);
}//if
// display form
include "$taxForm";
// end
exit(0);
// --------------------------------------------------------
function verify($request){
// checks the validity of the request parameters
// initially, no errors
$request->errors = array();
// Is the status valid?
$request->married = strtolower($request->married);
if($request->married != "yes" && $request->married != "no"){
// an error
$request->errors[] = "Invalid marital status [$request->married]";
}
// Is the number of children valid?
if(! preg_match("/^\s*\d+\s*$/",$query->children)){
// an error
$request->errors[] = "Incorrect number of children [$request->children]";
}
// Is the salary valid?
if(! preg_match("/^\s*\d+\s*$/", $request->salary)){
// an error
$request->errors[] = "Invalid salary [$request->salary]";
}
// radio button status
if($request->married=="yes"){
$request->chkoui = "checked";
$request->chkno = "";
}else{
$request->chkno = "checked";
$query->chkoui = "";
}
// return the query
return $query;
}//check
// --------------------------------------------------------
function calculateTaxes($taxDB, $query){
// calculate the tax amount
// $dbTaxes: dictionary containing the information needed to read the ODBC data source
// $query: the query containing information about the taxpayer
// we construct an ImpotsDSN object
$objTaxes = new TaxesDSN($dbTaxes);
// Any errors?
if(count($objTaxes->errors) != 0){
// add the errors to the query
$query->errors = $objTaxes->errors;
// done
return $query;
}//if
// calculate the tax
$person = array(married => "$request->married", children => "$request->children", salary => "$request->salary");
$request->taxes = $taxObject->calculate($person);
// return the result
return $request;
}//calculateTaxes
Comments:
- First, the ImpotsDSN class is included. This class forms the basis of the tax calculation and hides database access from us
- A number of initializations are performed to facilitate application maintenance. If parameters change, their values will be updated in this section. Generally, these configuration values are stored in a file or a database.
- We retrieve the three form parameters corresponding to the HTML fields optMarie, txtEnfants, and txtSalaire.
- These parameters may be completely or partially missing. This will be the case, in particular, during the initial request for the form. In that case, we simply send an empty form.
- Next, the validity of the three retrieved parameters is checked. If they are found to be incorrect, the form is returned as entered but with an additional list of errors.
- Once the validity of the three parameters has been verified, the tax can be calculated. This is done by the `calculerImpots` function. This function creates an `ImpotsDSN` object and uses that object’s `calculer` method.
- Creating the ImpotsDSN object may fail if the database is unavailable. In this case, the application displays a specific error page listing the errors that occurred.
- If everything went well, the form is returned as entered but with the amount of tax due added.
The error page is as follows:
<?php // error page ?>
<html>
<head>
<title>Tax application unavailable</title>
</head>
<body background="/poly/impots/images/standard.jpg">
<h3>Tax Calculator</h3>
<hr>
Application unavailable. Please try again later.<br><br>
<font color="red">
The following errors occurred<br>
<ul>
<?php
// display errors
for($i=0;$i<count($query->errors);$i++){
echo "<li>".$query->errors[$i]."</li>\n";
}
?>
</ul>
</font>
</body>
</html>
Here are a few examples of errors. First, an incorrect password is entered:

Incorrect data is entered into the form:

Finally, correct values are entered:

4.4. Tax Application: Version 2
In the previous example, the server validates the form parameters txtEnfants* and txtSalaire. Here, we propose validating them using a JavaScript script included in the form page. The browser then performs the validation. The server is only contacted if the parameters are valid. This saves on "bandwidth." The display page impots-form.php* becomes the following:
<?php //tax form ?>
<html>
<head>
<title>taxes</title>
<script language="JavaScript" type="text/javascript">
function clear(){
........
}//clear
//------------------------------
function calculate(){
// Check parameters before sending them to the server
with(document.frmImpots){
//number of children
champs = /^\s*(\d+)\s*$/ .exec(txtEnfants.value);
if(champs==null){
// the form is not validated
alert("The number of children was not provided or is incorrect");
nbEnfants.focus();
return;
}//if
//salary
fields = /^\s*(\d+)\s*$/ .exec(txtSalary.value);
if(fields==null){
// the pattern does not match
alert("Salary was not provided or is incorrect");
salary.focus();
return;
}//if
// OK—send the form to the server
submit();
}//with
}//calculate
</script>
</head>
<body background="/poly/impots/images/standard.jpg">
............
<td><input type="button" value="Calculate" onclick="calculate()"></td>
<td><input type="button" value="Clear" onclick="clear()"></td>
........
</body>
</html>
Note the following changes:
- The Calculate button is no longer a submit button but a button associated with a function called calculate. This function will validate the txtEnfants and txtSalaire fields. If they are valid, the form values will be sent to the server (submit); otherwise, an error message will be displayed.
Here is an example of what is displayed in case of an error:

4.5. Tax Application: Version 3
We are making a slight modification to the application to introduce the concept of a session. We now consider the application to be a tax calculation simulation tool. A user can then simulate different taxpayer "scenarios" and see what the tax liability would be for each one. The web page below provides an example of what could be achieved:

The code for the page above is as follows:
<?php //tax form ?>
<html>
<head>
<title>taxes</title>
<script language="JavaScript" type="text/javascript">
function clear(){
...
}//clear
//------------------------------
function calculate(){
...
}//calculate
</script>
</head>
<body background="/poly/impots/images/standard.jpg">
<center>
Tax Calculation
<hr>
<form name="frmImpots" method="POST">
<table>
<tr>
<td>Are you married?</td>
<td>
<input type="radio" name="optMarie" value="yes" <?php echo $request[chkoui] ?>>yes
<input type="radio" name="optMarie" value="no" <?php echo $request[chkno] ?>>no
</td>
</tr>
<tr>
<td>Number of children</td>
<td><input type="text" size="5" name="txtEnfants" value="<?php echo $request[children] ?>"></td>
</tr>
<tr>
<td>Annual salary</td>
<td><input type="text" size="10" name="txtSalary" value="<?php echo $query[salary] ?>"></td>
</tr>
<tr>
<td><font color="green">Tax</font></td>
<td><input type="text" size="10" name="txtTax" value="<?php echo $request[tax] ?>" readonly></td>
</tr>
<tr></tr>
<tr>
<td><input type="button" value="Calculate" onclick="calculate()"></td>
<td><input type="button" value="Clear" onclick="clear()"></td>
</tr>
</table>
</form>
</center>
<?php
// Are there any errors?
if(count($request[errors])!=0){
// display errors
echo "<hr>\n<font color=\"red\">\n";
echo "The following errors occurred<br>";
echo "<ul>";
for($i=0;$i<count($query[errors]);$i++){
echo "<li>".$request[errors][$i]."</li>\n";
}
echo "</ul>\n</font>\n";
}//if
// Are there any simulations?
else if(count($request[simulations])!=0){
// display simulations
echo "<hr>\n<h2>Simulation results</h2>\n";
echo "<table border=\"1\">\n";
echo "<tr><td>Married</td><td>Children</td><td>Annual income (F)</td><td>Taxes payable (F)</td></tr>\n";
for($i=0;$i<count($query[simulations]);$i++){
echo "<tr>".
"<td>".$query[simulations][$i][0]."</td>".
"<td>".$query[simulations][$i][1]."</td>".
"<td>".$query[simulations][$i][2]."</td>".
"<td>".$query[simulations][$i][3]."</td>".
"</tr>\n";
}//for
echo "</table>\n";
}//if
?>
</body>
</html>
The display program has minor differences from its previous version:
- the dictionary receives a $query dictionary instead of a $query object. We therefore write $query[children] and not $query->children.
- This dictionary has a `simulations` field, which is a two-dimensional array. `$query[simulations][i]` is simulation number i. This is itself an array of four strings: marital status, number of children, annual salary, and tax payable.
The main program has undergone more significant changes:
<?php
// processes the tax form
// libraries
include "ImpotsDSN.php";
// start session
session_start();
// application configuration
ini_set("register_globals", "off");
ini_set("display_errors", "off");
$taxForm = "impots_form.php";
$taxErrors = "tax_errors.php";
$taxDB = array(dsn => "mysql-dbimpots", user => "admimpots", pwd => "mdpimpots",
table=>taxes,limits=>limits,coeffR=>coeffR,coeffN=>coeffN);
// retrieve session parameters
$session = $_SESSION["session"];
// Is the session valid?
if(!isset($session) || !isset($session[objImpots]) || !isset($session[simulations])){
// start a new session
$session = array(objImpots => new ImpotsDSN($bdImpots), simulations => array());
// any errors?
if(count($session[objImpots]->errors)!=0){
$query = array(errors => $session[objImpots]->errors);
// display error page
include $taxErrors;
// end
$session = array();
endSession($session);
}//if
}//if
// retrieve the parameters of the current transaction
$request[married] = $_POST["optMarried"];
$request[children] = $_POST["txtChildren"];
$request[salary] = $_POST["txtSalary"];
// Do we have all the parameters?
if(!isset($query[married]) || !isset($query[children]) || !isset($query[salary])){
// display empty form
$request = array(chkoui => "", chknon => "checked", children => "", salary => "", taxes => "",
errors=>array(),simulations=>array());
include $taxForm;
// end
endSession($session);
}//if
// parameter validation
$request = validate($request);
// any errors?
if(count($request[errors])!=0){
// display form
include "$taxForm";
// end
endSession($session);
}//if
// calculate tax due
$query[taxes] = $session[taxObject]->calculate(array(married => $query[married],
children=>$request[children],salary=>$request[salary]));
// another simulation
$session[simulations][] = array($request[married], $request[children], $request[salary], $request[taxes]);
$request[simulations] = $session[simulations];
// display form
include "$taxForm";
// end
endSession($session);
// --------------------------------------------------------
function verify($request){
// checks the validity of the request parameters
// initially, no errors
$request[errors] = array();
// Is the status valid?
$request[married] = strtolower($request[married]);
if($request[married] != "yes" && $request[married] != "no"){
// an error
$request[errors][] = "Invalid marital status [$request[married]]";
}
// Is the number of children valid?
if(! preg_match("/^\s*\d+\s*$/",$request[children])){
// an error
$request[errors][] = "Invalid number of children [$request[children]]";
}
// Is the salary valid?
if(! preg_match("/^\s*\d+\s*$/", $query[salary])){
// an error
$request[errors][] = "Invalid salary [$request[salary]]";
}
// radio button status
if($request[married]=="yes"){
$request[chkoui] = "checked";
$request[chkno] = "";
}else{
$request[chkno] = "checked";
$query[chkoui] = "";
}
// return the request
return $request;
}//check
// --------------------------------------------------------
function endSession($session){
// store the session
$_SESSION[session] = $session;
// end script
exit(0);
}//terminateSession
- First of all, this application manages a session. So we start a session at the beginning of the script:
- The session stores a single variable: the $session dictionary. This dictionary has two fields:
- objImpots: an ImpotsDSN object. We store this object in the client’s session to avoid unnecessary repeated accesses to the ODBC database.
- simulations: the array of simulations to recall simulations from previous exchanges between exchanges.
- Once the session has started, the $session variable is retrieved. If it does not yet exist, the session is starting. An ImpotsDSN object and an empty simulations array are then created. If necessary, errors are reported.
<?php
...
// retrieve the session parameters
$session=$_SESSION["session"];
// Is the session valid?
if(!isset($session) || !isset($session[objImpots]) || !isset($session[simulations])){
// start a new session
$session = array(objImpots => new ImpotsDSN($bdImpots), simulations => array());
// any errors?
if(count($session[objImpots]->errors)!=0){
$query = array(errors => $session[objImpots]->errors);
// display error page
include $taxErrors;
// end
$session = array();
endSession($session);
}//if
}//if
- Once the session is properly initialized, the exchange parameters are retrieved. If not all expected parameters are present, an empty form is sent.
<?php
...
// retrieve the parameters for the current exchange
$request[married] = $_POST["optMarie"];
$request[children] = $_POST["txtChildren"];
$request[salary] = $_POST["txtSalary"];
// Do we have all the parameters?
if(!isset($request[married]) || !isset($request[children]) || !isset($request[salary])){
// display empty form
$request = array(chkoui => "", chknon => "checked", children => "", salary => "", taxes => "",
errors=>array(),simulations=>array());
include $taxForm;
// end
endSession($session);
}//if
- If all parameters are present, they are checked. If there are errors, they are reported:
<?php
...
// parameter validation
$request = check($request);
// Any errors?
if(count($request[errors])!=0){
// display form
include "$taxForm";
// end
endSession($session);
}//if
- if the parameters are correct, the tax is calculated:
<?php
...
// calculate the tax due
$request[taxes] = $session[taxObject]->calculate(array(married => $request[married],
children=>$request[children],salary=>$request[salary]));
- The current simulation is added to the simulation array, which is then sent to the client along with the form:
<?php
...
// one more simulation
$session[simulations][]=array($query[married],$query[children],$query[salary],$query[taxes]);
$request[simulations] = $session[simulations];
// display form
include "$taxForm";
// end
endSession($session);
- In any case, the script ends by saving the $session variable to the session. This is done in the terminateSession procedure.
<?php
...
function endSession($session){
// we store the session
$_SESSION[session] = $session;
// end script
exit(0);
}//terminerSession
Let’s finish by presenting the program that displays database access errors (impots-erreurs.php):
<?php // error page ?>
<html>
<head>
<title>Tax application unavailable</title>
</head>
<body background="/poly/impots/images/standard.jpg">
<h3>Tax Calculator</h3>
<hr>
Application unavailable. Please try again later.<br><br>
<font color="red">
The following errors occurred<br>
<ul>
<?php
// display errors
for($i=0;$i<count($query[errors]);$i++){
echo "<li>".$query[errors][$i]."</li>\n";
}
?>
</ul>
</font>
</body>
</html>
Here too, the $request object has been replaced by a $request dictionary.
4.6. Taxes Application: Version 4
We will now create a standalone application that will act as a web client for the previous Taxes web application. The application will be a console application launched from a DOS window:
dos>e:\php43\php.exe cltImpots.php
Syntax cltImpots.php urlImpots married children salary [token]
The parameters for the cltImpots web client are as follows:
// syntax $0 urlImpots married children salary token
// client of a tax service running on urlImpots
// sends the following three pieces of information to this service: married, children, salary
// optionally with the session token if it has been passed
// displays the table of simulations for the session
Here are a few usage examples. First, an example without a session token.
dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php yes 2 200000
Session token=[a6297317667bc981c462120987b8dd18]
Simulations:
[Married,Children,Annual salary (F),Taxes due (F)]
[yes,2,200000,22504]
We successfully retrieved the amount of tax due (22,504 F). We also retrieved the session token. We can now use it for a second query:
dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php yes 3 200000 a6297317667bc981c462120987b8dd18
Session token=[a6297317667bc981c462120987b8dd18]
Simulations:
[Married,Children,Annual salary (F),Taxes due (F)]
[yes,2,200000,22504]
[yes,3,200000,16400]
We successfully receive the table of simulations sent by the web server. The retrieved token remains the same, of course. If we query the web service while the database has not been started, we get the following result:
dos>e:\php43\php.exe cltImpots2.php http://localhost/poly/impots/6/impots.php yes 3 200000
Session token=[8369014d5053212bc42f64bbdfb152ee]
The following errors occurred:
Unable to open DSN database [mysql-dbimpots] (S1000)
When writing a web client application, it is necessary to know exactly what the server sends in response to the various possible requests from a client. The server sends a set of HTML lines containing useful information and other elements that are only there for HTML formatting. PHP regular expressions can help us find the useful information within the stream of lines sent by the server. To do this, we need to know the exact format of the server’s various responses. To do so, we can query the web service using a browser and examine the source code that was sent. Note that this method does not allow us to see the HTTP headers of the web server’s response. It is sometimes useful to know these. We can then use one of the two generic web clients discussed in the previous chapter.
If we repeat the previous examples, here is the HTML code received by the browser for the simulation table:
<h2>Simulation Results</h2>
<table border="1">
<tr><td>Married</td><td>Children</td><td>Annual salary (F)</td><td>Taxes payable (F)</td></tr>
<tr><td>yes</td><td>2</td><td>200000</td><td>22504</td></tr>
<tr><td>yes</td><td>3</td><td>200000</td><td>16400</td></tr>
</table>
This is an HTML table, the only one in the document sent. Therefore, the regular expression "|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|" should allow us to find the various simulations received within the document. When the database is unavailable, the following message is displayed:
Application unavailable. Please try again later.<br><br>
<font color="red">
The following errors occurred<br>
<ul>
<li>Unable to open the DSN database [mysql-dbimpots] (S1000)</li>
</ul>
</font>
To retrieve the error, the web client must search the web server's response for lines matching the pattern: "|<li>(.+?)</li>|".
We now have the guidelines for what to do when the user requests a tax calculation from the previous console client:
- verify that all parameters are valid and report any errors.
- connect to the URL provided as the first parameter. To do this, we will follow the pattern of the generic web client already presented and discussed
- in the server response stream, use regular expressions to either:
- find the error messages
- find the simulation results
The client code cltImpots.php is as follows:
<?php
// syntax $0 urlImpots married children salary token
// client of a tax service running on urlImpots
// sends the following three pieces of information to this service: married, children, salary
// optionally with the session token if it has been passed
// displays the table of simulations for the session
// check number of arguments
if(count($argv) != 5 && count($argv) != 6){
// error
fwrite(STDERR, "Syntax: $argv[0] urlTaxes married children salary [token]\n");
// end
exit(1);
}//if
// note the URL
$urlImpots = parseURL($argv[1]);
if(isset($taxURL[error])){
// error
fwrite(STDERR,"$urlImpots[error]\n");
// end
exit(1);
}//if
// Parse the user's request
$request = requestAnalysis("$argv[2], $argv[3], $argv[4]");
// Errors?
if($request[error]){
// message
echo "$request[error]\n";
// done
exit(1);
}//if
// we can make the request - we use the session token
$taxes = getTaxes($taxUrl, $request, $argv[5]);
// display the session token
echo "Session token=[$impots[token]]\n";
// any errors?
if(count($impots[errors])!=0){
//error messages
echo "The following errors occurred:\n";
for($i=0;$i<count($impots[errors]);$i++){
echo $taxes[errors][$i]."\n";
}//for
}else{
// no errors - display simulations
echo "Simulations:\n";
for($i=0;$i<count($taxes[simulations]);$i++){
echo "[".implode(",",$impots[simulations][$i])."]\n";
}//for
}//if
// end of program
exit(0);
// --------------------------------------------------------------
function analyzeURL($URL){
// Check the validity of the URL $URL
$url = parse_url($URL);
// the protocol
if(strtolower($url[scheme])!="http"){
$url[error] = "The URL [$URL] is not in the format http://machine[:port][/path]";
return $url;
}//if
// the machine
if(!isset($url[host])){
$url[error] = "The URL [$URL] is not in the format http://machine[:port][/path]";
return $url;
}//if
// the port
if(!isset($url[port])) $url[port]=80;
// the query
if(isset($url["query"])){
$url[error]="The URL [$URL] is not in the format http://machine[:port][/path]";
return $url;
}//if
// return
return $url;
}//analyzeURL
// -----------------------------------------------------------
function analyzeRequest($request){
// $request: string to parse
// must be in the form of married, children, salary
// valid format
if(! preg_match("/^\s*(yes|no)\s*,\s*(\d{1,3})\s*,\s*(\d+)\s*$/i",$request,$fields)){
// Invalid format
return(array(error=>"Invalid format (married, children, salary)."));
}
// OK
$married = strtolower($fields[1]);
return array(married=>$married, children=>$fields[2], salary=>$fields[3]);
}//analyzeRequest
// --------------------------------------------------------------
function getTaxes($taxUrl, $request, $token) {
// $taxURL: URL to query
// $request: dictionary containing the married, children, and salary fields
// $token: a possible session token
// Open a connection on port $urlImpots[port] of $urlImpots[host]
$connection = fsockopen($urlImpots[host], $urlImpots[port], &$errno, &$error);
// return if error
if(! $connection){
return array(errors=>array("Failed to connect to the site ($urlImpots[host],$urlImpots[port]): $error"));
}//if
// send HTTP headers to the server
POST($connection, $taxURL, $token,
array(optSpouse=>$request[spouse],txtChildren=>$request[children],txtSalary=>$request[salary]));
// read the server's response - first the HTTP headers
// the first line
$line = fgets($connection, 10000);
// URL found?
if(! preg_match("/^(.+?) 200 OK\s*$/",$line)){
// URL not found - return with error
return array(errors=>array("The URL $urlImpots[path] could not be found"));
}//if
// read the other HTTP headers
while(($line = fgets($connection, 10000)) && (($line = rtrim($line)) != "")){
// search for the token if it hasn't been found yet
if(! $token){
// search for the set-cookie line
if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$line,$fields)){
// found the token - store it
$token = $fields[1];
}//if
}//if
}//next line
// read the next document
$document="";
while($line = fread($connection, 10000)) {
$document.=$line;
}//while
// close the connection
fclose($connection);
// analyze the document
$taxes = getInfo($document);
// return the result
return array(token=>$token,errors=>$taxes[errors],simulations=>$taxes[simulations]);
}//getTaxes
// --------------------------------------------------------------
function POST($connection, $url, $token, $parameters) {
// $connection: the connection to the web server
// $url: the URL to query
// $parameters: dictionary of parameters to post
// prepare the POST
$post="";
while(list($parameter, $value) = each($parameters)) {
$post.=$parameter."=".urlencode($value)."&";
}//while
// Remove the last character
$post = substr($post, 0, -1);
// prepare the HTTP request in HTTP/1.0 format
$HTTP="POST $url[path] HTTP/1.0\n";
$HTTP.="Content-type: application/x-www-form-urlencoded\n";
$HTTP.="Content-length: ".strlen($post)."\n";
$HTTP.="Connection: close\n";
if($token) $HTTP.="Cookie: PHPSESSID=$token\n";
$HTTP.="\n";
$HTTP.=$post;
// send the HTTP request
fwrite($connection, $HTTP);
}//POST
// --------------------------------------------------------------
function getInfo($document){
// $document: HTML document
// we look for either the list of errors
// or the simulation table
// preparing the result
$taxes[errors] = array();
$taxes[simulations] = array();
// Is there an error?
// error line template
$errorPattern = "|<li>(.+?)</li>|";
// search the document
if(preg_match_all($errorPattern, $document, $fields, PREG_SET_ORDER)){
// retrieve errors
for($i=0;$i<count($fields);$i++){
$taxes[errors][] = $fields[$i][1];
}//for
// done
return $taxes;
}//if
// example of a row in the simulation table
// <tr><td>yes</td><td>2</td><td>200000</td><td>22504</td></tr>
$modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";
// search in the document
if(preg_match_all($modSimulation,$document,$fields,PREG_SET_ORDER)){
// retrieve simulations
for($i=0;$i<count($fields);$i++){
$taxes[simulations][]=array($fields[$i][1],$fields[$i][2],$fields[$i][3],$fields[$i][4]);
}//for
// return result
return $taxes;
}//if
// shouldn't normally get here
return $taxes;
}//getInfo
?>
Comments:
- The program begins by checking the validity of the parameters it has received. To do this, it uses two functions: `analyseURL`, which checks the validity of the URL, and `analyseDemande`, which checks the other parameters.
- The tax calculation is performed by the getImpots function:
- The getImpots function is declared as follows:
<?php
...
// --------------------------------------------------------------
function getTaxes($taxUrl, $request, $token) {
// $urlImpots: URL to query
// $request: dictionary containing the fields spouse, children, salary
// $token: a possible session token
- (continued)
- $urlImpots is a dictionary containing the following fields:
- host: machine where the web service resides
- port: its service port
- path: the path to the requested resource
- $request is a dictionary containing the following fields:
- married: yes/no: marital status
- children: number of children
- salary: annual salary
- $token is the session token.
- $urlImpots is a dictionary containing the following fields:
The function returns a dictionary with the following fields:
- errors: array of error messages
- simulations: array of simulations, where each simulation is itself an array of four elements (spouse, children, salary, tax)
- token: the session token
- Once the result of getImpots is obtained, the program displays the results and terminates:
<?php
...
// display the session token
echo "Session token=[$impots[token]]\n";
// Any errors?
if(count($impots[errors])!=0){
//error messages
echo "The following errors occurred:\n";
for($i=0;$i<count($impots[errors]);$i++){
echo $taxes[errors][$i]."\n";
}//for
}else{
// no errors - display simulations
echo "Simulations:\n";
for($i=0;$i<count($taxes[simulations]);$i++){
echo "[".implode(",",$impots[simulations][$i])."]\n";
}//for
}//if
// end of program
exit(0);
- Let's now analyze the function getImpots($urlImpots,$request,$token), which must
- create a TCP/IP connection on port $urlImpots[port] of the machine $urlImpots[host]
- send the HTTP headers the web server expects, including the session token if there is one
- send a POST request to the web server with the parameters contained in the $request dictionary
- parse the web server's response to find either a list of errors or an array of simulations.
- The HTTP headers are sent using a POST function:
<?php
...
// send the HTTP headers to the server
POST($connection, $taxURL, $token,
array(optMarried=>$request[married],txtChildren=>$request[children],txtSalary=>$request[salary]));
- Once the HTTP headers have been sent, the getImpots function reads the entire server response and stores it in $document. The response is read without being parsed, except for the first line, which tells us whether or not the web server found the requested URL. In fact, if the URL was found, the web server responds with HTTP/1.X 200 OK, where X depends on the version of HTTP used.
<?php
...
// URL found?
if(! preg_match("/^(.+?) 200 OK\s*$/",$line)){
<?php
...
// URL not found - return with error
return array(errors=>array("The URL $urlImpots[path] could not be found"));
}//if
- The document is parsed using the getInfos function:
- The result returned is a dictionary with two fields:
- errors: list of errors - may be empty
- simulations: list of simulations - may be empty
- Once this is done, the getImpots function can return its result as a dictionary.
<?php
...
// return the result
return array(token=>$token,errors=>$impots[errors],simulations=>$impots[simulations]);
- Now let's analyze the POST function:
<?php
...
// --------------------------------------------------------------
function POST($connection, $url, $token, $parameters) {
// $connection: the connection to the web server
// $url: the URL to query
// $parameters: dictionary of parameters to post
// prepare the POST
$post="";
while(list($parameter, $value) = each($parameters)) {
$post.=$parameter."=".urlencode($value)."&";
}//while
// Remove the last character
$post = substr($post, 0, -1);
// prepare the HTTP request in HTTP/1.0 format
$HTTP="POST $url[path] HTTP/1.0\n";
$HTTP.="Content-type: application/x-www-form-urlencoded\n";
$HTTP.="Content-length: ".strlen($post)."\n";
$HTTP.="Connection: close\n";
if($token) $HTTP.="Cookie: PHPSESSID=$token\n";
$HTTP.="\n";
$HTTP.=$post;
// send the HTTP request
fwrite($connection, $HTTP);
}//POST
While we’ve previously had the opportunity to request a web resource using a GET request, we haven’t yet had the chance to do so with a POST request. POST differs from GET in how it sends parameters to the server. It must send the following HTTP headers:
with
- path: the path of the requested web resource; here, $url[path]
- HTTP/1.X: the desired HTTP protocol. Here, we have chosen HTTP/1.0 to receive the response in a single chunk. HTTP/1.1 allows the response to be sent in multiple chunks (chunked).
- N denotes the number of characters the client is about to send to the server
The N characters constituting the request parameters are sent immediately after the blank line that terminates the HTTP headers sent to the server. These parameters are in the form param1=val1¶m2=val2&... where param1 is the parameter name and val1 is its value. The values vali may contain "problematic" characters such as spaces, the & character, the = character, etc. These characters must be replaced by a %XX string, where XX is their hexadecimal code. The PHP urlencode function performs this task. The reverse operation is performed by urldecode.
Finally, note that if there is a token, it is sent with an HTTP Cookie header: PHPSESSID=token.
- We still need to examine the function that parses the server's response:
<?php
...
// --------------------------------------------------------------
function getInfos($document){
// $document: HTML document
// we look for either the list of errors
// or the simulation table
// preparing the result
$taxes[errors] = array();
$taxes[simulations] = array();
- Note that errors are sent by the server in the form <li>error message</li>. The regular expression "|<li>(.+?)</li>|" should retrieve this information. Here, we used the | character to delimit the regular expression rather than the / character, since the latter also appears within the regular expression itself. The preg_match_all function ($pattern, $document, $fields, PREG_SET_ORDER) retrieves all occurrences of $pattern found in $document. These are placed in the $fields array. Thus, $fields[i] represents the i-th occurrence of $pattern in $document. $champs[$i][0] contains the string matching the pattern. If the pattern contained parentheses, the string corresponding to the first parenthesis is placed in $champs[$i][1], the second in $champs[$i][2], and so on. The code to retrieve the errors is therefore as follows:
<?php
...
// Is there an error?
// error message template
$errorPattern="|<li>(.+?)</li>|";
// search the document
if(preg_match_all($errorPattern, $document, $fields, PREG_SET_ORDER)){
// retrieve errors
for($i=0;$i<count($fields);$i++){
$taxes[errors][] = $fields[$i][1];
}//for
// done
return $taxes;
}//if
4.7. Tax Application: Conclusion
We have shown different versions of our client-server tax calculation application:
- Version 1: The service is provided by a set of PHP programs; the client is a browser. It performs a single simulation and does not retain any history of previous ones.
- Version 2: We add some browser-side capabilities by embedding JavaScript scripts in the HTML document loaded by the browser. It validates the form parameters.
- Version 3: We allow the service to remember the different simulations performed by a client by managing a session. The HTML interface is modified accordingly to display these.
- Version 4: The client is now a standalone console application. This allows us to revisit the development of programmed web clients.
At this point, a few observations can be made:
- Versions 1 through 3 support browsers with no capabilities other than the ability to execute JavaScript scripts. Note that a user always has the option to disable the execution of these scripts. The application will then only work partially in version 1 (the Clear option will not work) and not at all in versions 2 and 3 (the Clear and Calculate options will not work). It might be worth considering a version of the service that does not use JavaScript scripts.
When writing a web service, you must consider which types of clients you are targeting. If you want to reach the widest possible audience, you should write an application that sends only HTML to browsers (no JavaScript or applets). If you are working within an intranet and have control over the configuration of the workstations, you can then afford to be more demanding regarding client-side requirements.
Version 4 is a web client that retrieves the information it needs from the HTML stream sent by the server. Very often, we have no control over this stream. This is the case when we have written a client for an existing web service on the network that is managed by someone else. Let’s take an example. Suppose our tax calculation simulation service was written by Company X. Currently, the service sends the simulations in an HTML table, and our client uses this fact to retrieve them. It thus compares each line of the server’s response to the regular expression:
// example of a row from the simulation table
// <tr><td>yes</td><td>2</td><td>200000</td><td>22504</td></tr>
$modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";
Now suppose the application designer changes the visual appearance of the response by placing the simulations not in an array but in a list in the form:
In this case, our web client will need to be rewritten. This is the constant threat facing web clients of applications that we do not control ourselves. XML can provide a solution to this problem:
- instead of generating HTML, the simulation service will generate XML. In our example, this could be
<simulations>
<headers married="married" children="children" salary="salary" tax="tax"/>
<simulation married="yes" children="2" salary="200000" tax="22504" />
<simulation spouse="no" children="2" salary="200000" tax="33388" />
</simulations>
- A stylesheet could be associated with this response, instructing browsers on the visual presentation of this XML response
- Programmed web clients would ignore this style sheet and retrieve the information directly from the XML stream of the response
If the service designer wishes to modify the visual presentation of the results provided, they will modify the style sheet rather than the XML. Thanks to the style sheet, browsers will display the new visual format, and programmed web clients will not need to be modified.
