11. Tax Calculation: Exercise with a Web Service and a Three-Tier Architecture
We will revisit the IMPOTS exercise (see sections 4.2, 4.3, 6) and turn it into a client/server application. The server script will be broken down into three components:
- a layer called [DAO] (Data Access Objects) that will handle interactions with the MySQL database
- a layer called [business] that will calculate the tax
- a [web] layer that will handle communication with web clients.
![]() |
The client script [1]:
- sends the three pieces of information ($married, $children, $salary) needed to calculate the tax to the server script
- displays the server's response on the console
The server script [2] consists of the server’s [web] layer.
- At the start of a new client session, it will populate arrays with data from the MySQL database [dbimpots]. To do this, it will call upon the [DAO] layer. The arrays thus constructed will be placed in the client session so they can be used in subsequent client requests.
- When a client makes a request, it will pass the three pieces of information ($married, $children, $salary) to the [business logic] layer, which will calculate the tax $tax.
- The server script will return the calculated tax $tax.
11.1. The client script (clients_impots_05_web)
The client script will be a client of the tax calculation web service. It will send (POST) parameters to the server in the following format:
params=$married,$children,$salary where
- $married will be the string "yes" or "no",
- $children will be the number of children,
- $salary is the taxpayer's salary
It retrieves the three parameters above from a text file [data.txt] in the format (married, children, salary):
The client script
- will read the text file [data.txt] line by line
- will send the string params=$married,$children,$salary to the tax calculation web service
- retrieve the response from the service. This response can take two forms:
- will save the server's response to a text file [results.txt] in one of the following two formats:
The client-side script code is as follows:
<?php
// tax client
// error handling
ini_set("display_errors", "off");
// ---------------------------------------------------------------------------------
// a class of utility functions
class Utilities {
function cutNewLinechar($line) {
...
}
}
// main -----------------------------------------------------
// definition of constants
$DATA = "data.txt";
$RESULTS = "results.txt";
// server data
$HOST = "localhost";
$PORT = 80;
$serverUrl = "/web-examples/taxes_05_web.php";
// Taxable individuals' parameters (marital status, number of children, annual income)
// have been placed in the text file $DATA, with one line per taxpayer
// the results (marital status, number of children, annual income, tax due)
// or (marital status, number of children, annual income, error message) are placed in
// the text file $RESULTS, with one result per line
// Utilities class
$u = new Utilities();
// open the taxpayer data file
$data = fopen($DATA, "r");
if (!$data) {
print "Unable to open the data file [$DATA] for reading\n";
exit;
}
// Open the results file
$results = fopen($RESULTS, "w");
if (!$results) {
print "Unable to create the results file [$RESULTS]\n";
exit;
}
// process the current line of the taxpayer data file
while ($line = fgets($data, 100)) {
// remove any end-of-line characters
$line = $u->cutNewLineChar($line);
// retrieve the 3 fields married:children:salary that make up $line
list($married, $children, $salary) = explode(",", $line);
// calculate the tax
list($error, $tax) = calculateTax($HOST, $PORT, $serverUrl, $cookie, array($married, $children, $salary));
// display the result
$result = $error ? "$married:$children:$salary:$error" : "$married:$children:$salary:$tax";
fputs($results, "$result\n");
// next data
}
// close the files
fclose($data);
fclose($results);
// end
print "Done...\n";
exit;
function calculateTax($HOST, $PORT, $serverUrl, &$cookie, $params) {
// connects the client to ($HOST, $PORT, $serverURL)
// sends the $cookie if it is not empty. $cookie is passed by reference
// sends $params to the server
// processes the single line returned by the server
// Open a connection on port 80 of $HOST
$connection = fsockopen($HOST, $PORT);
// error?
if (!$connection)
return array("Error connecting to the server ($HOST, $PORT)");
// HTTP headers must end with an empty line
// POST
fputs($connection, "POST $serverURL HTTP/1.1\n");
// Host
fputs($connection, "Host: localhost\n");
// Connection
fputs($connection, "Connection: close\n");
// send the cookie if it is not empty
if ($cookie) {
fputs($connection, "Cookie: $cookie\n");
}//if
// now send the client instruction after encoding it
$infos = "params=" . urlencode(implode(",", $params));
// Specify the type of data to be sent
fputs($connection, "Content-type: application/x-www-form-urlencoded\n");
// send the size (number of characters) of the data to be sent
fputs($connection, "Content-length: " . strlen($info) . "\n");
// send an empty line
fputs($connection, "\n");
// send the data
fputs($connection, $data);
// display the web server's response
// and make sure to retrieve any cookies
while ($line = fgets($connection, 1000)) {
// cookie - only on the first response
if (!$cookie) {
if (preg_match("/^Set-Cookie: (.*?)\s*$/", $line, $fields)) {
$cookie = $fields[1];
}//if
}
// as soon as there is an empty line, the HTTP response is complete
if (trim($line) == "") {
break;
}
}//while
// read a line of the result
$line = fgets($connection, 1000);
// Close the connection
fclose($connection);
// calculate result
$error="";
$tax = "";
if (preg_match("/^<error>(.*?)<\/error>\s*$/", $line, $fields)) {
$error = $fields[1];
} else {
if (preg_match("/^<tax>(.*?)<\/tax>\s*$/", $line, $fields)) {
$tax = $fields[1];
} else {
$error = "unusable server result";
}
}
// return
return array($error, $tax);
}
Comments
The client-side script code includes elements we've seen before:
- lines 9–15: the [Utilities] class was introduced in Version 3, Section 6
- lines 17–68: the main program is similar to that in Version 1, Section 4.2. It differs only in the tax calculation, line 56.
- line 56: the tax calculation function accepts the following parameters:
- $HOST, $PORT, $serverURL: used to connect to the web service
- $cookie: is the session cookie. This parameter is passed by reference. Its value is set by the tax calculation function. On the first call, it has no value. After that, it has one.
- array($married, $children, $salary): represents a row from the [data.txt] file
The tax calculation function returns an array of two results ($error, $tax) where $error is a possible error message and $tax is the tax amount.
- Lines 70–134: This is a classic HTTP client, the kind we’ve encountered many times. Note the following points:
- Line 83: The parameters ($married, $children, $salary) are sent to the server via a POST request
- Lines 89–91: If the client has a session ID, it sends it to the server
- line 93: creation of the params parameter
- line 101: the `params` parameter is sent
- lines 104–115: the client reads all HTTP headers sent by the server until it encounters the empty line marking the end of the headers. It uses this opportunity to retrieve the session ID from the response to its first request.
- lines 123–125: process any line in the form <error>message</error>
- lines 126–128: we do the same with any line of the form <tax>amount</tax>
- line 133: the result is returned
11.2. The tax calculation web service
Here we are interested in the three scripts that make up the server:
![]() |
The corresponding NetBeans project is as follows:
![]() |
In [1], the server consists of the following PHP scripts:
- [impots_05_entites] contains the classes used by the server
- [impots_05_dao] contains the classes and interfaces of the [dao] layer
- [impots_05_metier] contains the classes and interfaces of the [business] layer
- [impots_05_web] contains the classes and interfaces of the [dao] layer
We begin by presenting two classes used by the different layers of the web service.
11.2.1. The web service entities (impots_05_entities)
The MySQL database [dbimpots] has a table [impots] that contains the data needed to calculate the tax [1]:
![]() |
We will store the data from the MySQL table [impots] in an array of Tranche objects, where Tranche is the following class:
<?php
// a tax bracket
class Tranche {
// private fields
private $limit;
private $coeffR;
private $coeffN;
// getters and setters
public function getLimit() {
return $this->limit;
}
public function setLimit($limit) {
$this->limit = $limit;
}
public function getRate() {
return $this->coeffR;
}
public function setCoeffR($coeffR) {
$this->coeffR = $coeffR;
}
public function getCoeffN() {
return $this->coeffN;
}
public function setCoeffN($coeffN) {
$this->coeffN = $coeffN;
}
// constructor
public function __construct($limit, $coeffR, $coeffN) {
$this->setLimit($limit);
$this->setCoeffR($coeffR);
$this->setCoeffN($coeffN);
}
// toString
public function __toString(){
return "[$this->limit,$this->coeffR,$this->coeffN]";
}
}
The private fields [$limite, $coeffR, $coeffN] will be used to store the columns [limites, coeffR, coeffN] of a row in the MySQL table [impots].
Additionally, the server code will use a custom exception, the ImpotsException class:
- Line 1: The [ImpotsException] class extends the [Exception] class predefined in PHP 5
- line 3: the constructor of the [ImpotsException] class accepts two parameters:
- $message: an error message
- $code: an error code
11.2.2. The [dao] layer (impots_05_dao)
The [dao] layer provides access to database data:
![]() |
The [dao] layer has the following interface:
The IImpotsDao interface exposes only the getData function. This function places the various rows of the MySQL table [dbimpots.impots] into an array of Tranche objects.
The implementation class is as follows:
<?php
// DAO layer
// dependencies
require_once "impots_05_entities.php";
// constants
define("TABLE", "impots");
// -----------------------------------------------------------------
// abstract implementation
abstract class ImpotsDaoWithPdo implements IImpotsDao {
// private fields
private $dsn;
private $user;
private $passwd;
private $brackets;
// getters and setters
public function getDsn() {
return $this->dsn;
}
public function setDsn($dsn) {
$this->dsn = $dsn;
}
public function getUser() {
return $this->user;
}
public function setUser($user) {
$this->user = $user;
}
public function getPasswd() {
return $this->passwd;
}
public function setPasswd($passwd) {
$this->passwd = $passwd;
}
// constructor
public function __construct($dsn, $user, $passwd) {
// save the parameters
$this->setDsn($dsn);
$this->setUser($user);
$this->setPasswd($passwd);
// retrieve data from the DBMS
// Connect ($user, $pwd) to the database $dsn
try {
// connection
$connection = new PDO($dsn, $user, $passwd, array(PDO::ATTR_PERSISTENT => true));
// read from the $TABLE table
$query = "select limits,coeffR,coeffN from " . TABLE;
// execute the $query on the $connection
$statement = $connection->prepare($query);
$statement->execute();
// process the query result
while ($columns = $statement->fetch()) {
$this->slices[] = new Slice($columns[0], $columns[1], $columns[2]);
}
// disconnect
$connection = NULL;
} catch (PDOException $e) {
// return with error
throw new ImpotsException($e->getMessage(), 1);
}
}
public function getData(){
return $this->brackets;
}
}
- line 5: the implementation of the [IImpotsDao] interface requires the classes defined in the [impots_05_entities] script.
- line 11: definition of an abstract class. An abstract class is a class that cannot be instantiated. An abstract class must be derived from in order to be instantiated. A class can be declared abstract because it cannot be instantiated (some of its methods are not defined) or because we do not want to instantiate it. Here, we do not want to instantiate the [ImpotsDaoWithPdo] class. We will instantiate derived classes.
- Line 11: The [ImpotsDaoWithPdo] class implements the [IImpotsDao] interface. It must therefore define the getData method. This method is found on lines 72–74.
- Line 14: $dsn (Data Source Name) is a string that uniquely identifies the DBMS and the database being used.
- Line 15: $user identifies the user connecting to the database
- line 16: $passwd is the password for the previous user
- line 17: $tranches is the array of Tranche objects in which the MySQL table [dbimpots.impots] will be stored.
- Lines 45–70: The class constructor. This code was already encountered in Version 4, Section 8.2. Note that the construction of the [ImpotsDaoWithPdo] object may fail. An [ImpotsException] is then thrown.
- Lines 72–74: The [getData] method of the [IImpotsDao] interface.
The [ImpotsDaoWithPdo] class is suitable for any DBMS. The class constructor, line 45, requires knowledge of the database’s Data Source Name. This string depends on the DBMS used. We have chosen not to require the user of the class to know this Data Source Name. For each DBMS, there will be a specific class derived from [ImpotsDaoWithPdo]. For the MySQL DBMS, this will be the following class:
class TaxDaoWithMySQL extends TaxDaoWithPDO {
public function __construct($host, $port, $db, $user, $passwd) {
parent::__construct("mysql:host=$host;dbname=$base;port=$port", $user, $passwd);
}
}
- In line 3, the constructor does not request the Data Source Name, but simply the name of the DBMS host machine ($host), its listening port ($port), and the database name ($base).
- Line 4: The Data Source Name for the MySQL database is constructed and used to call the parent class’s constructor.
Note that to adapt to another DBMS, simply write the appropriate class derived from [ImpotsDaoWithPdo]. In each case, the Data Source Name specific to the DBMS being used must be constructed.
11.2.3. The [business] layer (impots_05_metier)
The [business] layer contains the tax calculation logic:
![]() |
The [business] layer has the following interface:
<?php
// business interface
interface IImpotsMetier {
public function calculateTax($married, $children, $salary);
}
The [IImpotsMetier] interface exposes only one method, the [calculateTax] method, which calculates a taxpayer's tax based on the following parameters:
- $married: a string of "yes" or "no" depending on whether the taxpayer is married or not
- $children: the number of children the taxpayer has
- $salary: the taxpayer's salary
The [web] layer will provide these parameters.
The implementation of the [IImpotsMetier] interface is as follows:
// dependencies
require_once "impots_05_dao.php";
// ------------------------------------------------------------------
// implementation class
class BusinessTaxes implements IImpotsMetier {
// DAO layer
private $dao;
// array of [Tranche] objects
private $data;
// getter and setter
public function getDao() {
return $this->dao;
}
public function setDao($dao) {
$this->dao = $dao;
}
public function setData($data){
$this->data = $data;
}
public function __construct($dao) {
// retrieve the data needed to calculate the tax
$this->setDao($dao);
$this->setData($this->dao->getData());
}
public function calculateTax($married, $children, $salary) {
// $marié: yes, no
// $children: number of children
// $salary: annual salary
// number of shares
$married = strtolower($married);
if ($married == "yes")
$nbShares = $children / 2 + 2;
else
$nbParts = $children / 2 + 1;
// add 1/2 portion if there are at least 3 children
if ($children >= 3)
$nbParts+=0.5;
// taxable income
$taxableIncome = 0.72 * $salary;
// family quotient
$quotient = $taxableIncome / $numberOfChildren;
// is placed at the end of the limits array to stop the following loop
$N = count($this->data);
$this->data[$N - 1]->setLimit($quotient);
// tax calculation
$i = 0;
while ($i < $N and $quotient > $this->data[$i]->getLimit()) {
$i++;
}
// Since we placed $quotient at the end of the $limites array, the previous loop
// cannot overflow the $limites array
// now we can calculate the tax
return floor($taxableIncome * $this->data[$i]->getCoeffR() - $numberOfShares * $this->data[$i]->getCoeffN());
}
}
- Line 2: The [business] layer requires classes from the [DAO] layer and entities (Tranche, ImpotsException).
- line 6: the [ImpotsMetier] class implements the [IimpotsMetier] interface.
- lines 9–11: the class’s private fields:
- $dao: reference to the [dao] layer
- $data: array of objects of type [Tranche] provided by the [dao] layer
- Lines 26–30: The class constructor initializes the two previous fields. It receives a reference to the [dao] layer as a parameter.
- lines 32–61: implementation of the [calculerImpot] method of the [IimpotsMetier] interface. This method was introduced in version 1 (section 4.2).
11.2.4. The [web] layer (impots_05_web)
The [business] layer contains the tax calculation logic:
![]() |
The [web] layer consists of the web service that responds to web clients. Recall that these clients make a request to the web service by posting the following parameter: params=married,children,salary. This is a web service similar to those we built in the previous sections. Its code is as follows:
<?php
// business logic layer
require_once "impots_05_business.php";
// error handling
ini_set("display_errors", "off");
// UTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// ------------------------------------------------------------------------------
// the tax web service
// definition of constants
$HOST = "localhost";
$PORT = 3306;
$DATABASE = "dbimpots";
$USER = "root";
$PWD = "";
// The data needed to calculate the tax has been placed in the MySQL table IMPOTS
// belonging to the $BASE database. The table has the following structure
// limits decimal(10,2), coeffR decimal(6,2), coeffN decimal(10,2)
// the parameters for taxable individuals (marital status, number of children, annual salary)
// are sent by the client in the form params=marital status, number of children, annual salary
// the results (marital status, number of children, annual salary, tax due) are returned to the client
// in the form <tax>tax</tax>
// or in the form <error>error</error>, if the parameters are invalid
// retrieve the [business] layer from the session
session_start();
if (!isset($_SESSION['metier'])) {
// Instantiate the [DAO] layer and the [business] layer
try {
$_SESSION['metier'] = new ImpotsMetier(new ImpotsDaoWithMySQL($HOST, $PORT, $BASE, $USER, $PWD));
} catch (ImpotsException $ie) {
print "<error>Error: " . utf8_encode($ie->getMessage() . "</error>");
exit;
}
}
$job = $_SESSION['job'];
// retrieve the line sent by the client
$params = utf8_encode(htmlspecialchars(strtolower(trim($_POST['params']))));
$items = explode(",", $params);
// there must be exactly 3 parameters
if (count($items) != 3) {
print "<error>[$params]: invalid number of parameters</error>\n";
exit;
}//if
// the first parameter (marital status) must be yes/no
$married = trim($items[0]);
if ($married != "yes" and $married != "no") {
print "<error>[$params]: Invalid first parameter</error>\n";
exit;
}//if
// the second parameter (number of children) must be an integer
if (!preg_match("/^\s*(\d+)\s*$/", $items[1], $fields)) {
print "<error>[$params]: Invalid second parameter</error>\n";
exit;
}//if
$children = $fields[1];
// the third parameter (salary) must be an integer
if (!preg_match("/^\s*(\d+)\s*$/", $items[2], $fields)) {
print "<error>[$params]: Invalid third parameter</error>\n";
exit;
}//if
$salary = $fields[1];
// calculate the tax
$tax = $occupation->calculateTax($married, $children, $salary);
// return the result
print "<tax>$tax</tax>\n";
// end
exit;
- Line 4: The [web] layer needs the classes from the [business] layer
- lines 30–40: the reference to the [business] layer is scoped to the session. If we recall that this [business] layer has a reference to the [DAO] layer and that the latter stores the DBMS data, we can see that:
- that the client’s first request will trigger an access to the DBMS
- that subsequent requests from the same client will use the data stored by the [DAO] layer. Therefore, there is no access to the DBMS.
- Line 34: Construction of a [business] layer working with a [DAO] layer implemented for the MySQL DBMS
- Lines 35–37: Handling of a potential error in the previous operation. In this case, a <error>message</error> line is sent to the client.
- Line 43: Retrieve the 'params' parameter that was sent by the client.
- Lines 46–49: Check the number of items found in 'params'
- Lines 51–55: Check the validity of the first piece of information
- Lines 56–60: Same for the second piece of information
- lines 62-66: same for the third
- Line 69: The [business] layer calculates the tax.
- line 71: send the result to the client
Results
Recall that the client [client_impots_web_05] uses the following file [data.txt]:
Based on these lines (married, children, salary), the client queries the tax calculation server and writes the results to the text file [results.txt]. After the client runs, the contents of this file are as follows:
yes:2:200000:22504
no:2:200000:33388
yes:3:200000:16400
no:3:200000:22504
yes:5:50000:0
no:0:3000000:1354938
where each line is in the form (married, children, salary, calculated tax).






