2nd August 2003
Amended 1st March 2010
Since I wrote my original article I have received comments from various people, either via personal e-mail or through postings in the PHP newsgroup, concerning the efficacy of my endeavours. Some people ask intelligent questions while others say "your work is no good because it cannot do so-and-so". These people either haven't studied my work or cannot work out how it is done because I use a technique which is totally different from theirs. I cannot help being different because it is only by being different that I have a chance to be better, but I can explain some of the finer points of my approach in the hope that it may bring enlightenment to a few confused minds.
NOTE: Although I do not use any javascript in the core framework I have provided the ability for developers to add javascript into their own application subsystems should they so desire. Please refer to Can I add javascript to my application? for details.
Before I answer this, what exactly is a Front Controller? Is his book Patterns of Enterprise Application Architecture Martin Fowler offers this definition:
The term 'Web handler' refers to the logic which examines each incoming HTTP request to gather just enough information to know what to do with it while the 'command hierarchy' is some kind of organisational structure which the Front Controller can refer to, to decide what to do next, based on the information gathered by the 'Web handler'.
As a design pattern the Front Controller is described as:
The Front Controller design pattern defines a single component that is responsible for processing application requests. A front controller centralizes functions such as view selection, security, and templating, and applies them consistently across all pages or views. Consequently, when the behavior of these functions need to change, only a small part of the application needs to be changed: the controller and its helper classes.
Figure 1 - Diagram of a single Front Controller
A Front Controller would be invoked using a URL such as http://www.blah.com/controller.php?action=blah which will then do something, such as redirect to another script, depending on the value of the action parameter.
A front controller may be common practice with compiled languages for the simple reason that a compiled language normally produces a single executable program. When that program is executed processing always starts from a single fixed point, and can only branch to a particular subprogram by code at that start point.
PHP is not a compiled language, and the components within an application are not combined into a single executable with a single start point. PHP components are small, self-contained scripts, and it is possible for the web server (a separate product such as Apache) to activate any of these scripts directly without having to pass through a single control script beforehand. The URL http://www.blah.com/blah.php will cause the web server to invoke the script blah.php without any intermediate steps. If the web server can execute the desired page immediately, why should I introduce a potential bottleneck in the form of a front controller? Instead of a single Front Controller for the whole application I have a series of separate Transaction Controllers (aka Page Controllers), as shown in the following diagram:
Figure 2 - Diagram of multiple Transaction Controllers
This structure works as follows:
One argument put forward for using a front controller is that it becomes easy to perform 'standard' processing before invoking each individual page. This argument is pretty weak considering that it is also possible to perform any 'standard' processing as the very first action within each page (transaction) controller as soon as it has been activated. Provided that this 'standard' processing is performed before the page controller performs any other actions the result is the same.
A Front Controller may be used by some people to solve their particular problems, but as far as I am concerned it is not the only solution and it is certainly not the best solution, especially when I don't have those problems in the first place:
As I can achieve all the commonality and reusability I desire without using a Front Controller I consider its use to be superfluous, redundant, unnecessary and a complete waste of time. I am not the only one who shares this opinion - take a look at The Front Controller and PHP.
First, here are some definitions:
$foo = $object->getFoo();
$object->setFoo($foo);
After working with PHP for a short while I noticed that data coming from the client arrives in the format of an associative array (refer to $_POST and $_GET). I discovered that I could pass the whole array into the object with a single method, as in
$object->insertRecord($_POST);
Inside the class it is just as easy to examine a variable with
$this->array['name']
as it is with
$this->name
without any loss of functionality. This avoids the need to unpick the array and pass in each field one at a time, which uses less code and which is therefore more efficient. It also means that the component which feeds the data into an object can do so with a single generic method instead of requiring knowledge of the particular setters within that object. This is how I put the principal of polymorphism into practice.
Here is an example of code written the 'traditional' way within a controller that accesses an object:-
<?php $client = new Client(); $client->setUserID ( $_POST['userID' ); $client->setEmail ( $_POST['email' ); $client->setFirstname ( $_POST['firstname'); $client->setLastname ( $_POST['lastname' ); $client->setAddress1 ( $_POST['address1' ); $client->setAddress2 ( $_POST['address2' ); $client->setCity ( $_POST['city' ); $client->setProvince ( $_POST['province' ); $client->setCountry ( $_POST['country' ); if ($client->submit($db) !== true) { // do error handling } ?>
This is the code that I use in my controller:-
<?php $dbobject = new Client; $dbobject->updateRecord($_POST); $errors = $dbobject->getErrors(); ?>
What are the benefits of my method?
I am not the only one who thinks this way. Take a look at Why getter and setter methods are evil.
Similarly the data coming out of the database can easily be converted into an associative array, as in
$result = mysql_query($query, $link); // convert result set into a simple associative array for each row while ($row = mysql_fetch_ass($result)) { $array[] = $row; } // while
As the data array which is retrieved by an object does not need to be unpicked into individual fields it means that the receiving component can achieve this with a single generic method, as in
$array = $object->getData($where);
Nothing is done with this data array except to pass it as-is to a function which simply writes it out to an XML file. This means that the component which receives data from an object can do so with a single generic method instead of requiring knowledge of the particular getters within that object.
In my infrastructure the transaction controllers can feed data into and out of an object without any knowledge of the individual items of data contained within that object. This means that my transaction controllers are not tied to any individual object and can be used on any object. The level of reusability for my generic controllers is therefore far higher than in alternative systems where each individual object needs its own controller as it needs a different collection of getters and setters.
In article More on Getters and Setters the author explains why the use of getters and setters may expose implementation details which in the OO world is not considered to be 'a good thing'. In my method I do not need to know the internal representation of a field - a string, a date, an integer, a float, et cetera - as everything goes in and out as a string. Everything in the $_POST array is a string, and everything I put into the XML file is a string. Any necessary conversion between one data type and another is done within the object using information defined within the object.
Having experienced first hand the benefits of the 3 Tier architecture I wanted to completely separate the business logic from the presentation logic, so I looked for some sort of templating system to generate the HTML output. I had heard several HTML templating systems for PHP (such as Smarty) but I chose XSLT for the following reasons:
During the development of my infrastructure I found that there was nothing I wanted to do with XML/XSL that could not be done (although sometimes it took several attempts to find the right approach). It was also very useful that the order in which I retrieved data from the XML file during the XSL transformation process was not restricted by the order in which that data was written to the XML file in the first place. This meant that I could resequence the output without having to resequence the input.
I also found it very easy to put common code in reusable files, and with a subsequent enhancement I found that instead of having a separate XSL stylesheet for each database table where the table names, field names and field labels were hard-coded I could use a common stylesheet and supply the table names, field names and field labels as part of the XML data. This is documented in Reusable XSL Stylesheets and Templates.
For another opinion on this very subject I invite you to take a look at Proprietary template systems versus the standard - XSLT.
It is only by breaking down the whole thing into small parts that you can create parts that can be reused, and it is the number of reusable parts that makes an infrastructure more efficient for the developer. Although my infrastructure looks complicated with its fourteen different components the most important fact is that each component is responsible for a single aspect of the application, and this produces a level of reusability which is extremely high. This means that the developer need only create a small number of new scripts in order to create working components. The process is documented in FAQ 36.
For screens which show multiple occurrences (rows) from the database it is generally not a good idea to show all available occurrences as that may be a huge number. This will result in a huge screen which the users will probably find to be unmanageable. It is therefore good practice to break down the total number of occurrences into smaller chunks of, say, 10 or 20. These chunks are often referred to as 'pages', hence the term 'pagination'. This facility makes use of the LIMIT and OFFSET clauses of the sql SELECT statement and is described in Pagination - what it is and how to do it. There is a default page size ($rows_per_page) defined for use within each multi-line screen, but this can be overridden by hyperlinks on the navigation bar.
The actual stages are performed in the following sequence:
$numrows - the total number of rows retrieved for this page, which may be less than the value in $rows_per_page.$pageno - the page number actually retrieved, which may be less than the one requested if records have been deleted.$lastpage - the last page number that is available using the current selection criteria.$pageno and $lastpage will be written out to the XML file, and during the XSL transformation process a standard XSL stylesheet will use this information to create the pagination area in the HTML output.
The data in the XML file will resemble the following:
<pagination> <page id="main" numrows="12" curpage="2" lastpage="2"/> </pagination>
A sample of the XSL template used for pagination can be found here.
http://www.domain.com/script.php?page=3.// obtain the required page number (optional) if (isset($_GET['page'])) { $dbobject->setPageNo($_GET['page']); } // if
After a script has run for the first time the contents of each object is serialised into the $_SESSION array so that it can be retrieved, unserialised and reused for all subsequent invocations. This means that any settings (page number, sorting, selection criteria, et cetera) which are established will be reused until they are changed.
As you can see the process is quite straightforward, but it does require some action in the presentation layer as well as some action in the data layer. There are some people who insist that this process should take place entirely in the presentation layer, but I find the notion totally impractical and without merit.
For screens which show multiple occurrences (rows) from the database it is often useful to be able to sort the details in a different order, either by a different column, or descending instead of ascending. This ability is provide in my infrastructure by means of the following:
http://www.domain.com/script.php?orderby=name1.// obtain the 'orderby' field (optional) if (isset($_GET['orderby'])) { $dbobject->setOrderBy($_GET['orderby']); } // if
setOrderBy() method will load the field name into variable $orderby, and the variable $order will toggle between 'ascending' and 'descending'.$orderby and $order will be passed down to the DML object where, if not blank, will be built into the sql SELECT statement.$orderby and $order ('asc' or 'desc') will be added to the <params> area of the XML file. This information will be used by a standard XSL stylesheet to insert a gif image after the selected column heading in the HTML output.After a script has run for the first time the contents of each object is serialised into the $_SESSION array so that it can be retrieved, unserialised and reused for all subsequent invocations. This means that any settings (page number, sorting, selection criteria, et cetera) which are established will be reused until they are changed.
By default the sql SELECT statement which is created when using the getData($where) method on a database object will be as follows:
$query = "SELECT * FROM $this->tablename $where_str"
If a value has been supplied in the $where parameter then $where_str will contain WHERE ..., otherwise it will be empty. As you can see this will result in all columns being retrieved from a single table, but what happens if the developer wants something more complicated?
If any relationships with parent tables have been defined in the Data Dictionary then it may not be necessary to insert any custom code as the framework can use the $parent_relations array to automatically construct an SQL query containing JOINs to all the foreign tables, as described in Using Parent Relations to construct sql JOINs. It is also possible to take this automatically extended query and append manual extensions as described in How to manually extend the automatically extended sql SELECT statement.
The ability to create more complicated sql SELECT statements is provided as follows:
class Default_Table
{
var $pageno; // used as OFFSET
var $rows_per_page; // used as LIMIT
var $sql_select; // list of column names
var $sql_from; // table names in a JOIN statement
var $sql_where; // fixed portion of WHERE clause
var $sql_groupby; // contents of GROUP BY clause
var $sql_having; // contents of HAVING clause
var $sql_orderby; // contents of ORDER BY clause
var $sql_orderby_seq; // contents of ORDER BY clause
...
// identify extra parameters for SELECT statement $sql_select = 'person.*, pers_type.pers_type_desc'; $sql_from = 'person ' .'LEFT JOIN pers_type ON (person.pers_type_id = pers_type.pers_type_id)'; $sql_groupby = ''; $sql_having = ''; $sql_where = '';
If you use any alias names then please identify them using the 'AS' keyword, as in 'something AS alias'. Although the use of 'AS' is optional in some DBMS engines, it is required in Radicore so that it can more easily detect than an alias is being used.
Note that it is also possible to put these changes in the database table class instead, as described in step (6) below.
$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; // the following values may be supplied by the user if (isset($_GET['pagesize'])) { $dbobject->setRowsPerPage($_GET['pagesize']); } // if if (isset($_GET['page'])) { $dbobject->setPageNo($_GET['page']); } // if if (isset($_GET['orderby'])) { $dbobject->setOrderBy($_GET['orderby']); } // if $where = $_SESSION['where']; // created by previous page $data = $dbobject->getData($where);
$sql_where (fixed) and $where (variable) are combined into a single string, then all these variables are passed to the DML object using code similar to the following:
function _dml_getData ($where)
// Get data from the specified database table.
// Results may be affected by $where and $pageno.
{
$DML = $this->_getDBMSengine();
$DML->pageno = $this->pageno;
$DML->rows_per_page = $this->rows_per_page;
$DML->sql_from = $this->sql_from;
$DML->sql_groupby = $this->sql_groupby;
$DML->sql_having = $this->sql_having;
$DML->sql_orderby = $this->sql_orderby;
$DML->sql_orderby_seq = $this->sql_orderby_seq;
$DML->sql_select = $this->sql_select;
$DML->sql_where = $this->sql_where;
$array = $DML->getData($this->dbname, $this->tablename, $where);
$this->errors = array_merge($DML->getErrors(), $this->errors);
$this->numrows = $DML->getNumRows();
$this->pageno = $DML->getPageNo();
$this->lastpage = $DML->getLastPage();
return $array;
} // _dml_getData
SELECT statement with the following structure:
$query = 'SELECT ' .$select_str .' FROM ' .$from_str .' ' .$where_str .' ' .$group_str .' ' .$having_str .' ' .$sort_str .' ' .$limit_str;where each
$..._str is a string constructed from the relevant variables passed down by the calling database object. Note that some of these may be empty. The end result will (should?) always be a valid sql SELECT statement.
By default each field will appear in the HTML output as a textbox control, but this can be changed to a dropdown list or radio group quite easily. To achieve this it is necessary to have the XML file contain data similar to the following:
<?xml version="1.0"?>
<root>
<person>
<person_id size="8" pkey="y" required="y">FB</person_id>
<pers_type_id size="6" required="y"
control="dropdown"
optionlist="pers_type_id">ANON</pers_type_id>
<first_name size="20" required="y">Fred</first_name>
<last_name size="30" required="y">Bloggs</last_name>
....
</person>
<lookup>
<pers_type_id>
<option id=" "></option>
<option id="ACTOR">Actor/Artiste</option>
<option id="ANON">Anne Oni Mouse</option>
<option id="BORING">Boring Person</option>
<option id="CARTOO">Cartoon Character</option>
....
</pers_type_id>
</lookup
</root>
Notice the following:
Sample XSL code can be located here:
To set the control type to dropdown or radio group you must do the following:
<tablename>.dict.inc file. This should then contain values similar to the following:
$fieldspec['pers_type_id'] = array('type' => 'string', 'size' => 6, 'required' => 'y', 'control' => 'dropdown', 'optionlist' => 'pers_type_id');
To supply values for the lookup element in the XML file you must do the following:
function _cm_getExtraData ($where, $fieldarray)
// Perform custom processing for the getExtraData method.
// $where = a string in SQL 'where' format.
// $fieldarray = the contents of $where as an array.
{
// get contents of foreign table PERS_TYPE and add to lookup array
$pers_type =& singleton::getInstance('x_pers_type');
$array = $pers_type->getValRep('pers_type_id');
$this->lookup_data['pers_type_id'] = $array;
return $fieldarray;
} // _cm_getExtraData
The code inside this method is used to communicate with a foreign table and obtain its contents for inclusion in an array of lookup (picklist) data. The term ValRep is short for Value+Representation where Value is what is used internally and Representation is what is displayed to the user.
function _cm_getValRep ($item=NULL, $where=NULL) // get Value/Representation list from this table { $array = array(); if (strtolower($item) == 'pers_type_id') { // get data from the database $this->sql_select = 'pers_type_id, pers_type_desc'; $this->sql_orderby = 'pers_type_desc'; $this->sql_orderby_seq = 'asc'; $data = $this->getData($where); // convert each row into 'id=desc' in the output array foreach ($data as $row => $rowdata) { $rowvalues = array_values($rowdata); $array[$rowvalues[0]] = $rowvalues[1]; } // foreach return $array; } // if return $array; } // _cm_getValRep
If the data for the dropdown list is not supplied from the contents of a database table but from a fixed list (such as signs of the zodiac) then code similar to the following will be required instead:
function _cm_getExtraData ($where, $fieldarray)
// Perform custom processing for the getExtraData method.
// $where = a string in SQL 'where' format.
// $fieldarray = the contents of $where as an array.
{
// get list for star_sign and insert into lookup array
$array = $this->getValRep('star_sign');
$this->lookup_data['star_sign'] = $array;
return $fieldarray;
} // _cm_getExtraData
_cm_getValRep() with contents similar to the following:-
function _cm_getValRep ($item=NULL) // get Value/Representation list from this table { $array = array(); if (strtolower($item) == 'star_sign') { $array = getLanguageArray('star_sign'); return $array; } // if return $array; } // _cm_getValRep
Note that this uses the getLanguageArray() function which is part of my Internationalisation (I18N) facility. This will supply user text in the language of the user. The entry in each <subsystem>/text/<language>/language_array.inc file should look something like the following:
$array['star_sign'] = array('ARI' => 'Aries', 'AQU' => 'Aquarius', 'CAN' => 'Cancer', 'CAP' => 'Capricorn', 'GEM' => 'Gemini', 'LEO' => 'Leo', 'LIB' => 'Libra', 'PIS' => 'Pisces', 'SAG' => 'Sagittarius', 'SCO' => 'Scorpio', 'TAU' => 'Taurus', 'VIR' => 'Virgo');
Note that the lookup array need not contain a blank entry to signify "no selection" as this can be inserted automatically by the framework, as described in FAQ 75.
The information which decides on which output control is to be used for each field is held within the $fieldspec array within the database table class, but what is defined within this array can be regarded as being the default value as it can be changed at runtime. It is therefore possible to change the HTML control for any field to suit whatever circumstances are encountered.
Note that dropdown lists and radio groups will only allow the user to make a single selection. If multiple selections are required then take a look at How to incorporate a dropdown list with multiple selections.
There may be some circumstances in which a field that can normally be amended by the user must be made read-only or even hidden completely from view. As all the information regarding each database field from its validation rules to its display format is held with the $fieldspec array within each database table class then it is a simple matter to change the contents of this array.
To make these settings the default you should do the following:
<tablename>.dict.inc file. This should then contain values similar to the following:
$fieldspec['field1'] = array('type' => 'string',
'size' => 20,
'noedit' => 'y');
$fieldspec['field2'] = array('type' => 'string',
'size' => 16,
'nodisplay' => 'y');
These keywords will then be included in the XML file as attributes for their respective fields. Code within a standard XSL template will detect the existence of these attributes and take the appropriate action.
To change these settings temporarily during the execution of a particular script you may use code similar to the following:
$this->fieldspec['field1']['noedit'] = 'y'; $this->fieldspec['field2']['nodisplay'] = 'y';
To clear these settings at runtime you may use code similar to the following:
unset($this->fieldspec['field1']['noedit']); unset($this->fieldspec['field2']['nodisplay']);
While both contain hyperlinks (or buttons) which will enable you to jump to another task (or transaction) within the system there is a difference between them.
The Menu bar has the following characteristics:
The Navigation bar has the following characteristics:
Primary validation of user input is handled automatically by using the field specifications contained within the $fieldspec array within each database table class. This will examine each field in isolation of the others to ensure that required fields are not empty and that the data for each field is valid for that field type (number, date, time, et cetera). This is all handled in my data validation class.
Secondary validation is that which cannot be handled by my standard validation class, such as comparing the contents of one field with another, so must be handled by custom code within the database table class. It is also possible to extend this custom validation to perform lookups on other database tables.
When a controller script accesses a database table class to to insert or update a record, standard code which is inherited from the generic table class will, after performing all standard validation, pass control to one or more customisable methods.
For a pictorial representation of the processing flow of various transactions please take a look at UML diagrams for the RADICORE Development Infrastructure.
The prefix '_cm_' is used to signify a method which is defined within the superclass but which contains no code. In order to achieve anything this method must be copied into the individual subclass where it can then be filled with custom code. The customised version in the subclass will then override the empty (or abstract) version in the superclass.
These dummy (or abstract) methods have the following definitions:
function _cm_commonValidation ($fieldarray, $originaldata)
// perform validation that is common to INSERT and UPDATE.
{
return $fieldarray;
} // _cm_commonValidation
// ****************************************************************************
function _cm_validateInsert ($fieldarray)
// perform custom validation before insert.
{
return $fieldarray;
} // _cm_validateInsert
// ****************************************************************************
function _cm_validateUpdate ($fieldarray, $originaldata)
// perform custom validation before update.
{
return $fieldarray;
} // _cm_validateUpdate
// ****************************************************************************
Here is an example of one containing customised code:
function _cm_commonValidation ($fieldarray, $originaldata)
// perform validation that is common to INSERT and UPDATE.
{
if ($fieldarray['start_date'] > $fieldarray['end_date']) {
$this->errors['start_date'] = 'Start Date cannot be later than End Date';
$this->errors['end_date'] = 'End Date cannot be earlier than Start Date';
} // if
return $fieldarray;
} // _cm_commonValidation
Note that two input arrays are made available:
$originaldata - data from the database (before being changed by the user).$fieldarray - containing data from the screen (after being changed by the user).The 'before' and 'after' sets of data is for those situations where action need only be taken when a field value is actually changed.
It is also possible to define secondary validation in separate classes which can be shared among several application subsystems. Please refer to Extending the Validation class for more details.
Within each database table class there is a standard variable called $errors which should be use to hold all error messages. As more than one error message may be generated this variable should be treated as an array and not a string.
Error messages which are related to particular fields should be inserted as follows:
$this->errors['fieldname'] = 'error message';
Error messages which are not related to particular fields should be inserted as follows:
$this->errors[] = 'error message';
Note: with the implementation of my Internationalisation feature it is possible to obtain a message in the user's language using code similar to the following:
$this->errors[] = getLanguageText('e1234');
During the construction of the XML file as the value for each individual field is copied from the database object the $errors array is examined for an entry with a key which matches the field name. If one is found it is added to the XML file as an error attribute. Any error messages which are left over after all the fields have been processed will be added as separate lines to the message area. This is shown in the following example:
<root>
<person>
....
<start_date size="12"
required="y"
error="Start Date cannot be later than End Date"
>02 Jan 2006</start_date>
<end_date size="12"
error="End Date cannot be earlier than Start Date"
>02 Jan 2005</end_date>
....
</person>
<message>
<line>This message is not attached to any field</line>
</message>
</root>
During the XSL transformation process as each field is written to the HTML output the contents of the error attribute, if present, will appear immediately below the field value as shown in this screen sample. Any messages will be shown in the message area at the bottom of the screen, as shown in this screen sample.
Referential integrity refers to the rules that need to be applied when dealing with a relationship between two tables. This comes in two flavours - foreign key integrity and delete integrity.
| Foreign Key Integrity | If Table B (the child table) has a foreign key that points to a field in Table A (the parent table) referential integrity would prevent you from adding a record to Table B that cannot be linked to Table A. This is handled by the fact that foreign key values are never keyed in directly - they are either chosen from dropdown lists or popups or passed down as context from the previous script. |
| Delete Integrity | If a parent table has related entries on a child table then some action may need to be taken when deleting, or attempting to delete, an entry from the parent table. There are three possibilities:
|
Note that I do not rely on the database engine to deal with referential integrity, so the fact that MySQL does not (currently) have any method of enforcing referential integrity is of absolutely no consequence. Even if I were to use a database engine that had such capabilities I would probably avoid them, for the following reasons:
A candidate key is a unique key which is in addition to the primary key. Each table must have a primary key, but candidate keys are entirely optional. Any number of candidate keys may be defined, and each key may be comprised of any number of fields.
Dealing with candidate keys in the RADICORE infrastructure is straightforward - just identify the candidate keys in the $unique_keys array within the table structure file (this is handled automatically by the dictionary IMPORT and EXPORT functions) and the standard code within the DML class will take care of the rest as follows:
SELECT count(*) from $tablename WHERE ... with a WHERE clause constructed from the current record data. The code looks something like this:
// there may be several keys with several fields in each
foreach ($this->unique_keys as $key) {
$where = NULL;
foreach ($key as $fieldname) {
if (empty($where)) {
$where = "$fieldname='{$fieldarray[$fieldname]}'";
} else {
$where .= " AND $fieldname='{$fieldarray[$fieldname]}'";
} // if
} // foreach
$this->query = "SELECT count(*) FROM $tablename WHERE $where";
$count = $this->getCount($dbname, $tablename, $this->query);
if ($count <> 0) {
// set error message for each field within this key
foreach ($key as $fieldname) {
$this->errors[$fieldname] = 'A record already exists with this key.';
} // foreach
return;
} // if
} // foreach
A popup is a type of picklist. When options are to be chosen from a foreign table and there are too many to display in a radio group or a dropdown list, then the only alternative is to use to another form instead of a control or widget within the current form. The popup form will display the contents of the foreign table and allow the user to choose either a single entry, or in some cases multiple entries.
A popup form is identical to a LIST form, but with the addition of a CHOOSE button in the action bar.
The availability of a popup in a form is signified with a popup button
situated to the right of the data field. By pressing this the current form is suspended and a new form, the popup form, will appear in its place.
By default the user cannot enter any text before activating the POPUP form, but this behaviour can be amended using the information provided in FAQ 81.
If the SELECT column of the popup form contains radio buttons the user may only select a single entry, but if it contains checkboxes then multiple selections will be allowed. The user selects the entry or entries required and presses the CHOOSE button. This will cause the selection details to be passed back to the previous form where they will be processed.
In order to populate a field using the popup control you must perform the following steps:
CONCAT(field1, ' ', field2) AS foreign_descWhen this information is exported from the Data Dictionary it will appear in the
<tablename>.dict.inc file similar to:
$this->parent_relations[] = array('parent' => 'foreign_table', 'parent_field' => 'foreign_desc', 'fields' => array('primary_key' => 'foreign_key'));
<tablename>.dict.inc file similar to:
$fieldspec['foreign_id'] = array('type' => 'integer',
'size' => 4,
'required' => 'y',
'control' => 'popup',
'task_id' => 'task_identity',
'foreign_field' => 'foreign_desc');
When the form containing the popup is processed the generated XML document will contain something which is similar to the following:
<foreign_id size="4" control="popup" foreign_field="foreign_desc" task_id="tran#task_identity">5</foreign_id> <foreign_desc noedit="y">Description from foreign table</foreign_desc>
The HTML which is generated for this control will look similar to the following:
<div class="popuptext">
<input type="hidden"
name="foreign_id"
value="5"/>Description from foreign table</div>
<div class="popupbutton">
<input type="image"
name="task#task_identity"
src="images/popup.gif"
alt="Call popup form to obtain value"/>
</div>
Whenever a popup button is pressed the following processing takes place:
task#. This tells it that either a popup button or navigation button has been pressed. The characters which follow task# provide the identity of the task which is to be run.$where string to be passed to the popup form, and defining any settings which can be passed to the popup form.$where string was passed to it when retrieving data from the database, and will use the select_one setting to determine if the select column should be populated with check boxes (when select_one=FALSE) or radio buttons (when select_one=TRUE).choose_single_row has been set then that row will automatically be selected without waiting for the user to press the CHOOSE button.$selection string which will be returned to the calling form. This string will be in the format of the WHERE clause of an SQL query, and can deal with single selections or multiple selections where the primary keys are comprised of single or multiple fields, as shown in the following examples:
field1='value1'
field1='value1' AND field2='value2'
(field1='value1' AND field2='value2') OR (field1='value3' AND field2='value4') OR ...
By default only those fields which form the primary key will be included in the $selection string, but sometimes it may be useful to return a non-key field as well. This can be achieved by using the _cm_getPkeyNames() method to temporarily alter the list of key fields before the $selection string is constructed.
task_id set to $return_from so that it can initialise the field identified in foreign_field.foreign_field.foreign_field displayed in front of the popup button.By default the output from each table class contains values which are only from the database table with which it is associated. This output may include calculated fields such as those created by means of CONCAT or a similar function. There may be occasions when it is desired to incorporate values from other database tables, such as to replace a foreign key with a description from the foreign table. In order to gather information from more than one database table it is necessary to perform what is known in the database world as a JOIN, and within this framework a JOIN can be performed in any of the following ways.
In this method the table object reads data from its own table, which results in an array of zero or more entries, but before this array is passed back to the presentation layer it is modified to include additional data from one or more other tables. To do this the database object must iterate through its array of database data, and for each occurrence it must fetch an additional array of data from another database table (using another database object), then merge this additional array with the original array. This could be achieved with code similar to the following:
function _cm_getForeignData ($fieldarray)
// Retrieve data from foreign entities.
{
require_once 'tree_node.class.inc';
$dbobject = new Tree_Node;
foreach ($fieldarray as $row => $rowdata) {
if (!empty($rowdata['node_id']) and empty($rowdata['node_desc'])) {
// get description for selected node
$dbobject->sql_select = 'node_desc';
$foreign_data = $dbobject->getData("node_id='{$rowdata['node_id']}'");
// merge with existing data
$fieldarray[$row] = array_merge($rowdata, $foreign_data[0]);
} // if
} // foreach
return $fieldarray;
} // _cm_getForeignData
Note that in most cases such code is redundant by virtue of the fact that the framework can use the contents of the $parent_relations array (which is constructed using data entered via the Add/Update Relationship task) to generate and execute the relevant code automatically at runtime by calling the getForeignData() method.
Also note that it is not very efficient to obtain data from parent tables after the child table has been retrieved, especially if there are multiple occurrences of the child table and multiple parent tables. It is far more efficient to get the database to perform all this processing in a single operation by constructing an SQL query which contains the relevant JOIN clauses, as documented in How to handle a JOIN in the database.
When it is necessary to obtain data from more than one table the most efficient method is to construct an SQL query which contains the relevant JOIN clauses so that the database can retrieve the data in a single operation and return that data in a single result set. Within this framework there are two methods of constructing such a query:
Yes. When you access a database table through its own table class the name of the database is built into the class and does not have to be specified again. When the database table object communicates with the DML object it will supply the table name and database name as well as the table data. The DML object will use this information to select the correct database. It is therefore possible within the same transaction to access a number of database table objects where each table exists within a different database.
When using a JOIN with an sql SELECT statement you must remember to use the format databasename.tablename otherwise the database engine will look for the table within the database associated with the current database table object.
Yes. It is normal practice for an installation to have all its databases in a single database server, and the identity of this server is defined in a single place in the CONFIG.INC file as $GLOBALS['dbms']. Within the constructor of each database table class is code similar to the following:
$this->dbms_engine = $GLOBALS['dbms'];
$this->dbname = 'foo';
$this->tablename = 'bar';
Whenever a database table class needs to communicate with the database it does so by communicating with a DML object for the specified DBMS engine. This object handles the connection to that DBMS engine, and the construction and execution of all SQL queries. Regardless of how many database table classes are used in a script there will only ever be a single instance of the DML object for a particular DBMS engine.
In my development environment I have the same data held on a MySQL server, a PostgreSQL server and an Oracle server, and it is possible for me to switch from one server to another simply by changing the value for $GLOBALS['dbms'] within the CONFIG.INC file.
However, it is also possible for an installation to have different databases on different servers, and to switch from one DBMS engine to another on a per database basis instead of per installation. In order to achieve this the following steps are necessary:
config.ini file.As a single RADICORE installation is comprised of several subsystems, this procedure will allow each subsystem to have its database handled by a different DBMS engine. It is also possible for a single script to access more than one database, with each of those databases served by a different DBMS engine, but this does impose the following limitations:
When accessing a MySQL database which is earlier than version 4.1 you use the MySQL functions, but to access version 4.1 and above you will need to use the Improved MySQL Extension instead. This could present some difficulties to developers of lesser ability, but due to the fact that my infrastructure design is based on the 3-Tier Architecture where all data access is through a Data Access layer all I have to do is switch a single component, my DML class, and everything is tickety-boo, hunky-dory, and smelling of roses.
Because it would be unusual for a PHP installation to have both the MySQL functions and Improved MySQL Extension installed at the same time it is possible to detect which is available at runtime and to create an object from the relevant class. I have amended the code described in FAQ 20 as follows::
if ($engine == 'mysql') {
if (function_exists('mysqli_connect')) {
// use 'improved' mysql functions
require_once "dml.mysqli.class.inc";
} else {
// use standard mysql functions
require_once "dml.mysql.class.inc";
} // if
} else {
require_once "dml.$engine.class.inc";
} // if
This means that when I change my version of MySQL to 4.1 or above I do not have to take any further action as my code will detect the change and automatically switch to the correct functions. So when someone tells you that implementing the 3-Tier Architecture is an unnecessary investment just ask them how much effort it will take them to upgrade their software to use the new extension.
I originally built this infrastructure to run with PHP 4, so I used the DOM XML functions to construct my XML files and the XSLT (Sablotron) functions to perform the XSL Transformations. Now that PHP 5 is here I discover that these two extensions have been moved out to the PECL repository, and I have to use the DOM and XSL extensions instead.
This could present some difficulties to developers of lesser ability, but as I had the foresight to put the function calls to these extensions in a set of user-defined functions within their own include() file I found that all I had to do was create a new version of this include() file to contain the calls to the alternate functions. I thus ended up with one file for PHP 4 and another for PHP 5. As it is possible to detect at runtime which version of PHP is being used it is an easy process to load the file which is relevant to that PHP version. The code that I use is similar to the following:
// detect which version of PHP is being used if (version_compare(phpversion(), '5.0.0', '<')) { require 'include.xml.php4.inc'; } else { require 'include.xml.php5.inc'; } // if
Each of these two files contains the same user-defined function names, so none of the code which calls these functions needs to be changed. The important thing is that the contents of these user-defined functions is relevant to the version of PHP which is being used. I can now switch my application between PHP 4 and PHP 5 at the drop of a hat without having to worry about any incompatibilities.
When I started to teach myself to access a database with PHP using samples found in various books and online tutorials I noticed that in all cases each of the sql SELECT, INSERT, UPDATE and DELETE statements was individually hard-coded for each database table. After generating these statements for a small number of tables myself I asked a simple question - would it be possible to automate the generation of these statements?
When you consider that each of these sql statements is nothing more than a string variable which is passed to the database engine, and that PHP's string manipulation functions are very powerful, it did not take me long to find the answer.
Take a look at the structure of the various statements:
INSERT INTO <tablename> SET fieldname='value', fieldname='value', ... UPDATE <tablename> SET fieldname='value', fieldname='value', ... WHERE primarykey='value' DELETE FROM <tablename> WHERE primarykey='value' SELECT <select> FROM <from> <where> <group> <having> <sort> <limit>
As the data I pass into the INSERT, UPDATE and DELETE methods is an associative array of name=value pairs you should see that it is a simple exercise to iterate through this array to construct the SET fieldname='value' portion of each statement. As the $fieldspec array within each table structure file identifies the primary key field(s) it is just as simple to construct the WHERE primarykey='value' portion.
The SELECT statement is a little more complicated as there are potentially more components. In my getData() method the where is supplied as an optional argument, but the select, from, group, having, sort and limit portions are object variables which are set with appropriate values by the calling script. These are then processed at runtime and merged into a single string. This is described in more detail in FAQ 8.
The purpose of my generic table class can be summarised as follows:
The purpose of my DML class (or Data Access Object) is to isolate the construction and execution of all SQL queries from objects in the business layer (sometimes referred to as 'domain' objects). This means that I can switch from one DBMS engine to another simply by switching to an alternative DML class.
When I first produced my generic table class it also included all calls to the MySQL functions to communicate with the database. I knew that at some point in the future I may want to use a different database engine, such as PostgreSQL or Oracle, so I wanted a mechanism which would make this switch as simple and painless as possible.
The first step was to extract all the database function calls and put them into a separate class. As these function calls deal with the Data Manipulation Language I called it the DML class. As the first of these was for MySQL I named it dml.mysql.class.inc. I then changed my generic table class to pass control to my DML class whenever it wanted to communicate with the database.
Instead of being passed a complete SQL query for execution I decided it would give me greater flexibility if the final assembly of each query were to be left to entirely to the DML object. Thus it is only SQL fragments that are passed to the DML object where they are assembled immediately before being executed. An example of how this is done for a SELECT is shown in FAQ 8. Example for INSERT, UPDATE and DELETE are shown in Using PHP Objects to access your Database Tables (Part 1).
The advantage that this particular method has given me over other methods I have seen is that should a particular query need to be assembled slightly differently for any DBMS engine then I only have to adjust the code in a single place - within the DML class for that particular DBMS engine. In other infrastructures dealing with such a change may mean applying updates to multiple components.
Whenever I wish to use a different database engine all I have to do now is as follows:
dml.<engine>.class.inc.$dbms_engine variable in my generic table class to contain the <engine> name.The code I use to load the relevant class file is described in FAQ 20.
Another advantage of this design is the fact that I have been able to incorporate a audit logging facility into all my applications simply by modifying the code within my DAO. This is far more efficient than having to modify individual table classes one by one.
Notice that this code also deals with the switch between the 'original' and 'improved' MySQL functions, as documented in FAQ 21.
As has been stated in FAQ 20 this DML class isolates all database function calls within a single object, which makes the switching from one database engine to another very easy. It is also possible to access different database tables through different engines within the same transaction.
The generic table class contains code which is common to every database table, but it cannot be instantiated into an object because it does not contain such details as database name, table name, table structure, validation rules, et cetera. This type of class is known as an abstract class, and it needs the addition of a subclass before it can be instantiated into a usable object.
The implementation details for each individual database table are therefore supplied in separate database table classes (subclasses) which extend the generic table class (superclass) and combine with the generic code through the process of inheritance. All the knowledge required to access a database table is contained or 'encapsulated' within its database table class.
Whenever a component needs to communicate with a database table all it need do is create an object from that table's class and then call one of the standard methods and everything is handled within that object, either by the generic code within the superclass or the custom code within the subclass.
There are some people who say that it is 'not good OOP' to have a separate class for each database table, but I wholeheartedly disagree.
Some people seem to thinks so as they each break down the application into 3 separate areas or layers, but if you examine their descriptions carefully you will see the differences:
As you can see there is some overlap between the two, but not an exact match. As there is no rule in either architecture that says there can be only one script (or program or module) in each area, it is possible to split any of these areas down into smaller parts for convenience (that is why some people refer to 3-Tier as N-Tier where 'N' can be any number). It is therefore possible to create a development infrastructure which contains the features of both architectures, as shown in the following diagram:
Figure 5 - The MVC and 3-Tier architectures combined
By combining both of these architectures it is therefore possible to create an application infrastructure which has more features and advantages than either one on its own.
In a screen which deals with two database tables in a parent-child relationship, such as a LIST 2 screen, there is no problem if they are different tables as the table names are used to identify which data goes where in the screen. But what happens if the relationship is actually between a table and itself? How can you keep the data from the parent and child parts separate?
Although it is possible to reference the same table through both a $parent and $child object within the PHP code this will cause a problem when trying to build the screen during the XSL transformation as the two entity names within the XML data will be the same. This will result in both sets of data being written to both areas in the screen instead of each set of data being written out to its own area.
The solution is to change one of the table references to a different name, but how can this be done? The solution in a previous language was to create a reference to a database table using an alias or subtype or subclass, and I have built a similar solution into my infrastructure which is described in Using subclasses to provide alias names.
The important thing to note is that when transferring data out of an object into the XML data what I actually use is the class name and not the physical table name. In order to reference a database table using an alias name all I have to do is create a subclass from the original database table class. Here is an example taken from my sample application:
Contents of file tree_node_snr.class.inc:
<?php require_once 'tree_node.class.inc'; class Tree_Node_Snr extends Tree_Node { } ?>
When I reference an object from the tree_node_snr class I will actually be referencing the same database table as an object from the tree_node class. When the data is written to the XML file one set of data will be labelled tree_node_snr and the other tree_node, thus it will be easy to keep the two sets of data separate from one another.
You will notice in the above example of extending a class into a subclass all I am doing is supplying an alternative class name - I am not changing any properties or methods, although I could if I wanted to. It would be possible for me to supply new properties and methods, or even to provide replacement properties and methods to override those which exist in the superclass.
On some web applications that I have seen the way to navigate from a parent LIST screen to a child screen is to click on a detail line where every field in the line, or even the whole line, has been coded as a hyperlink. Each hyperlink will jump to a child screen with the details of the selected entry pre-loaded. This process involves a single step whereas in my infrastructure it is two stages - select an entry, then press a button. Why is this? My reasons are as follows:
My method has the following advantages:
This information is posted back to the parent script which then does the following:
(field1='value' AND field2='value').(field='value1') OR (field='value2').It would be possible for me to use a combination of hyperlinks for one child screen and buttons for the others, but this would be inconsistent and probably confusing to the users. As the poor dears are often confused enough I do not wish to add to their burden.
This situation arose while I was building a prototype for a web-base survey application. A survey could have any number of questions, and the answer to each question could be one of the following:
This means that when building the HTML output for the screen I need to know what type of answer is expected so that I can generate the correct HTML tags. In some development infrastructures this may be a complex process, but in my infrastructure it is incredibly easy. This is made possible because of the following facets of my design:
$fieldarray which is an associative array of data obtained from the database via the DML class.$fieldspec which is an array of specifications for each field within that database table. This is defined separately within each database table class.$errors which is an array of error messages, indexed by field name, which may be generated at run-time.$fieldspec array identifies which HTML control is to be used for each field. This information is inserted into the XML document as a series of attributes for the field in question. During the XSL transformation as each field is being processed it uses the field attributes to decide which HTML control to use.This means that the HTML control which is to be used for each field is defined with the $fieldspec array of each database table class, therefore to change the type of control all you have to do is change the contents of this array. This can be done using code similar to the following:
function _cm_getExtraData ($where, $fieldarray) // Perform custom processing after the getData method. { ... switch ($fieldarray['answer_type']) { case 'M': // answer is multiple choice from a dropdown list $this->fieldspec['answer_text'] = array('type' => 'string', 'size' => 12, 'required' => 'y', 'control' => 'dropdown', 'optionlist' => 'answer_id'); break; case 'N': // answer is a number $this->fieldspec['answer_text'] = array('type' => 'integer', 'unsigned' => 'y', 'size' => 6, 'required' => 'y'); if (isset($min_value)) { $this->fieldspec['answer_text']['minvalue'] = $min_value; } // if if (isset($max_value)) { $this->fieldspec['answer_text']['maxvalue'] = $max_value; } // if break; default: // answer is free-format text $this->fieldspec['answer_text'] = array('type' => 'string', 'control' => 'multiline', 'rows' => 5, 'cols' => 50, 'size' => 255, 'required' => 'y'); } // switch ... return $fieldarray; } // _cm_getExtradata
Also note the following:
Can it be done as easily as that in your application?
The reason that I do not subscribe to this theory can be explained using the following diagram:
Figure 6 - The false 3-Tier Architecture
Although there may appear to be 3 tiers or layers you should notice that the web application exists in a single place, the web server. What exists outside of this area is not actually part of the application.
The location of the logic within a web application can be represented in the following diagram:
Figure 7 - All application logic in a single component
The three areas of logic can be broken down as follows:
If all these pieces of application logic exist within a single component then that component is 1-tier, pure and simple. In order to become 3-tier each area of logic must be contained within a separate component, as shown in the following diagram:
Figure 8 - Application logic in 3 components
The above diagram clearly shows that the application code is split into 3 distinct and separate components which inter-communicate at run-time. Note that there is no direct communication between the presentation layer and the data access layer. Each layer is independent of the other, and any layer can be modified without necessarily having to modify any other, this arrangement can truly be called 3-tier.
In a recent Sitepoint blog someone stated that the 3 Tier architecture seemed to be an expensive option as:
It's main function (independence of user interface, business rules and data storage/retrieval) only helps when migrating or extending to another script-language/data engine/platform. But this only happens very very few times in an Application Lifetime.
Your view of the benefits of the 3 tier architecture is very narrow as in reality they are not restricted to changes in the scripting language, database engine or platform.
The main advantages of the 3 Tier Architecture are often quoted as:
Being able to change from one database engine to another by changing just one component is not just a fancy expensive option that is rarely used. Take the case of MySQL, for example. For versions up to 4.0 you must use the mysql_* functions, but for 4.1 and above you must use the improved mysqli_* functions. How complicated would that be if you had hundreds of scripts to change instead of just one? You must also consider the case where a supplier creates an application which is then run on customers own machines with the database of their choice. If it is coded so that it only runs with MySQL but they actually want PostgreSQL or Oracle or whatever then how difficult would it be to cater for the customer's needs?
Having presentation logic separated from business logic has other advantages besides a switch to a totally different user interface device (for example, from client/server to the web). In the first place the creation of efficient, artistic and user-friendly web pages requires more than a passing knowledge of (X)HTML and CSS (and perhaps javascript) which a lot of PHP coders are without. The people with these skills may have little or no abilities with PHP, so by having separate layers you can have a different set of experts to deal with each layer. Another more common requirement is to have the ability to change the style of a web application with relative ease. By ensuring that all output is strict XHTML with all style specified in an external CSS stylesheet it is possible to change the entire 'look' of an application by changing a single CSS file.
In my infrastructure all my XHTML output is produced from a small set of generic XSL stylesheets, which means that should I need to make changes to my 350+ screens that cannot be done by altering the CSS file then all I have to do is change my generic XSL stylesheets, which are currently about 10 in number. You may think that such changes are rare, but what about when the time comes to convert your existing web application from HTML forms to XFORMS, the latest W3C standard? I can do that by changing 10 XSL stylesheets. Can you?
In the same Sitepoint blog someone stated:
The two seem inexorably tied together in a practical and realistic sense. After all, what is your business logic without data to work with, and at that, should your business logic really be able to handle ANY data you pass to it? Is that healthy?
The primary purpose of having a separate object in the data access layer (sometimes known as a Data Access Object or DAO) is that it should be possible to switch the entire application from one data source to another simply by changing this one component. Thus if I want to switch my application from MySQL to PostgreSQL (or Oracle, or whatever) I simply change my DAO.
In order to make this work in practice my own implementation is as follows:
insertRecord(), updateRecord() and deleteRecord() methods, but as well as the validated contents of the $_POST array it is also given a second array which contains all the table structure details. Using these two arrays it is easy to construct the relevant SQL query string before calling the relevant database API.In this way my business object contains business rules, but no calls to database APIs, and my DAO contains calls to database APIs but no business rules. This is clear separation of logic.
Switching from one DBMS to another is simple to achieve in my infrastructure. In my generic table superclass I have a variable called $dbms_engine which is set to 'mysql', 'postgresql', 'oracle' or whatever. This will then apply to all database tables unless overridden in any individual subclass. When the business object wants to talk to the data access object it first needs to instantiate an object from a class which is defined within a separate include() file. The name of this file is in the format 'dml.<engine>.class.inc' where <engine> is replaced by the contents of variable $dbms_engine. I have a separate version of this include() file for every DBMS that I use. All I need to do before accessing a new DBMS is to create a new version of the 'dml.<engine>>.class.inc' file and I'm up and running.
Another advantage of this mechanism is that it would even be possible to talk to different database tables through different DBMS engines within the same transaction. Hows that for flexibility?
This question came about because each of my database table classes in the business layer contains information in the $fieldspec array which is processed by the DML object in the data access layer.
Where is the separation of logic if the business entity knows both the business logic and has knowledge of the structure of the associated data storage mechanism?
Each database table class contains both business rules in the form of custom code and a description of the table's physical structure as described in the $fieldspec array. By containing all this information within a single class I am adhering to one of the fundamental principles of OOP which is encapsulation.
Although this information is defined within a business object it is not used to access the persistent data store (i.e. database) until it is passed to my Data Access Object (DML class). This uses the information given to it - the table structure and some data - to construct the relevant query and then pass it to the specified database engine via the relevant API.
There is nothing in the principles of OOP that says I cannot define information in one object, then pass it to another for processing. It is where this information is actually processed which is important. My $fieldspec array actually contains information which is used in three different places:
If I were to define this information in three separate places surely this would break encapsulation?
Remember that my data access object contains no information about any database table whatsoever, so this information has to be passed to it from an external source. This does not make the external source part of the data access object, now does it? Similarly the XSL stylesheet, which is used to construct the XHTML output, is useless without an XML file containing the data. This data originates from the business layer, but that does not make the business layer part of the XSL stylesheet, now does it?
If you study the Model-View-Controller design pattern you will see that information comes out of the model but has to be processed by the view before it is displayed to the user. Using your argument because the information is processed within the view it must also originate from within the view. Does that make sense? I think not.
If you are prepared to treat the term logic as where information is processed rather than where information originates you will see that my usage of the term 'separation of logic' is entirely justified whereas yours is questionable.
If you have a User class that performs some business logic that doesn't interact with the database, wouldn't you want a separate class that mapped a user to the database, either inserting or deleting or what-have-you?
I disagree with this idea, for the following reasons:
A better way to do it would be to have all properties and methods defined within a single class, and if a particular method requires to access the database then it should do so. If this involves communicating with a separate 'mapper' class (or in my case a data access object), then so be it. The important thing is that the calling script should not know or even care about how a function is implemented. This allows for the implementation to be changed without requiring the calling script to have knowledge of any such change, or being required to change the way in which the method is invoked to accommodate the change.
If you look at the description for a Data Mapper from Martin Fowler's Patterns of Enterprise Application Architecture (PoEAA) you should notice that this type of solution is only required when the object schema and the relational schema don't match up. As I do not have this problem with the two schemas I do not see why I need this solution.
I looked at a few samples of existing code before I built my infrastructure, but as none of them worked the way I wanted them to I decided that the best solution was to build my own entirely from scratch.
Here is a sample of code from the PEAR manual:
<?php // Create a valid DB object named $db // at the beginning of your program... require_once 'DB.php'; $db =& DB::connect('pgsql://usr:pw@localhost/dbnam'); if (DB::isError($db)) { die($db->getMessage()); } // proceed with query $result =& $db->query("select * from clients"); // Always check that $result is not an error if (DB::isError($result)) { die ($result->getMessage()); } .... ?>
There are two things I do not like with this approach:
One problem is where do you put all this additional code? Within each object in the business layer? Not a good idea as the construction of SQL query strings does not really belong there - it should be in the data access layer. The typical answer to this problem I have seen others use is to create an additional object in the data access layer between each business object and the Data Access Object (DAO). This produces a structure similar to the following:
Figure 9 - DAO with additional SQL objects
I dislike this idea because there are too many SQL objects, and the code in each object is more or less the same, with the only difference being the table and column names, which are hard-coded. I wanted something more generic than this, so I found a way to eliminate all the SQL objects altogether. This produces a much leaner and meaner structure similar to the following:
Figure 10 - DAO without additional SQL objects
With this structure instead of the SQL queries being assembled outside of the DAO then passed in as a single string to be executed I pass in all the individual fragments that will be used in the SQL query (see FAQ 8). With this mechanism if the fragments need to be adjusted or assembled differently for any particular DBMS engine then I only have to change the code in a single place - the DAO for that particular engine - and not in every SQL object.
Note that some people would say that my system of layering is broken as I allow SQL fragments to exist in any object in any layer, not just the data access layer. The only requirement of the 3 Tier Architecture is that no layer other than the data access layer may access the database. I interpret the word 'access' to mean execute an SQL query using an API which connects to a physical database and returns a result set. The fact that an object in the presentation layer has a variable which contains a fragment of SQL does not mean that it is 'accessing' the database. It is merely holding a piece of data in a variable. It is not until these various fragments have been passed to the DAO that they are finally assembled into a complete SQL query and executed, therefore I do not consider that I am breaking any rules.
The code to establish a connection with the database has also been moved to within the DAO. This means that ALL the API's which communicate with any particular DBMS engine are contained within a single object and not scattered around the system. This obviously makes it simpler to cater for another DBMS engine which has a different set of API's as there is only one object to change instead of multiple objects.
This degree of control, flexibility and simplicity was not obtainable from any 'off the shelf' database abstraction layers, so that is why I created my own. This decision made it very easy for me to incorporate my audit logging facility into all my applications as it could be achieved by modifying the code within a single object, my DAO, instead of the individual table objects.
If you want to build new components as part of a new project/subsystem then first make sure that you have read and followed the instructions in What do I do to start a new project/subsystem?.
Each application component will require the following scripts:
<tablename>.class.inc file created by the data dictionary export function.NOTE: The previous steps can now be performed automatically. Please refer to Radicore Tutorial - Generate Transactions.
Note that when you create a family of forms some of the scripts can be shared by members of that family:
<tablename>.class.inc.Before you can actually run these scripts you will have to update the Menu and Security (RBAC) system as follows:
<subsys>/text/<language>/language_text.inc file to include the following details:
<subsys>/text/<subsys>.menu_export.txt which is created by the Export Subsystem function.
To demonstrate this procedure I shall create a new table in the database, then generate a series of components to maintain it.
CREATE TABLE `foobar` ( `foobar_id` varchar(6) NOT NULL default '', `foobar_desc` varchar(40) default NULL, `foobar_value` decimal(10,2) default NULL, `start_date` date NOT NULL default '0000-00-00', `end_date` date NOT NULL default '9999-12-31', PRIMARY KEY (`foobar_id`) ) ENGINE=MyISAM;
foobar.class.inc which will be based on <tablename>.class.inc. Note that this file will not be overwritten by any future export operations as this would remove any customisations.foobar.dict.inc which will be based on <tablename>.dict.inc. Note that this file will be replaced in any future export operations in order to incorporate any changes in the dictionary details, which includes a re-import to synchronise the dictionary with any changes to the physical database structure.foobar.list.php as follows:
<?php $table_id = 'foobar'; // table name $screen = 'foobar.list.screen.inc'; // screen structure require 'std.list1.inc'; // activate controller ?>
foobar.list.screen.inc as follows:
<?php $structure['xsl_file'] = 'std.list1.xsl'; $structure['tables']['main'] = 'foobar'; $structure['main']['columns'][] = array('width' => 5); $structure['main']['columns'][] = array('width' => 6); $structure['main']['columns'][] = array('width' => '*'); $structure['main']['columns'][] = array('width' => 12); $structure['main']['columns'][] = array('width' => 12); $structure['main']['columns'][] = array('width' => 12); $structure['main']['fields'][] = array('selectbox' => 'Select'); $structure['main']['fields'][] = array('foobar_id' => 'ID'); $structure['main']['fields'][] = array('foobar_desc' => 'Description'); $structure['main']['fields'][] = array('foobar_value' => 'Value'); $structure['main']['fields'][] = array('start_date' => 'Start Date'); $structure['main']['fields'][] = array('end_date' => 'End Date'); ?>
foobar.add.php as follows:
<?php $table_id = 'foobar'; // table name $screen = 'foobar.detail.screen.inc'; // screen structure require 'std.add1.inc'; // activate controller ?>
foobar.del.php as follows:
<?php $table_id = 'foobar'; // table name $screen = 'foobar.detail.screen.inc'; // screen structure require 'std.delete1.inc'; // activate controller ?>
foobar.enq.php as follows:
<?php $table_id = 'foobar'; // table name $screen = 'foobar.detail.screen.inc'; // screen structure require 'std.enquire1.inc'; // activate controller ?>
foobar.search.php as follows:
<?php $table_id = 'foobar'; // table name $screen = 'foobar.detail.screen.inc'; // screen structure require 'std.search1.inc'; // activate controller ?>
foobar.upd.php as follows:
<?php $table_id = 'foobar'; // table name $screen = 'foobar.detail.screen.inc'; // screen structure require 'std.update1.inc'; // activate controller ?>
foobar.detail.screen.inc as follows:
<?php $structure['xsl_file'] = 'std.detail1.xsl'; $structure['tables']['main'] = 'foobar'; $structure['main']['columns'][] = array('width' => 70); $structure['main']['columns'][] = array('width' => '*'); $structure['main']['fields'][] = array('foobar_id' => 'ID'); $structure['main']['fields'][] = array('foobar_desc' => 'Description'); $structure['main']['fields'][] = array('foobar_value' => 'Value'); $structure['main']['fields'][] = array('start_date' => 'Start Date'); $structure['main']['fields'][] = array('end_date' => 'End Date'); ?>
Note that all the previous detail screens will share the same screen structure script.
Once the basic components have been created you have all that is required to view and maintain the contents of that database table. Primary data validation is performed automatically using the field definitions exported from the Data Dictionary. Secondary validation can be added in by copying the relevant empty methods from the generic table class and inserting the required custom code.
Additional tables can be added to the database and additional components created to maintain those tables using the procedure outlined above. Note that there is a range of different component templates to choose from, each of which utilises a different controller script and XSL stylesheet. Separate documentation is available which will help with choosing which template to use.
The $fieldspec array should only contain fields which actually exist within the database otherwise when SQL statements are constructed within the data access object the inclusion of invalid field names would result in a fatal error. But what happens when you want to accept input into a field which does not exist in the database?
The answer is that you make temporary modifications to the $fieldspec array so that when a component in the presentation layer asks for field specifications it gets the amended array instead of the original.
Note that the inclusion of non-database fields in the $fieldspec array is not necessary if the field is display only as the default behaviour is to display such fields as plain text. Modification of the $fieldspec array is only necessary in the following circumstances:
For example, suppose when a user enters a new password you want him to re-enter that password into a second field so that you can trap spelling mistakes. You would need to put code similar to the following in the _cm_changeConfig() method:
// create 'new_password' field $fieldarray['new_password1'] = ''; $this->fieldspec['new_password1']['type'] = 'string'; $this->fieldspec['new_password1']['size'] = $this->fieldspec['user_password']['size']; $this->fieldspec['new_password1']['password'] = 'y'; $this->fieldspec['new_password1']['required'] = 'y'; // get user to repeat input to avoid mistakes $fieldarray['new_password2'] = ''; $this->fieldspec['new_password2']['type'] = 'string'; $this->fieldspec['new_password2']['size'] = $this->fieldspec['user_password']['size']; $this->fieldspec['new_password2']['password'] = 'y'; $this->fieldspec['new_password2']['required'] = 'y';
In this example a calculated field is defined so that the formatData() method will automatically format the value with the correct number of decimal places:
$this->fieldspec['order_value'] = array('type' => 'numeric', 'precision' => 11, 'scale' => 2, 'blank_when_zero' => 'y', 'noedit' => 'y', 'nondb' => 'y');
Note the use of the nondb option. This is used to prevent a field with that name from being qualified with a table name if it appears in the $where, $search or $orderby variables as this would cause the generated SQL query to be invalid.
A subclass is where a particular class definition "extends" that of a previously defined class, known as a superclass. The subclass automatically inherits all the properties and methods of its superclass, but is allowed to add additional ones of its own. In the case of class methods if one in the superclass is redefined in the subclass, then the subclass method is used at runtime while the superclass method is ignored. It is possible for a subclass to have its own subclass, and so on and so on, to produce a class hierarchy which is many levels deep.
To the OO zealot a subclass is considered obligatory as soon as it is recognised that an object may come in different flavours. For example, you have a USER class with a variable called USER_TYPE to differentiate between 'supervisor', 'team leader', 'worker', 'dogsbody' et cetera. The zealot will, without further thought, immediately extend the USER class into a separate subclass for each possible value of USER_TYPE.
I do not. Why? Because I create my classes around database tables, and there is only one USER table which holds data for all users regardless of their type. USER_TYPE is nothing more than an attribute, a piece of data, on the USER table. There may be an additional table called USER_TYPE which is linked to USER in a one-to-many relationship, but that is an entirely different table with its own class definition. With this approach I am able to add to or remove from the list of USER_TYPEs without having add to or remove from my catalog of classes. I do not have to create a separate class for each possible value for an item of data.
The OO zealot then comes up with an argument like this:
What happens if each user type has a different set of permissions which allows or denies access to different functionality within the application? Doesn't this mandate the use of separate subclasses?
Not in my book. What you are talking about can be covered by a data structure similar to the following:
Figure 11 - Structure of a permissions system
As there are four separate database tables there are four separate classes. When you wish to combine the data from several tables you either perform an SQL JOIN or you employ object composition. Obtaining the permissions for a user requires nothing more than the following statement:
SELECT * FROM permissions WHERE user_type='whatever'
This returns an array of data, and what happens next depends on the contents of that array. Although the data may be different, the code required to obtain and process that data is exactly the same regardless of the value of USER_TYPE, therefore a separate subclass for each value of USER_TYPE is, in my opinion, a total waste of time.
As a general rule the only time I use subclassing is with database tables. When I discovered that 90% of the code used to access a database table is exactly the same regardless of the physical table, I decided to put all the common code into a generic (abstract) table class which is then extended into a separate subclass for each individual database table. In this way all the common code is defined just once, then shared with all its subclasses through the mechanism of inheritance.
There is one reason where I may extend a table subclass into another subclass, and that is where I want the same table class to execute different code when being accessed by different tasks (known as task-specific behaviour). One method would be to put all the different code into the same class then to execute it conditionally based on the task or script identity, but this becomes messy if there are large amounts of different code to execute. It could also lead to problems should the task or script be subject to a change in name. An alternative procedure I have used with great success is to create a subclass of the table class to contain all the code which is specific to a particular task, then have the component script refer to this subclass instead.
My approach to subclasses can be summarised as follows:
A typical database table class, which extends the abstract table class, looks like the following:
<?php require_once 'std.table.class.inc'; class foobar extends Default_Table { // **************************************************************************** // class constructor // **************************************************************************** function foobar () { // save directory name of current script $this->dirname = dirname(__file__); $this->dbms_engine = $GLOBALS['dbms']; $this->dbname = 'foo'; $this->tablename = 'foobar'; // call this method to get original field specifications // (note that they may be modified at runtime) $this->fieldspec = $this->getFieldSpec_original(); } // foobar // **************************************************************************** } // end class // **************************************************************************** ?>
To create a subclass of 'foobar' called 'foobar_jnr' is as simple as the following:
<?php require_once 'foobar.class.inc'; class foobar_jnr extends foobar { // **************************************************************************** } // end class // **************************************************************************** ?>
Any method which you define within the subclass will then be executed instead of a method with the same name than exists in the superclass. Please note the following:
There may be a situation where a transaction has a screen with two zones, which means that the controller will require the names of two classes which will provide the data for each of those zones. Although it is possible for the controller to use a single class to create objects for both zone #1 and zone #2, when the data within each of those objects is transferred to the XML document before being processed by the XSL transformation there needs to be some mechanism to identify which data goes into which zone. If the identifier is the table name, then having both zones supplied with data from the same table would cause an immediate problem.
Take the situation where there is a table called PERSON which contains the details of lots of people. Some of the people may be the parents of some of the other people, so there may be a transaction which shows a single entry from the PERSON table in zone #1 as the parent, and potentially multiple entries from the PERSON table in zone #2 as the children of that parent. If the XML document contains two entries which both exist under the <person> node then which one is the parent and which one is the child? Which entry goes into zone #1 and which entry goes into zone #2?
My solution is not to use the table name as the identifier in the XML document but to use the class name instead, although for the initial database table class they are one and the same. By creating a subclass I start with something which is an exact copy of the original, but with a different name, so using my example above I could create a subclass called person_snr for accessing the parent details and another called person_jnr for any children. In this way the XML document contains entries which have different identifiers, namely <person_snr> and <person_jnr>, so there is no confusion as to what goes where.
Generally speaking when I create a table subclass I keep the original table name and add a suffix which can come in one of the following flavours:
_snn which is an underscore, the letter 'S' and a 2-digit number._xxxx which is an underscore followed by a mnemonic such as 'jnr', 'snr', 'source' or 'target'.I use the _snn suffix where I have a set of code which only needs to be executed on particular occasions, not every occasion, as described in How do you deal with task-specific behaviour?
I use the _xxxx suffix where the screen needs to have multiple data areas (zones) which come from the same database table but need to have separate identities in the XML file so that the XSL stylesheet knows which data goes into which zone, as described in Using subclasses to provide alias names. For example, when dealing with a hierarchy which has a senior and a junior relationship I would probably use subclass names such as node_id_snr and node_id_jnr. When dealing with movements between one location and another I would probably use subclass names such as location_from and location_to.
Note that instead of using PHP's own get_class() function to obtain the name of a class I use my own user defined function (UDF) called getClassName() instead. This will examine the suffix on the class name, and if it is _snn it will return the name without any suffix.
The simple answer is because I use Table Oriented Design (TOD) instead of Object Oriented Design (OOD). This means that I design my database first, then build my software components to match this design, instead of designing my software components first without any knowledge of the data structures that they are supposed to manipulate.
Every database application can be broken down into two basic components - a relational database and the software - and the effectiveness and efficiency of an application depends entirely on how these two are constructed and how well they work together. A major problem arises when these two components are designed using totally different techniques as the result is invariably a set of incompatible structures. If the two components of an application have incompatible structures then the effectiveness and efficiency of that application will suffer. This incompatibility is so common that it has been given its own name - Object-Relational Impedance Mismatch.
The usual answer to this problem is to create an additional component called an Object Relational Mapper (ORM) which acts as an intermediary between the database and the software, and converts the data from one structure to the other in all communication between the two. Rather than eliminating the problem it actually increases it by adding another layer of complexity and another place for errors to creep in.
If problems are caused by having database structures and software structures which are incompatible, then surely the most effective method of removing these problems is to remove the incompatibilities? This means that the design methodology for both the application and the database should produce structures that have as few incompatibilities as possible, and ideally no incompatibilities at all. This is where you hit a brick wall as the design methodologies used - Object Oriented Design for the software and Database Normalisation for the database - follow totally different rules are are therefore virtually guaranteed to produce different results.
To the typical OO programmer the database is the last thing which needs to be considered and can be dismissed as a mere "implementation issue". I do not share this opinion. I have designed databases for use with non-OO languages for many years, and that experience has taught me that a properly designed database will always produce better results than a badly designed database. I have also designed and built the applications which used those databases, and that experience has taught me that a software structure which is designed around the database structure will always produce better results than when the two structures are different and have incompatibilities.
This has resulted in the practice of designing the database first, and doing it properly according to the rules of Database Normalisation, then designing the application around that database. When I moved to an objected oriented language I continued this long standing and successful practice by creating a separate class for every table within the database. This has been so successful that I have built an entire framework around the practice which includes the ability to generate a class file for each database table at the touch of a button. The idea of deliberately creating an application structure which is incompatible with the database structure goes against everything I have learnt and is therefore not something that I will entertain.
This particular topic is discussed in more detail in Object Relational Mappers are EVIL.
The fact that my approach works is ignored by most OO zealots as they do not like any approach which is different to theirs. In their fanatical eyes any difference is "impure" and therefore tantamount to heresy. According to them all classes are supposed to be designed around a 'separation of responsibilities' which implies that they should be based around a particular operation that can be performed on some data rather than based around a set of data on which various operations can be performed. They then point to the classes which exist in my business layer and loudly proclaim:
These classes are not based around operations, therefore you have not achieved proper separation of responsibilities, therefore your whole design is bad!
This is yet another case where they are letting a particular interpretation of somebody's 'add-on' rule get in the way of efficient programming. Let me give you an example. Suppose I start with the four basic operations that can be applied to an entity - Create, Read, Update and Delete (known as CRUD for obvious reasons). Their argument is that I should start with a separate class for each of these operations, then add in the data by subclassing. For a series of different entities this would produce the following class hierarchy:
Figure 12 - Class hierarchy based on operation with multiple entities
My classes are constructed to comply with the description of encapsulation which states:
Encapsulation is the act of placing data and the operations that perform on that data in the same class. The class then becomes the 'capsule' or container for the data and operations.
Because of this I do not build a separate class for each operation and then link these to separate classes which contain the data. This would imply that I would need multiple inheritance in order to link to each of the CREATE, READ, UPDATE and DELETE classes, and PHP (like some other languages) does not support multiple inheritance. Instead I have built one abstract superclass which contains every possible operation that can be performed on a database table, so when I build a concrete class for each physical database table it always inherits everything from the single superclass. While the abstract superclass is quite large, each of the concrete table classes is quite small as all it contains is the table name and the table structure.
Figure 13 - Class hierarchy based on entity with multiple operations
You should notice that the number of (sub)classes in each of these hierarchies is somewhat different:
As a pragmatic programmer I think that an application which has one class per entity will be far easier to maintain than one that has multiple classes per entity.
However, if you examined my infrastructure you would notice that it is only the business layer where the classes are built around the data. The components in the other layers are constructed around the operations that may be performed on that data:
When I first saw examples of how other PHP programmers went about validating user input from each HTML form I was amazed at how much code was duplicated each time. Being a lazy programmer I wanted to find a better way, and being a competent programmer I quickly found it.
For many years I have worked with programming languages which used data dictionaries which removed the need to write reams of code to validate user input. It was enough to say FieldA is a date
, FieldB is a number
, FieldC is a whatever
and the language would automatically check that the user's input conformed to those specifications.
PHP does not come with a dictionary, so how easy would it be to emulate one? Fortunately in my database class I was already using a simple field list which identified all the fields which existed on a particular database table and I had already extended it to identify which fields were part of the primary key. It was therefore a simple step to change the fieldlist array into an array of field specifications.
The next step was to write a procedure which would take the user's input (The $_POST array), compare it with the field specifications and throw out an error if anything was wrong. This procedure is automatically accessed from within the code that is inherited from the generic table class therefore no additional code is necessary.
In order to validate user input the developer needs to amend the relevant database table class as follows:
The $where variable is used as the WHERE clause in an sql SELECT statement to provide selection criteria when retrieving data from the database. This is used as an argument on the getData() method in all database table objects. Dynamic selection criteria is provided by the user at runtime, and can be provided in any of the following ways:
(field1='a' and field2='b') OR (field1='c' and field2='d') ...
In order to extract the primary key details for each of the selected occurrences it is important that the select_list actually contains all the primary key fields, even though they may not be displayed on the screen.
This string will be written to the $_SESSION array, then control will be passed to the child component identified by that particular navigation button. Here the selection string will appear in $GLOBALS['selection']. The child component will use this as its $where string before accessing the database.
If the child component can only show one selected entry at a time it will use LIMIT 1 with a varying value for OFFSET in the sql SELECT statement combined with a set of scrolling links which will allow the user to move backwards and forwards through the selected occurrences.
The RESET button on the action bar cannot be used to clear this selection criteria.
The child component may also have a search button which will allow additional selection criteria to be defined. This will be used in conjunction with, not instead of, any selection criteria which was provided by the parent component.
It is also possible for the parent screen to be a non-list screen, such as ENQUIRE 1, which means that the $where string which is passed to the child component will be the primary key of the current record.
field1 LIKE '%a' AND field2 LIKE 'b%' ...
This string will be written to the $_SESSION array, then control will be passed back to the previous form, which will usually be a LIST screen with multiple occurrences. Here the search string will appear in $GLOBALS['search']. When this LIST screen is activated the page controller will insert this string into the database object before calling the getData($where) method where the $search and $where strings will be merged into one before being used to access the database.
The search screen can be activated as many times as is required in order to modify the selection criteria.
The RESET button on the action bar can be used to clear any additional selection criteria provided from a search screen. This will cause the original selection criteria to be reinstated, the current page to be reset to 1, and any column sorting to be reset.
Selection criteria can either be dynamic, as shown here, but it can also be static, or even a mixture of the two.
Firstly, it is important to understand that an infrastructure (aka 'framework') is not an application such as would be written for end users, it is the 'glue' that holds an application together. The infrastructure contains components which are not specific to any particular application, but which perform standard tasks that can be used by any application. Although an infrastructure may contain components which resemble those of an end-user application, such as online maintenance screens, these are usually only accessible by a system administrator.
Some people may say that each application requires a separate infrastructure due to its specific requirements, but my response would be that they have not reached the correct level of abstraction in identifying which is 'application independent' and which is 'application specific'. It is possible to create a single infrastructure which can be the controlling framework for any number of applications. I know this for the simple reason that in the past 20 years I have designed and built such infrastructures in 3 different languages.
NOTE: Application components which are written to operate within a particular infrastructure cannot usually be ported to another infrastructure without extensive modification as they may contain calls to infrastructure components which either do not exist in that other infrastructure or which may operate differently.
Before you can start building a sophisticated infrastructure you need to identify those functions which can safely be extracted and re-used by other applications. My own experience has produced the following list:
In my current PHP application framework for writing web applications certain architectural decisions have enabled me to provide even more standardised and reusable components. After having spent a lifetime writing 1-tier and 2-tier components I have been converted to the benefits of the 3-tier architecture. This aims to split application code into the following areas of responsibility:
In my infrastructure the Data Access layer contains a single object which handles all communication with a particular database engine. I can therefore switch from one database engine to another (for example, from MySQL to PostgreSQL or Oracle) simply by switching a single Data Access Object (DAO).
The Business layer contains a separate object for each entity within the application. All of these objects are created as subclasses of a superclass. It is the superclass which handles all communication between the Presentation and Data Access layers. This means that all standard code is inherited from the superclass and does not have to be redefined within each subclass.
Being familiar with XML and XSL before learning PHP I decided to use these technologies to create all HTML output as it enabled me to perform standard processing via reusable XSL stylesheets. This meant that I was effectively splitting my Presentation layer into 2 separate parts - a PHP controller and an XML/XSL view. As the components in the Business layer fit the description of the Model I was also effectively implementing a version of the Model-View-Controller (MVC) design pattern. Over a period of time I managed to refactor the code to such an extent that I eventually ended up with a standard set of controllers and XSL stylesheets which could be reused many times over. This is preferable to having to create customised versions of the controllers and stylesheets for each individual transaction.
This level of reusability means that when defining components (transactions, aka tasks) the following functions are automatically provided and do not require additional effort by the developer:
Question: With so much processing dealt with by standard code what is left for the developer to do?
Answer: As little as possible.
Each application has its own set of entities, and each of these entities will require its own component (class/object) in the business layer in which the business rules and task-specific behaviour can be defined. As has been stated previously processing which is common to all business objects is inherited from a standard superclass therefore does not have to be recoded.
All communication with the database is handled by the Data Access Object coupled with standard code within the business layer superclass, therefore does not have to be recoded.
This just leaves some small components in the Presentation layer. In order to actually 'do' something with a business object, such as List, Search, Create, Read, Update or Delete, it is necessary to define a transaction (task) script. This is a simple script which identifies just 3 things:
The view portion is actually defined within a separate script as the Search, Create, Read, Update and Delete transactions all share the same screen structure. The view script is again quite simple as all it does is specify the following:
Question: How is the developer supposed to know which controller and which XSL stylesheet to use for a particular transaction?
Answer: Consult Transaction Patterns for Web Applications.
This document identifies a standard set of transaction patterns (aka templates or dialog types) which are broken down by structure and behaviour. Each of these templates makes use of a particular controller and a particular XSL stylesheet. Each template has a unique name, such as LIST1, LIST2, ADD1, UPDATE1, et cetera, and this identity is built into the controller and stylesheet names:
std.<name>.inc for controllersstd.<name>.xsl for XSL stylesheetsThese two files take care of structure and behaviour while the remaining aspect - content (the table and column names) - is provided by the view script.
As you can see the effort required to build application components is concentrated mostly in the business layer where the business objects, business rules and application-specific behaviour are defined. Apart from small transaction scripts and view scripts all other processing is standard and is supplied by standard components within the infrastructure library. Plugging a new component into the infrastructure requires the use of a few maintenance screens:
This means that the developers do not have to waste time in writing, testing and debugging code that is already provided as part of the infrastructure. This in turn means shorter development times, lower costs and quicker time-to-market (TTM).
THAT is the benefit of an infrastructure such as mine.
There are several different technologies used within this infrastructure, and each has its own attitude towards the difference between upper and lower case.
The golden rule is that unless you really, really, REALLY know what you are doing then all identities (column names, keywords, etc) should be defined in lowercase otherwise a lookup may not find anything, and unless you know about the case sensitivity problem you will never be able to identify why something is being ignored even though it is definitely there.
Begin rant...
This is one of those areas in computing that really makes me see red. I have been in this business since the early 1970s and for the vast majority of that time there was no such thing as case-sensitivity. Neither the early mainframe operating systems nor their computer languages had any problem with being case-insensitive, and the same situation was passed down through all the mini-computers and micro-computers (now called 'personal computers') and the various software that I worked on. All of the text editing or document processing software that I used was case-insensitive. In those cases where case-sensitivity was recognised as being important it was provided as an option (check out the 'match case' option in Microsoft's Notepad, Wordpad and WinWord). It was not until the arrival of Unix that case sensitivity became a problem. Then people with knowledge of nothing but Unix began to create programming languages, and this disease called 'case-sensitivity' began to propagate and infest the software universe.
Why do I blame the authors of Unix? Because Unix was not written by professionals for professionals, it was written by amateurs for amateurs. I count academics working in an academic environment as amateurs for the simple reason that they are not earning a living in providing solutions for 'real world' situations. These amateurs either did not realise that case-sensitivity would cause a problem, or they did not have the technical ability to write case-insensitive software, so they labelled this 'bug' as a 'feature' and left it at that.
If you consider my attitude to be unjustified, then answer these two simple questions:
End rant...
Yes. This is all documented in Internationalisation and the Radicore Development Infrastructure (Part 1) and Part 2.
A POPUP form is the same as a LIST form, but with the addition of a CHOOSE button in the action bar. The SELECT column enables the user to mark one or more rows as 'selected' before pressing a button in order to 'do something' with the selected row(s). By pressing any button in the navigation bar the selection will be passed down to a child form, but by pressing the CHOOSE button in the action bar the selection will be returned to the previous form.
In a LIST form the SELECT column will contain checkboxes so that multiple selections can be made. In a POPUP form the SELECT column may contain checkboxes, or it may contain radio buttons which will allow only a single entry to be selected.
As mentioned in FAQ 17 the default behaviour for POPUP forms is to allow a single selection only using the default code in the _cm_popupCall() method. If multiple selections are to be allowed then the line
$settings['select_one'] = TRUE;must be changed to
$settings['select_one'] = FALSE;
Yes, but they will have no effect unless the stylesheet is programmed to deal with those parameters.
In my original implementation any parameters were specified during the XSL Transformation process, but I later decided to build them into the <params> element of the XML document so that they could be both visible and easily amendable during testing.
If any parameters are required they can be loaded into the $xsl_params array which is defined within the generic table class. Any entries will then be extracted from the object and automatically inserted into XML document by the standard code. Once inside the XML document the existence of any parameters can be detected by code within the XSL stylesheet and the appropriate action taken.
Here is an actual working example:
$dbouter->xsl_params = array('outer_noedit' => 'y');
buildXML():
$xsl_params = array_merge($xsl_params, $object->xsl_params);
$xsl_params array are written out to the XML document by function addParams2XMLdoc().<xsl:call-template name="display_vertical"> <xsl:with-param name="zone" select="'outer'"/> <xsl:with-param name="noedit" select="//params/outer_noedit"/> </xsl:call-template>If the parameter
outer_noedit exists (and regardless of its value) this will instruct the stylesheet to make all fields in the 'outer' zone to be non-editable or display-only. Without this setting any fields in this zone would be editable. This allows a single stylesheet to be used for different situations.
A database transaction is identified by issuing a START TRANSACTION (or equivalent) before attempting any database updates, and issuing either a COMMIT if the update was successful or a ROLLBACK if there was any failure.
A database update can only take place when the SUBMIT button is pressed on a relevant form. This uses the POST method to send a request to the web server. As the handling of all GET and POST methods is done within the various page controller scripts which already contain a call to the model to update the database, it is a minor matter to surround this call with additional calls to startTransaction() and commit()/rollback() methods, as demonstrated in the following code snippet:
if ($_SERVER['REQUEST_METHOD'] == 'POST') { $dbobject->startTransaction(); // update this data in the database $fieldarray = $dbobject->updateRecord($_POST); if ($dbobject->errors) { $errors[] = $dbobject->getErrors(); } // if $messages = $dbobject->getMessages(); if (empty($errors)) { $errors = $dbobject->commit(); } // if if (!empty($errors)) { $dbobject->rollback(); } //if } // if
The startTransaction() method shown here is defined within the abstract table class from which all individual table classes are extended. This in turn calls the _cm_getDatabaseLock() method followed by the startTransaction() method within the DML class which contains the actual code which communicates with the database. As there is a different DML class for each database engine each class contains the code which is specific to that engine.
The commit() and rollback() methods are handled in a similar manner.
By default the only database locking built into this infrastructure takes place during the execution of the updateRecord() method. Immediately prior to the database update is a call to _dml_ReadBeforeUpdate() which re-reads the specified record to determine what fields, if any, have changed. If nothing has been changed then no database update is required. This is also necessary so that the 'before' and 'after' details can be passed to the Audit Logging module. During construction of the sql SELECT string in _dml_ReadBeforeUpdate() the clause 'FOR UPDATE' is appended so that the specified record is locked for the duration of the current database transaction.
If this default locking is insufficient then two alternatives are available:
Table locking can be specified within the _cm_getDatabaseLock() method which can be copied from the abstract table class into the table class and modified as required. The default code is as follows:
function _cm_getDatabaseLock () // return array of database tables to be locked in current transaction. { $GLOBALS['lock_tables'] = FALSE; // TRUE/FALSE $GLOBALS['lock_rows'] = FALSE; // FALSE, SR (share), EX (exclusive) // the format of each $lock_array entry is one of the following: // $lock_array[] = 'tablename' (within current database) // $lock_array[] = 'dbname.tablename' (within another database) // $lock_array['READ'][] = '...' (for a READ lock) switch ($GLOBALS['mode']){ case 'insert': $lock_array[] = $this->tablename; break; case 'update': $lock_array[] = $this->tablename; break; case 'delete': $lock_array[] = $this->tablename; break; default: $lock_array = array(); } // switch return $lock_array; } // _cm_getDatabaseLock
To turn on table locking simply set $GLOBALS['lock_tables'] to TRUE and enter the required table names into $lock_array. Note that it is possible to load the array with different details depending on the current mode. Note also that two types of table locks are supported:
$lock_array[] = '...' will set a WRITE lock.$lock_array['READ'][] = '...' will set a READ lock.The actual sql query which sets the lock is constructed and issued using the _setDatabaseLock() method in the DML class.
Row locking can be turned on in the following ways:
$GLOBALS['lock_rows'] to 'SR' (shared) or 'EX' (exclusive) locking in _cm_getDatabaseLock().$this->row_locks to 'SR' (shared) or 'EX' (exclusive) locking in _cm_pre_updateRecord().$object->row_locks to 'SR' (shared) or 'EX' (exclusive) locking before calling the getData() or getData_raw() method on that object before updating it.This has the effect of appending the relevant lock statement to every sql SELECT which is issued via the getData() method during that database transaction. The $GLOBALS method will apply the same lock to all table objects used within the current database transaction, but this can be overridden within individual objects using the second (local) method.
If a database table is read during the processing of a database transaction (i.e. between a 'start transaction' and 'commit/rollback') and no table locks have been specified then the default behaviour is to change the SQL 'select' statement to append 'LOCK IN SHARE MODE' (MySQL) or 'FOR UPDATE' (PostgreSQL) so that the same record cannot be updated by anyone else during this database transaction.
This is where the same object is used by several tasks, but where different behaviour (i.e. different code) is required in each task.
In most cases each different task uses a different controller to communicate with the table object, and each of these controllers uses a particular set of methods, with some methods being shared by several controllers. For example, the ADD 1 controller uses the getInitialData() and insertRecord() methods while the UPDATE 1 controller uses the getData() and updateRecord() methods, but a lot of controllers use the initialise() method.
However, there may be circumstances when different tasks use the same controllers and hence the same methods, but where different code needs to be executed. There are two possible solutions to this dilemma:
The problem with this approach is that it becomes cumbersome when there are large amounts of code inside several conditions. Errors can creep in such as executing a block of code when you shouldn't, or not executing a block of code when you should.
Because this arrangement gives me the ability to run the same script with different task settings, thus changing its behaviour. If the differences in behaviour between one task and another are very slight, this saves the effort of having to create a complete new script for just a minor difference.
For example, in the RBAC system there is a script called mnu_task_list.php which will read records from the MNU_TASK table and display them. However, entries on this table are categorised by task_type and I may want the list automatically restricted to one task_type or another. I can achieve this by creating different tasks which access the same script, but which supply that script with different options at runtime, as follows:
| task_id | description | options |
|---|---|---|
| mnu_task(list) | List Task (All) | |
| mnu_task(list)a | List Task (Procedures) | task_type='PROC' |
| mnu_task(list)b | List Task (Menus) | task_type='MENU' |
The options can be supplied via the Update Task screen in any of the following fields:
| Selection (fixed) | The contents will be made available in the $where string, but will not be cleared by the RESET button nor altered in any SEARCH screen. |
| Selection (temporary) | The contents will be made available in the $where string, but will be cleared if the RESET button is pressed, and may be altered in any SEARCH screen. |
| Settings | The contents will be made available as variables, not as part of the $where string. It is therefore up to the application code to detect these and take the necessary action. |
Although there is no substitute for an IDE with an integrated debugger which will allow you to step though the code as it is being executed, to examine or change variables, and to set breakpoints, this framework does provide some means of providing information that may be useful prior to stepping through the code line by line. These are as follows:
Each HTML screen is produced by performing an XSL transformation on an XML document, so if there is a problem with a screen the first place to look should be the contents of the XML document. This is created in memory and discarded after the XSL transformation, but under certain conditions it can also be written out to a disk file for examination later:
CONFIG.INC file so that the variable $GLOBALS['log_xml_document'] is set to TRUE. This will cause each XML document to be written out to a disk file with the name /<subsystem>/xsl/<script_id>.xml for all sessions.log_xml_document value just for the current session.If the data you want to appear in the screen is not in the XML document, then you need to look in your code to see what data is being selected within each database object.
If the data is in the XML document, then you need to examine the screen structure script to see if you have instructed the system to display it in the screen. Have you specified the correct table and column names? Have you spelled them correctly? Have you used the correct case?
If you think that the wrong data is being retrieved from the database the first place to look should be the actual SQL query which was used by the framework. This is especially useful in those situations where the query is generated by the framework instead of being defined manually. There are two ways to see what SQL queries are being issued:
CONFIG.INC file so that the variable $GLOBALS['log_sql_query'] is set to TRUE. This will cause each SQL query to be written out to a disk file with the name /<subsystem>/sql/<script_id>.sql for all sessions.log_sql_query value just for the current session.If you want to check what changes are being made to the database the first place to look should be the contents of the Audit Log. This will show all changes - inserts, updates and deletes - to all tables though one of the following routes:
By default a SEARCH screen will only allow the user to enter a single value for each field. If you want the user to be able a range of values then a little customisation is required. In the following example a table has a field called DATE, but I want separate fields for DATE_FROM and DATE_TO to appear on the screen.
if ($GLOBALS['mode'] == 'search') {
// add extra (non-database) fields to search screen
$this->fieldspec['date_from'] = array('type' => 'date',
'size' => 12);
$this->fieldspec['date_to'] = array('type' => 'date',
'size' => 12);
} // if
This serves the following purposes:
$structure['main']['fields'][1][] = array('label' => 'Date From'); $structure['main']['fields'][1][] = array('field' => 'date_from'); $structure['main']['fields'][1][] = array('label' => 'To'); $structure['main']['fields'][1][] = array('field' => 'date_to');
date_from LIKE '2006-03-01' AND date_to LIKE '2006-03-21'into something which refers to the proper field name of DATE, such as:
date BETWEEN ('2006-03-01' AND '2006-03-21')This can be done using code similar to the following:
if (!empty($this->sql_search)) { // convert from string to an associative array $fieldarray = where2array($this->sql_search, false, false); // deal with values which have ranges if (!empty($fieldarray['date_from']) OR !empty($fieldarray['date_to'])) { $fieldarray['trn_date'] = rangeFromTo($fieldarray['date_from'], $fieldarray['date_to'], true); unset($fieldarray['date_from']); unset($fieldarray['date_to']); } // if } // if
It is possible for a database table to contain both a START_DATE and an END_DATE to signify that the record is only "live" between those two dates. It may therefore be useful to have a mechanism which will allow the user to quickly enter selection criteria to limit the selection to those entries which are one of the following:
The RADICORE framework has the following facilities to help deal with this situation:
curr_or_hist in your screen structure script then this dropdown list will magically appear.
curr_or_hist='C/H/F' string into valid SQL as follows:
curr_or_hist='H' will be replaced with end_date < '$today'curr_or_hist='C' will be replaced with start_date <= '$today' AND end_date >= '$today'curr_or_hist='F' will be replaced with start_date > '$today'curr_or_hist='A' (for ALL records) will be removed, not replaceddate(current) will set curr_or_hist='C'date(historic) will set curr_or_hist='H'date(future) will set curr_or_hist='F'This will allow the user to redisplay the contents of the current screen with new selection criteria with a single click.
Any selection criteria can be removed by pressing the RESET button on the action bar. This is equivalent to setting curr_or_hist='A'.
curr_or_hist='C' into the Selection (temporary) field.If you use field names other than START_DATE and END_DATE then you should define them as alias names in the Data Dictionary, otherwise the setCurrentOrHistoric() and currentOrHistoric functions will have to be performed manually.
Other languages or tools with which you may be familiar may have naming conventions or practices which are not recognised by RADICORE. You should therefore make yourself familiar with the RADICORE Programming Guidelines so that any incorrect assumptions can be identified.
Before you start building components for a new project/subsystem you should follow these simple steps. This will ensure that the files for your new components are not jumbled up with those of any existing subsystems.
NOTE: The previous steps can now be performed automatically. Please refer to Radicore Tutorial - Initialisation Procedure.
<tablename>.class.inc - this is the class definition through which all access to the table will be channelled.<tablename>.dict.inc - this contains the specifications for all columns, keys and relationships for the table.Should the physical structure of any database table change in the life of a project then the details in the data dictionary can be brought into line simply by rerunning the import columns function which will detect and deal with any amendments, deletions ad additions. The amended details can then be exported out to the application which will cause the <tablename>.dict.inc file to be overwritten. The <tablename>.class.inc will not be touched as it may contain custom code.
Once you have completed these preliminary steps you can then start to build user transactions for your new project. See FAQ 36 for details.
It is a common requirement to want to lock users out of the system so that important maintenance tasks can be performed, such as backing up the database or upgrading the software. If anybody tries to access the system during this period it could cause problems. Sending people a polite request via email will often be ignored, either deliberately or accidentally, so a more foolproof method is required.
The method employed in RADICORE allows the system administrator to schedule a shutdown period in advance, for one or more days in the week, and to automatically kick all users back to the logon screen during the designated time period on the designated days. It is also possible to display a warning message for a period beforehand so that no-one can complain that they were kicked out of the system without any warning.
The shutdown periods can be defined in the Update Menu Control data screen using the following fields:
| Shutdown Start | Time | Optional. This identifies the start of the shutdown period. |
| Shutdown End | Time | Optional. This identifies the start of the shutdown period. |
| Shutdown Warning | Time | Optional. This identifies the start of the warning period. |
| Shutdown Days | Boolean | Optional. These identify the days on which the shutdown times are effective. There is a separate checkbox for each day of the week. |
The shutdown period is only active on those days where the relevant checkbox is switched ON. All these times are deemed to be in the same day, so it it not possible to have a mixture of times which are before and after midnight as this would span two separate days. All these times will be in the time zone of the server.
If anybody accesses the system during the period between SHUTDOWN_WARNING and SHUTDOWN_START they will see the following message:
System will be shutting down between <shutdown_start> and <shutdown_end>
Note that the times displayed will be converted to the time zone of the client.
If anybody except an administrator accesses the system during the period between SHUTDOWN_START and SHUTDOWN_END they will be kicked back to the logon screen with the following message:
System has been shut down. It will be available at <shutdown_end>
Note that the time displayed will be converted to the time zone of the client. In order to identify the correct value when he/she is not logged on a cookie named timezone_client will be created at each successful logon, and this value will be used in any time zone conversions.
An administrator (one belonging to the 'GLOBAL' role) will see the shutdown message but will not be kicked back to the logon screen.
RADICORE provides a framework for running transactions "online" (i.e. via a web server), but what about running them in "batch" (i.e. via the command line), such as for a cron job? This may be necessary for a process which runs for longer than the max_execution_time for web pages. While running a PHP script from the command line is possible, it does require a little preparation as a PHP instance run from the command line does not have the same variables set as is available from a web server. In order to get around this the following procedure should be followed:
batch.ini and std.batch.inc from the DEFAULT directory to the directory where the script you wish to run resides.batch.ini to provide values for the following:
radicore/default/batch.php into your subsystem's folder and rename it as appropriate. It should look something like this:
<?php $stdout = '../logs/#tablename#.html'; $csvout = '../logs/#tablename#.csv'; $pdfout = '../logs/#tablename#.pdf'; ini_set('include_path', '.'); require 'std.batch.inc'; batchInit(__FILE__); // custom code starts here // custom code ends here batchEnd(); ?>
You will need to edit this file to change the value for #tablename#, and to inset the code to perform the necessary processing.
stdout, or the file specified in the $stdout variable, so that you can check whether it worked or it failed.Here is an example which can be found in radicore/xample/fix-last-addr-no(batch).php:
<?php $stdout = '../logs/fix_last_addr_no.html'; ini_set('include_path', '.'); require 'std.batch.inc'; batchInit(__FILE__); // this checks that person.last_addr_no = count(person_addr.person_id) $dbobject = singleton::getInstance('x_person'); $dbobject->sql_select = 'x_person.person_id, x_person.last_addr_no, count(address_no) as count'; $dbobject->sql_from = 'x_person LEFT JOIN x_person_addr USING (person_id)'; $dbobject->sql_groupby = 'x_person.person_id, x_person.last_addr_no'; $dbresult = $dbobject->getData_serial(); $dbobject->startTransaction(); $count = 0; while ($row = $dbobject->fetchRow($dbresult)) { if ($row['last_addr_no'] <> $row['count']) { $row['last_addr_no'] = $row['count']; echo '<p>Updated person_id ' .$row['person_id'] .', last_addr_no=' .$row['count'] .'</p>'; $dbobject->skip_validation = true; $row = $dbobject->updateRecord($row); check_errors($dbobject); $count++; } // if } // while $dbobject->commit(); echo "<p>$count records updated</p>\n"; batchEnd(); ?>
Note the use of the getData_serial() and fetchRow() methods. This will allow you to fetch the applicable rows one at a time instead of being given the entire collection in a single array. This will enable you to fetch a row and process it before fetching the next row.
Here is an example which does the same processing as an online task:
<?php //***************************************************************************** // this outputs INVENTORY_ITEM data to a CSV file in a background process //***************************************************************************** $stdout = '../logs/inventory_item(csv).html'; $csvout = '../logs/inventory_item.csv'; ini_set('include_path', '.'); require 'std.batch.inc'; batchInit(__FILE__); // custom code starts here $table_id = 'inventory_item_s03'; // table name require 'std.output1.inc'; // activate page controller // custom code ends here batchEnd(); ?>
If you wish to start a batch job from a web page then please refer to FAQ119.
A virtual table does not actually exist, therefore contains no data, but sometimes it can be convenient to create a virtual table during the life of a particular user transaction so that the transaction can operate in a more user-friendly fashion.
For example, take the situation where only two tables exist on the database, but a particular transaction would operate better if there were three. Such a situation exists in the Classroom Scheduling prototype where the following table structure exists:
Figure 14 - The physical database structure
There is a separate entry for each classroom which is keyed on ROOM_ID. The schedule table contains data which is keyed on ROOM_ID, DAY_NO (1=Monday, 2=Tuesday, etc), START_TIME and LESSON_ID, and therefore contains data for the whole week. But suppose I wanted to show each day's schedule separately instead of having all the different days mixed together? This would best be implemented using the following table structure:
Figure 15 - The virtual database structure
This can be achieved in the RADICORE framework by using an object for the mythical "DAY" table which constructs its data at runtime instead of reading it from a database table. This can be done with the following steps:
crs_schedule_x01.$this->primary_key = array('room_id', 'day_no');This will allow the transaction controller to pass the correct information from the "DAY" object to the "SCHEDULE" object.
if (!empty($where)) { // store $where string in its component parts $where = $this->setScrollArray($where); } // if return $where;
This populates $this->scrollarray with data. Entries will be retrieved from this array instead of being retrieved from the database. This is because the entries do not exist in the database.
$this->skip_getdata = TRUE; // retrieve a single entry from the constructed array $where = $this->getScrollItem($this->pageno); // convert from string to an associative array $array = where2array($where); $this->fieldarray = array(); // merge with $where passed down from parent object $this->fieldarray[] = array_merge($where_array, $array); // create $where string for child object $where = array2where($this->fieldarray[0]); return $where;
This tells the abstract table class not to bother trying to populate $this->fieldarray with data from the database as it is going to be obtained from $this->scrollarray. Entries will be picked out one at a time using $this->pageno as the key.
// get array of day numbers $array = $this->getValRep('day_no'); // create array of WHERE clauses, one for each day of the week $array2 = array(); foreach ($array as $dayno => $dayname) { $array2[$dayno]['day_no'] = $dayno; } // foreach return $array2;
As you can see the RADICORE framework does not care where the data inside an object comes from. It can be retrieved from the database, constructed in memory, or even a combination of the two. This allows the user's view of the data to be customised without being restrained by the physical structure of the database.
By default all web pages are served using the HTTP protocol which means that all communication between client and server is in plain text. This has security implications, especially when a web page is used to enter a user's identity and password, as it makes it possible for anyone who can eavesdrop on a line to read that information in plain text.
The solution is to switch to a secure protocol for those pages which contain sensitive information. This will cause the request to be encrypted before it is transmitted, then decrypted when it is received by the web server. This means that an eavesdropper will see encrypted text and not plain text. The use of this secure protocol is signified by the prefix HTTPS instead of HTTP in the browser's address window. Note that it is not usual to have every page on a website transmitted using HTTPS as the overhead of encrypting and decrypting each and every page is considered too great. Instead everything is HTTP except those pages which contain sensitive information.
So how does RADICORE handle the switch between HTTP and HTTPS? The answer comes in two parts:
http with https is not good enough as the following possibilities exist:
(a) https://www.yourdomain.com/ (b) https://secure.yourdomain.com/ (c) https://secure.sharedserver.com/~yourdomain/
In RADICORE point (1) is addressed by setting variables in the CONFIG.INC file. If there is no secure server then these variables must remain blank, as in:
$GLOBALS['http_server'] = ''; $GLOBALS['https_server'] = ''; $GLOBALS['https_server_suffix'] = '';
If a secure server is available then these variables must be set differently depending on the server name.
For option 1(a) use the following:
$GLOBALS['http_server'] = 'www.yourdomain.com'; $GLOBALS['https_server'] = 'www.yourdomain.com'; $GLOBALS['https_server_suffix'] = '';
For option 1(b) use the following:
$GLOBALS['http_server'] = 'www.yourdomain.com'; $GLOBALS['https_server'] = 'secure.yourdomain.com'; $GLOBALS['https_server_suffix'] = '';
For option 1(c) use the following:
$GLOBALS['http_server'] = 'www.yourdomain.com'; $GLOBALS['https_server'] = 'secure.sharedserver.com'; $GLOBALS['https_server_suffix'] = '/~yourdomain';
Provided that a secure server has been identified in point (1) above, point (2) is satisfied in the following ways:
$GLOBALS['use_https'] = TRUE can be set in the CONFIG.INC file.FAQ #9 shows how to implement a standard dropdown list which allows the user to make a single selection, but what if you want to allow multiple selections? Fortunately this type of control is catered for in HTML, so all that is needed is the right code to bring it into play. To achieve this it is necessary to have the XML file contain data similar to the following:
<?xml version="1.0"?> <root> <person> <person_id size="8" pkey="y" required="y">FB</person_id> <favourite_food control="multidrop" optionlist="favourite_food" rows="5"> <array id="1" /> <array id="3" /> <array id="4" /> </favourite_food> .... </person> <lookup> <favourite_food> <option id="1">Eggs</option> <option id="2">Bacon</option> <option id="3">Chips</option> <option id="4">Beans</option> <option id="5">Sausages</option> <option id="6">Mushrooms</option> <option id="7">Tomatoes</option> <option id="8">Hash Browns</option> <option id="9">Toast</option> <option id="10">Fried Bread</option> .... </favourite_food> </lookup </root>
Notice the following:
The text that goes which each id is obtained from the file <subsystem>/text/<language>/language_array.inc which is defined in the following format:
$array['favourite_food'] = array('1' => 'Eggs', '2' => 'Bacon', '3' => 'Chips', '4' => 'Beans', '5' => 'Sausages', '6' => 'Mushrooms', '7' => 'Tomatoes', '8' => 'Hash Browns', '9' => 'Toast', '10' => 'Fried Bread');
Different versions of the same file can exist in different <language> sub directories to provide different language translations. This facility is described in more detail in Internationalisation and the Radicore Development Infrastructure.
When using MySQL the datatype which allows multiple values is SET. Notice here that the SET contains the id and not the textual value for each entry. MySQL will ensure that only entries from that list can be used.
MySQL: CREATE TABLE `person` ( ...., `favourite_food` SET('1','2','3','4','5','6','7','8','9','10') default NULL, ...., PRIMARY KEY (`person_id`) );
PostgreSQL does not have the SET datatype, but the nearest equivalent is the ARRAY which will accept multiple values in a single field. Notice that it is not possible to define what the possible range of values is, nor the maximum number of entries, just the datatype of those entries.
PostgreSQL: CREATE TABLE person ( ...., favourite_food varchar(2)[], .... );
When the table structure is exported from the Data Dictionary the entry for multi-dropdown fields will look something like the following:
$fieldspec['favourite_food'] = array('type' => 'set', <!-- or 'array' for PostgreSQL -->
'control' => 'multidrop',
'optionlist' => 'favourite_food',
'rows' => 5);
For common instructions on how to implement dropdown lists and radio groups please refer to FAQ #9.
When viewed in input or edit mode the control will look similar to Figure 16. Each entry which has been selected will be highlighted. Note that the number of entries which can be displayed at any one time (the size of the scrollable area) is governed by the rows parameter. Different browsers have different defaults for size, but setting this value will cause all browsers to behave the same.
Figure 16 - a Dropdown list with multiple selections
Note that when viewing a dropdown list which is not editable the scroll bar is frozen, which means that it is not possible to check if any entries outside the current scroll area have been selected. For this reason I do not display the dropdown control but instead simply output the selected entries as a simple string with a comma delimiter between each entry, as shown in Figure 17:
Figure 17 - a read-only list of multiple selections
This is described in the User Guide to the Menu and Security System - Appendix F.
Even though the core framework does not use javascript (for reasons stated in Why don't you use javascript?), developers now have the ability to add javascript into their own application subsystems should they so desire. The techniques used are documented in RADICORE for PHP - Inserting optional JavaScript.
By default the output from each task is sent to the client browse as HTML, but there are transaction patterns available which will use other formats.
All HTML output is produced from an XSL transformation which uses the instructions contained within an XSL stylesheet and the data contained within an XML document. This has two points of great significance:
If you are testing a new HTML screen and it does not contain the expected fields then the very first place to look is the XML document. Although these are constructed in memory and usually discarded immediately after use, there is a way to have them written out to disk so that their contents can be examined later. Please refer to FAQ 51 for details. The data from an application database should be easy to spot as it uses the same table names and column names, as shown in the following XML fragment:
<table1> <column1 attribute1="attr1" attribute2="attr2">value1</column1> <column2>value2</column2> ............ <columnN>valueN</columnX3> </table1> <table1> ............ </table1> <anothertable> ............ </anothertable>
Note the following about this XML fragment:
table1 and anothertable. An XML document can contain data from many different tables.table1 table. An XML document can contain any number of occurrences for each table.How does this data get written into the XML document? Each transaction pattern uses one or more database objects, and when these have finished their processing the contents of their internal data arrays will be extracted and written to the XML document according to the following rules:
name=value pairs, so it is a simple process to iterate through the array and write each value out to the XML document with the same name. Whatever is found in the array will be written out to the XML document.noedit attribute set.The RADICORE framework uses a small number of generic and reusable XSL stylesheets which do not contain any hard-coded table or field names. In order to determine which piece of data goes where each XML document contains a separate structure element which is obtained from a screen structure file. The XML structure element looks similar to the following:
<structure> <main id="person"> <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> .... </main> </structure>
This is processed by the XSL stylesheet as follows:
main, outer, middle, inner, parent or child. The structure element must therefore identify which database table goes into which zone. In the above example the zone called main will be populated with data from the person table.rows each of which contains a number of cells. Each cell will either contain a label or data for a named field. Note that the structure element above contains all the necessary row, cell, label and field details that it needs to construct the HTML output.Please note the following:
ENUM fields are peculiar to MySQL, so they should not be used if you ever plan to port your database to another DBMS engine.
An ENUM field is constructed using a DDL statement similar to the following:
ALTER TABLE `foobar` ADD `enum_field` ENUM( 'red', 'green', 'blue' ) NULL ;
In order to update this field you must supply a value which is either 'red', 'green' or 'blue', and MySQL will store the index number which is either 1, 2 or 3. The index number of 0 is reserved for an empty value.
The best way to present the user with the available choices is with a dropdown list as described in FAQ #9. You then need the following code in your _cm_getExtraData() method:
function _cm_getExtraData ($where, $fieldarray)
{
// get values for enum_field and insert into lookup array
$array = $this->getValRep('enum_field');
$this->lookup_data['enum_field'] = $array;
return $fieldarray;
} // _cm_getExtraData
This will also require the following code in your _cm_getValRep() method:
function _cm_getValRep ($item, $where)
// get Value/Representation list as an associative array.
{
$array = array();
if ($item == 'enum_field') {
$array = $this->getEnum($item);
return $array;
} // if
return $array;
} // _cm_getValRep
This will go to the database to obtain the array of values using code similar to the following:
function getEnum ($dbname, $tablename, $fieldname)
// get the contents of an ENUM field and return it as an array.
{
$this->connect($dbname) or trigger_error($this, E_USER_ERROR);
// obtain 'enum' values for the specified column
$this->query = "SHOW COLUMNS FROM $tablename LIKE '$fieldname'";
$result = mysql_query($this->query) or trigger_error($this, E_USER_ERROR);
$query_data = mysql_fetch_array($result);
// convert the 'enum' list into an array
// 1st, extract everything between '(' and ')'
if (eregi("('.*')", $query_data['Type'], $enum)) {
// 2nd, remove all single quotes
$enum = ereg_replace("'", "", $enum[1]);
// 3rd, insert dummy entry so that real entries start at 1
$enum = ',' .$enum;
// last, turn list into an indexed array
$enum_array = explode(',', $enum);
} // if
mysql_free_result($result);
return $enum_array;
} // getEnum
This will return an array in the format:
array(0 => '', 1 => 'red', 2 => 'green', 3 => 'blue');
Note that the real values start with the index number of 1. Index 0 represents a blank/null value.
If you wish to populate the dropdown list with text in different languages then you must replace
$array = $this->getEnum($item);
with:
$array = getLanguageArray('enum_field');
This will require an entry in your text/<language>/language_array.inc file similar to the following:
$array['enum_field'] = array('red' => 'rouge', 'green' => 'verte', 'blue' => 'bleu');
Alternatively, instead of defining the ENUM field with actual descriptions, as in:
star_sign ENUM('Aries', 'Aquarius', 'Cancer', 'Capricorn', ... , 'Virgo'),
you can define it with keys to an array, as in:
star_sign ENUM('ARI', 'AQU', 'CAN', 'CAP', ... , 'VIR'),
The relevant descriptions can be obtained from an entry in your text/<language>/language_array.inc file as follows:
$array['star_sign'] = array('ARI' => 'Aries', 'AQU' => 'Aquarius', 'CAN' => 'Cancer', 'CAP' => 'Capricorn', ..., 'VIR' => 'Virgo');
After passing through the logon screen the user is taken immediately to the menu/home page which will show a series of options in the menu bar, but the options that each user sees are configurable and not fixed. The RADICORE framework contains the following configuration methods:
Installation instructions are available here. They are also available in the readme.txt file which is included in the downloadable zip file.
There is a design document available at An activity based Workflow Engine for PHP which describes how Petri Nets form the basis of RADICORE's Workflow system. There is also a User Guide for all the maintenance screens.
In the download package at radicore/workflow/docs/workflow-example.html is a document which describes sample workflows which have been created, and which are shown in Figure 18:
Figure 18 - list Workflow entries
In order to test any of these you will need to install the XAMPLE subsystem.
Note that there can be no more than one active workflow for the same start task, so only one of these should be made 'active' at any one time. This is done by setting its end date in the future instead of the past. This means that you can switch from one workflow example to another without have to delete the 'old' definition and add in the 'new' one.
When a task which has been nominated as a workflow start task is processed this will cause a workflow case to be initiated. If that task is the start task in more than one workflow then anything other than the first workflow will be ignored.
RADICORE is for building restricted-access administrative web applications, not open-access web sites, and is based on years of experience with developing administrative applications for the desktop. Such applications are by their very nature governed by strict security protocols - nobody can access any part of the system until they pass through a login screen, and even then they can only access those parts of the system for which permission as been explicitly granted.
Because of this every page request undergoes the same validation checks before being allowed to proceed:
In order to cater for anonymous users both of these checks would have to be turned off, which could open up a huge security hole.
If you really require anonymous access to the data which is maintained by the RADICORE system then the solution is simple - create your own set of pages which do not include the RADICORE security checks. You can still reuse the existing components in the business layer and data access layer, but you will have to create a completely different set of components for the presentation layer which avoid the use of RADICORE's page controllers. This will also give you the opportunity to create HTML output without the use of XML documents and XSL stylesheets.
Bookmarking a page is the act of capturing the state of a session at a particular moment in time so that the same page can be replayed at another time, perhaps even by a different person on a different computer. Such things are quite common for open-access web sites, but they are quite rare for restricted-access administrative web applications, and completely unknown for administrative desktop applications.
RADICORE does not support bookmarking for the simple reason that session state is not carried around in any URL, it is maintained in data files on the server using PHP's session handling functions. This is why, for example, when you select one or more entries in a LIST screen and press a navigation button to pass control to a child screen that you do not see any reference to what has been selected in the URL. It is not considered good practice to expose any primary key details in any URLs as this may present a security threat. This is why RADICORE keeps details of all selections on the server by recording them in the $_SESSION array instead of sending them to the client in any URL.
RADICORE was designed to be the front-end for administrative web applications which have restricted access, not web sites which are open-access and which can be viewed by the whole world. The use of session data which is maintained on the server plays a vital role in RADICORE's security mechanism:
As the use of bookmarks would compromise and conflict with vital security requirements their use is not supported in RADICORE.
Other information regarding RADICORE's use of server-side session data is described in the following documents:
When a user employs a standard update transaction (e.g. Update1) this will read the specified database record, show the current values on the screen so that changes can be made, then apply those changes to the database when the user presses the SUBMIT button. However, It is possible for a second user to update the same record in that time interval between the 'read' and the 'update' of the first user, and if they have changed the same field to different values then the values in the earlier update would be overwritten by those in the later update.
This is normal behaviour. If it causes a problem the first question that should be asked is "Why are two users trying to update the SAME fields on the SAME database record to different values?" This clearly points to some sort of breakdown in the administrative procedures.
If there is a genuine need to force the system to prevent simultaneous updates of the same database record then this option can be turned ON for particular database tables by following this procedure:
When the updateRecord() method is processed and $fieldarray contains a value for 'rdcversion' then this value will be appended to the primary key in the $where string which is passed to the _dml_ReadBeforeUpdate() method. If the value for 'rdcversion' has changed then the record which matches that $where string will not be found, causing the update to be aborted and the error message "Could not locate original <tablename> record for updating" to be displayed.
If the field 'rdcversion' exists on the table but no current value is supplied in $fieldarray then there is no value which can be appended to the $where string, and the check for a simultaneous update cannot be performed. This can occur if the SELECT string which is used to read the original data from the database before the update does not include the 'rdcversion' field.
The $where variable is used as the WHERE clause in an sql SELECT statement to provide selection criteria when retrieving data from the database. This is used as an argument on the getData() method in all database table objects.
By default when a task is activated from a menu screen it does not have any selection/search criteria, so it will select every available record on its particular database table. Selection criteria can subsequently be defined by pressing the SEARCH button in the navigation bar, which activates a SEARCH screen. This enables the user to enter whatever criteria is desired to filter the data so that only those records which match the selection criteria are displayed.
In some cases it may be useful to have selection criteria which is preset for the task and which can be used without user intervention. Several methods are possible:
<?php $table_id = 'mnu_task'; // table name $screen = 'mnu_task.list.screen.inc'; // file identifying screen structure $sql_where = "pattern_id='ADD1'"; require 'std.list1.inc'; // activate page controller ?>
The contents of $sql_where is fixed for the task and cannot be altered by the user.
$where string. This selection criteria will be fixed for the component.
The RESET button on the action bar cannot be used to clear this fixed selection criteria.
Additional selection criteria may be provided from a separate search screen.
The advantage of this approach is that it is possible to have several different tasks all using the same component script but with different selection criteria, as documented in FAQ 50.
$where string. This temporary selection criteria will be used for the initial activation of the component, but may be changed.
The RESET button on the action bar will clear this temporary selection criteria.
Any selection criteria provided by a search screen will be used instead of, not in addition to, this temporary selection criteria.
Example: A table contains columns named start_date and end_date which means that individual records can be one of the following:
Without any selection criteria the task will show all available records regardless of their dates, but suppose the user preferred a default view of "current", but with the ability to change it? This can be done by setting Selection (temporary) on the task data to curr_or_hist='C'. This is a reserved word in the RADICORE framework which is used to set the correct selection criteria for tables which contain fields to hold start and end dates, as described in FAQ 53. However, this selection criteria is temporary, not fixed, which means that it can be replaced with different criteria - either by using a separate SEARCH screen, or a navigation button as shown in FAQ 72 - or even cleared altogether.
If the same task has initial values defined at both the USER level and the ROLE level then the USER values will take precedence.
The RESET button on the action bar cannot be used to clear this fixed selection criteria.
Additional selection criteria may be provided from a separate search screen.
If the same task has initial values defined at both the USER level and the ROLE level then the USER values will take precedence.
The RESET button on the action bar cannot be used to clear this fixed selection criteria.
Additional selection criteria may be provided from a separate search screen.
Selection criteria can either be static, as shown here, but it can also be dynamic, or even a mixture of the two.
The purpose of a SEARCH screen is to allow the user to specify selection criteria which can be passed back to the previous screen for inclusion in the WHERE clause of an SQL query. However, it is also possible to create a task which passes back pre-defined selection criteria without any user intervention. All that is required is a component script such as the following:
<?php require_once 'include.general.inc'; initSession(); // initialise session // send search criteria back to the previous script $prev_script = getPreviousScript(); $prev_task = getPreviousTask($prev_script); $_SESSION[$prev_script][$prev_task]['search'] = "<selection criteria goes here>"; $this->scriptPrevious(null, 'OK'); ?>
The selection criteria is any number of valid expressions which can appear in the WHERE or HAVING clause of an sql SELECT statement.
This task can then be added to another task's navigation bar so that the selection criteria can be incorporated into the parent task with a single button click. Different tasks can be created to return different selection criteria.
By default all field values in the HTML output are displayed using the same attributes, but it may be useful in certain circumstances to change the attributes for certain fields to highlight a particular condition, such as displaying all negative financial value in red for example. It is possible to change the display attributes for any field in any row by adding the relevant CSS class name to the $css_array argument in the _cm_formatData() method, as in the following example:
function _cm_formatData ($fieldarray, &$css_array) // perform custom formatting before values are shown to the user. // Note: $css_array is passed BY REFERENCE as it may be modified. { if ($fieldarray['count'] <= 50) { $css_array['count'] = 'whatever'; } // if return $fieldarray; } // _cm_formatData
This means that in any row where the value of field count is less than or equal to 50 then that value in the HTML output will be enclosed in a <DIV> with the specified class name, as in the following:
<td><div class="whatever">49</div></td>
Of course the condition can be whatever you want, and the CSS class name can be whatever you want. It is possible to specify multiple class names if there is a space as a separator between each name, as in "class1 class2". The CSS class(es) should be specified in the style_custom.css file which belongs to that subsystem so that it does not conflict with any custom CSS styles for other subsystems.
This feature will also work with PDF output in List View and Detail View provided that the CSS class name has also been defined as a style in the the PDF Style File. Note that it it not possible specify multiple class names with PDF output, so the value "class1 class2" will not be valid.
They can be found at RADICORE Programming Guidelines. There is also a document regarding Database Design.
Lookup arrays are used to populate dropdown lists or radio groups. They may be hard-coded into the application. obtained from a database table, or from a language_array.inc file. Each array should contain the non-blank entries which are to be used in that dropdown list or radio group, but sometimes a blank entry is required to indicate that no choice has yet been made. This can be done automatically by the framework when the lookup array is loaded into the XML document by adding an entry with a null key and a suitable description as follows:
Blank entries will NOT be added to multi-choice dropdown lists, or ENUM fields which already contain an entry for index 0, or arrays which already contain an entry with a key of ' ' (single space character), so the framework needs to check the control type of the field before it knows whether to insert a blank entry or not. The name of the lookup array need not be the same as the field into which that array will be loaded, so the framework will determine the field name, and hence the control type, as follows:
The only data types I have tested across the various databases are the ones that I have actually used within the various RADICORE applications. These are all covered within the data access classes which I have written, starting with MySQL, then PostgreSQL and Oracle. The data types are as follows:
Some databases do not support a BOOLEAN data type, in which case a CHAR(1), or in the case of MySQL a TINYINT(1) field, can be used instead. After the table details have been imported into the Data Dictionary this data type can be updated to BOOLEAN before the table details are exported to the application. Once a field has been identified as BOOLEAN in the Data Dictionary it is then possible to identify the values to be used as TRUE and FALSE by the application. For numeric fields this is usually 1 and 0, but for string fields this can be YES/NO, Y/N, or T/F.
Both MySQL and PostgreSQL have separate data types for storing DATE only, TIME only or DATE+TIME combined, but Oracle has a single DATE datatype to cover all three. After the table details have been imported into the Data Dictionary this data type can be updated to either DATE, TIME or DATETIME before the table details are exported to the application. This will enable the data to be validated and displayed in a consistent manner.
If you have a table which contains fields called START_DATE and END_DATE to indicate when that entity is live or when it has expired, you may wish to take a look at FAQ 53 and Dealing with null End Dates.
Auto-increment columns are available in all three databases, but with different implementations:
When the table details are imported into the Data Dictionary the AUTO_INCREMENT keyword will automatically be detected and passed into the <tablename>.dict.inc script.
<tablename>_<colname>_seq and automatically fills in the next number from this sequence when an INSERT operation is performed. The number assigned can be obtained by the application by executing the SELECT currval('<sequence>') query.
When the table details are imported into the Data Dictionary the use of the SERIAL or BIGSERIAL data types will cause the AUTO_INCREMENT option to be passed into the <tablename>.dict.inc script.
CREATE TABLE foobar ( foobar_id number(10) NOT NULL, ...., PRIMARY KEY (foobar_id) ); CREATE SEQUENCE foobar_seq; GRANT SELECT,INSERT,DELETE,UPDATE ON foobar TO PUBLIC; GRANT SELECT,ALTER ON foobar_seq TO PUBLIC;
The next number must extracted manually from this sequence and inserted into the relevant column with an SQL query similar to the following:
INSERT INTO foobar (foobar_id, ...) VALUES (foobar_seq.nextval, ...)
The number assigned can be obtained by the application by executing the SELECT <sequence>.currval query.
When the table details are imported into the Data Dictionary the code will look for the following:
<tablename>_seq.If both are found then the AUTO_INCREMENT option will be set and passed into the <tablename>.dict.inc script.
Because the RADICORE framework uses a different class file for each database engine the differences in implementation for AUTO_INCREMENT columns are handled within each class file and do not require any coding within the application.
Because I can.
Some people like to tell me that I can't do something because it breaks their rules, but as I do not follow their rules why should I care? Besides, I am results-oriented and not rules-oriented, so I prefer to choose a solution that produces the best results, not one which adheres to an arbitrary set of rules.
Because I don't have to.
Some people seem to think that what they have been taught is the only way, the one true way, and that anyone who does something different is a deviant or a heretic. My decades of experience has taught me that there is usually more than one solution for a problem, and once I have found a solution that works I see no reason why I should switch to a different solution in which I have absolutely no confidence. That's the main reason why I don't use any of the following:
A POPUP screen is used instead of a dropdown list where the number of selectable options is too large. It is exactly the same as a LIST screen, but includes a CHOOSE button in the action bar. This allows the user to select one or more entries in the screen, and when the CHOOSE button is pressed that selection is passed back to the previous screen. If only a single selection is possible the SELECT column will contain radio buttons. If multiple selections are possible it will contain checkboxes.
A POPUP2 screen is modelled on a LIST2 screen, which means that it deals with two tables which have a one-to-many relationship. It is first necessary to select an occurrence from the ONE table before associated occurrences from the MANY table can be made available for selection. This will cause a problem as by default the WHERE string is empty when a popup screen is called, therefore the POPUP2 will terminate with the message "Nothing selected from popup screen" as it cannot retrieve anything from the MANY table until an occurrence on the ONE table has been selected. There are two different ways to solve this problem:
function _cm_popupCall ($popupname, $where, $fieldarray, &$settings)
// if a popup button has been pressed the contents of $where amy need to
// be altered before the popup screen is called.
// NOTE: $settings is passed BY REFERENCE as it may be altered.
{
if ($popupname == 'x_tree_structure(popup)') {
$where = "tree_type_id='ORG'";
} // if
// allow only one entry to be selected
$settings['select_one'] = true;
return $where;
} // _cm_popupCall
function _cm_initialise ($where)
// perform any initialisation for the current task.
{
if (empty($where)) {
if (isset($GLOBALS['return_from'])) {
if ($GLOBALS['return_from'] == 'rq_request(popup1)') {
// nothing selected from popup screen
$this->scriptPrevious($GLOBALS['errors']);
} // if
} // if
$pattern_id = getPatternId();
if (strtolower($pattern_id) == 'popup2') {
// request_id has not been supplied yet, so get it now via a popup
$this->scriptNext('rq_request(popup1)');
} // if
} // if
return $where;
} // _cm_initialise
This will cause the POPUP2 screen to be suspended before it is displayed, and activate a POPUP1 screen instead. When a selection has been chosen in the POPUP1 screen it will be passed back to the POPUP2 screen which will display that selection in the ONE area, thus allowing the user to make a second selection from the MANY area.
It is also possible to put the POPUP1 screen in the navigation bar of the POPUP2 screen so that the user can change the selection that appears in the ONE area of the POPUP2 screen.
This is referring to the column headings in LIST screens which are shown as hyperlinks, which will cause the data to be retrieved and sorted on that column. The comment "not sorted as I expect" refers to the possibility that after sorting on a particular column the values in that column on successive rows do not appear to be in sequence. This is quite possible in valrep (value/representation) situations where the value obtained from the database is converted into a different representation before being displayed to the user. This is common where dropdown lists or radio groups are involved as the value held on the database is a short code while the representation is a longer string.
For example, take the situation where an application deals with days of the week. Here it is quite normal for the database table to hold a 1 digit DAY_NUMBER while the text for DAY_NAME is provided within the application, possibly with a different translation for different languages. This produces the following:
| DAY_NUMBER (value) | DAY_NAME (representation) |
|---|---|
| 0 | Monday |
| 1 | Tuesday |
| 2 | Wednesday |
| 3 | Thursday |
| 4 | Friday |
| 5 | Saturday |
| 6 | Sunday |
As all sorting is performed within the database by adding an ORDER BY clause to the sql SELECT statement the default behaviour is to sort on DAY_NUMBER rather than DAY_NAME. This is because the database contains a field called DAY_NUMBER but knows nothing about DAY_NAME.
In situations where the representation is obtained from another database table, where the value is a foreign key which links to a foreign table, it is possible to sort on either the value or the representation, depending on which of these two columns has been nominated in the screen contents.
When a relationship is defined within RADICORE's Data Dictionary it is possible to nominate a field on the parent/senior table which will automatically be retrieved when the child/junior table is accessed. This is because the framework has the information it needs to construct the following SQL query:
SELECT child.*, parent.foreign_desc FROM child LEFT JOIN parent ON (parent.primary_key=child.foreign_key)
This makes it possible for the database to sort on either the value (FOREIGN_KEY) or the representation (FOREIGN_DESC).
The default behaviour within RADICORE is to automatically replace the contents of FOREIGN_KEY with the contents of FOREIGN_DESC when the HTML output is being constructed, so even though the user is seeing the contents of FOREIGN_DESC the field name is still FOREIGN_KEY. If the column sorting hyperlink is pressed the field name which is passed down to the Data Access Object for inclusion in the ORDER BY clause of the sql SELECT statement is FOREIGN_KEY and not FOREIGN_DESC. This is why the record sequence after the sort may not be what was expected.
To change the behaviour of the sort the solution is simple - go to the relevant screen structure file and replace FOREIGN_KEY with FOREIGN_DESC. Although this will display the same information to the user it will cause the field name on the screen and therefore in the column sorting hyperlink to be different.
By default when a popup control is used for a field on an input form all the user sees is the popup button in the field's data area as shown in Figure 19:
Figure 19 - Default POPUP display
The user clicks on the popup button, the popup form is displayed, the user makes a selection and presses the CHOOSE button, which causes the selection to be passed back to the previous form.
By default no selection criteria is passed to the POPUP form, but this behaviour can be overridden by placing code in the _cm_popupCall() method.
In order to display the popup field as a text box in which the user can enter data it will be necessary to add 'allow_input' => 'y' to the field's attributes in the $fieldspec array. This can be done in the _cm_changeConfig() method using code similar to the following:
function _cm_changeConfig ($where, $fieldarray)
// Change the table configuration for the duration of this instance.
// $where = a string in SQL 'where' format.
// $fieldarray = the contents of $where as an array.
{
if ($GLOBALS['mode'] == 'insert') {
$this->fieldspec['discount_code']['allow_input'] = 'y';
} // if
return $fieldarray;
} // _cm_changeConfig
This will produce the result shown in Figure 20:
Figure 20 - POPUP display which allows user input
In order to pass any value entered by the user to the POPUP form you will need to modify the _cm_popupCall() method using code similar to the following:
function _cm_popupCall ($popupname, $where, $fieldarray, &$settings)
{
// clear out the contents of $where
$where = '';
// allow only one entry to be selected (the default)
$settings['select_one'] = true;
if ($popupname == 'pro_price_component_discount(popup1)') {
// replace $where for this popup
$where = "discount_code='{$fieldarray['discount_code']}'";
} // if
return $where;
} // _cm_popupCall
By default the POPUP form will use whatever is passed in $where as selection criteria before displaying the list of qualifying entries, then wait for the user to select an entry and press the CHOOSE button. It is possible to alter the behaviour of the POPUP form so that no screen is displayed - instead it will issue an SQL SELECT using the contents of $where and return the result immediately, either 'record found' or 'record not found'. This can be done by inserting code into the _cm_post_getData() method similar to the following:
function _cm_post_getData ($rows, &$where)
{
if (count($rows) == 1) {
$GLOBALS['settings']['choose_single_row'] = true;
} elseif (count($rows) < 1) {
// "Nothing retrieved from the database"
$this->errors[] = getLanguageText('sys0085');
} // if
return $rows;
} // _cm_post_getData
Note that if you have set the value of foreign_field for that popup item to a different field, so that it displays a description instead of the key, then you will have to insert another line into _cm_changeConfig similar to the following:
$this->fieldspec['discount_code']['foreign_field'] = 'discount_code';
If you do not do this then the key field which is returned from the popup will be converted into the value for foreign_field, and when you press the SUBMIT button the key field will contain the wrong value, which in turn could lead to further errors (such as a database error if the value does not conform to the field specifications).
Within any form a popup task is tied to a particular field by means of the task_id entry in the $fieldspec array, as shown in the following example:
$fieldspec['foreign_id'] = array('type' => 'integer',
'size' => 4,
'required' => 'y',
'control' => 'popup',
'task_id' => 'task_identity',
'foreign_field' => 'foreign_desc');
If you try to use the same task_id with more than one field the framework will always link it to the first field and completely ignore any others.
The solution is simple - use a different task_id with each different field so that a particular task_id is only ever linked with a single field.
Note that this does NOT mean that you have to create another PHP script for each different popup task. It is a feature of the RBAC system that the script_id (the name of the PHP script) is separate from the task_id (the key to the MNU_TASK table), so it is possible, as described in FAQ 50, to have more than one task referring to the same PHP script. It is a simple procedure to view the current task details, press the COPY button, go into the 'Add Task' function, press the PASTE button, then change the task_id value to make it unique. It would also be a good idea to change the task description to be more meaningful, as shown in the following example:
| script_id | task_id | task_description |
|---|---|---|
| location(popup1).php | xxx_location(popup1) | Choose Location |
| location(popup1).php | xxx_location(popup1)from | Choose FROM Location |
| location(popup1).php | xxx_location(popup1)to | Choose TO Location |
This should result in a $fieldspec array containing something like the following:
$fieldspec['location_id_from'] = array('type' => 'integer', 'size' => 4, 'required' => 'y', 'control' => 'popup', 'task_id' => 'xxx_location(popup1)from', 'foreign_field' => 'location_desc_from'); $fieldspec['location_id_to'] = array('type' => 'integer', 'size' => 4, 'required' => 'y', 'control' => 'popup', 'task_id' => 'xxx_location(popup1)to', 'foreign_field' => 'location_desc_to');
This arrangement actually presents us with the following problems:
location_id='..', but this needs to be converted into either location_id_from or location_id_to in order to be of use in the current function.location_desc_from or location_desc_to in order to be of use in the current function.Both of these problems can be solved by having the relationships defined correctly in the Data Dictionary so that when the table structure is exported it contains entries similar to the following:
$this->parent_relations[] = array('parent' => 'location',
'alias' => 'location_from',
'parent_field' => 'location_desc AS location_desc_from',
'fields' => array('location_id_from' => 'location_id'));
$this->parent_relations[] = array('parent' => 'location',
'alias' => 'location_to',
'parent_field' => 'location_desc AS location_desc_to',
'fields' => array('location_id_to' => 'location_id'));
This information can be used in the popupReturn() method as follows:
'parent' => 'location'
'parent_field' => 'location_desc AS location_desc_from'
'fields' => array('location_id_from' => 'location_id')location_id as returned by the popup into location_id_from so that it can be processed within the context of the current table.SELECT location_desc AS location_desc_from FROM location WHERE location_id='..'
If the automatic conversion of field names does not work as expected it can be overridden with code in the _cm_popupReturn() method.
If the automatic lookup on the foreign table does not work as expected it can be overridden with code in the _cm_getForeignData() method.
Navigations buttons provide a means of transferring control from the current function to a new function, while allowing some sort of context to be passed to the new function.
No coding is required to make an entry appear in the navigation bar as it is all controlled from the contents of the RBAC database. Each navigation button identifies a different child task that is somehow associated with the current parent task. The steps for maintaining a parent task's navigation buttons are as follows:
When any PHP script is executed the final function of the page controller is to build the XML file which is passed to the XSL transformation process in order to create the HTML output which is sent to the client browser. Part of this processing involves reading the NAVIGATION_BUTTON table for the current task in order to build the contents of the navigation bar. Any task in this list which is not accessible to the current user will be removed from the list and therefore not displayed. This avoids the annoying situation where the user sees a button and presses it, only to be told that he does not have permission to press that button.
This is described in Appendix I of the User Guide to the Menu and Security (RBAC) System.
It may be completely obvious, but it should be pointed out that it would not be a good idea to create a navigation button for a child task which is unable to use the context which is passed down from the parent task. For example, if the parent task passes down a $where/$selection string containing values for field names which do not exist on the child table, and the child task is unable to convert that string into anything which it can use, then the child task will always fail to read anything from the database.
Unless given instructions to the contrary the default sql SELECT statement which is constructed during the execution of the getData() method will be as follows:
SELECT * FROM <tablename> WHERE ....
SELECT mnu_nav_button.*,
mnu_task_snr.task_desc AS task_desc_snr,
mnu_task_jnr.task_desc AS task_desc_jnr
FROM mnu_nav_button
LEFT JOIN mnu_task AS mnu_task_snr ON (mnu_task_snr.task_id=mnu_nav_button.task_id_snr)
LEFT JOIN mnu_task AS mnu_task_jnr ON (mnu_task_jnr.task_id=mnu_nav_button.task_id_jnr)
WHERE mnu_nav_button.task_id_snr='mnu_dialog_type(list)'
If it becomes necessary to take this automatically constructed query and extend it even further, for example to include some JOINs to tables which are not identified in the $parent_relations array, or to include some aggregate columns, then there is a slight problem. The best place to put this customisation would be the _cm_pre_getData() method, but this is called BEFORE the _sqlForeignJoin() method which uses the contents of $parent_relations to construct the query, and _sqlForeignJoin() will NOT be called if $this->sql_from is not empty.
The best way to solve this problem is to manually call _sqlForeignJoin() in the _cm_pre_getData() method, then extend the result as necessary, as shown in the following example:
function _cm_pre_getData ($where, $where_array, $fieldarray=null)
{
if (empty($this->sql_from)) {
// construct default SELECT and FROM clauses using parent relations
$this->sql_select = null;
$this->sql_from = null;
$this->sql_groupby = null;
$this->sql_having = null;
$this->sql_from = $this->_sqlForeignJoin($this->sql_select, $this->sql_from, $this->parent_relations);
// add code to obtain item count
$this->sql_select .= ", (SELECT COUNT(*) FROM stockcheck_dtl"
." WHERE stockcheck_dtl.stockcheck_id=stockcheck_hdr.stockcheck_id) AS item_count";
// add code to obtain facility_name
$this->sql_select .= ', facility_name';
$this->sql_from .= ' LEFT JOIN facility ON (facility.facility_id=stockcheck_hdr.facility_id)';
} // if
return $where;
} // _cm_pre_getData
In some cases (such as within the LINK entity of a LINK1 pattern) you will need to change the call to $this->_sqlForeignJoin() with a call to $this->_sqlAssembleWhere() as in:
$where_str = $this->_sqlAssembleWhere($where, $where_array);
If you wish to perform the same functionality in another object then this can be done using code similar to the following:
$dbobject =& singleton::getInstance('whatever');
$dbobject->sqlSelectDefault();
$dbobject->sql_select .= '....';
$dbobject->sql_from .= '....';
$data = $dbobject->getData('....');
This item is now redundant as the necessary processing is now performed within the framework using the following logic:
It is possible, using the techniques described here and here, to construct a complex SQL query containing any number of JOINS, subselects, aggregate columns or aliased columns. It is even possible to have an aggregate or aliased column available on a SEARCH screen so that the user can search for rows with particular values. By default any input to a SEARCH screen is automatically appended to the $where string, and this will cause an SQL error as aggregates and aliases can only be referenced in the HAVING clause of a query, not the WHERE clause.
Although the framework cannot deal with this situation automatically, only a little custom code is necessary in order to remedy the situation, as shown in the following example:
function _cm_pre_getData ($where, $where_array, $fieldarray=null)
{
// construct default SELECT and FROM clauses using parent relations
$this->sql_select = null;
$this->sql_from = null;
$this->sql_groupby = null;
$this->sql_having = null;
$this->sql_from = $this->_sqlForeignJoin($this->sql_select, $this->sql_from, $this->parent_relations);
// add code to obtain item count
$this->sql_select .= ", (SELECT COUNT(*) FROM stockcheck_dtl
WHERE stockcheck_dtl.stockcheck_id=stockcheck_hdr.stockcheck_id) AS item_count";
// add code to obtain facility_name
$this->sql_select .= ', facility_name';
$this->sql_from .= ' LEFT JOIN facility ON (facility.facility_id=stockcheck_hdr.facility_id)';
if (!empty($this->sql_search)) {
// transfer certain values from SEARCH (which is appended to WHERE) to HAVING
$search_array = where2array($this->sql_search, false, false);
$having_array = where2array($this->sql_having, false, false);
if (isset($search_array['item_count'])) {
$having_array['item_count'] = $search_array['item_count'];
unset($search_array['item_count']);
} // if
$this->sql_search = array2where($search_array);
$this->sql_having = array2where($having_array);
} // if
return $where;
} // _cm_pre_getData
As you can see this code handles the transfer of column 'item_count' from $this->sql_search to $this->sql_having regardless of what other columns are in either string. Simple yet effective.
It may sometimes be necessary to change environmental parameters for individual subsystems, such as changing the INCLUDE_PATH or defining new global functions, and this can now be done by means of a file called include.subsystem.inc which can be created in the top-level directory for each subsystem. This file, if it exists, will be processed during the inclusion of include.general.inc which in turn is referenced on the very first line of each page controller.
Here is an example of how it can be used to modify the INCLUDE_PATH:
<?php // modify INCLUDE_PATH $include_path = ini_get('include_path'); $include_path .= PATH_SEPARATOR .'../product'; $include_path .= PATH_SEPARATOR .'../shipment'; $include_path .= PATH_SEPARATOR .'../order'; ini_set('include_path', $include_path); unset($include_path); ?>
Any changes defined in this file will only be applied when running scripts within this subsystem.
You can from version 1.28.0, as documented in Creating PDF output - Barcode Generation. This allows any of the following barcode types to be generated:
Other options include:
Row Level Security (RLS) is used where the same database table contains data for multiple accounts, but where each user can only see or modify the data that belongs to their account. This feature can now be implemented in RADICORE using the techniques discussed in RADICORE for PHP - Implementing Virtual Private Databases.
There are certain reserved words which, if encountered as column/field names within a database table, will cause the RADICORE framework to behave in a certain way. These reserved words are:
| Reserved Word | Meaning |
|---|---|
| curr_or_hist | Refer to How can I search for records with historic, current or future dates? |
| selected | This dummy field is used in LIST screens to mark the row as selected so that its primary key can be passed to a child screen when a navigation button is pressed. |
| rdcversion | Refer to How can I prevent simultaneous updates of the same database record? |
| rdcaccount_id | Refer to How can I implement Row Level Security (RLS)? |
| rdc_rowspecs | Refer to the following: |
Although the column names start_date and end_date are not reserved words, if a table contains both of these columns then the behaviour documented in How can I search for records with historic, current or future dates?? will take place automatically. If you do not want this behaviour then you must remove the curr_or_hist field from the object using code such as he following:
function _cm_changeConfig ($where, $fieldarray)
{
if ($GLOBALS['mode'] == 'search') {
unset($this->fieldspec['curr_or_hist']);
unset($fieldarray['curr_or_hist']);
} // if
return $fieldarray;
} // _cm_changeConfig
When a dropdown list or radio group is shown in a form it requires a list of options from which one (or more) can be selected as documented in How to incorporate dropdown lists or radio groups. This procedure assumes that each row from the database will use exactly the same list of options, but sometimes this is not the case. How you deal with this situation depends on which transaction pattern you are using:
'[]' characters to the value of the optionlist attribute, as in the following:
$this->fieldspec['choices'] = array('type' => 'string',
'control' => 'radio',
'optionlist' => 'choices[]',
'align_hv' => 'v',
'required' => 'y');
$this->lookup_data array. Note that while the '[]' characters are valid within the XML document as an attribute value, they are not valid within a node name, so each list must be given a name with the '[]' characters removed and replaced with '.n' where 'n' is the row number (which starts at zero). This can be done using code similar to the following:
foreach ($rows as $rownum => $rowdata) {
$array = array(..., ..., ...);
$this->lookup_data['choices.'.$rownum] = $array;
} // foreach
<root>
<order_header>
....
<order_item>
....
<choices required="y" control="radiogroup" optionlist="choices[]" align_hv="v" />
....
</order_item>
<order_item>
....
<choices required="y" control="radiogroup" optionlist="choices[]" align_hv="v" />
....
</order_item>
</order_header>
<lookup>
<choices.0>
<option id="1">FOO (rating=Good, lead time=5)</option>
<option id="2">BAR (rating=Good, lead time=6)</option>
<option id="3">FOOBAR (rating=Good, lead time=7)</option>
</choices.0>
<choices.1>
<option id="1">Room #1 (available qty=10)</option>
</choices.1>
</lookup>
</root>
When the XSL stylesheet processes this document it will detect the '[]' characters at the end of the optionlist name and obtain the list contents from /root/lookup/choices.n where 'n' is the value of position() for that entry within the XML document. This will produce the output shown in Figure 21:
Figure 21 - Variable radio group contents
All screen layouts are defined in a series of screen structure files, one per task, which are read in and processed whenever that task is run. These files are predefined, which means that the contents are static and therefore unchanging. But what happens if the screen needs to be manipulated at runtime in order to add or remove columns depending on the circumstances?
Although the contents of the screen structure files are static, it is important to know how the framework uses these files. This is done as follows:
This means that at any time during the execution of a business layer object the contents of the $GLOBALS['screen_structure'] array can be modified so that the structure of the HTML output can be tailored to suit particular circumstances.
See also: How can I hide/remove columns in a multi-row display? and How can I modify screen labels at runtime?
By default all database access is performed through a single connection to a database server using the following variables in the CONFIG.INC file:
$GLOBALS['dbhost'] = 'localhost'; $GLOBALS['dbms'] = '??'; // 'mysql', 'pgsql' or 'oracle' $GLOBALS['dbusername'] = '??'; $GLOBALS['dbuserpass'] = '??'; $GLOBALS['dbprefix'] = '';
Note that the database engine can be switched between MySQL, PostgreSQL or Oracle by changing a single configuration variable.
The database server may contain any number of different databases (or schemas), but they must all be accessible using the single username/password combination. The advantage of using a single server is that it is possible to JOIN across multiple databases within a single SQL statement, and to include updates to all of those databases in a single database transaction. This is not possible with database tables that are accessed on different servers.
However, in some circumstances it may be necessary to connect to more than one database server, either because different databases engines are being used or because the databases exist at different locations. This can be achieved by using the optional $servers array, as in the following example:
// this demonstrates the multi-server option if (eregi('^(127.0.0.1|localhost)$', $_SERVER['SERVER_NAME'])) { // settings for the test server global $servers; // server 0 $servers[0]['dbhost'] = '192.168.1.64'; $servers[0]['dbengine'] = 'pgsql'; $servers[0]['dbusername'] = '??'; $servers[0]['dbuserpass'] = '??'; $servers[0]['dbprefix'] = ''; $servers[0]['dbnames'] = 'xample,classroom,survey'; // server 1 $servers[1]['dbhost'] = 'localhost'; $servers[1]['dbengine'] = 'mysql'; $servers[1]['dbusername'] = '??'; $servers[1]['dbuserpass'] = '??'; $servers[1]['dbprefix'] = ''; $servers[1]['dbnames'] = '*'; } else { // settings for the live server .... } // if
This identifies that the xample, classroom and survey databases are to be found on a PostgreSQL server, while all the others are to be found on a MySQL server. Please note the following:
'*' so that it catches any database which is not specifically named in any prior entry.Remote Authentication Dial In User Service (RADIUS) is a protocol for controlling access to network resources. This can be used to validate a LOGON password against a RADIUS server instead of the USER table, and may involve Two Factor Authentication (2FA) or Two Token Authentication (TTA) as an extra layer of security. A username and password are sent to a RADIUS server for authentication which produces a response which is either "accepted" or "rejected". There is an additional "challenge" response within the RADIUS protocol, but this is not used within the RADICORE framework.
In order to use this facility it is first necessary to create a RADIUS server which contains details of all your users. This can be obtained from numerous sources, either as a proprietary or open source product, and may be hosted either locally or on a remote managed server. Each user is given device or token, which may either be hardware or software, which will provide the RADIUS password. A separate PIN number may be used to generate the password, or may be included when the password is submitted to the RADIUS server.
Once the RADIUS server has been established and each user has been given the means to generate his/her password it is then necessary to inform the RADICORE framework that it must communicate with this server. This is done with the following steps:
radius.config.inc file in your INCLUDES directory by copying radius.config.inc.default and modifying the contents as necessary.
// up to 10 servers may be specified, either IP addresses or domain names $radserver[] = 'auth.radius.com'; $radport = 1812; $shared_secret = 'theAnswerIs42'; $timeout = 3; $max_tries = 3; $auth_type = 'pap'; // pap, chap, mschapv1, mschapv2
During the logon process the user password will be sent to the RADIUS server for authentication for ALL users EXCEPT those who have been excluded by one of the following methods:
If the RADIUS userid is different from the LOGON userid, it can be stored in the external_id field on the USER record.
NOTE: It may also be possible to implement TFA/TTA via an LDAP server, which can be used as an alternative to a RADIUS server.
An initial value is defined as the value which is pre-loaded into an input field before it is displayed to the user, and which may be modifiable by the user. This is different from a default value which is used only when the user does not supply a value.
Before any initial values can be defined for a task it is first necessary to have entries on the TASK_FIELD table to identify which fields in which tasks can be dealt with in this way.
Initial values for a task may be set up in any of the following ways:
Whenever a new task is activated the initialise() method will call the _getInitialValues() method to load data from one of these tables (the ROLE table will only be examined if there are no entries on the USER table). How this data (if any) is handled depends on the task's pattern:
This feature can be used when different users have access to the same task, but need need the data filtered using different selection criteria. For example, a "List Purchase Orders" task will, by default, retrieve all entries for all suppliers. If any suppliers are given access to this task then they should only be allowed to view those entries which apply to them. This can be done by giving each supplier a unique user_id for the LOGON screen, then creating an entry on INITIAL_VALUE_USER which sets SUPPLIER_ID to the relevant value for the "List Purchase Orders" task.
Although a number of different users may be able to add, view, amend and delete records in the same table, it may be necessary to prevent any of those records from being amended or deleted by anyone except the record's creator. For example, an order application may have an ORDER_NOTES table where different users can add their own notes to an order. While a user is able to view the notes made by others, he is only allowed to amend or delete the entries which he made himself.
This restriction can only be applied if the identity of the user who created the record is stored in the database table, typically via a field called created_user. Then it is a matter of amending the task details for the relevant UPDATE1 or DELETE1 task so that the settings field contains the string created_user=$logon_user_id. When the task is run the framework will check that the created_user field contains the same value as logon_user_id, and if it does not it will generate an appropriate error message and prevent the operation from continuing.
Although access to a RADICORE application is not possible without first navigating through the LOGON screen, it may be that the user has already supplied these credentials to a different application and wishes to enter RADICORE without having to enter the same credentials again. It is now possible to achieve this by having a hyperlink to the LOGON screen which includes the values for user_id and user_password in the argument list, as in the following example:
<a href="radicore/menu/logon.php?user_id=FOO&user_password=BAR">Logon to Radicore</a>
The LOGON script will process these values as if they had been entered through its own screen, and provided that authentication is successful control will be passed to the user's home page. The LOGON screen will only be displayed if authentication fails.
This is documented in Appendix I of the Menu System User Guide, with additional notes in Associated/Related Rows.
This is documented in Appendix I of the Menu System User Guide, with additional notes in Associated/Related Rows.
By default the hyperlinks above the menu bar are plain text, as in the following:
Figure 22 - Menu Bar with text links
There is the option to change the text into images, as in the following:
Figure 23 - Menu Bar with image links
This can be done by creating a file called xsl_params.inc in the radicore/css/ directory with contents similar to the following:
<?php // identify icons to use above menu bar in hyperlinks $xsl_params['icon']['logged-in-as'] = '/images/user.png'; $xsl_params['icon']['help'] = '/images/help.png'; $xsl_params['icon']['logout'] = '/images/logout.png'; $xsl_params['icon']['logout-all'] = '/images/logout-all.png'; $xsl_params['icon']['print'] = '/images/print.png'; $xsl_params['icon']['noprint'] = '/images/noprint.png'; $xsl_params['icon']['new-session'] = '/images/new-session.png'; $xsl_params['icon']['recover-pswd'] = '/images/recover-pswd.png'; // set display size of these icons (in pixels) $xsl_params['icon']['size'] = 20; // identify icon to use on home page $xsl_params['icon']['home'] = '/images/home.png'; // remove text entries to remove corresponding hyperlinks //unset($xsl_params['text']['logout-all']); //unset($xsl_params['text']['recover-pswd']); //unset($xsl_params['text']['new-session']); //unset($xsl_params['text']['print']); //unset($xsl_params['text']['noprint']); ?>
This file can be copied from radicore/css/xsl_params.inc.default.
Please note the following:
$xsl_params['icon'] entries then no image will be displayed, only the text.$xsl_params['icon']['size'] entry will set the display size for all the hyperlink images.$xsl_params['text'] entries then the hyperlink will not be displayed at all. Note that the print/noprint hyperlinks operate as a pair, so it would be stupid to disable only one of them.$xsl_params['icon']['home'] entry will cause an image to be displayed in the title of the home page, as in the following:
Figure 24 - Home Page with image
Adding your own logo to an application is sometimes known as "branding". This can be achieved in the RADICORE framework without the need to modify any core code as images can be included in the final HTML output by modifying the copy of file style_custom.css which exists in each subsystem directory.
As an example I shall take a logo
and a background image
and position them at the top of each page so that the logo starts on the left margin with the background repeated all the way to the right margin. It will also have a border above and below the images, but not on the sides.
First, the necessary images are placed in the radicore/images/ folder. Then the following code is added to the style_custom.css file:
div.header {
text-align: left;
background-image: url(../images/sample-background.jpg);
background-position: 0px /*2px*/; <!-- IE will implement this -->
background-repeat: repeat-x;
border-top: 2px solid black;
border-bottom: 2px solid maroon;
padding-bottom: 0px;
margin-bottom: 0px;
}
div.header p {
padding: 0;
margin: 0;
background-image: url(../images/sample-logo.jpg);
background-repeat: no-repeat;
height: 50px;
}
form {
padding-top: 0;
margin-top: 5px
}
This makes use of the fact that each HTML document contains the following:
<body>
<div class="header">
<p/>
</div>
The result of these changes is shown in Figure 25:
Figure 25 - Branding example
Note also that the <p> tag inside the <div class="header"> can be populated with text from customisable text files, either logon_header.txt (for the logon screen) or header.txt (for all other screens). These text files may contain HTML tags, and these will be executed as HTML except if you perform client-side XSL transformations with the Firefox browser which will convert <, > and & to <, > and & respectively. This will display the HTML tags as text instead of executing them as HTML.
While an application running under the RADICORE framework is usually only available to a single organisation and therefore only requires a single LOGON screen, it is possible that a different set of users may require a different logon screen. For example, an order processing system which deals with products, customers, suppliers, sales orders and purchase orders may need a supplier portal so that suppliers can log on and view their purchase orders directly. This portal should have a separate URL, and it should be possible to customise it to give it a different look. This is a simple two step process:
Create a script in the relevant subsystem directory, such as supplier_portal.php in the order subsystem directory, with the following contents:
<?php // ***************************************************************************** // This is the supplier portal logon screen. // ***************************************************************************** $external_auth_off=true; // turn external authentication OFF require '../menu/logon.php'; // use standard logon processing ?>
You will notice that all it does is pass control to the standard logon script, so all the processing is identical.
This can be accessed with a URL ending in /order/supplier_portal.php instead of /menu/logon.php.
The optional line $external_auth_off=true; will cause external authentication via a RADIUS or LDAP server to be deactivated for this screen.
Your will also need to copy logon.screen.inc from menu/screens/en/ to the order/screens/en/ directory.
The standard logon screen contains the following HTML:
<body class="logon">
The logon screen generated from supplier_portal.php contains the following HTML:
<body class="supplier_portal">
This means that the order/style_custom.css file can be modified to give the new logon screen a totally different style. In addition the header and footer areas can be populated with text by creating the following files in the same directory:
The title for the new screen can be supplied by creating an entry in the language_text.inc file as follows:
// menu details for subsystem ORDER $array['logon'] = 'Supplier Portal Logon screen';
Most of the database tables within the RADICORE framework, and this includes the sample applications, contain the following fields:
`created_date` datetime NOT NULL default '2000-01-01 00:00:00', `created_user` varchar(16) NOT NULL default 'UNKNOWN', `revised_date` datetime default NULL, `revised_user` varchar(16) default NULL,
The reason for this is purely historic. Most of the database designs I used in the decades before I switched to programming in PHP included these fields as a sort of audit trail feature, and the practice has stuck. It is also a useful way of being able to isolate recent inserts or updates with simple SQL queries.
The important thing to note is that these fields are not automatically added by the framework. The data dictionary IMPORT facility will only ever import the details of fields (columns) which have been defined in the database schema, so if you don't want them don't define them.
In order for these fields to be handled automatically by the framework their details need to be updated after they have been imported into the data dictionary. The settings are as follows:
This can either be done manually using the online screen, or by using the script update_created_date.sql which can be found in the radicore/dict/sql/ directory.
These settings have the following effect:
The values inserted by the framework will depend on the field's data type:
An aggregate function operates on sets of values and returns a single numeric result, such as the following:
Although it is possible to use the getData_raw() method, it is easier to use the getCount() method which works as follows:
$count = $this->getCount(); will construct and execute: SELECT COUNT(*) FROM $this->tablename
$count = $this->getCount("column='X'"); will construct and execute: SELECT COUNT(*) FROM $this->tablename WHERE column='X'
It is also possible to replace the $where clause with a complete query which will be executed without any modification, as in the following:
$count = $this->getCount("SELECT MAX(seq_no) FROM table27 WHERE column='X'");
$count = $this->getCount("SELECT SUM(quantity) FROM order_item WHERE order_id=42");
This second option will allow any valid SQL statement to be executed.
In screens which allow multiple rows to be displayed in a horizontal arrangement the navigation bar contains a set of hyperlinks which allow the number of rows in each page to be altered. In some cases it may be that the number of rows which can be displayed is fixed, therefore these links are redundant and should be removed from the screen. This can be achieved with the following code:
$this->xsl_params['noshow'] = 'y'; // remove 'show 10/show 25/...' hyperlinks
In screens which allow multiple rows to be displayed in a horizontal arrangement the navigation bar contains a set of hyperlinks which allow the selectbox at the front of all rows to be turned either on or off. In some cases it may be that none of the rows contain a selectbox, therefore these links are redundant and should be removed from the screen. This can be achieved with the following code:
$this->xsl_params['noselect'] = 'y'; // remove 'select all/unselect all ' hyperlinks
Transaction patterns such as MULTI2, MULTI3 and MULTI4 contain an area with multiple rows, all of which are editable. However, in some circumstances it may be that an entire row contains fields which should not be modified, in which case it would be best if that row were to be displayed as non-editable. For example, in a timesheet entry screen there is a row for each work category which has a separate column for each day of the week into which the hours for that day can be entered. However, the last row shows a series of totals for each day, and as these values are accumulated internally there is no point in showing then as editable to the user. So, instead of displaying the screen shown in Figure 26 to the user
Figure 26 - all rows editable
it would be better if it could be displayed as shown in Figure 27:
Figure 27 - all rows editable except one
This can be achieved with code similar to the following:
function _cm_post_getData ($rows, &$where) { if (!empty($rows)) { $lastrow = count($rows)-1; $rows[$lastrow]['rdc_rowspecs'] = array('noedit' => 'y'); } // if return $rows; } // _cm_post_getData
Note that pseudo-column rdc_rowspecs is a reserved word.
All the transaction patterns which show multiple rows in a horizontal display have a selectbox at the front of each row which allows that row to be marked as 'selected' before a button on the navigation bar is pressed. This allows details of all selected rows to be passed to another task for further processing.
However, in some cases it may be that a particular row cannot be processed further, therefore should be excluded from the selection process. An example of this is shown in Figure 27 where the last row is simply a set of accumulated totals and does not represent a physical row in the database. Rather than allowing that row to be selected, then rejecting that selection, it would be better to remove the selectbox from that row, as shown in Figure 28:
Figure 28 - all rows selectable except one
This can be achieved with code similar to the following:
function _cm_post_getData ($rows, &$where) { if (!empty($rows)) { $lastrow = count($rows)-1; $rows[$lastrow]['rdc_rowspecs'] = array('noselect' => 'y'); } // if return $rows; } // _cm_post_getData
Note that pseudo-column rdc_rowspecs is a reserved word.
All the transaction patterns which show multiple rows have the identity of the columns which are to be displayed pre-defined in the screen structure file. However, in some circumstances it may be that some of the columns are redundant, in which case it would be convenient if they could be removed from the display entirely. For example, Figure 29 shows a timesheet entry screen which has a separate column for each day of the week, but in this case the columns for Saturday (day#1) and Sunday (day#2) are not being used.
Figure 29 - screen with redundant columns
The users may be annoyed at having fields in the screen which are never used, so they would prefer to see the screen shown in Figure 30:
Figure 30 - screen with redundant columns removed
This can be achieved by calling the setColumnAttributes() function, as shown in the following example:
function _cm_post_getData ($rows, &$where) { $attribute_array['day_1'] = array('nodisplay' => 'y'); $attribute_array['day_2'] = array('nodisplay' => 'y'); $result = setColumnAttributes('inner', $attribute_array); return $rows; } // _cm_post_getData
Note that this does not display the columns with null values, it actually removes those columns completely from the HTML output by instructing the XSL transformation process to ignore any column which has the nodisplay attribute set.
This can be reversed using the unsetColumnAttributes() function with exactly the same $attribute_array.
All the transaction patterns which show multiple rows have a series of labels which appear above each column. Although these labels are hard-coded within the screen structure file, it is possible to change them at runtime. For example, the screen for a timesheet entry program has a separate column for each day of the week, with default labels of day#1 to day#7 as shown in Figure 29 and Figure 30. As each timesheet covers a single week where the week ending date is known, it would be very nice if the heading above each column could show the actual date, as shown in Figure 31:
Figure 31 - customised column headings
This is produced with the following code in the screen structure file:
// identify the field names and their screen labels $structure['inner']['fields'][] = array('selectbox' => 'Select'); $structure['inner']['fields'][] = array('work_effort_name' => 'Work Effort'); $structure['inner']['fields'][] = array('day_1' => 'Day#1', 'nosort' => 'y'); $structure['inner']['fields'][] = array('day_2' => 'Day#2', 'nosort' => 'y'); $structure['inner']['fields'][] = array('day_3' => 'Day#3', 'nosort' => 'y'); $structure['inner']['fields'][] = array('day_4' => 'Day#4', 'nosort' => 'y'); $structure['inner']['fields'][] = array('day_5' => 'Day#5', 'nosort' => 'y'); $structure['inner']['fields'][] = array('day_6' => 'Day#6', 'nosort' => 'y'); $structure['inner']['fields'][] = array('day_7' => 'Day#7', 'nosort' => 'y'); $structure['inner']['fields'][] = array('total' => 'Total', 'nosort' => 'y');
This can be achieved by using the replaceScreenHeadings() function with code similar to the following:
function _cm_post_getData ($rows, &$where) { $replace['day_3'] = 'Monday 7th April'; $replace['day_4'] = 'Tuesday 8th April'; $replace['day_5'] = 'Wednesday 9th April'; $replace['day_6'] = 'Thursday 10th April'; $replace['day_7'] = 'Friday 11th April'; $result = replaceScreenHeadings($replace); return $rows; } // _cm_post_getData
Each of the transaction patterns contains one or more zones which are populated with data from different database objects. If there is a single zone it is called 'main', but if there are multiple zones these are given names such as 'outer', 'middle' and 'inner', where processing starts with the 'outer' zone/object and ends with the 'inner' zone/object. The 'inner' zone has the capability of dealing with several database rows, and depending on the particular pattern each of these rows may have fields which are editable.
While it is possible to make an individual field non-editable by adding 'noedit' => 'y' to that field's entry in the $fieldspec array, is it possible to achieve the same thing with all the rows and fields in an entire zone? Yes it is, and with a single command, as shown in the following:
function _cm_post_getData ($rows, &$where) { if ($rows[0]['timesheet_status'] == 'P') { // edit mode is allowed unset($this->xsl_params['inner_noedit']); } else { // edit mode is not allowed $this->xsl_params['inner_noedit'] = 'y'; // "Cannot amend timesheet if status is not 'Pending'" $this->errors[] = getLanguageText('e0012'); } // if return $rows; } // _cm_post_getData
Please note the following:
$this->xsl_params['inner_noedit'] = 'n';
use this instead:
unset($this->xsl_params['inner_noedit']);
as it is the existence of the key, not its value, which is the deciding factor.The workflow system is based on Petri Nets which contain places and transitions which are joined together by arcs. The state of a workflow is indicated by the position of tokens on any of the places, which indicate which transition (workitem) is to be fired next. When a transition is fired all the tokens are moved from its input place(s) to its output place(s). The movement of a token might close a case, or it might indicate which transition is to be fired next.
Each of these transitions equates directly to a transaction (task) on the TASK table of the MENU database, and it is the transaction which actually performs processing on behalf of the application. This processing can be anything you want as the only thing which the workflow system needs to know is when the transaction has been completed so that it can move some tokens.
The workflow system is aware of nothing other than that which is stored in the workflow database. When a workflow case becomes active all it does is move tokens and fire transitions. When a transition is fired it performs the designated transaction, and it is the transaction which performs the processing which is required by the application.
So if you want a workflow to send an email you must create an application transaction (task) with the relevant functionality, and link this task to a transition in a workflow. When that transition is fired the associated task will be activated, it will do whatever it has been programmed to do (which may or may not include the sending of an email), and when it has finished the state of the workflow will be updated.
By default each screen will contain the text page created in nnn seconds below the action bar. This can be turned off by adding the following line to each subsystem's include.subsystem.inc file:
$GLOBALS['no_script_time'] = true;
Dates can be input in any of the following formats:
| Pattern | Description |
|---|---|
| d(d)?m(m)?(yyyy) | 1 or 2 digits, separator, 1 or 2 digits, separator, 4 digits |
| d(d)?MMM?(yyyy) | 1 or 2 digits, separator, 3 alpha, separator, 4 digits |
| d(d)MMM(yyyy) | 1 or 2 digits, 3 alpha, 4 digits |
| MMM?d(d)?(yyyy) | 3 alpha, separator, 1 or 2 digits, separator, 4 digits |
| MMMddyyyy | 3 alpha, 1 or 2 digits, 4 digits |
| yyyy?m(m)?d(d) | 4 digits, separator, 1 or 2 digits, separator, 1 or 2 digits |
| ddmmyyyy | 2 digits, 2 digits, 4 digits |
| yyyymmdd | 4 digits, 2 digits, 2 digits |
| yyyy?MMM?d(d) | 4 digits, separator, 3 alpha, separator, 1 or 2 digits |
The separator is any character which is not alphabetic or numeric.
Dates are displayed according to the value in $GLOBALS['date_format'] in the CONFIG.INC file.
Lightweight Directory Access Protocol (LDAP) is an application protocol for querying and modifying directory services running over TCP/IP. This can be used to validate a LOGON password against an LDAP server instead of the USER table, and may involve Two Factor Authentication (2FA) or Two Token Authentication (TTA) as an extra layer of security. A username and password are sent to an LDAP server for authentication which produces a response which is either "accepted" or "rejected".
In order to use this facility it is first necessary to create an LDAP server which contains details of all your users. This can be obtained from numerous sources, either as a proprietary or open source product, and may be hosted either on a local or remote server. An entry is created for each user with a minimum of ID and PASSWORD. The password may either be static or supplied by a One Time Password (OTP) generator.
Once the LDAP server has been established and each user has been given a userid and password it is then necessary to inform the RADICORE framework that it must communicate with this server. This is done with the following steps:
ldap.config.inc file in your INCLUDES directory by copying ldap.config.inc.default and modifying the contents as necessary.
$ldap_host = 'localhost'; $ldap_port = 10389;
During the logon process the user password will be sent to the LDAP server for authentication for ALL users EXCEPT those who have been excluded by one of the following methods:
If the LDAP userid is different from the LOGON userid, it can be stored in the external_id field on the USER record.
This option is possible with MySQL version 4.1 and above. It requires the addition of extra entries in the $servers array in the CONFIG.INC file as follows:
global $servers;
// server 0
$servers[0]['dbhost'] = '192.168.1.64';
$servers[0]['dbengine'] = 'mysql';
$servers[0]['dbusername'] = 'foo';
$servers[0]['dbuserpass'] = 'bar';
$servers[0]['dbprefix'] = '';
$servers[0]['dbnames'] = 'xample,classroom,survey';
// details for SSL encryption
$servers[0]['ssl_key'] = ''; // The path name to the key file.
$servers[0]['ssl_cert'] = ''; // The path name to the certificate file.
$servers[0]['ssl_ca'] = ''; // The path name to the certificate authority file.
$servers[0]['ssl_capath'] = ''; // The pathname to a directory that contains trusted SSL CA certificates
// in PEM format.
$servers[0]['ssl_cipher'] = ''; // A list of allowable ciphers to use for SSL encryption.
For details on what these ssl_* values mean please refer to http://www.php.net/manual/en/mysqli.ssl-set.php.
The default character set for PHP is iso-8859-1 (latin1) which is OK for languages such as English which do not have accented characters as each character can be represented in a single 8-bit byte, the standard ASCII set. If your application requires to handle a variety of different languages which cannot be represented in the iso-8859-1/latin1/ASCII character set then you must switch to an alternative character set. The best one to deal with the highest number of languages is UTF-8 which uses a multi-byte character set.
In order to switch your application you need to perform the following:
default_charset = "utf-8"
<meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>
This will ensure that all data is displayed in UTF-8 in the client browser, and all data which is posted back to the application from the client browser is in UTF-8.
mbstring.internal_encoding = utf-8 mbstring.func_overload = 2
character_set_server=utf8This will ensure that the server stores all data in UTF-8.
SET NAMES utf8;NOTE: this is done automatically by the RADICORE framework.
By default all entries in a radio group are displayed using the same CSS style, but sometimes it may be useful if individual entries could be displayed using a different style. This can now be achieved by adding the required CSS class name to the $this->lookup_css array, as shown in the following example:
authentication has the following entry in $this->fieldspec:
$fieldspec['authentication'] = array('type' => 'string',
'size' => 2,
'required' => 'y',
'uppercase' => 'y',
'control' => 'radiogroup',
'optionlist' => 'authentication');
The optionlist entry tells the framework to load its contents from $this->lookup_data['authentication'].
if (!array_key_exists('authentication', $this->lookup_data)) {
$array = getLanguageArray('authentication');
$this->lookup_data['authentication'] = $array;
} // if
Figure 32 - standard radio group display
$this->lookup_css['authentication']['INTERNAL'] = 'green';
with the following entry in the CSS file:
div.green {
background: green;
color: white;
font-weight: bold;
}
this will result in the display shown in Figure 33:
Figure 33 - customised radio group display
There may be times when, during the processing of a particular task, that you decide you want to jump to another task without waiting for the user to press a button. Although this can be done by using the Workflow subsystem this may be overkill in some circumstances, or the jump is conditional, or the identity of the new task is not determined until runtime.
The RADICORE framework offers several ways to achieve this, with the major difference being how the program stack is affected. The program stack is the list of tasks which are shown in the breadcrumbs area (the bottom row of the menu bar). If the current stack is Home>>Subsystem>>TaskA>>TaskB (where TaskB is the current task) and the jump-to task is TaskC then the effect on the stack will be as follows:
TaskB) finishes, then remove it from the stack before activating the jump-to task (TaskC). The new stack will then be Home>>Subsystem>>TaskA>>TaskC. When TaskC finishes it will return control to TaskA.TaskB) before the jump-to task (TaskC) is activated. The new stack will then be Home>>Subsystem>>TaskA>>TaskB>>TaskC. When TaskC finishes it will return control to TaskB.This is a continuation of FAQ56 which describes how to create a script which can be run from the command line or via a cron job. In some cases it may be useful to initiate such a script from a web page, and this can be achieved by creating a task using the Batch pattern.
The search mechanism which is built into the RADICORE framework deals easily with searches on a single table, but sometimes it is necessary to include a related table in the search. In this example I shall show how to a search on the PRODUCT table can also include the related PRODUCT_FEATURE table. A PRODUCT can have any number of FEATURES, so it may be useful to identify those PRODUCTS which have a certain FEATURE. The following steps are required.
feature_id field in the $fieldspec array. This can be done by modifying the _cm_changeConfig() method in the PRODUCT class as follows:
function _cm_changeConfig ($where, $fieldarray) // Change the table configuration for the duration of this instance. { if ($GLOBALS['MODE'] == 'search') { $this->fieldspec['feature_id'] = array('type' => 'mediumint', 'control' => 'popup', 'foreign_field' => 'feature_name', 'task_id' => 'feature(popup1)'); } // if return $where; } // _cm_changeConfig
feature_id field will be filtered out. This can be solved by modifying the _cm_pre_getData() method with code similar to the following:
function _cm_pre_getData ($where, $where_array, $fieldarray=null) // perform custom processing before database record(s) are retrieved. { if (empty($this->sql_from)) { // construct default SELECT and FROM clauses using parent relations $this->sql_groupby = null; $this->sql_having = null; $this->sql_from = $this->_sqlForeignJoin($this->sql_select, $this->sql_from, $this->parent_relations); // include reference to additional tables $this->sql_from .= " LEFT JOIN product_feature" " ON (product_feature.product_id=product.product_id)"; } // if return $where; } // _cm_pre_getData
Notice here that _sqlForeignJoin() is called to construct the default SELECT statement containing references to any parent tables, after which it is possible to add references to any child tables.
This option should not be used if there can be multiple entries on the inner joined table and no search criteria for that table has been supplied otherwise it will return a separate row of the outer table for each entry on the inner table.
function _cm_pre_getData ($where, $where_array, $fieldarray=null) // perform custom processing before database record(s) are retrieved. { if (empty($this->sql_from)) { // construct default SELECT and FROM clauses using parent relations $this->sql_groupby = null; $this->sql_having = null; $this->sql_from = $this->_sqlForeignJoin($this->sql_select, $this->sql_from, $this->parent_relations); } // if if (!empty($this->sql_search)) { $search_array = where2array($this->sql_search); if (!empty($search_array['feature_id'])) { $search_array[] = "AND EXISTS (SELECT feature_id" ." FROM product_feature" ." WHERE product_feature.product_id=product.product_id" ." AND product_feature.feature_id LIKE '{$search_array['feature_id']}')"; unset($search_array['feature_id']); } // if $this->sql_search = array2where($search_array); } // if return $where; } // _cm_pre_getData
Once you have identified the SQL statement that you want to execute it is a relatively simple matter of getting the framework to generate that statement for you.
Refer to the replaceReportHeadings() function. This performs the same function as FAQ109, but on a report structure file. For example, take the report headings shown in Figure 34:
Figure 34 - report headings
This is produced with the following code in the report structure file:
// identify column names and associated labels $structure['body']['fields'][] = array('subsys_id' => 'Subsys Id'); $structure['body']['fields'][] = array('subsys_desc' => 'Description'); $structure['body']['fields'][] = array('subsys_dir' => 'Directory'); $structure['body']['fields'][] = array('task_prefix' => 'Task Prefix'); $structure['body']['fields'][] = array('count' => 'Count');
These labels can be amended with code such as the following:
function _cm_pre_output ($string) // perform any processing required before the output operation { if ($GLOBALS['mode'] == 'pdf-list') { // replace report headings $replace['subsys_id'] = 'NEW subsys_id'; $replace['subsys_desc'] = 'NEW subsys_desc'; $replace['subsys_dir'] = 'NEW subsys_dir'; $result = replaceReportHeadings ($replace); } // if return $string; } // _cm_pre_output
Although any database table may contain a column which is a date or date+time, sometimes there may be a need for some additional processing. The purpose of this section is to describe how the RADICORE framework can assist in this processing.
The first point is to identify how date and datetime values are stored in the database. Some people like to store them as integers (such as UNIX timestamps) which represent the number of days/seconds since the start of an arbitrary epoch, but there is nothing wrong with the data type which is provided by the underlying RDBMS:
The format in which dates can be displayed to or input by the user are described in FAQ113
Here are some of the other date facilities which are provided in the RADICORE framework:
These settings will ensure that the field cannot be set or modified by the user, but will be set to the equivalent of NOW() when the record is inserted or updated. This feature is commonly used for the columns called CREATED_DATE and REVISED_DATE.
SELECT ... FROM ... WHERE end_date <= '$today' OR end_date IS NULLIt is safer to always store a proper value in the END_DATE field, which is why the Data Dictionary has the INFINITY_IS_NULL setting. This will allow an unknown date in the future (infinity) to be shown as blank to the user but stored as '9999-12-31' in the database. This will allow the SQL SELECT statement to be simplified to:
SELECT ... FROM ... WHERE end_date <= '$today'
Any of these conditions can be incorporated into the SQL SELECT statement by adding one of the following to the $this->sql_search string:
curr_or_hist='C' for records with current dates.curr_or_hist='H' for records with historic datescurr_or_hist='F' for records with future dates.curr_or_hist which will enable the user to quickly select a date range. All the developer has to do is include that field name in the screen structure file.curr_or_hist='C' into the Selection (temporary) field using the Update Task screen.date(current) will set curr_or_hist='C' date(historic) will set curr_or_hist='H'date(future) will set curr_or_hist='F'The way that the Data Dictionary within RADICORE works is as follows:
So why is step #2 performed manually? Why can't the dictionary identify the relationships from the database schema?
The simple answer is that relationships are not defined in the database, not even foreign keys, only foreign key constraints, and they are not the same thing. Foreign key constraints are not used in any sql SELECT statements, they only define what action to take when a table in that relationship, either the parent or the child, is inserted, updated or deleted. It is possible to construct an sql SELECT statement which uses a relationship between two tables without having that relationship defined within the database schema in any way whatsoever.
The differences between the database approach and the RADICORE approach are shown in the following grid:
| database constraint | Radicore | |
|---|---|---|
| 1 | Currently in the MySQL database foreign key constraints can only be used if both the parent and child tables use the INNODB engine. | There are no restrictions. |
| 2 | Once defined in the database schema the constraints cannot be turned off or altered within individual transactions. | Relationship details exist in the table structure file in the form of the $parent_relations array and the $child_relations array, so it is possible for the contents of these arrays to be temporarily altered within any transaction. |
| 3 | Foreign keys are not used to help in the construction of sql SELECT statements. | When the framework constructs an sql SELECT statement during the execution of the getData() method on a table object, it will automatically include JOINS to any parent tables as documented in Using Parent Relations to construct sql JOINs. |
| 4 | Foreign key constraints can only be defined between two tables when both of them exist within the same database schema. | RADICORE will allow the parent and child tables in a relationship to exist within different databases. The only restriction is that they must both be available in the same server connection at runtime. |
The RADICORE approach may involve the execution of code within the application layer rather than the database layer, but it offers more flexibility. Besides, all the necessary code is built into the framework so does not require any additional effort from any application builders.
The RADICORE framework was designed specifically to aid in the development of back office web applications, not front end web sites. If you do not understand the differences between the two then please read Web Site vs Web Application.
The User Interface (UI) of a RADICORE application is constructed using a standard library of page controllers and XSL stylesheets, which is usually too inflexible for a front-end web site which is supposed to be sexy and slick and full of gizmos and fancy widgets. A web site which is open to casual visitors also does not need a Role Based Access Control system which forces users to pass through a login screen.
This does not mean that none of the RADICORE components can be used in a front-end web site. If you are familiar with the design of the RADICORE infrastructure you should notice that it is based on the 3 Tier Architecture which has separate components for the Presentation, Business and Data Access layers. It is therefore possible to build the software for a front-end web site which has its own set of components in the presentation layer, but which uses the RADICORE components for the business and data access layers. This is documented further in Using RADICORE components in a front-end web site.
It is possible for clients to exist in a different time zone than the server on which the RADICORE application is being hosted, and this could mean that when times are displayed they are not in the client's time zone, and this could be confusing.
This problem can now be fixed using the new DateTimeZone class, but only if your PHP version is 5.2 or above
In order for this new feature to work it is first necessary to identify the time zone for the server. This is done by calling the date_default_timezone_get() function. You can override this setting by inserting the following line into your CONFIG.INC file:
$GLOBALS['server_timezone'] = 'America/New_York';
You can use any one of the List of Supported Time Zones.
The second step is to identify the user's time zone. It is not possible to determine this using information in the HTTP headers as nothing suitable is available, so the only workable alternative is to define this as an extra field on the MNU_USER table. It is an optional field, populated using the values obtained from the DateTimeZone::listIdentifiers() function, and should only be specified when the user's time zone is different from that of the server. This information will also be written out to a cookie called timezone_client for use whenever a user visits the LOGON page during a period when the system is shut down.
Having identified the different time zones, what is needed next is a function to convert the datetime value from one time zone to the other. This is achieved with the following:
function convertTZ ($datetime, $tz_in, $tz_out)
// convert datetime from one time zone to another
{
if (empty($datetime) OR empty($tz_in) OR empty($tz_out)) {
return $datetime; // no conversion possible
} // if
if ($tz_in == $tz_out) {
return $datetime; // no conversion necessary
} // if
if (version_compare(phpversion(), '5.2.0', '>=')) {
// define datetime in input time zone
$timezone1 = new DateTimeZone($tz_in);
$dateobj = new DateTime($datetime, $timezone1);
// switch to output time zone
$timezone2 = new DateTimeZone($tz_out);
$dateobj->setTimezone($timezone2);
$result = date_format($dateobj, "Y-m-d H:i:s");
return $result;
} else {
return $datetime;
} // if
return $datetime;
} // convertTZ
When will this function be called? There are two places:
This also affects any messages which are displayed by the shutdown function.
In the PDF List View the way that each row is printed can be customised by changing the settings for the body style in the PDF style file as follows:
Figure 35 - List View with fillcolour = array(224,235,255)
Figure 36 - List View with border => 'LRTB'
Figure 37 - List View with border => 'P'
You can mix 'fillcolour' and 'border' settings to create whatever effect you desire.
FAQ109 shows how a screen label can be replaced at runtime, but there may be circumstances in which it would be desirable to replace the column name as well as its label so that a different piece of data can be displayed.
Figure 38 shows a screen based on the Multi 4 pattern where the outer entity is used to pass selection criteria to the inner entity. When the "Group by Country" field is set to 'N' it shows a line for each individual sales order with its id.
Figure 38 - original 'id' column
When the "Group by Country" field is set to 'Y' the id field is irrelevant, so it could be replaced by the count of orders for that country, as shown in Figure 39:
Figure 39 - 'id' replaced with 'count' column
This is achieved by calling the replaceScreenColumns() function in the _cm_pre_getData() method, as shown in the follwing example:
if (is_True($group_by_country)) {
$this->sql_select .= ', COUNT(h.order_id) AS order_count';
$this->sql_groupby = 'country_id WITH ROLLUP';
$replace_array['order_id'] = array('order_count' => 'Count', 'nosort' => 'y');
$result = replaceScreenColumns($replace_array);
} else {
$this->sql_groupby = 'h.order_id WITH ROLLUP';
} // if
The workflow system is a self-contained system which has its own database and its own processing engine. It has no knowledge of any application, or any application data, and works independently of any application, but may be plugged into an application at any time. This separation of responsibilities results in the following:
No application task is aware that is part of a workflow, and none of the code within an application task has to be amended in order for it to be included in a workflow case.
The only link between the workflow system and an application is when a workflow transition is linked to an application transaction (task). The workflow system does not know what this transaction does or which parts of the application database it reads and updates.
When an application task which is part of a workflow is completed (fired) then the workflow engine will consume a token on the transition's input place and create a new token on the transition's output place. If this place is the input place for another transition then this other transition will be enabled (waiting to be fired).
The state of any workflow case is limited to the location of any tokens which are waiting to be consumed. These tokens identify which transitions are waiting to be fired, and each transition identifies an application transaction. When a transition is fired the context value provides the identity (primary key) of the application object which needs to be processed by that application transaction.
It is the application transaction which performs whatever processing is required by the application and updates any values in the application database, therefore the state of the application is the responsibility of the application.
In a one-to-many relationship between two tables it is possible for different column names to be used between the primary key of the parent (one) table and the foreign key of the child (many) table. The mapping of column names is defined when the relationship details are entered into the Data Dictionary. This information is then included in the table structure file so that it is available to the application.
When the primary key is extracted from one database table object and passed to another object in the $where string it is often assumed that the framework can automatically convert the column names to the foreign key of the receiving table object, but it does not. Why is this?
The answer is that the contents of the $parent_relations array is only used to include JOIN clauses when a table object is constructing an sql SELECT statement. This involves processing every entry in the $parent_relations array. It is not possible to use this information to alter the contents of the $where string as the framework does not know which entry is the right one. Even if it knew the name of parent table this would still not be sufficient as there may be more than one relationship with that table, so it would not know which one to use.
Here is an example which deals with the relationship between the mnu_task and mnu_nav_button tables in the MENU database. The primary key of mnu_task is task_id, but mnu_nav_button has two foreign keys, task_id_snr and task_id_jnr, which both link back to mnu_task. This information appears in mnu_nav_button.dict.inc as follows:
$this->parent_relations[] = array('parent' => 'mnu_task',
'alias' => 'mnu_task_snr',
'parent_field' => 'task_desc AS task_desc_snr',
'fields' => array('task_id_snr' => 'task_id'));
$this->parent_relations[] = array('parent' => 'mnu_task',
'alias' => 'mnu_task_jnr',
'parent_field' => 'task_desc AS task_desc_jnr',
'fields' => array('task_id_jnr' => 'task_id'));
When the sql SELECT statement is constructed inside the mnu_nav_button object the result will look something like the following:
SELECT mnu_nav_button.*, mnu_task_snr.task_desc AS task_desc_snr, mnu_task_jnr.task_desc AS task_desc_jnr FROM mnu_nav_button LEFT JOIN mnu_task AS mnu_task_snr ON (mnu_task_snr.task_id=mnu_nav_button.task_id_snr) LEFT JOIN mnu_task AS mnu_task_jnr ON (mnu_task_jnr.task_id=mnu_nav_button.task_id_jnr) WHERE mnu_nav_button.task_id_snr='mnu_dialog_type(list)' ORDER BY mnu_nav_button.sort_seq asc
When the WHERE string was passed into this object it contained task_id='...', but how was it translated into task_id_snr='...'? The answer is that it was converted by custom code in the _cm_pre_getData() method using code similar to the following:
$where = str_replace('task_id=', 'task_id_snr=', $where);
As there are two possible alternatives to this field name, task_id_snr and task_id_jnr, I created a separate subclass to deal with each one. Each of these subclasses is then used in a different task as follows:
If there are any pending workflow items they will appear as hyperlinks on the menu/home page with the text constructed in the format '<task_desk> where <context>'. Using the procedure outlined below it is now possible to customise this text so that it can be more 'user friendly'.
In the 'radicore/workflow/classes/custom-processing' directory there is a file called 'example.zip' which contains some customisable classes. If you wish to customise the hyperlink text then unzip these files and modify them as appropriate in order to create a column called link_text in each workitem record after it has been retrieved from the database. If the link_text column exists when the menu/home page is constructed then its contents will be used instead of the default text.
Below is an example of the custom code:
function _cm_post_getData ($rows, &$where) // perform custom processing after database record(s) are retrieved. { // insert custom text into LINK_TEXT foreach ($rows as $rownum => $rowdata) { switch ($rowdata['task_id']) { case 'x_person_addr(add)': ini_set('include_path', ini_get('include_path') .PATH_SEPARATOR .'../xample'); $dbobject =& singleton::getInstance('x_person'); $data = $dbobject->getData($rowdata['context']); if (!empty($data)) { $data = $data[0]; $link_text = $rowdata['task_desc'] .' for ' .$data['first_name'] .' ' .$data['last_name']; $rowdata['link_text'] = $link_text; } // if default: break; } // switch $rows[$rownum] = $rowdata; } // foreach return $rows; } // _cm_post_getData
Please note the following:
In the Workflow system there can be several arcs coming out of a transition in an Explicit OR Split, and the choice of which arc is used as the path down which the token will progess is governed by the pre-conditions or guards. Each pre-condition will be examined in sequence, and the first one which evaluates to TRUE will be chosen. If none evaluates to TRUE then the default path will be taken. But what is this sequence, and what identifies the default path?
Each outward arc goes from a transition to a place, and the sequence in which these arcs will be evaluated is governed by the place name. These are purely descriptive and have no meaning to the workflow engine, so can contain any value of your choosing. It would therefore be a good idea to prefix each place name with an explicit sequence identifier such as number or a letter, as in '1-' or 'A-'. In this way the sequence would be entirely under your control and not randomly picked at runtime.
The default path is the one which does not have a pre-condition. This means that in a sequence of Explicit OR Splits every path must have a pre-condition except the last one. When stepping through the sequence of outward arcs the workflow engine will go down the first path which has a pre-condition which evaluates to TRUE, or the path with an empty pre-condition (whichever comes first). It is therefore vitally important that the place name of the default path contains a value which ensures that it comes at the end of the sorted sequence. Depending on what you use as your prefix it could be something like '99-' or 'Z-'.
© Tony Marston
2nd August 2003
http://www.tonymarston.net
http://www.radicore.org