|
 
Building a time-tracking and billing application with Adobe AIR and PHP
Richard Bates
As a developer, you already know that many times, one
programming language, platform, or technology just isn't
enough to do the job right. Recently, there's been a lot of
buzz and information about using PHP on the server side with
Adobe Flex
or AIR
applications as the front end. You can use this technique to
gain a lot of power and flexibility, and that's one of the
topics I discuss in depth in this article. Sometimes,
however, one interface isn't enough.
In my scenario, this is precisely the case. The scenario
calls for a lightweight, cross-platform desktop application
that does its job and stays out of the users' way. It also
needs a powerful management interface that can be accessed
from anywhere. To address both needs, you first create a
desktop application for AIR that leverages PHP back-end
services for persistent storage and extra horsepower. Then,
you create a simple and quick PHP/Hypertext Markup Language
(HTML) browser-based interface for management and output
applications.
About clocked!
The time-tracking and billing system you create in this
article is called Clocked! It is specifically designed for
freelancers, contractors, and others who work on computers
and need to track their time. To best address the needs of
these users, Clocked! has the following requirements:
- Runs on Mac, Windows, and Linux. Because your users are
running all of these platforms in more distributed
proportions than regular computer users, you need to support
all three.
- Easily track time in different increments. This feature
is key; your users need a simple, intuitive, unobtrusive way
to track their time that requires as little effort as
possible. This whole project is predicated on the belief
that their time is valuable, and you don't want to waste it.
- Store the information in a central location. Many of
your users work in teams or as subcontractors, so
information about their hours accrued needs to be accessible
from anywhere.
- Organizes information in a natural way. Your users work
on projects for clients and invoice those clients based on
hours attributed to each project. The Clocked! application
needs to organize all these entities (clients, projects,
invoices, and time slips) in a logical way.
The best way to accomplish all these goals at the same time
is to divide the feature set into two applications. The
Clocked! application consists of a desktop application for
time tracking and a web-based, PHP-driven interface for
management.
Here's how this two-part approach meets your needs. You take
care of the first two requirements at the same time. Because
Clocked! runs on AIR, users on Windows, Mac, and Linux can
use it. AIR applications also run on the desktop, so users
can launch and run the program without opening a separate
browser window or tab. Finally, AIR applications run in an
Adobe Flash
Player-based environment, so you can give your users a
sleek, unobtrusive interface with ease.
Because the management side of Clocked! is a web
application, managers and users will be able to access
Clocked! data from any web browser. Using PHP allows you not
only to create a fast, lean application quickly, but you
also gain the ability to communicate with the AIR-based
client efficiently. The Zend_Amf extension of the Zend
Framework allows native Action Message Format (AMF)
communication between AIR applications and PHP services,
which means that server calls will be as fast as possible,
and you'll be able to leverage the full power of PHP when
dealing with data from the desktop clients.
Finally, you have to plan carefully to make sure the data
structures in Clocked! make sense. You do that by clearly
defining all of your objects-clients, projects, invoices,
and time slips-and organizing them both in the code and in
the MySQL database you'll be using for persistent storage.
Defining the objects
Because invoices, time "tickets," and projects are all
predicated to some extent on clients, you first create the
Client object. Some incarnation of a client or customer
object is present in most applications, so the structure
presented in Figure 1 is probably a familiar concept.
Click here for larger image
Figure 1. The Client object structure
This Client object has the standard informational fields,
such as an address and contact information, as well as an
extra field-rate. The rate property allows users to set
different rates for different clients.
Your next object is the Project, and its structure is
represented in Figure 2.

Figure 2. The Project object structure
The Project object is associated with its owner
through the client_id property. It also has a
code property that can be used to store a
client's internal reference or project number.
The Ticket object, depicted in Figure 3, is the time-
tracking object. It contains the time slips, or tickets,
that users submit using the AIR client.
Click here for larger image
Figure 3. The Ticket object structure
The Ticket is the most complex object so far, and it
contains several key attributes to help you store, classify,
and present them:
account_id: Links a ticket to a user
category: Used to track how time is spent-for example,
development, on-site, or research
start_time: A UNIX time stamp indicating when the ticket
was first opened
end_time: A UNIX time stamp indicating when the ticket
was last closed
A ticket's timer can be paused and restarted, so the start
and end values refer to the first clock-in and final clock-
out for a particular ticket, respectively. All times are
represented as UNIX time stamps, or seconds elapsed since
midnight, January 1, 1970.
Finally, the Invoice object represents the invoices that
will be generated. Its structure is shown in Figure 4.

Figure 4. The Invoice object structure
Setting up the database
For this project, you'll be storing persistent data in a
MySQL database. In addition to tables for each object, you
need a table for the users. Make sure your users table has
at least an ID, user name, and password for each user. To
set up your database, create a new empty database called
clocked. Then, you can use the clocked.sql
script to create the necessary tables and add some sample
values.
Creating the PHP services
The first step in setting up the PHP services needed for the
Clocked! AIR application is obtaining the Zend Framework.
Navigate to the Zend Framework
website, and click "Download Now." There are several
options on the ensuing page, but for this article, it is
assumed that you're using the full Zend Framework version
1.7.3. Figure 5 shows the appropriate download link.
Click here for larger image
Figure 5. Download the Zend Framework
You'll need to sign in to your Zend account or create one at
no cost. When the download is complete, extract the
downloaded archive to a convenient location. The result
should look similar to Figure 6.
Click here for larger image
Figure 6. Folder structure for the extracted Zend Framework
Next, navigate to the web root folder of your web server.
For example, if you're using WAMPServer on Windows, the
default web root is C:\wamp\www; and for MAMP
on the Mac, the default web root is
Applications/MAMP/htdocs. Within this folder,
create a new directory named clocked. This folder
will be the base directory for the application. If you're
using Windows, copy the library folder from the Zend
Framework download (pictured in Figure 6) into the directory
containing your web root folder. If you're using
WAMPServer, the default is C:\wamp. If you're
using MAMP on a Mac, the MAMP folder already contains a
library directory. In this case, copy the /Zend
subfolder to the MAMP/Library directory to
enable your application to use components from the Zend
Framework, including Zend_Amf.
Create the folder structure
Now, you need to set up the directory structure of the
application. Within the clocked folder, create a new
directory called include. Then, open the new include
folder and create a folder called services. Within
the /services folder, create a folder named
VO. Figure 7 illustrates the proper resulting
structure.

Figure 7. The Clocked! application's directory structure
Write the PHP services
With the folders in place, its time to write the PHP
services, begin by creating a new PHP file in the /VO
directory named Ticket.php. You'll use this file to define
the Ticket object in PHP:
<?php class Ticket { public $id; public $duration; public $category; public $details; public $client_id; public $project_id; public $account_id; public $invoice_id; public $start_time; public $end_time; } ?>
Next, create a service class to expose methods to the AIR
client. In the /services folder, create a new file called
ClockedService.php. Begin the file by importing the
/VO/Ticket.php file and defining the new class:
<?php
include 'VO/Ticket.php';
class ClockedService {
Next, use a comment block or PHPDoc block to describe the
first method, saveTicket(), then define the method:
/**
***description: Saves a new work ticket
***@param Ticket
***@return string
**/
function saveTicket(Ticket $ticket) {
This method accepts a new Ticket object from
the AIR client and returns a string. Next, assign the
properties of the passed-in Ticket to local
variables. Doing so increases clarity and makes it easier to
act on these properties before progressing through the
function:
// place ticket attributes into vars in case you want to act on them somehow $duration = $ticket->duration; $category = $ticket->category; $details = $ticket->details; $client_id = $ticket->client_id; $project_id = $ticket->project_id; $account_id = $ticket->account_id; $start_time = $ticket->start_time; $end_time = $ticket->end_time;
Now, connect to your MySQL database, and build an INSERT
query for the new ticket:
$dbh = mysql_connect('localhost','root','root');
$sql = 'INSERT INTO `clocked`.`tickets` (`id`, `duration`,
`category`, `details`, `client_id`, `project_id`, `submitted_time`,
`invoice_id`, `account_id`, `start_time`, `end_time`)
VALUES (NULL, ' . $duration . ', ' . $category . ', ' .
$details . ', ' . $client_id . ', ' .
Finally, execute the query, and return a string so the
client will know that the insertion was successful:
$result = mysql_insert($sql, $dbh);
return 'OK';
}
Now you have the code necessary to insert new tickets that
the AIR application submits. But the application also needs
two more pieces of information: the list of clients and the
list of projects. The lists are used to assign tickets where
they belong. When users open a new ticket, they select the
client and project from drop-down lists. The
getClients() function returns an array of
clients to the AIR application:
/**
***description: returns array of clients
***@return array
**/
function getClients() {
$sql = 'SELECT * FROM `clocked`.`clients`';
$dbh = mysql_connect('localhost','root','root');
$result = mysql_query($sql, $dbh);
$retVal = array();
while ($client = mysql_fetch_object($result)) {
array_push($retVal, $client);
}
return $retVal;
}
The client is selected first, so the next step for the
ticket application is to get the list of projects belonging
to the selected client. The getProjects() function does just
that:
/**
***description: returns a client's projects
***@param int
***@return array
**/
function getProjects($client_id) {
$sql = 'SELECT * FROM `clocked`.`projects`
WHERE `client_id` = '. $client_id .'';
$dbh = mysql_connect('localhost','root','root');
$result = mysql_query($sql, $dbh);
$retVal = array();
while ($project = mysql_fetch_object($result)) {
array_push($retVal, $project);
}
return $retVal;
}
Finally, add a login method. This function is like most
PHP/MySQL login functions: It accepts an array containing
the user-supplied user name and password, then returns the
authenticated user's ID to the client. If the login is
unsuccessful, it returns the distinctly non-verbose string
"bad":
/**
***description: checks credentials, returns the user's id
***@param array
***@return string
**/
function logIn($info) {
$user = $info[0];
$pass = $info[1];
$dbh = mysql_connect('localhost','root','root');
$sql = "SELECT * FROM `clocked`.`accounts`
WHERE username = '". $user ."' AND password ='". $pass ."'";
$result = mysql_query($sql, $dbh);
if (mysql_num_rows($result) > 0) {
$row = mysql_fetch_object($result);
$userid = $row->id;
return id;
}
else {
return "bad";
}
}
}
?>
Remember to close out the class and <?php tag, then save the file.
Make the service available
You now have all the basic parts of a Zend_Amf-compatible
service. To make this service available to your
applications, you also need a "gateway" file. The gateway
file instantiates the Zend_Amf server and passes it
information about your services and custom objects. Create
the gateway.php file in the main application directory,
/clocked. This folder should be inside your web server's web
root.
Begin the gateway file's code with an include path declaration like this one:
<?php
if(set_include_path('/Applications/MAMP/Library/') === false){
die('Include path failed');
}
This statement adds the location of the Zend Framework to
the PHP path variable. Be sure to change this path to
reflect where you placed the library folder on your
computer. Alternatively, you can permanently add the Zend
Framework's location to your PHP path. Next,
require_once the Zend_Amf server file and your
ClockedService class:
require_once 'Zend/Amf/Server.php';
require_once 'include/services/ClockedService.php';
These next lines instantiate the server and tell it that the
ClockedService class should be exposed. The
latter is accomplished using the server's
setClass() method:
// Instantiate the server
$server = new Zend_Amf_Server();
$server->setClass('ClockedService');
Next, map the Ticket class to the class you'll
use in ActionScript on the AIR client. Call it
TicketVO. Set this class mapping with the
server's setClassMap() method, passing in the
ActionScript class name first and the PHP class name second:
$server->setClassMap('TicketVO', 'Ticket');
The final lines of the gateway file instruct the server on
what it should accept and what it should return:
$server->setProduction(false); $response = $server->handle(); echo $response; ?>
The setProduction() function is set to False,
which causes the server's output to be more verbose. This
setting is useful during testing, but it should never be set
to False if the server is publicly available; the debugging
information is more than you would want to share with the
whole Internet. Next, assign the $response
variable to the server's handle() method to
instructs the server to handle the request. Finally, the
response is echoed back to the client. Be sure to add this
part, because without it, your clients will not get a
response. Figure 8 shows how your files and folders should
now be arranged.
Click here for larger image
Figure 8. The current directory structure
To test your work so far, open a web browser and navigate to
http://
localhost/clocked/gateway.php. You should see the words
Zend Amf Endpoint. Your browser may prompt you to download a
file instead of displaying Zend Amf Endpoint, and that is
also a successful test. If neither of those happens, review
your code and check your PHP error log for problems. If your
test was successful, you're ready to begin creating the
desktop component of clocked!
Join us next week as we go through the nuts and bolts of
creating the Clocked! desktop client using AIR.
|