RADICORE - A Development Infrastructure for PHP

RADICORE是一个基于PHP的快速应用开发框架,采用三层架构和MVC设计模式,支持XML/XSL转换生成HTML页面,实现前后端分离。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Rapid Application Development toolkit for building Administrative Web Applications

RADICORE - A Development Infrastructure for PHP

By Tony Marston

2nd August 2003
Amended 15th July 2005

As of 10th April 2006 the software discussed in this article can be downloaded from www.radicore.org

Introduction - Background The 3 Tier Architecture - Data Access layer - Business layer - Presentation layer The Model-View-Controller (MVC) design pattern - The Model component - The View component - The Controller component Infrastructure Design - Forms Families - Structure, Behaviour, Content ... - ... and Style Infrastructure Implementation - Component scripts - Transaction Pattern (Controller) scripts - Generic (abstract) Table class - Database Table (Model) classes - Validation class - DML class - Screen Structure (View) scripts - XML files - XSL Stylesheets - XSL Transformation process - HTML output - CSS files - AUDIT class - Workflow Engine Levels of Reusability Extending this Infrastructure References Amendment History

Introduction

With every programming language I have worked in it has become normal practice, after having developed an initial series of programs, to identify a common structure to which all subsequent programs should be built. This may take some time as it involves a bit of trial and error in playing with the different ways in which a task can be achieved in order to find the methodology that gives the most advantages in the long term. Eventually this development infrastructure/environment should contain the following:-

  • A description as to how the user interfaces should be built so as to provide a consistent look and feel.
  • A list of naming conventions to identify how objects, functions and programs should be named.
  • A description of the directory structure used in the development environment so everybody knows where each type of object is supposed to be located.
  • A description of the strategies to be used when building programs.
  • A library of standard functions that can be used to perform common tasks.

The list can be extended even further, but that will do as a starting point.

Background

The PHP development environment that I have devised was created by taking a set of sample programs which I had assembled for a previous language and rewriting them in PHP. These sample programs were used as patterns or templates for all other programs in my applications as they represented all the combinations of structure and behaviour that I had encountered. To build a new component all I had to do was identify the right template, then specify which database tables(s) and columns I wished to show on the screen for the new component. Each database table was accessed via its own separate service component which contained all the business rules associated with that table plus the code to communicate with the database. As each user screen accessed each database table by its own service component it meant that business logic was shared and not duplicated. Both user screens and service components used shared code in the form of 'include' files, so it was possible to update the shared library and have the changes automatically picked up by the various components.

From the outset my aim was to produce an environment with the following characteristics:-

  • To be based on the 3 Tier Architecture so that the logic for the presentation layer, the business layer and the data access layer would be contained within separate components. This architecture allows the code within any one of the layers to be changed without affecting any of the other layers.
  • To make use of as many standard reusable scripts as possible. I have been long familiar with the advantages of having libraries of reusable subroutines and functions in other development languages, and my previous language, a 4th generation tool called UNIFACE, had the extra ability of components being able to inherit code from component templates.
  • To make use of the OO capabilities of PHP, but only where I saw an advantage in doing so. I do not believe OO is a universal panacea as any old problems which it claims to solve are counter-balanced by new problems which it creates. OOP techniques do not prevent errors, they simply create a new class of error.

What I actually produced is as follows:-

  • As I was familiar with XML and XSL before learning PHP, and because PHP already incorporates modules for those two disciplines, I chose to generate all HTML output via XML/XSL transformations. I did read about other templating engines that are available for PHP, but as these are written in PHP and tied to PHP I rejected them as I wanted something that was totally independent of the underlying language and preferably written to 'open' standards and therefore accessible to a larger community.
  • After building several screens with XSL stylesheets I identified a great deal of common code and moved it to a series of smaller XSL files which can be 'included' at runtime. Now when I build a stylesheet for a component it contains a large number of calls to my library of common templates.
  • All HTML output is written to W3C standards and is therefore browser-independent. The output is actually XHTML 1.0 Strict with all formatting specified via Cascading Style Sheets (CSS). There is no JavaScript as this is not controlled by any W3C standard and there are too many incompatibilities with its implementation between the different browsers. This decision allows the software to work as intended on any browser, the only condition being that the browser complies to W3C standards. If anyone has a non-standard browser then any problems are theirs, not mine.
  • With UNIFACE I had successfully implemented the 3 Tier Architecture by using service components in the middle layer, and as there are distinct similarities between 'components' and 'objects' (see Using PHP Objects to access your Database Tables (Part 1) for details) I found it very easy to build a class for each database table where previously I had used a service component. The ability for a component to inherit code from a component template was replaced by the ability for a class to inherit code from it's superclass.
  • In my first implementation I had a standard set of functions within my abstract database class which dealt with the creation and execution of all DML (Data Manipulation Language) statements. I have subsequently been able to extract all of those functions and place them in a class of their own, thus creating a totally separate DML object in the data access layer.
  • I have thus ended up with a 'true' 3 Tier structure as
    1. Only my data access layer has any form of communication with the database.
    2. All business rules are processed by objects within the business layer.
    3. All interfacing with the user is done by components within the presentation layer.
    4. There is no direct communication between the presentation and data access layers. All communication is routed through the business layer.
  • Although I had not intended to use the Model-View-Controller (MVC) design pattern in my infrastructure (a previous encounter with someone else's disastrous implementation had not convinced me of any benefits), I later realised that what I had produced was a good fit to the MVC principles. It just goes to show that it is not what you implement but how you implement it that is important.

Interestingly enough my decision to have all HTML output generated through XSL transformations instead of directly by PHP code actually paid enormous dividends by producing a great deal more reusable code than I had originally anticipated. I started by creating a script which performed an operation on a database table then wrote a second script to perform the same operation on a different table. I then compared the two scripts to see what was duplicated and could therefore be put into a sharable file, and what was different and which would have to remain in a script of its own. By careful engineering of the code I ended up with the situation where there were basically only two differences:

  • The name of the database table to be accessed.
  • The name of the XSL file to be used to transform the output.

I ended up with three types of PHP script in my presentation layer:

When building the XSL stylesheets I came across common code which I was able to move into separate files as XSL templates (subroutines). These templates can be accessed by any number of stylesheets using the <xsl:include> command. A later improvement meant that instead of having a separate XSL stylesheet for each screen where the field names and field labels were hard-coded I could use a much smaller number of generic stylesheets and have the list of field names and field labels supplied within the XML file. The type of HTML control to be used for each field is written to the XML file as a series of field attributes, and a standard XSL template uses these attributes to generate the correct HTML code.


The 3 Tier Architecture

Any piece of software can be subdivided into the following areas:

  • Presentation logic = User Interface, displaying data to the user, accepting input from the user.
  • Business logic = Business Rules, handles data validation and task-specific behaviour.
  • Data Access logic = Database Communication, constructing SQL queries and executing them via the relevant API.

If you put the code which deals with presentation logic (the generation of HTML documents), business logic (the processing of business rules) and data access logic (the generation and execution of DML (SQL) statements) into a single component then what you have is a single tier structure, as shown in Figure 1.

Figure 1 - 1 Tier architecture

infrastructure-01 (5K)

If you split off all the code that handles the communication with the physical database to a separate component then you have a 2 tier architecture, as shown in Figure 2.

Figure 2 - 2 Tier architecture

infrastructure-02 (8K)

If you go one step further and split the presentation logic from the business logic you have a 3 Tier Architecture, as shown in Figure 3. Note that there is no direct communication between the presentation and data access layers - everything must go through the business layer in the middle.

Figure 3 - 3 Tier Architecture

infrastructure-03 (8K)

When this architecture is implemented the benefits will become apparent as more code can be shared instead of being duplicated. Several components in the presentation layer can share the same component in the business layer, and all components in the business layer share the same component in the data access layer. This is shown in Figure 4. Note also that a presentation layer component can access more than one business layer component, and a business layer component can access other business layer components.

Figure 4 - 3 Tier Architecture in operation

infrastructure-04 (8K)

The big advantage of a 3-tier system is that it is possible to change the contents of any one of the tiers/layers without having to make corresponding changes in any of the others. For example:

  • A change from one DBMS to another would only require a change to the component in the data access layer.
  • A change in the Use Interface, for example from desktop to the web, would only require changes to the components in the presentation layer.

An advantage of having the presentation and business layers written in different languages is that it is possible to use different teams of developers to work on each. That means I can need only PHP skills on the business and data access layers, and (X)HTML, CSS and XSL skills for the presentation layer. It may be easier to find developers with skills in one of these areas rather than both.

Another advantage of using XML/XSL in the presentation layer is that it is possible to switch the output from HTML to WML or PDF or whatever simply by using a different XSL stylesheet. XSL files can be used to transform XML documents into a variety of formats, not just HTML.

The environment which I have created is based on the 3-tier architecture, as shown in Figure 5.

Figure 5 - Environment/Infrastructure Overview

Component ScriptController ScriptDatabase Table ClassAbstract Table ClassValidation ClassDML ClassScreen Structure ScriptXML fileXSL StylesheetXSL Transformation ProcessHTML OutputCSS FileAudit ClassWorkflow Engine infrastructure-05 (12K)

The various components in Figure 5 are described as follows:

  1. Component scripts
  2. Transaction Pattern (Controller) scripts
  3. Database Table (Model) classes
  4. Generic (abstract) table class
  5. Validation class
  6. DML class
  7. Screen Structure (View) scripts
  8. XML files
  9. XSL Stylesheets
  10. XSL Transformation process
  11. HTML output
  12. CSS files
  13. AUDIT class
  14. Workflow Engine

Note that this is proper 3-Tier Architecture, not the pseudo variety as claimed by many who seem to think that the arrangement of web browser, web server and database automatically constitutes a 3 Tier system. It is the construction of the software in the middle that decides whether the system is 1, 2 or 3 Tier. It is only when a system has separate components to deal with the different areas of logic that it can be truly described as 3-tier.

The infrastructure described in this document has the following degrees of separation:-

Data Access layer

This consists of a one or more DML objects which issue the functions to communicate with the physical database(s). There is a separate DML class for each database engine (MySQL, PostgreSQL, Oracle). This is sometimes referred to as a Data Access Object (DAO).

Not only is it possible within the same transaction to access tables in different databases, it is also possible to access tables through different database engines.

This layer also communicates my AUDIT class in order to record all changes made to an application database in a separate 'audit' database so that they can be reviewed using online enquiry screens.

Business layer

This contains a separate table class for each database table or business entity. Each table class is a subclass of a generic table class so that it can inherit as much generic code as possible.

All communication with the physical database is handled by the generic table class through a separate DML class.

Primary data validation is handled by the generic table class through a validation class. Secondary data validation is performed by customised functions within each table class.

This layer also communicates my Workflow Engine in order to determine if a new workflow case needs to be created, and to progress each case through its various stages.

Presentation layer

This contains a separate component script for each transaction or task within the system. This is a simple mechanism which identifies which model, view and controller components to use.

There is a screen structure script (the 'view' component) which identifies which XSL stylesheet to use for the HTML output, and a list of field names with their associated labels.

There is a transaction pattern script (the 'controller' component) which deals with the request (input) and generates the response (output). It does this by communicating with one or more table objects in the business layer. The data from these objects is written out to an XML file which is then converted into HTML output by an XSL transformation process.

The XML file is generated automatically by the transaction pattern script. It will contain the data obtained by the table classes as well as any other data required by that component.

The XSL stylesheet contains the commands which will transform the XML input into HTML output during the XSL transformation process. This will usually be one of the generic stylesheets, although it is possible to create a custom stylesheet for particular circumstances.

The XSL transformation process takes the XML input and transforms it into HTML output using the rules defined within the XSL stylesheet. This is a standard process built into the language.

The HTML output is the text file which is sent back to the client's web browser. This is rendered into a viewable page with the assistance of one or more CSS files.

The CSS files are the recommended way of specifying a standard style in a group of HTML documents.


The Model-View-Controller (MVC) design pattern

It was some time after I had developed this infrastructure that I discovered that it also contained an implementation of the MVC design pattern. This is discussed in more detail in The Model-View-Controller (MVC) Design Pattern for PHP.

Figure 6 - The Model-View-Controller structure

infrastructure-06 (6K)

The Model component

A model is an object representing data or even an activity. In my infrastructure this is implemented as a series of table classes, one for each table in the database. All communication with the physical database is routed through a separate DML Object.

The View component

A view is some form of visualisation of the state of the model. In my infrastructure this is implemented as a series of screen structure scripts which are combined with the output from each database table class to produce an XML file which is then transformed into HTML output by using one of the generic XSL stylesheets. For each database table there is typically a list view (containing multiple occurrences with data arranged horizontally) and a detail view (containing a single occurrence with data arranged vertically).

The Controller component

A controller offers facilities to change the state of the model. It accepts input from the user and instructs the model to perform actions based on that input, then updates the view to show the results of those actions.

In my infrastructure this is implemented as a series of component scripts which link to one of a series of transaction pattern (controller) scripts


Infrastructure Design

Some of my approaches to infrastructure design are based on experiences which I have had in previous languages. It is encouraging to know that some of my design decisions as just as valid now as they were then. It just goes to show that quality is ageless.

Forms Families

Some designers have the peculiar notion that the complexity of a system is directly proportional to the number of components it contains, therefore they try to pack as many functions as possible into a single component. In order to maintain the contents of a typical database table it is usual to provide the following functionality:

  • The ability to browse through all or selected occurrences (rows).
  • The ability to define selection criteria in order to retrieve selected occurrences.
  • The ability to create/insert new occurrences.
  • The ability to read/enquire the details of existing occurrences.
  • The ability to amend/update existing occurrences.
  • The ability to delete existing occurrences.

It is possible to put all this functionality into a single component, but the end result is a very large, very complex component. If this approach is duplicated throughout the entire system the end result is a collection of very large, very complex components. In my experience the size of a component is directly proportional to the amount of effort needed to maintain it, so smaller is better.

The alternative approach, one which I first found to be successful when COBOL was my primary language and which was just as successful when I switched to UNIFACE, is to provide each of these facilities in a separate component. This may produce a large number of components, but at least they are small and simple. The arguments for the 'small and simple' approach against the 'large and complex' are explored in more detail in my article Component Design - Large and Complex vs. Small and Simple.

When I read that when designing components for web pages the 'small and simple' approach was preferred over the 'large and complex' this did not pose a problem for me as this has been my design philosophy for 20 years.

Now when I want to write software to maintain the contents of a typical database table I build a 'family' of small components where each one performs just one of the previously mentioned functions. This produces a family of components (sometimes referred to as forms or screens) with the structure shown in Figure 7. Each of these components has its own Transaction Pattern (Controller) script.

Figure 7 - A typical Family of Forms

infrastructure-07 (1K)

In this structure the LIST (parent) component is the only one that is available on a menu button - all the other child components can only be selected from a navigation button within a suitable parent component. In most cases the child component will need the primary key of an occurrence in the parent component before it can load any data on which it is supposed to act. In this case the required occurrence in the parent screen must be marked as selected using the relevant checkbox before the hyperlink or control button for the child component is pressed.

Another difference between these components is that the LIST component shows multiple rows of details, one occurrence per row, whereas the others will show the details for a single occurrence. As the layout for the SEARCH, INSERT, UPDATE, DELETE and ENQUIRE screens is extremely similar I have managed to provide for their construction with a single XSL file. This means that for each database table I only need 2 XSL files, as shown in Figure 8.

Figure 8 - XSL files required for a family of forms

infrastructure-08 (3K)

This is made possible as one of the parameters used in the XSL transformation process is $mode. This is used within the XSL stylesheet to determine if each field can be input/amended by the user or should be display only.

In my original infrastructure each database table required its own version of the list.xsl and detail.xsl files as the table, field names and field labels had to be hard-coded inside them, but I have since enhanced my XSL library so that a small number of generic stylesheets can be used for any number of database tables. This is documented in Reusable XSL Stylesheets and Templates and uses updated versions of my std.list1.xsl and std.detail1.xsl stylesheets which allow the table name(s), field names and field labels to be passed in as part of the XML data.

Structure, Behaviour, Content ...

In my long career as a software engineer I have written countless hundreds of components, and many times I have come across the situation where I have been asked to create a new component which is "just like that one, but which works on this set of data". In this situation it is necessary to identify those parts of the original component which can be reused 'as is' and those parts which have to be altered. In order to do this I break down each component into the following areas:

  • Structure - how many tables or objects it deals with, arranged in different areas or zones, and how they are arranged in relation to one another.
  • Behaviour - what action it performs, such as listing multiple rows, or creating/reading/updating/deleting a single row.
  • Content - which database table(s) and field(s) does it deal with.

The trick now is to make different templates or patterns based on a particular combination of structure and behaviour so that when you build a component from a template all you have to do is specify the content. No two languages provide the same method of creating reusable templates, so what works in one particular language may be totally impossible in another. With PHP the method I have devised is to produce scripts in two categories - Screen Structure scripts which identify the content and reusable Transaction Pattern (Controller) scripts which deal with the structure and behaviour.

... and Style

A feature of HTML documents is that the visual presentation of each page can be altered quite easily. By 'visual presentation' I mean any of the following:

  • Fonts - different parts of the page can use different fonts of different sizes.
  • Colours - different parts of the page can use different foreground and/or background colours.
  • Images - background colours can be replaced by background images.
  • Positioning - elements of a page may be positioned using either absolute or relative co-ordinates.

Although it is possible to include all style specifications within an HTML document it is not considered to be good practice. The most efficient method is to extract all style specifications and keep them in a separate Cascading Style Sheet (CSS) file or files. In this way it is possible to update the contents of a single CSS file and have that change automatically inherited by all the pages which reference the styles defined within that CSS file. Without the use of a CSS file it would be necessary to update each page individually, which on a large site could be a long and laborious process.

The term 'cascading' means that an HTML document can actually refer to a series of CSS files. These files will be scanned in the order in which they were defined and their contents merged so that a single specification is the result. In my infrastructure I use several CSS files


Infrastructure Implementation

Although this infrastructure appears to be quite complex due to the large number of components, each component is responsible for just a small area and is therefore relatively simple. The trick is to know which components have to be created by the developer, which components have already been written and are available for immediate use, and how they all hang together.

Component scripts

This is item (1) in Figure 5.

Each script in this category simply specifies the Model and View before handing control over to a particular Controller.

Sample scripts for each pattern can be found in the /radicore/default/ directory.

Here is an example of one of my scripts:

<?php
$table_id = "person";                      // identify the Model
$screen   = 'person.detail.screen.inc';    // identify the View
require 'std.enquire1.inc';                // activate the Controller
?>

As you can see this is a simple script whose purpose is to identify the following:

  1. $table_id identifies the Model part of MVC. This is one of the generated database table classes.
  2. $screen identifies the View part of MVC. This is one of the generated screen structure scripts.
  3. include identifies the Controller part of MVC. This is one of the pre-written controller scripts.

Transaction Pattern (Controller) scripts

This is item (2) in Figure 5.

This can also be referred to as a Transaction Controller or a Page Controller.

Each controller script contains the code to perform a particular action on an unspecified database table. For example:

  • The LIST1 controller will allow the user to browse the contents of a database table.
  • The SEARCH1 controller will allow the user to specify selection criteria which will be used by the LIST transaction to filter its results.
  • The ADD1 controller will allow the user to add a record.
  • The ENQUIRE1 controller will allow the user to view the contents of a selected record.
  • The UPDATE1 controller will allow the user to update the contents of a selected record.
  • The DELETE1 controller will allow the user to delete a selected record.

For a full list of my Transaction Patterns please refer to Transaction Patterns for Web Applications.

Each script in this category is based on a particular combination of structure and behaviour. The actual content is identified by the Screen Structure script which is set in the calling Component script. This means that one of these Controller scripts may be called by many different Component scripts, as shown in Figure 9.

Figure 9 - Many Component scripts to one Transaction Pattern (Controller) script

infrastructure-09 (2K)

None of these Controller scripts outputs any HTML output directly. What is actually produced is an XML file which is transformed into HTML using a separate XSL stylesheet.

Here is an example of one of my scripts:

<?php
// name = std.enquire1.inc

// type = enquire1

// This will display a single selected database occurrence using $where
// (as supplied from the previous screen)

require 'include.inc';

// identify mode for xsl file
$mode = 'enquire';

// initialise session
initSession();

// look for a button being pressed
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
   if (isset($_POST['finish']) or (isset($_POST['finish_x']))) {
      // cancel this screen, return to previous screen
      scriptPrevious();
   } // if
} // if

// create a class instance for the main database table
require "classes/$table_id.class.inc";
$dbobject = new $table_id;

$dbobject->sql_select  = &$sql_select;
$dbobject->sql_from    = &$sql_from;
$dbobject->sql_where   = &$sql_where;
$dbobject->sql_groupby = &$sql_groupby;
$dbobject->sql_having  = &$sql_having;
// check that primary key is complete
$dbobject->checkPrimaryKey = TRUE;

// define action buttons
$act_buttons['finish'] = 'FINISH';

// retrieve profile must have been set by previous screen
if (empty($where)) {
   scriptPrevious('Nothing has been selected yet.');
} // if
   
// get data from the database
$fieldarray = $dbobject->getData($where);
   
if ($dbobject->getErrors()) {
   // some sort of error - return to previous script
   scriptPrevious($dbobject->getErrors());
} // if
	
// check number of rows returned
if ($dbobject->getNumRows() < 1) {
   scriptPrevious('Nothing retrieved from the database.');
} // if

// use contents of first row only
$fieldarray = $fieldarray[0];
   
// rebuild selection using primary key of retrieved row
$where = array2where($fieldarray, $dbobject->getPkey());

// get any extra data and merge with $fieldarray
$fieldarray = array_merge($fieldarray, $dbobject->getExtraData($fieldarray));
	
// build list of objects for output to XML data
$xml_objects[]['root'] = &$dbobject;

// build XML document and perform XSL transformation
buildXML($xml_objects, $errors, $message);

?>

Please note the following points:

  • All files containing classes for database tables are in the format '<table_id>.class.inc', so all the Component script need do is supply a value for $table_id and the Controller script can create an object from that class.
  • By setting the object variable checkPrimaryKey to TRUE I will trigger the code to check that the $where string contains values for all fields which make up the primary key for this database table. The primary key details are specified in the $fieldspec array.
  • Each database table class contains a standard getData method which returns the $fieldarray array of rows (starting at row zero), and each row contains an associative array of fieldname=fieldvalue pairs.
  • By default the getData method will select all columns from the specified table, but this can be altered to specify any number of columns from any number of tables by settings in the $sql_??? variables.
  • The entire contents of $fieldarray will be output to the XML file. Note that no field names need be specified in the script as they are all extracted from $fieldarray.
  • The $fieldspec array from the database object may contain entries which will be included in the XML output as attributes in order to affect the outcome of the XSL transformation. For example noedit will cause a field to be read-only, while nodisplay will cause the field to be excluded from the HTML output altogether.

Here is a brief explanation of my user-defined functions which are contained within file include.inc:

  • initSession() - carries out all processing required when a script starts. This includes reading the session data to obtain the contents of the variable $where which contains any selection criteria passed down from the previous script.
  • scriptPrevious() - will return processing to the previous script with an optional error message. Note that this is not the same as pressing the browser's back button.
  • array2where() - transforms an associative array into a string which can be used as the WHERE clause in an SQL SELECT statement. The first parameter is the array, the optional second parameter identifies the subset of fields to be included.
  • buildXML() - takes the $xml_objects array and transfers all the data to an XML file, after which it will perform the XSL Transformation process using the specified XSL stylesheet to produce the HTML output.

You should notice that the above script does not contain any hard-coded database, table or field names, therefore it can be used for any database table within the system. The points to consider are:

  • The name of the table to work on is passed down in the $table variable which is set by the component script.
  • That variable name is used to obtain a class definition from an 'include' file, and an object is instantiated from that class with a generic name, which in this case is $object.
  • The controller communicates with the object using standard method names which are common to all objects as they are inherited from the generic table class.
  • The data which comes out of the object is a standard associative array of fieldname=fieldvalue pairs. This is processed by a standard function which simply copies the entire array out to the XML file in an equivalent fieldname=fieldvalue format.

I have some controller scripts which work on more than one database object, such as when dealing with a parent-child relationship or a many-to-many relationship, but the principles are exactly the same.

Generic (abstract) table class

This is item (4) in Figure 5.

This is an abstract class which is based on the ideas outlined in Using PHP Objects to access your Database Tables (Part 1) and (Part 2). It is called an 'abstract' class as it does not contain any implementation details and therefore cannot be instantiated into an object. Implementation details for each physical database table are supplied in separate subclasses, and it is only these subclasses that can be instantiated into objects.

This abstract database class contains generic methods and properties which can be used by any database table in the system. These methods and properties are automatically inherited by every subclass. Code which is specific to any particular table must be built into in the subclass for that particular table.

This generic class also contains the following:

  • It contains the standard methods and properties which are accessed by the Transaction Pattern (Controller) scripts. Amongst these are:
    • getData ($where) - will retrieve any number of records from the database using the specified WHERE criteria. The sql SELECT statement will be constructed automatically from variables supplied at runtime. The result is an associative array of 'name=value' pairs, indexed by row number.
    • insertRecord ($fieldarray) - will insert a single row using the contents of $fieldarray, which is an associative array of 'name=value' pairs. The sql INSERT statement will be constructed automatically from the contents of $fieldarray.
    • updateRecord ($fieldarray) - will update a single row using the contents of $fieldarray, which is an associative array of 'name=value' pairs. The sql UPDATE statement will be constructed automatically from the contents of $fieldarray.
    • deleteRecord ($fieldarray) - will delete a single row using the contents of $fieldarray, which is an associative array of 'name=value' pairs. The sql DELETE statement will be constructed automatically from the contents of $fieldarray.
  • It passes the input array (usually the $_POST array) to the validation class for all input and update operations. Error messages are returned in the $errors array
  • When actions on the physical database are required these are passed to the DML class for processing.
  • During a delete operation it will use the contents of the $child_relations array to check that the record can be deleted and to update/delete all records from child tables if necessary.
  • It also contains a series of empty methods which are accessed in set places within the processing cycle. These can be replaced in a subclass with versions that contains custom code, and at runtime the customised method in the subclass will be executed instead of the empty method in the superclass.

Database Table (Model) classes

This is item (3) in Figure 5.

Each database table (or business entity) is represented by its own class which extends (is a subclass of) the generic table class.

Although each table subclass will inherit all the generic code from the superclass, in order to be usable it must also contain information which is specific to the database table to which it relates. This information is as follows:

  • class constructor - this defines the basic settings for the table such as:
  • $fieldspec array - this identifies each field (column) in the table, and for each field it describes its characteristics (type, size, et cetera) so that primary validation can be performed via the validation class when inserting or updating records. This array also contains information which is used by the XSL transformation process to identify what control (text box, dropdown list, radio group, etc) should be used for each field in the HTML output.
  • $primary_key array - used to define the field(s) which provide the primary means of uniquely identifying each record in a table. Most databases do not allow primary keys to be changed, so they must be validated during insert only.
  • $unique_keys array - used to enable the validation of candidate keys (optional unique keys in addition to the primary key). Unlike the primary key a candidate key can be changed, so it must be validated during both insert and update.
  • $parent_relations array - this identifies any parent tables which are related to this table in a parent-child (one-to-many) relationship. This is used when any data from the parent table needs to be added to the query result. The framework is capable of adding in the necessary JOIN clause when the query is constructed.
  • $child_relations array - this identifies any child tables which are related to this table in a parent-child (one-to-many) relationship. This is used when any record is deleted to enforce basic referential integrity.
  • custom processing - within the generic table class are a series of abstract methods which exist in the processing flow but which are empty of any code. Versions containing custom code can be created in each subclass which at runtime will replace the empty abstract methods in the superclass. These abstract methods provide the ability to perform custom processing at predetermined places in the standard processing flow or to carry out secondary data validation which cannot be performed by the generic validation class.

Note that this class can deal with any number of database rows - I do not have one version to deal with a single row and a second version to deal with a collection of rows.

The class file for each database table does not have to be generated by hand - with the introduction of A Data Dictionary for PHP Applications it is possible to import the table structures directly from the database schema into the data dictionary, then to export those structures into files which can be accessed directly by the application code.

Validation class

This is item (5) in Figure 5.

The generic validation class handles primary validation (sometimes referred to as declarative checking) of all user input. It compares the contents of the input array with the contents of the $fieldspec array to check that the input data for each field conforms to that field's specifications. It puts any error messages in the current object's $errors array.

The input array (usually the $_POST array) is an array of fieldname=fieldvalue pairs.

The $fieldspec array is an array of fieldname=fieldspec pairs. The fieldspec portion is another associative array of keyword=value pairs.

The $errors array is an array of fieldname=errormsg pairs. It can therefore contain error messages for any number of fields.

Primary validation is limited to the following checks:

  • That all required fields have a non-null value.
  • That fields do not exceed their maximum size.
  • That date fields contain valid dates.
  • That time fields contain valid times.
  • That numeric fields contain valid numbers, with options for minimum/maximum values and number of decimal places.
  • A string field may have an optional subtype of email_address which causes the string to be checked against the relevant pattern.
  • A string field may have an optional subtype of file which causes a check to ensure that a file with that name exists.

Secondary validation, such as comparing one field against another, must be defined within the individual table subclass using the empty classes provided in the superclass.

It is also possible to supplement the generic validation with the addition of plug-ins, as described in Extending the Validation class.

DML class

This is item (6) in Figure 5.

This is the only object in the system which carries out any communication with the physical database. It receives requests from the Generic Table class from which it generates the appropriate DML (Data Manipulation Language) or SQL (Structured Query Language) commands. It then executes these commands by calling the relevant API for the database in question.

As this object exists in the Data Access layer it is sometimes referred to as the Data Access Object (DAO).

There is a separate class for each database engine as each engine has its own set of APIs. This design also allows me to isolate and deal with any differences between the various engines. The name of the class file is in the format dml.???.class.inc where '???' can be mysql, postgresql, oracle or whatever. A default value for $dbms_engine is defined within the Generic Table class although this can be overridden in any individual table subclass. At runtime a DML object is instantiated from the relevant DML class according to the value in $dbms_engine. This means that it is possible to switch from one database engine to another simply by changing the value in $dbms_engine either globally for all tables or locally for individual tables.

This class is based on the ideas outlined in Using PHP Objects to access your Database Tables (Part 1) and (Part 2). Some of the methods it contains are as follows:

  • getData ($dbname, $tablename, $where) - will retrieve any number of records from the database using a SELECT statement which is constructed as required. The result is an associative array of 'name=value' pairs indexed by row number. There are pagination options which break down large volumes of data into separate pages for display purposes.
  • insertRecord ($dbname, $tablename, $fieldarray) - will insert a single row using the contents of $fieldarray (usually the $_POST array). A check is made before the INSERT to ensure that the primary key and any candidate keys are currently unused.
  • updateRecord ($dbname, $tablename, $newarray, $oldarray) - will update a single row using the contents of $newarray (usually the $_POST array). This is first compared with $oldarray (the current database values) so that only those fields which have changed are included in the DML statement. The identity of the primary key for use in the WHERE clause is extracted using the contents of the $fieldspec array. If any candidate key has changed it is first checked for uniqueness.
  • deleteRecord ($dbname, $tablename, $fieldarray) - will delete a single row using the contents of $fieldarray. The identity of the primary key for use in the WHERE clause is extracted using the contents of the $fieldspec array.

Note that within a single transaction it is possible to access tables in more than one database and through more than one database engine.

Screen Structure (View) scripts

This is item (7) in Figure 5.

These are simple scripts which do nothing but identify the view or content for the output screen. Each one identifies the name of an XSL stylesheet and a list of table names, field names and field labels that will be used during the XSL transformation process to produce the HTML output.

Sample scripts for each pattern can be found in the /radicore/default/screens/en/ directory with the name <pattern>.screen.inc.

Scripts for each subsystem can be found in the /radicore/<subsystem>/screens/<language>/ directory. The default value for <language> is 'en' (English), but other language codes can be used - refer to Internationalisation and the Radicore Development Infrastructure for details.

Although the parent LIST screen in Figure 7 will require its own Screen Structure file, all the CHILD screens can share the same one as they all use the same structure. The differences in how the fields are displayed for each of the child components is handled by a combination of the $mode parameter within the Transaction Pattern (Controller) script (insert, update, delete, enquire) and individual field attributes within the XML file. These attributes can be specified within the $fieldspec array for that table class, or can be supplied at runtime through custom code.

Here is a sample file:

<?php
// this identifies which XSL stylesheet to use
$structure['xsl_file'] = 'std.detail1.xsl';

// this identifies which XML data is to go into which XSL zone
$structure['tables']['main'] = 'person';

// this specifies the width of each column
$structure['main']['columns'][] = array('width' => 150);
$structure['main']['columns'][] = array('width' => '*');

// the following may also be used 
$structure['main']['columns'][] = array('class' => 'classname');

// this identifies the label and field which is to be displayed in each row
$structure['main']['fields'][] = array('person_id' => 'ID');
$structure['main']['fields'][] = array('first_name' => 'First Name');
$structure['main']['fields'][] = array('last_name' => 'Last Name');
$structure['main']['fields'][] = array('initials' => 'Initials');
$structure['main']['fields'][] = array('nat_ins_no' => 'Nat. Ins. No.');
$structure['main']['fields'][] = array('pers_type_id' => 'Person Type');
$structure['main']['fields'][] = array('star_sign' => 'Star Sign');
$structure['main']['fields'][] = array('email_addr' => 'E-mail');
$structure['main']['fields'][] = array('value1' => 'Value 1');
$structure['main']['fields'][] = array('value2' => 'Value 2');
$structure['main']['fields'][] = array('start_date' => 'Start Date');
$structure['main']['fields'][] = array('end_date' => 'End Date');
$structure['main']['fields'][] = array('selected' => 'Selected');
?>

In this example there is a single data zone called main which is linked with an object called person. Some screens have two or more zones which are linked to different objects. At runtime the fields will be extracted from each object and displayed in the relevant zone. Note that a field must exist both within the object and within the screen structure file in order for it to be dislayed.

The name of this file is provided by the Component script in the $screen variable. It is read in by the Transaction Pattern (Controller) script and its contents are added to the XML file to appear something like this:

<root>
  ......
  <structure>
    <main id="person">
      <columns>
        <column width="150"/>
        <column width="*"/>
      </columns>
      <row>
        <cell label="ID"/>
        <cell field="person_id" />
      </row>
      <row>
        <cell label="First Name"/>
        <cell field="first_name"/>
      </row>
      <row>
        <cell label="Last Name"/>
        <cell field="last_name"/>
      </row>
      <row>
        <cell label="Initials"/>
        <cell field="initials"/>
      </row>
      
      ....

      <row>
        <cell label="Start Date"/>
        <cell field="start_date"/>
      </row>
      <row>
        <cell label="End Date"/>
        <cell field="end_date"/>
      </row>
    </main>
  </structure>
</root>

Several different layouts are now available for displaying user data. For more details on how these can be specified please refer to XSL Structure files in The Model-View-Controller (MVC) Design Pattern for PHP.

XML files

This is item (8) in Figure 5.

XML (Extensible Markup Language) is a simple but flexible text format. It is based on an open standard which is maintained by the World Wide Web Consortium. It is used in this infrastructure to provide the XSL transformation process with all the data it needs to produce the HTML output.

The XML file is generated automatically at runtime by the Transaction Pattern (Controller) script. The technique which is used to create this file in PHP 4 is described in Using PHP 4's DOM XML functions to create XML files from SQL data. For PHP 5 refer to Using PHP 5's DOM functions to create XML files from SQL data instead.

Each XML file can contain any of the following data:

XSL Stylesheets

This is item (9) in Figure 5.

Each component requires an XSL stylesheet in order to transform the data in the XML file into HTML output. In an earlier version of this infrastructure I used different stylesheets for each database table which had the table names, field names and field labels all hard-coded, but I have subsequently found a way to use a smaller number of generic stylesheets. Instead of having the field details hard-coded within the stylesheet I am now able to extract that information from within the XML file using information supplied in a Screen Structure script. This is is documented in Reusable XSL Stylesheets and Templates.

Although my whole web application uses fewer than ten generic stylesheets there is still some code which is needed in more than one stylesheet. This code has been extracted and placed in a library of XSL templates which can be incorporated into any stylesheet at runtime by means of an <xsl:include> command. This is, in effect, a library of standard XSL subroutines.

Using the components in Figure 7 as an example I would use a generic LIST stylesheet for the parent component and a generic DETAIL stylesheet for all the child components. Variations in how the individual fields are displayed within the various child components is handled primarily by the $mode variable which is passed as a parameter during the XSL transformation process. This is used as follows:

  • If $mode = 'input' or 'search' then all fields are editable.
  • If $mode = 'read' or 'delete' then all fields are non-editable.
  • if $mode = 'update' then primary key fields are non-editable.
  • If $mode = 'search' then any boolean fields are given a third option to emulate a tri-state checkbox (yes, no, undefined).

In addition to the $mode parameter the handling of individual fields can be affected by specific attributes in the XML file. These can either be set into the $fieldspec array or altered at runtime using custom code.

  • The noedit attribute will make the field non-editable.
  • The nodisplay attribute will make the field invisible.

The type of HTML control (textbox, dropdown, radio group, etc) to be used for each field in the HTML output is completely dynamic in nature. This is a 3 stage process:

XSL Transformation process

This is item (10) in Figure 5.

This process will take the contents of an XML document and transform it to another document (in this case an HTML document) using rules contained within an XSL stylesheet. These are all open standards which are supervised by the World Wide Web Consortium.

It is possible to send both the XML and XSL files to the client and have the transformation performed within the client's browser (client-side transformation), but this is unreliable due to the different levels (sometimes non-existent) of XML/XSL support in different browsers. It is much safer to perform the transformation in a single place (the web server) where the software is under the control of the web developer. This is known as a server-side transformation.

The technique which I use to perform XSL transformations in PHP 4 is described in Using PHP 4's Sablotron extension to perform XSL Transformations. For PHP 5 refer to Using PHP 5's XSL extension to perform XSL Transformations instead.

HTML output

This is item (11) in Figure 5.

This is the document which is sent back to the client's browser is response to the request. Its content should conform to the (X)HTML specification which is supervised by the World Wide Web Consortium.

In an effort to make my output viewable on as many web browsers as possible I stick to the following guidelines:

  • All output is XHTML 1.0 Strict which is structurally clean and free of any style details. All style specifications (fonts, colours and layout) are held within separate CSS files.
  • There is no javascript (some users have javascript disabled).
  • There are no third party controls or plugins (ActiveX or Flash).
  • There are no proprietary extensions.

HTML documents can be validated by the W3C Markup Validation Service.

CSS files

This is item (12) in Figure 5.

These are Cascading Style Sheets which hold all the styling information (fonts, colours, sizes, positioning, etc) for all HTML documents produced by the application. The tags within each HTML document refer to a style by a class name, and the specifications for each of these classes is held within a CSS file. In this way it becomes possible to change the style specifications for any tag in all documents simply by changing the specifications within a single CSS file.

The following CSS files are available:

  • global - a selection of files exist within the CSS subdirectory which set the style for the entire application. It is possible to choose any one of these using the style/theme option in the Update Session data screen. Additional CSS files may be created and copied into this subdirectory, in which case they will automatically become available for selection.
  • local - if it is required to change the global setting for a CSS element, or to create a new CSS element within a single subsystem, then instead of creating a complete global CSS file it is possible to insert these local modifications into a local CSS file called 'style_custom.css', which exists in every subdirectory. When the HTML document is rendered it will use the contents of both the global and local CSS files. If any setting exists in both files then the local setting will override the global setting.

CSS files can be validated using the W3C CSS Validation Service.

AUDIT class

This is item (13) in Figure 5.

This class is responsible for detecting all database changes (INSERTs, UPDATEs and DELETEs) and recording them in a separate 'audit' database so that they can be reviewed using online enquiry screens. This is documented in Creating an Audit Log with an online viewing facility.

The only additional code required in any database table class is the setting of a class variable called $audit_logging. By default this is TRUE (the table will be logged) but it can be set to FALSE to disable logging.

Workflow Engine

This is item (14) in Figure 5.

Sometimes when a particular task is performed, such as 'Take Customer Order', this has to be followed by a series of other tasks such as 'Charge Customer', 'Pack Order' and 'Ship Order'. Without a Workflow Engine these subsequent tasks must be processed manually, which is where mistakes and inefficiencies can arise.

The purpose of a Workflow System is to manage these tasks in a controlled fashion. This system should have the following components:

  • A method whereby different Workflow processes can be defined. This must identify the triggering task and the sequence of subsequent tasks.
  • A mechanism which automatically creates a new workflow case when a triggering task is processed, then progresses that case through its various stages.
  • A method whereby outstanding tasks (workitems) in workflow cases which require human intervention appear in a list which prompts the relevant users that intervention is required. A task should be activated simply by clicking on its entry in this list.
  • A method whereby the status of any individual workflow case can be reviewed.

The Workflow Engine which I have created as an extension to this development infrastructure is documented in An activity based Workflow Engine for PHP. The engine is activated from within my generic table class therefore no additional programmer coding is required.


Levels of Reusability

The single aspect of any development infrastructure which enables Rapid Application Development (RAD) is the volume of reusable code that it contains. Reusable code in the form of a library of standard modules provides the following advantages:

  • The library modules contain code which has already been written to provide certain functionality, therefore it is not necessary to waste more time in writing more code to provide the same functionality.
  • The library modules contain code which has already been tested, therefore this cuts down the testing phase of new components which use these modules.
  • By using standard library modules for standard tasks the developer does not have to waste time in reinventing the wheel (and possibly creating a square wheel).
  • By using standard code to provide certain functionality it means that the same functionality will be provided in a consistent manner across multiple components. This will be less confusing to the user.
  • By using standard library modules it becomes possible to make a change in a single module and have that change immediately incorporated into every component that references that module.

Although the infrastructure described within this document contains a large number of components it should be pointed out that the majority of them do not require any additional effort on the part of the developer. These are:

The following components are generated automatically at runtime and do not require any programmer effort:

This leaves the following components for the developer to work on:

An idea of the amount of reusability can be shown in figure 10:

Figure 10 - Levels of Reusability

infrastructure-10 (9K)

This shows the following:

I have spent 20+ years working for software houses where the task has been to develop many different applications for many different customers in a swift and cost-effective manner. To be truly reusable an infrastructure should not only work for different components within the same application but for different applications entirely. This offers two advantages:

  • It avoids the necessity of constructing a new infrastructure for each new application.
  • It avoids the non-productive time in learning a new infrastructure when developers switch from one application to another.

I have witnessed at first hand the advantages of being able to use such infrastructures as I have used them with two entirely different languages - COBOL and UNIFACE. As I was personally responsible for creating those two infrastructures I had all the relevant experience to create a new one in PHP.


Extending this Infrastructure

Having an infrastructure with which you can build applications is one thing, but in my long experience I have also found it useful to build utility components which may be of benefit to those applications. This type of component is not specific to any particular application, but may be used in conjunction with any number of different applications. Amongst those I have built are:


References


© Tony Marston
2nd August 2003

http://www.tonymarston.net
http://www.radicore.org

Amendment history:

15 July 2005Added a reference to a new article entitled Internationalisation and the Radicore Development Infrastructure.
21 June 2005Amended Screen Structure (View) scripts to show the new layouts caused by the provision of more flexible options.
17 June 2005Added a reference to a new article entitled A Data Dictionary for PHP Applications.
16 Sep 2004Added a reference to a new article entitled An activity based Workflow Engine for PHP.
10 Sep 2004Added section Extending this Infrastructure.
10 Aug 2004Added better descriptions for the individual components. Moved all the Frequently Asked Questions to a separate document.
03 June 2004Added a section on Style which explains the advantages of using Cascading Style Sheets.
02 May 2004Added a reference to The Model-View-Controller (MVC) Design Pattern for PHP.
28 Apr 2004Added a reference to Reusable XSL Stylesheets and Templates which describes a method which enables me to use a single generic stylesheet for many database tables instead of having a customised stylesheet for each individual database table.
10 Nov 2003Created a sample application to demonstrate the techniques described in this document. This is described in A Sample PHP Application. The code can be run on my website here, or can be downloaded here and run locally.
08 Sep 2003Split my abstract database class again so that the code which performs generic validation (declarative checks) is now contained within its own class. Refer to Business layer for details.
31 Aug 2003Split my abstract database class into two so that the construction and execution of all DML statements is now contained within its own class. Refer to Data Access layer for details.

counter

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值