Using Data Objects with the WI-Engine and the API
This is a presentation I gave at my job in September 2004. The WI-Engine and the API are home-grown tools we use for UI design and database access. This presentation concerns improving our methods for data management in PHP applications. For more information see this blog entry. We have implemented the “possible future framework” described below (this means there is no more SQL hardcoded in the methods).
Motivation
The WI-Engine provides a robust, OO environment for developing web user interfaces, and the API provides a variety of OO tools and security features for database access. However, our current toolset does not include a means for using the data itself in an OO fashion. Our current approach is to perform API calls from WI Engine panels and stuff the returned data into arrays. Our data is thus represented in a generally “flat” row-like or hash-like fashion. Quite often this is perfectly sufficient and appropriate, but there are circumstances where this approach can be limiting.
For the current Admissions project, we encapsulated our data in objects (to distinguish application data objects from the WI Engine’s user interface objects, we’ll refer to them as “data objects”). The following table represents how projects are approached in the current SOMIS framework, how we approached the Admissions project, and how SOMIS might combine the two approaches in the future.
| Current SOMIS Framework | Admissions Framework | Possible Future Framework | |
|---|---|---|---|
| UI Logic | WI Engine (data represented in arrays) |
WI Engine | WI Engine |
| Business Logic: Representing Data | Data Objects (data represented in objects) (direct SQL to access database) |
Data Objects | |
| Business Logic: Accessing Data | API | API |
We had several reasons for wanting to take an object oriented approach to storing and manipulating our data:
- No API: The Admissions data is in SQL Server, so the API was not available to us. We wanted to upgrade the UI to use the WI Engine, but given the complexity of the application, we didn’t want to entangle all of our data access and business logic in the UI code. We wanted to keep the business logic separate from the UI logic, particularly since we’re planning to move the Admissions data to Oracle next year. Without the API, we needed to develop another framework for handling our data.
- Encapsulation and “information hiding”: we created an “Applicant” object which serves as a container for all of an applicant’s data (we also created a “Tabs” object for managing the complex set of factors that control which tabs appear in the UI). An applicant has a significant amount of data that comes from a number of different tables. Rather than keeping track of all the data in a bunch of different arrays, we use a single Applicant object as a place to hold and rationally organize all of an applicant’s data. Hiding data inside the object reduces programming complexity. While the API offers encapsulation for data access, the Admissions objects also encapsulates the data itself.
- Method calls and data protection: manipulation of the data occurs in object methods. We currently have 47 different methods to handle all the different kind of things that we do with applicant data. Having clearly defined object methods allows for better control over how an object is accessed and how its data is changed. This provides data protection similar to the API.
- Further separation of business logic from UI logic: UI logic itself can often be quite complex, so it’s organizationally beneficial to have the WI-Engine panels focused on handling the display, and to have the data manipulation happening in the object. This is a higher-order separation of business logic and presentation logic than what we have with the API alone. The data object can do much more than manipulation of database records. For example, our Applicant and Tabs objects interact to evaluate a complex set of conditions to determine the user’s navigation options in each panel. These evaluations occur in the object methods, rather than in the WI Engine panels. This approach can simplify upgrades: panels can be completely reworked with minimal disruption of the business logic, and vice versa. Our database calls are contained inside the Applicant object and its methods. Next year we will move the data to Oracle, and we will be able to revise the inner workings of the Applicant object without having to disturb the WI Engine code at all. We have found that most of our debugging has been in the object, which means we haven’t had to do a lot of hunting through panels to track down problems.
- Persistence: with a minor change to the WI Engine, it became possible to easily pass a data object from one panel to another. This means your object is easily accessible anytime you need it. In combination with encapsulation, this makes life easier when programming in the stateless web: you don’t have to keep track of the availability and the state of a multitude of individual variables across multiple pages: your variables are stored and persisted inside your objects.
- Reusability and inheritance: while something like an “applicant” is fairly application-specific, we could have other objects (e.g. an object for generic form validation and error handling) that could be easily made available for use across a wide variety of applications. This sort of thing can be done with shared functions, but what typically happens there is someone needs to tweak the generic function in some way, and rather than using the shared version, they make a copy and change it. This leads to code maintenance nightmares. With inheritance, a programmer could still make use of the shared object, and then simply extend it as needed for the specific purposes of an application.
Implementation
- main_init.inc
$this->require_panel_lib('admissions/applicant.phl'); if (!is_object($app->vars['applicant'])) { $app->vars['applicant'] = new Applicant($app_year,$aamc_id); } - Applicant.phl
class Applicant { var $applicant_data; // Constructor function Applicant($app_year,$aamc_id) { [SQL Server database call to get applicant data] $this->applicant_data = $stmt->fetchRowHash(); return true; } // Method example - updating data function updateMobile ($number) { $sql = "update upenn_som..misc_info set mobile_phone = '".$number."' where bio_nbr = ".$this->applicant_data['bio_nbr']; if (!$GLOBALS['db']->execute($sql, $rows)) { $this->sendError($sql); return $error=$GLOBALS['db']->getError(); } else { $this->applicant_data['mobile_phone'] = $number; $this->applicant_data['mobile_phone_display'] = $this->formatPhone($number); return TRUE; } } // Another method example - testing for a condition function isFeeOK () { // returns flag if fee has been paid/waived, returns true for unverified // applicants with waiver, returns false if applicant claims waiver, // but is not yet verified $verified = $this->getEligibliity($verify=1); if (($this->applicant_data['fee_waiver']=='Y' && $this->applicant_data['fee_info']=='fee waived') || (($this->applicant_data['fee_info']!='fee to be paid') && $this->applicant_data['fee_info']!='') && ($this->applicant_data['fee_info']!='fee waived')) { return true; } else { return false; } } } - A panel that interacts with the applicant object:
# Get the applicant object $applicant = &$app->vars['applicant']; $tpl->assign('applicant', $this->vars['applicant'] = $applicant->applicant_data); # See if the fee is paid - this determines whether we show a "finish later" # button (we don't show it if the fee is already paid) $tpl->assign('fee_paid', $applicant->isFeeOK());
Lessons Learned
- Data structure: any data you add to the object should be in the form of a hash. If you use simple arrays, you risk creating duplicate data in the object in subsequent calls.
- References: always refer to your object by reference(i.e.
$applicant = &$app->vars['applicant']) - that way any changes you make are retained in the object after you leave the current panel. - WI Engine state management: if someone clicks the browser “back” button, the WI Engine will roll back the state of the application. This means the state of your object is also rolled back. This can be tricky with form submissions: for example, if you set a flag in your object indicating a SQL insert has been done (and shouldn’t be done again), the state of your flag will roll back, resulting in a duplicate insert attempt.
- Garbage can (or, using the right object for the job): since we had created the infrastructure for the Applicant object, and it was available to us in every panel, we unwittingly gave into the temptation to use it as place to store all kinds of data. For example, we made all the dates involved in the admissions cycle part of the applicant object (application deadlines, etc.). Beyond just being a conceptual no-no, it had real negative consequences: we eventually needed to be able to access the dates on the login page, but we didn’t have an applicant object available until after the user logged in.
- Object handles: we stored the Applicant object and the Tabs object in the WI Engine’s $app->vars, but we didn’t come up with a way for objects to interact with each other directly. This need didn’t come up, so it didn’t pose a problem. However, our next version will have more than just two kinds of objects, and they will need to interact. For this we will use object handles - here is a very simplified example of how a “Dates” object can be made available within an “Applicant” object:
class Applicant { var $dates; function Applicant(&$dates) { $this->dates = &$dates; } } class Dates { } $dates = new Dates(); $applicant = new Applicant($dates); $dates->a = 17; var_dump($dates); var_dump($applicant);
Development Decisions
- Data objects as WI Engine extensions vs. API extensions: we initially discussed making our data objects as API extensions, rather than as WI Engine extensions. We did not fully explore the option of API extensions, since the API was not available to us in this application. It may be something worth a second look in the future.
- Determining when it’s appropriate to use data objects: discuss




