Travel Expense Report

Travel Expense Report

The outstanding road map that we have now is the following:

Travel Expense Report:

  • Story 2: Travel Expense Report, 7 points

  • Story 3: Expense Report Comments, 2 points

  • Story 10: Persisted Expense Report Cash Advance, 2 points

  • Story 4: Expense Report as Spreadsheet, 4 points

Travel Expense Services:

  • Story 5: Accountant Notification, 3 points

  • Story 7: Sales Person Notification, 1 point

  • Story 13: Expense Report Export, 3 points

outstanding stories:

  • Story 6: Sales Manager Notification, 3 points

  • Story 8: Mail Room Notification, 2 points

By the sheer number of points, the Travel Expense Report will not be just an easy knockoff.

As seen previously in Figure 22-2, the expense report is pretty slim regarding details. Time to get to work talking. From conversations with Wendy and Edwina, you learn that what they really want is something that looks more like Figure 22-8.


Figure 22-8

Although it took the better part of the day to order and modify the categories (Lodging, Meals, and so on), what you now have is a close approximation to the appearance of the original Widget World faxable form. But now that your clients realize that things can change, then, well, they just may. Because even though a certain amount of flexibility and unknowns plague just about any project, you're going to roll with the punches and not overengineer every "just in case" feature that comes to mind.

Oh yeah, and the XXX in "Miles at XXX / mile" heading is a dollar amount based on federal tax law. Thankfully, it is a constant, so you don't need to track it for every state/province and you need to deal with only one country, but the rate changes from year to year, so you need to record the amount at the time of submission.

The all-important cash advance also needs to be part of the calculation and if the advance is enough, it might spill over into money owed to the company, represented by "Due Employee" and its Boolean counterpart "Due Company."

Ignoring for now the complexity, just start with the simple bits. The core of this sheet is a dollar amount associated with a date and a descriptor.

A quick scribble gets you the following data item:

   CREATE TABLE travel_expense_item (      emp_id           integer      NOT NULL,      expense_date     date         NOT NULL,      description      varchar(40)  NOT NULL,      amount           decimal(9,2) NOT NULL); 

Add the weekly information that needs to be captured:

   CREATE TABLE travel_expense_week (      emp_id           integer NOT NULL,      week_start       date    NOT NULL,      comments         text,    territory_worked varchar(60),    cash_advance     decimal(9,2),    mileage_rate     decimal(3,2) NOT NULL); 

As a technical note, you still need to add defaults, indexes, and so on to the database and, on the business side, note that you're tracking the cash_advance and mileage_rate. With each travel expense submitted on a weekly basis, the weekly subtotals sections of the form can be easily calculated, so storage of the subtotals is not required.

Travel Expense Item

Let's get to work on the tests. The simple case is simply validating the minimum required information, like this:

        function testValidTravelExpenseItem() {             $tvi = new TravelExpenseItem (                 array ('emp_id'             => "1",                        'expense_date'       => "1980-01-01",                        'description'        => "one",                        'amount'             => "1.0"));             $this->assertEquals(true, $tvi->isValid(), "valid expense");         }         function testInvalidTravelExpenseItem() {             $tvi = new TravelExpenseItem (                 array ('emp_id'             => "1",                        'expense_date'       => "", // date required                        'description'        => "one",                        'amount'             => "1.0"));             $this->assertEquals(false, $tvi->isValid(), "valid expense");         } 

What else does it need to do? If given an array, it can populate itself, so the travel expense item needs to persist itself accordingly. Add another test to the travel expense item:

       function testTravelExpenseItemPersistence() {            $this->_session->getDatabaseHandle()->query("delete FROM    travel_expense_item WHERE emp_id = 1 and expense_date = '1980-01-01'"); //    remove multiples            $tvi = new TravelExpenseItem (                array ('emp_id'            => "1",                       'expense_date'      => "1980-01-01",                       'description'       => "one",                       'amount'            => "1.0"),                $this->_session->getDatabaseHandle());            $result = $this->_session->getDatabaseHandle()->query("select * FROM    travel_expense_item WHERE emp_id = 1 and expense_date = '1980-01-01'");            $this->assertEquals(0, $result->numRows());            $tvi->persist();            $result = $this->_session->getDatabaseHandle()->query("select * FROM    travel_expense_item WHERE emp_id = 1 and expense_date = '1980-01-01'");            $this->assertEquals(1, $result->numRows());        } 

Remember to connect to your testing database when running these tests. The last thing you want is to accidentally torch production data with an errant DELETE statement.

Stub out your travel expense item in the new file, lib/expense.phpm:

   class TravelExpenseItem {        public function isValid() { }        public function persist() { }    } 

Make sure that the tests run (and fail) and then get to work satisfying them.

Using ContactVisit as a template, it's surprisingly easy:

   class TravelExpenseItem extends PersistableObject {       function __construct ($results, $dbh = null) {           parent::__construct ($results, $dbh);       }       private function isEmpty($key) {           if (array_key_exists($key, $this->contentBase) == false) return true;           if ($this->contentBase[$key] == null) return true;           if ($this->contentBase[$key] == "") return true;           return false;       }       public function isValid() {           if ($this->isEmpty("emp_id") == true) return false;           if ($this->isEmpty("expense_date") == true) return false;           if ($this->isEmpty("description") == true) return false;           if ($this->isEmpty("amount") == true) return false;           return true;       }       public function persist() {           return $this->persistWork (               "travel_expense_item",               array ( "emp_id",                       "expense_date",                       "description",                       "amount"));       }       public function getSqlWhere() {           return " emp_id = ".$this->emp_id." and expense_date = '".$this- >expense_date."' and description = '".$this->description."'";     } } 

Recalling the definition of ContactVisit:

   class ContactVisit extends PersistableObject {        function __construct ($results, $dbh = null) {}        private function isEmpty($key) {}        public function isValid() {}        public function persist() {}    } 

Of course, the function isEmpty() is now redundant between the two classes, which share a common parent. What does that say to you? It screams "move IsEmpty() up into PersistableObject"! Don't forget to make isEmpty() protected, or subclasses can't call it.

When you're done with that, make sure that the ContactVisit's test continues to pass. The respective classes should now look like this:

   class PersistableObject {        protected $contentBase = array();        protected $dbh = null; // database handle        protected $dispatchFunctions = array ("role" => "getrole");        function __get ($key) {}        function __construct ($results, $dbh = null) {}        public function implodeQuoted (&$values, $delimiter) {}        public function generateSqlInsert ($tableName, &$metas, &$values) {}        public function generateSqlUpdate ($tableName, &$metas, &$values) {}        public function generateSqlDelete ($tableName) {}        public function getSqlWhere() {}        public function isValid() {}        protected function isEmpty($key) {}        public function persistWork ($tablename, $meta) {}    }    class ContactVisit extends PersistableObject {        function __construct ($results, $dbh = null) {}        public function isValid() {}        public function persist() {}    }    class TravelExpenseItem extends PersistableObject {        function __construct ($results, $dbh = null) {}        public function isValid() {}        public function persist() {}        public function getSqlWhere() {}    } 

Now go and eat a cookie before taking on the Travel Expense Week class.

Travel Expense Week

By now you can easily knock out the simple test cases for travel expense week:

       function testValidTravelExpenseWeek() {            $tvi = new TravelExpenseWeek (                array ('emp_id'             => "1",                       'week_start'         => "1980-01-01",                       'comments'           => "comment",                       'mileage_rate'       => "0.31",                       'territory_worked'   => "Midwest"));            $this->assertEquals(true, $tvi->isValid(), "valid expense");         }         function testInvalidTravelExpenseWeek() {           $tvi = new TravelExpenseWeek (               array ('emp_id'             => "1",                      'week_start'         => "", // date required                      'comments'           => "comment",                      'mileage_rate'       => "0.31",                      'territory_worked'   => "Midwest"));           $this->assertEquals(false, $tvi->isValid(), "valid expense");         }         function testTravelExpenseWeekPersistence() {            $this->_session->getDatabaseHandle()->query("delete FROM    travel_expense_week WHERE emp_id = 1 and week_start = '1980-01-01'"); //    remove multiples            $tvi = new TravelExpenseWeek (                array ('emp_id'             => "1",                       'week_start'         => "1980-01-01",                       'comments'           => "comment",                       'territory_worked'   => "Midwest",                       'mileage_rate'       => "0.31",                       'cash_advance'       => "0"),                $this->_session->getDatabaseHandle());            $result = $this->_session->getDatabaseHandle()->query("select * FROM    travel_expense_week WHERE emp_id = 1 and week_start = '1980-01-01'");            $this->assertEquals(0, $result->numRows(), "pre check");            $tvi->persist();            $result = $this->_session->getDatabaseHandle()->query("select * FROM    travel_expense_week WHERE emp_id = 1 and week_start = '1980-01-01'");            $this->assertEquals(1, $result->numRows(), "persist");        } 

Also consider the fact that something has to be in charge of the lifecycle of all your TravelExpenseItems. TravelExpenseWeek? TraveExpenseItem? Does one of the two choices sound as though it would make a good container?

An (extremely explicit) test of TravelExpenseWeek as a container would look like the following:

       function testTravelExpenseWeekContainerRead() {            // clear out the test database            $this->_session->getDatabaseHandle()->query("delete FROM    travel_expense_item WHERE emp_id = 1 and expense_date >= '1980-01-06' and    expense_date <= '2001-09-15'");            $dbh = $this->_session->getDatabaseHandle();            // monday            $item1 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "1980-01-06",                       'description' => "lodging_and_hotel", 'amount' => "1.1"),                $dbh);            $item2 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "1980-01-06",                       'description' => "meals_breakfast", 'amount' => "2.2"),                $dbh);            $item3 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "1980-01-06",                       'description' => "misc_supplies", 'amount' => "3.3"),                $dbh);            // tuesday            $item4 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "2001-09-10",                       'description' => "lodging_and_hotel", 'amount' => "4.4"),                $dbh);            $item5 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "2001-09-10",                       'description' => "meals_breakfast", 'amount' => "5.5"),                $dbh);            $item6 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "2001-09-10",                       'description' => "misc_supplies", 'amount' => "6.6"),                $dbh);            // wednesday            $item7 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "1980-01-01",                       'description' => "lodging_and_hotel", 'amount' => "7.7"),                $dbh);            $item8 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "1980-01-01",                       'description' => "meals_breakfast", 'amount' => "8.8"),                $dbh);            $item9 = new TravelExpenseItem (                array ('emp_id' => "1", 'expense_date' => "1980-01-01",                       'description' => "misc_supplies", 'amount' => "9.9"),                $dbh); 

The TravelExpenseItems populate the first three days of the week and are each a different type of item: meals_breakfast, misc_supplies, and lodging_and_hotel.

   $item1->persist();    $item2->persist();    $item3->persist();    $item4->persist();    $item5->persist();    $item6->persist();    $item7->persist();    $item8->persist();    $item9->persist();    $week = new TravelExpenseWeek (        array ('emp_id'              => "1",               'week_start'          => "1980-01-06"),        $this->_session->getDatabaseHandle());    $week->readWeek();    // monday    $this->assertEquals(1.1, (float)                        $week->getExpenseAmount(0, 'lodging_and_hotel'));    $this->assertEquals(2.2, (float)                        $week->getExpenseAmount(0, 'meals_breakfast'));    $this->assertEquals(3.3, (float)                        $week->getExpenseAmount(0, 'misc_supplies'));    // tuesday    $this->assertEquals(4.4, (float)                        $week->getExpenseAmount(1, 'lodging_and_hotel'));    $this->assertEquals(5.5, (float)                        $week->getExpenseAmount(1, 'meals_breakfast'));    $this->assertEquals(6.6, (float)                        $week->getExpenseAmount(1, 'misc_supplies'));    // wednesday    $this->assertEquals(7.7, (float)                        $week->getExpenseAmount(2, 'lodging_and_hotel'));    $this->assertEquals(8.8, (float)                        $week->getExpenseAmount(2, 'meals_breakfast'));    $this->assertEquals(9.9, (float)                        $week->getExpenseAmount(2, 'misc_supplies'));    } 

After the TravelExpenseItems have been persisted, this test shows that the new TravelExpenseWeek class will be able to retrieve a week of TraveExpenseItems, given the employee ID and starting week.

This is an important test in other respects, because here is where you are defining the interface of your Web page. Recall that TravelExpenseItem is day-based and the Web page is week-based, necessitating TravelExpenseWeek. Thus, the ability to iterate over the TravelExpenseItems in a weekly manner should be paramount.

Note that because TravelExpenseWeek knows which day is the first of the week (for Americans, Sunday), you don't need to explicitly spell out your target day; instead you can use an offset. Use 0 for Sunday, 1 for Monday, and so on.

The Spike

Before going on, consider how you'll display what you need to display on the screen.

Go back and eyeball Figure 22-8 again. Note that something will have to take care of the subtotaling, not to mention the order.

Something has to associate human names with what exists in the database.

Also remember that "something" ultimately stops with you and that anything that makes your life easier can't be a bad thing. Clearly, hard-coding the HTML will be a maintenance nightmare.

To avoid a hard-coded HTML maintenance nightmare, consider using a data structure like this following, which can be used to generate what you need to display as well as mapping it to your data fields:

   array(        array('name' => 'Lodging',              'code' => 'lodging',              'data' => array('Lodging &amp; Hotel','Other','Tips'),              'persist' => array('lodging_and_hotel', 'lodging_other,                                 'lodging_tips')),        array('name' => 'Meals',              'code' => 'meals',              'data' => array('Breakfast', 'Lunch &amp; Snacks',                              'Dinner', 'Tips', 'Entertainment'),              'persist' => array('meals_breakfast', 'meals_lunch',                                 'meals_dinner', 'meals_tips',                                 'meals_entertainment')),        array('name' => 'Transportation',              'code' => 'trans',              'data' => array('Airfare', 'Auto Rental', 'Auto Maint./Gas',                              'Local Transportation', 'Tolls/Parking'),              'persist' => array ('trans_airfare', 'trans_auto_rental',                                  'trans_auto_maint', 'trans_local',                                  'trans_tolls', 'trans_miles_traveled')),        array('name' => 'Miscellaneous',              'code' => 'misc',              'data' => array('Gifts', 'Telephone &amp; Fax', 'Supplies',                              'Postage', 'Other'),              'persist' => array('misc_gifts','misc_phone',                                 'misc_supplies', 'misc_postage',                                 'misc_other'))); 

The first level is an array of sections, which means that each section gets a subtotal and each section contains line items.

Each section has a data array, simply the human-viewable description as well as its computerized counterpart persist, which will presumably be the name of each item in the database. The code item in the associated array exists in order to automate the creation of semi-meaningful JavaScript variables that must be calculated per section, for example, one for Meals, Lodging, and Transportation. This data structure will be a some of the glue that holds the HTML, JavaScript math, database, and PHP objects together; it's your roadmap.

More Travel Expense Week Tests

Going from PHP's travel expense form response to the database is an important step. This is where you'll be defining what you expect to receive from the page. Remember that you need a way to take the entire response and easily create TravelExpenseItems from that. Sounds like a good job for the TravelExpenseWeek container.

Recall that the naming convention for determining the unique values of each individual cell is lodging_sun_0, which represents "section" + "day" + "line item". Because there is a nearly infinite number of ways to describe the same column/row, vertical/horizontal, spreadsheet-cell relationship, this naming convention is quite adequate.

Additionally, you can easily determine which horizontal row you happen to be working with as a result of its section offset (the "0'' in lodging_sun_0). Day and line item are different types for a reason: your sanity. Manipulating data in different dimensions is hard enough; tracking which way against which one is harder and remembering what they are based on is asking for trouble. With all things being equal, there is more semantic information contained with lodging_sun_0 than with lodging_0_0.

Here is the test for parsing the request:

        function testTravelExpenseWeekContainerParseRequest() {             $response = array ('lodging_sun_0' => "1.1",                                'meals_sun_0'   => "2.2",                                'misc_sun_2'    => "3.3",                                'lodging_mon_0' => "4.4",                                'meals_mon_0'   => "5.5",                                'misc_mon_2'    => "6.6",                                'lodging_tue_0' => "7.7",                                'meals_tue_0'   => "8.8",                                'misc_tue_2'    => "9.9");         $week = new TravelExpenseWeek (             array ('emp_id' => "1",                    'week_start' => "1980-01-06"));         $week->parse($response);         $this->assertEquals(1.1,                             $week->getExpenseAmount(0, 'lodging_and_hotel'));         $this->assertEquals(2.2,                             $week->getExpenseAmount(0, 'meals_breakfast'));         $this->assertEquals(3.3,                             $week->getExpenseAmount(0, 'misc_supplies'));         $this->assertEquals(4.4,                             $week->getExpenseAmount(1, 'lodging_and_hotel'));         $this->assertEquals(5.5,                             $week->getExpenseAmount(1, 'meals_breakfast'));         $this->assertEquals(6.6,                             $week->getExpenseAmount(1, 'misc_supplies'));         $this->assertEquals(7.7,                             $week->getExpenseAmount(2, 'lodging_and_hotel'));         $this->assertEquals(8.8,                             $week->getExpenseAmount(2, 'meals_breakfast'));         $this->assertEquals(9.9,                             $week->getExpenseAmount(2, 'misc_supplies')); } 

The $response array is simulating what you'll receive from your form input, and remember that the $response values and the persist meta-values are reflected in your road-map data structure.

Create one more test for persisting the container. It is very similar to the travel expense items test:

       function testTravelExpenseWeekContainerWrite() {            $this->_session->getDatabaseHandle()->query("delete FROM    travel_expense_item WHERE emp_id = 1 and expense_date >= '1980-01-06' and    expense_date <= '2001-09-15'");            $response = array (               'lodging_sun_0'=>"1.1",'meals_sun_0'=>"2.2",'misc_sun_2'=>"3.3",               'lodging_mon_0'=>"4.4",'meals_mon_0'=>"5.5",'misc_mon_2'=>"6.6",               'lodging_tue_0'=>"7.7",'meals_tue_0'=>"8.8",'misc_tue_2'=>"9.9");            $week = new TravelExpenseWeek (                array ('emp_id'             => "1",                       'week_start'         => "1980-01-06",                       'territory_worked'   => "Midwest",                       'comments'           => "comment",                       'cash_advance'       => "0",                       'mileage_rate'       => "0.31"),                $this->_session->getDatabaseHandle());            $week->parse($response);            $this->assertEquals(true, $week->persist());            $week = new TravelExpenseWeek (                array ('emp_id' => "1",                       'week_start'         => "1980-01-06",                       'territory_worked'   => "Midwest",                       'comments'           => "comment",                       'cash_advance'       => "0",                       'mileage_rate'       => "0.31"),                $this->_session->getDatabaseHandle());            $week->readWeek();            $this->assertEquals(1.1, (float)                                $week->getExpenseAmount(0, 'lodging_and_hotel'));            $this->assertEquals(2.2, (float)                                $week->getExpenseAmount(0, 'meals_breakfast'));            $this->assertEquals(3.3, (float)                                $week->getExpenseAmount(0, 'misc_supplies'));            $this->assertEquals(4.4, (float)                                $week->getExpenseAmount(1, 'lodging_and_hotel'));            $this->assertEquals(5.5, (float)                                $week->getExpenseAmount(1, 'meals_breakfast'));            $this->assertEquals(6.6, (float)                                $week->getExpenseAmount(1, 'misc_supplies'));            $this->assertEquals(7.7, (float)                                $week->getExpenseAmount(2, 'lodging_and_hotel'));            $this->assertEquals(8.8, (float)                                $week->g etExpenseAmount(2, 'meals_breakfast'));            $this->assertEquals(9.9, (float)                                $week->getExpenseAmount(2, 'misc_supplies'));    } 

In this test, TravelExpenseWeek parses the $response, persists the TravelExpenseItemsin $week->persist(), rereads them from the database $week->readWeek(), and finally validates what was read.

Note that these tests make heavy use of copy-and-paste programming. Keeping things tidy is a general rule, so if a majority of the tests require the same setup, put it in the setup but don't prematurely refactor your tests. Your tests are the first stop to where change happens, so they should be easy to read and easy to modify.

Satisifying the Travel Expense Week Tests

Satisifying TravelExpenseWeek 's validity and persistence tests should be predictably easy by now:

   class TravelExpenseWeek extends PersistableObject {        public $items = array();        function __construct ($results, $dbh = null) {            parent::__construct ($results, $dbh);        }        public function isValid() {            if ($this->isEmpty("emp_id") == true) return false;            if ($this->isEmpty("week_start") == true) return false;            if ($this->isEmpty("territory_worked") == true) return false;            if ($this->isEmpty("mileage_rate") == true) return false;            return true;        }        public function persist() {            return $this->persistWork ("travel_expense_week",                                       array ( "emp_id",                                               "week_start",                                               "comments",                                               "territory_worked",                                               "cash_advance",                                               "mileage_rate"));        }        public function getSqlWhere() {        return " emp_id = ".$this->emp_id." and week_start = '".$this-    >week_start."'";        }        public function parse(&$request) { }        public function readWeek() { }        public function getExpenseAmount($offset, $description) { }    } 

The functions parse, readWeek, and getExpenseAmount are required by the TravelExpenseWeekContainerParseRequest and TravelExpenseWeekContainerWrite tests and thus are simply syntactic stubs.

Satisifying the Parse Request Test

The TravelExpenseWeekContainerParseRequest test may be a bit tricky because it involves data manipulation. Your first order of business is to take care of the dependencies, which is part of your meta-array road map that you were thinking about earlier. Adding this function to the TravelExpenseWeek class in lib/expense.phpm will take care of the job:

   public function getExpensesMetaArray () {        return array(            array('name' => 'Lodging',                  'code' => 'lodging',                  'data' => array('Lodging &amp; Hotel','Other','Tips'),                  'persist' => array('lodging_and_hotel', 'lodging_laundry',                                     'lodging_tips')),            array('name' => 'Meals',                  'code' => 'meals',                  'data' => array('Breakfast', 'Lunch &amp; Snacks',                                  'Dinner', 'Tips', 'Entertainment'),                  'persist' => array('meals_breakfast', 'meals_lunch',                                     'meals_dinner', 'meals_tips',                                     'meals_entertainment')),            array('name' => 'Transportation',                  'code' => 'trans',                  'data' => array('Airfare', 'Auto Rental', 'Auto Maint./Gas',                                  'Local Transportation', 'Tolls/Parking'),                  'persist' => array ('trans_airfare', 'trans_auto_rental',                                      'trans_auto_maint', 'trans_local',                                      'trans_tolls', 'trans_miles_traveled')),            array('name' => 'Miscellaneous',                  'code' => 'misc',                  'data' => array('Gifts', 'Telephone &amp; Fax', 'Supplies',                                  'Postage', 'Other'),                  'persist' => array('misc_gifts','misc_phone',                                     'misc_supplies', 'misc_postage',                                     'misc_other')));    } 

Along with the meta-array, you also need a way to retrieve each TravelExpenseItem. Recall from your TravelExpenseWeekContainerParseRequest test that your tests require the TravelExpenseWeek->getExpenseAmount() function:

   $this->assertEquals(8.8, $week->getExpenseAmount(2, 'meals_breakfast')); 

For now, the function is simply implemented by iterating through TravelExpenseWeek->item. Make a note to yourself about the ugliness by adding this to the TravelExpenseWeek in lib/expense .phpm:

   /**    * todo: put into an associative array    */    public function getExpenseAmount($offset, $description) {        $targetDate = $this->addDays($this->week_start, $offset);        foreach ($this->items as $item) {            if ($item->expense_date == $targetDate &&                $item->description == $description) {                return $item->amount;        }    }    return "";    } 

Note that you're calling an addDays() function with an offset to get the current string-based representation of the date.

Better make an addDays() method, then a quick three-minute hack for adding the number of seconds in a day (86400 is 60*60*24). TravelExpenseWeek now contains:

   /**     * todo: will this fail on daylight savings time?     */    public function addDays($start, $days) {        return date("Y-m-d", strtotime($start)+$days*86400);    } 

But this may come with a catch. When you think about it, during the jump to and from daylight-savings time in America, there is a difference of one hour's worth of seconds.for that particular day. How does PHP return the day? Is it local time or GMT? You'd better figure it out or in a mere six months you'll be sweating it out flipping burgers at your local fast-food chain. For now, note the ugliness and continue satisfying your TravelExpenseWeekContainerParseRequest test.

The killer function of the TravelExpenseWeek object is parsing the PHP's travel expense form response in order to create individual TravelExpenseItems, or an associative array of the data.

Recall that you are mapping the following data structure:

   array(        array('name' => 'Lodging',              'code' => 'lodging',              'data' => array('Lodging &amp; Hotel','Other','Tips'),              'persist' => array('lodging_and_hotel', 'lodging_other,                                 'lodging_tips')) 

with the following naming convention contained in PHP's response object:

   lodging_sun_0 

After you've got that figured out, it's just a matter of creating TravelExpenseItems and storing them in the TravelExpenseWeeks->items array. Add the parse function to TravelExpenseWeek in lib/expense.phpm:

   /**     * This function bridges the gap between the day-based DB and the     * week-based view     */    public function getWeekArray() {        return array ('sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat');    }    public function parse (&$request) {        // section loop        foreach ($this->getExpensesMetaArray() as $sectionlist) {            // row loop            for ($i=0; $i < count ($sectionlist['persist']); $i++) {                $daynum = 0;               // day loop               foreach ($this->getWeekArray() as $day) {                   $index = $sectionlist['code']."_".$day."_".$i;                   if (array_key_exists($index, $request) and                       $request[$index] <> null and                       $request[$index] <> "") {                      // create new item and store in $this->items                       array_push (                           $this->items,                           new TravelExpenseItem (                                array ('emp_id' => $this->emp_id,                                       'expense_date' =>                                       $this->addDays($this->week_start,                                                      $daynum),                                       'description' =>                                           $sectionlist['persist'][$i],                                       'amount' => (float) $request[$index]),                                $this->dbh));                   }                   $daynum++;              }          }       }    } 

Together these five functions constitute the majority of the functionality implemented in TravelExpenseWeek. The class takes on the not-so-trivial task of modifying a web-based data-structure with a database centric one.

Satisfying the Travel Expense Week Container Read and Write Tests

Given the minimum amount of data, the employee ID, and the starting week, your test should populate the TravelExpenseWeek with TravelExpenseItems to and from the database. Recall that the test roughly does the following:

   $week = new TravelExpenseWeek (        array ('emp_id'             => "1",               'week_start'         => "1980-01-061980-01-06",               'territory_worked'   => "Midwest",                'comments'          => "comment",               'cash_advance'       => "0",                'mileage_rate'      => "0.31"),        $this->_session->getDatabaseHandle());    $week->readWeek(); 

This should do the job nicely. Add the readWeek() function to TravelExpenseWeek in lib/expense.phpm:

   public function readWeek() {        $sql = "select * from travel_expense_week where";        $sql .= " emp_id = ".$this->emp_id." and ";        $sql .= " week_start = '".$this->week_start."'";        $result = $this->dbh->query($sql);        if (DB::isError($result) <> true and $result->numRows() > 0) {            $row = $result->fetchRow();            $this->contentBase['comments'] = $row['comments'];            $this->contentBase['territory_worked'] = $row['territory_worked'];            $this->contentBase['cash_advance'] = $row['cash_advance'];            $this->contentBase['mileage_rate'] = $row['mileage_rate'];        }        $sql = "select * from travel_expense_item where";        $sql .= " emp_id = ".$this->emp_id." and ";        $sql .= " expense_date >= '".$this->week_start."' and";        $sql .= " expense_date <= '".$this->addDays($this->week_start, 6)."'";        $this->items = array();        $result = $this->dbh->query($sql);        if (DB::isError($result) or $result->numRows() == 0) return;        while ($row = $result->fetchRow()) {            array_push ($this->items, new TravelExpenseItem ($row));        }    } 

Note that there are really two halves to this. The first database lookup returns the state of the TraveExpenseWeek; the second half returns the associated TravelExpenseItems, adding them to the TraveExpeneWeek->items array.

But do you notice a subtle problem? See how we're lazily passing the entire $row to TravelExpenseItem in its constructor? Recall that the constructor of PersistableObject, TravelExpenseItem's parent, is

   function __construct ($results, $dbh = null) {        $this->dbh = $dbh;        if ($results <> null) {            $this->contentBase = $results; // copy        }    } 

Thus, TravelExpenseItem blindly stores whatever we give it? Regardless of what data it is? Hmmm, this is definitely wasteful, and although the database contains nothing that isn't found in the TraveExpenseItem, the object gets its behavior from PersistableObject, of which TravelExpenseWeek, Contact, and ContactVisit are all children. Passing any of those objects PHP's $_REQUEST would result in a criminal waste of resources.

To change PersistableObject's default behavior from "store everything'' to "store what you expect to find,'' take a step back and create a new test, one that tests the undesired behavior. Because TravelExpenseItem is simple, it's an ideal candidate:

   function testIgnoreExtra() {        $response = array ('emp_id'            => "1",                          'expense_date'       => "1980-01-01",                          'description'        => "one",                          'amount'             => "1.0",                          'extra'              => "extra bits");        $tvi = new TravelExpenseItem($response);        $this->assertEquals(null, $tvi->extra);    } 

Sure enough, this test fails. It also means that TravelExpenseWeek, which is expected to parse out PHP's $_RESPONSE object, will be hugely bloated if we do something silly such as pass in the response during construction.

One way to get around the problem is to know ahead of time what is required by the object in order to ignore anything else. So, rather than have the TravelExpenseItem->persist() function looking like:

   public function persist() {        return $this->persistWork (            "travel_expense_item",            array ("emp_id",                   "expense_date",                   "description",                   "amount"));    } 

the persist function would be made more concise by passing in some internal state:

   public function persist() {        return $this->persistWork (            $this->contentMetaTable,            $this->contentMetaOnly);    } 

Put it all together and you end up with the rewritten class. It's not that different; things are just moved around a bit:

   class TravelExpenseItem extends PersistableObject {        protected $contentMetaTable = null;        protected $contentMetaOnly = null;        function __construct ($results, $dbh = null) {            $this->contentMetaTable = "travel_expense_item";            $this->contentMetaOnly = array ("emp_id",                                            "expense_date",                                            "description",                                            "amount");            $content = array();            foreach ($this->contentMetaOnly as $key) {                if (array_key_exists($key, $results))                    $content[$key] = $results[$key];            }            parent::__construct ($content, $dbh);        }        public function isValid() {            if ($this->isEmpty("emp_id") == true) return false;            if ($this->isEmpty("expense_date") == true) return false;            if ($this->isEmpty("description") == true) return false;            if ($this->isEmpty("amount") == true) return false;            return true;        }        public function getSqlWhere() {            return " emp_id = ".$this->emp_id." and expense_date ='".$this-    >expense_date."' and description = '".$this->description."'";        }        public function persist() {            return $this->persistWork (                $this->contentMetaTable,                $this->contentMetaOnly);        }    } 

In this case, you check to see whether what is being passed in via the constructor is what you're looking for. Repair the last remaining TravelExpenseWeek test and then refactor.

The TravelExpenseWeek write takes significantly less work, considering that each TravelExpenseItem already knows how to persist itself. All you need to do is call the persist() function of each one.

Simply add the small loop at the end of your persistence function of TravelExpenseWeek in lib/expense.phpm:

   public function persist() {        $this->persistWork ("travel_expense_week",                            array ( "emp_id",                                    "week_start",                                    "comments",                                    "territory_worked",                                    "cash_advance",                                    "mileage_rate"));        // persist each item to the database        foreach ($this->items as $item) {            $item->persist();        }        return true;    } 

Congratulations. All your unit tests now pass. However there are some remaining features you need to add, including a quick refactoring.

A Quick Refactoring

Having all your unit tests satisfied is an important matter, because during the next refactoring they'll tell you if you have accidentally broken a seemingly unrelated section of code.

Now that TravelExpenseItem and TravelExpenseWeek have more similar structures but with varying degrees of error checking, move that functionality up into the PersistableObject.

Essentially, you want to add this to PersistableObject:

   protected $contentMetaTable = null;    protected $contentMetaOnly = null;    public function persist() {        return $this->persistWork (            $this->contentMetaTable,            $this->contentMetaOnly);    } 

There is also the same loop in both constructors that can be moved up also:

   foreach ($this->content as $key) {        if (array_key_exists($key, $results))            $this->content[$key] = $results[$key];    } 

Here's a snapshot of the resulting object:

   class PersistableObject {        protected $contentBase = array();        protected $contentMetaTable = null;        protected $contentMetaOnly = null;        protected $dbh = null; // database handle        protected $dispatchFunctions = array ("role" => "getrole");        function __get ($key) {            // content removed for brevity        }        function __construct ($results, $dbh = null) {            $this->dbh = $dbh;            if ($this->contentMetaOnly <> null) {                foreach ($this->contentMetaOnly as $key) {                    if (array_key_exists($key, $results)) {                        $this->contentBase[$key] = $results[$key];                    }                }           } elseif ($results <> null) {               $this->contentBase = $results; // copy           }        }        public function implodeQuoted (&$values, $delimiter) {            // content removed for brevity        }        public function generateSqlInsert ($tableName, $metas, $values) {            // content removed for brevity        }        public function generateSqlInsert ($tableName, $metas, $values) {            // content removed for brevity        }        public function generateSqlUpdate ($tableName, $metas, $values) {            // content removed for brevity        }        public function generateSqlDelete ($tableName) {            // content removed for brevity        }        public function getSqlWhere() {            // content removed for brevity        }        protected function isEmpty($key) {            // content removed for brevity        }        public function isValid() {            // content removed for brevity        }        public function persistWork ($tablename, $meta) {            // content removed for brevity        }        public function persist() {            return $this->persistWork (                $this->contentMetaTable,                $this->contentMetaOnly);        }    } 

The constructor is responsible for determining whether data is keepable. Also, note that the behavior is backward compatible because if the contentMetaOnly array is null, then PersistableObject simply copies the $results into its contentBase.

Now your TravelExpenseItem can be squeezed down ever smaller:

   class TravelExpenseItem extends PersistableObject {       function __construct ($results, $dbh = null) {           $this->contentMetaTable = "travel_expense_item";           $this->contentMetaOnly = array ( "emp_id",                                            "expense_date",                                            "description",                                            "amount");           parent::__construct ($results, $dbh);       }       public function isValid() {           if ($this->isEmpty("emp_id") == true) return false;           if ($this->isEmpty("expense_date") == true) return false;           if ($this->isEmpty("description") == true) return false;           if ($this->isEmpty("amount") == true) return false;           return true;       }       public function getSqlWhere() {           return " emp_id = ".$this->emp_id." and expense_date = '".$this-    >expense_date."' and description = '".$this->description."'";        }    } 

Do you see how the TraveExpenseItem's constructor sets up its PersistableObject state and then passes the arguments up to PersistableObject?

So although you are charged with making an explicit call to the constructor, it gives you the freedom to preliminarily set state in the parent object, PersistableObject.

Now make the same modifications to TravelExpenseWeek:

   class TravelExpenseWeek extends PersistableObject {        public $items = array();        function __construct ($results, $dbh = null) {            $this->contentMetaTable = "travel_expense_week";            $this->contentMetaOnly = array ( "emp_id",                                             "week_start",                                             "comments",                                             "territory_worked",                                             "cash_advance",                                             "mileage_rate");            parent::__construct ($results, $dbh);       }       public function isValid() {           // content removed for brevity       }       public function persist() {           if (parent::persist() == false) return false;           // persist each item to the database           foreach ($this->items as $item) {               if ($item->persist() == false) return false;           }           return true;       }       public function getSqlWhere() {            return " emp_id = ".$this->emp_id." and week_start = '".$this-    >week_start."'";        }        public function getExpensesMetaArray () {            // content removed for brevity        }        public function getWeekArray() {            // content removed for brevity        }        public function addDays($start, $days) {            // content removed for brevity        }        public function parse (&$request) {            // content removed for brevity        }        public function readWeek() {            // content removed for brevity        }        public function getExpenseAmount($offset, $description) {            // content removed for brevity        }    } 

Note that TravelExpenseWeek overrides PersistableObject->persist() function. However, it first calls the PersistableObject->persist() and then persists every TraveExpenseItem that it's holding. Remember that because the logic behind an insert/update/delete is in

PersistableObject->persistWork(), it continues to work and all you need to do is remember to implement the getSqlWhere function in your concrete classes such as TravelExpenseWeek. The tests are still passing? Good job, but not so fast, considering that a cash advance is not required (as shown in the validity test):

   function testValidTravelExpenseWeek() {        $tvi = new TravelExpenseWeek (            array ('emp_id'             => "1",                   'week_start'         => "1980-01-01",                   'comments'           => "comment",                   'mileage_rate'       => "0.31",                   'territory_worked'   => "Midwest"));        $this->assertEquals(true, $tvi->isValid(), "valid expense");    } 

But every other test sets cash_advance to zero. What happens if you remove cash_advance from the test? It breaks! This is because PersistableObject blindly creates SQL based on every field of database record.

Modify the persistence test to reflect that neither cash_advance nor comments are required:

       function testTravelExpenseWeekPersistence() {            $this->_session->getDatabaseHandle()->query("delete FROM    travel_expense_week WHERE emp_id = 1 and week_start = '1980-01-01'");    // remove multiples            $tvi = new TravelExpenseWeek (                array ('emp_id'             => "1",                       'week_start'         => "1980-01-01",                       'territory_worked'   => "Midwest",                       'mileage_rate'       => "0.31"),                $this->_session->getDatabaseHandle());            $result = $this->_session->getDatabaseHandle()->query("select * FROM    travel_expense_week WHERE emp_id = 1 and week_start = '1980-01-01'");            $this->assertEquals(0, $result->numRows(), "pre check");            $this->assertEquals(true, $tvi->persist(), "save");            $result = $this->_session->getDatabaseHandle()->query("select * FROM    travel_expense_week WHERE emp_id = 1 and week_start = '1980-01-01'");            $this->assertEquals(1, $result->numRows(), "persisted ok");            $row = $result->fetchRow();            $this->assertEquals(0.0, (float) $row['cash_advance'], "cash advance    default");        } 

The default values of cash_advance are an easy fix, too; just modify the constructor of TravelExpenseWeek to set the default of zero and an empty string:

   class TravelExpenseWeek extends PersistableObject {        function __construct ($results, $dbh = null) {            $this->contentMetaTable = "travel_expense_week";            $this->contentMetaOnly = array ( "emp_id",                                             "week_start",                                             "comments",                                             "territory_worked",                                             "cash_advance",                                             "mileage_rate");            $this->contentBase['comments'] = "";            $this->contentBase['cash_advance'] = "0.0";            parent::__construct ($results, $dbh);        } 

The tests are again passing, and that means that you're done now, right? Doing a quick "grep todo" uncovers this nagging problem:

   /**     * todo: will this fail on daylight savings time?     */     public function addDays($start, $days) {         return date("Y-m-d", strtotime($start)+$days*86400);     } 

Reading up on it, you think there may indeed be an issue. Daylight Saving Time ends on the last Sunday in October in most places of America, which means that the clocks are set back and there are more than 86400 seconds during that day. Write up a test to confirm that it fails:

   function testDaylightSavingTime() {        $tvw = new TravelExpenseWeek (array());        $this->assertEquals("2004-10-30", $tvw->addDays("2004-10-29", 1));        $this->assertEquals("2004-10-31", $tvw->addDays("2004-10-29", 2));        $this->assertEquals("2004-11-01", $tvw->addDays("2004-10-29", 3),        "no DST");    } 

Sure enough, there is a problem. Adding three days to October 29th should land in November rather than October 31, twice. Reading up on your functions, strtotime() can do the date math for you by accepting date offsets. Recode the addDays() function of PersistableObject without the presumption of seconds per day:

   public function addDays($start, $days) {        return date("Y-m-d", strtotime($start." +".$days." days"));    } 

Because your remaining "to do" of modifying the getExpenseAmount function is really an optimization, it can wait until later. There is no need for preoptimization, and it's important that you keep the momentum going.

Although that was indeed a lot of work, you now have reasonably well-written persistable objects that are completely backward compatible, your system is growing in stages, and you'll update Contact and ContactVisit the next time you touch them, so continue along with the current task of the travel expense report.



Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
ISBN: N/A
EAN: N/A
Year: 2003
Pages: 182
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net