Rapid Application Development toolkit for building Administrative Web Applications

FAQ on the Radicore Development Infrastructure

By Tony Marston

2nd August 2003
Amended 1st November 2022

Introduction
References
Amendment History
Frequently Asked Questions
General
Framework
JavaScript
Database
Programming
HTML forms
Menu and Navigation buttons
HTML controls
Dropdown lists, Radio Buttons and Checkboxes
Popup Buttons
Role Based Access Control
Data Dictionary
Workflow
PDF Reports

General
Why do you ...?
Why don't you ...?
Framework
Why re-invent the wheel by creating yet another framework?
How can you call your framework Object Oriented if it still contains procedural functions?
What are the benefits of an infrastructure such as yours?
Why does your infrastructure contain so many different components?
What are the benefits of the 3-Tier architecture?
Aren't the MVC and 3-Tier architectures the same thing?
Isn't every web application automatically 3-Tier?
Why don't you use a Front Controller?
What is the purpose of your generic table class?
What is the purpose of your DML (DAO) class?
What is the purpose of each database table class?
What do I do to build new components?
Why is your design centered around data instead of functions?
What parts of the infrastructure are case sensitive?
Does your infrastructure deal with Internationalisation (I18N)?
What do I do to start a new application/project/subsystem?
How can I make the system inaccessible during periods of maintenance?
How can I run a batch job?
How can start a batch job from a web page?
How can I use a secure server in my application?
How does the HELP facility work?
How do I install RADICORE?
How can I make global environmental changes for individual subsystems?
What reserved words exist within the RADICORE framework?
Can I have different initial values for different users?
Can I logon without seeing the LOGON screen?
How can I add my company's logo to all web pages?
How can I build a separate logon screen?
Why is RADICORE no good for building web sites?
How can I use RADICORE components in my front-end web site?
How can I alter times to be shown in the client's time zone?
How can I execute a task before the first menu screen is displayed?
To where does the error log get written?
JavaScript
Why don't you use javascript?
Can I add javascript to my application?
Can I specify javascript globally instead of per class?
Database
Why do you use your own DAO instead of an existing one, like PEAR?
Why don't you use PDO instead of your own database abstraction class?
How to extend the sql SELECT statement
How to manually extend the automatically extended sql SELECT statement
How do I include an aggregate in a SELECT statement?
What is the best way to perform a simple SQL aggregate function?
How do you handle referential integrity?
How do you handle candidate keys?
What is a JOIN and how is it handled?
How do you handle a JOIN in the application?
How do you handle a JOIN in the database?
Can you access tables from more than one database?
Can you access tables from more than one database engine?
How can you handle the switch to the 'improved' MySQL extension?
How can you deal with a table that is related to itself?
How do you deal with database transactions?
How do you deal with database locking?
How to deal with ENUM fields
How can I prevent simultaneous updates of the same database record?
How can I maintain data consistency with concurrent updates?
How can I implement Row Level Security (RLS)?
Can I restrict update and delete operations to a record's creator?
What data types are supported across the various databases?
How to perform a search on an aggregate or aliased column
How can I access different databases on different servers?
How do I set up SSL encryption for a remote MySQL database?
How do I deal with multi-byte characters?
How can I consolidate all the Radicore databases into a single database?
How can I update a field using a database function instead of a literal?
Can I execute an arbitrary SQL query?
Can I execute multiple SQL queries in a single step?
Can I filter records in the _cm_post_getData() method?
How can you have foreign keys without foreign key constraints?
How can I provide different SQL queries for different database engines?
How can I find out the version number of the current DBMS?
How can I use a recursive Common Table Expression (CTE) for traversing a hierarchy?
How can I use a non-recursive Common Table Expression (CTE)?
How can I use Subqueries and Derived Tables?
Programming
What programming guidelines exist for RADICORE?
Why don't you use SETTERS and GETTERS for individual database fields?
How can you handle the switch to PHP 5?
Is it really possible to separate business logic from data access logic?
Is it really possible to separate business logic from presentation logic?
How can business logic and data access logic be separate if they exist in the same class?
Why don't you put the table structure in a separate Mapper class?
How do you deal with non-database fields?
What is a subclass and how do you use it?
How do you use subclasses?
How do you use subclasses to provide alias names?
What is your naming convention for subclasses?
How do you validate user input?
What do you mean by 'primary' and 'secondary' validation?
How do you define 'secondary' validation?
How do you handle error messages?
How do you provide dynamic selection criteria in the $where variable?
Can you pass additional parameters to the XSL stylesheet?
How do you deal with task-specific behaviour?
What debugging aids exist in this framework?
How can I display data from a virtual table?
How can I define preset/static search criteria for the $where variable?
What are the CREATED_DATE/USER and REVISED_DATE/USER fields?
What are the valid formats for the input and output of dates?
How can I force a process to jump to another task?
How do I perform a search on a related table?
What facilities are there for date processing?
What options are there for hyperlinks and images?
Can I have a filepicker task which works with subdirectories?
What does 'Uncaught exception from DOMException, message = Invalid Character Error' mean?
How do I enable the QuickSearch Bar?
How can I update several tables in a single operation?
How can I dynamically add or remove rows from an ADD5 or MULTI2/3/4/5/6 screen?
How does the 'singleton' class work?
How can I use a manual sequence?
HTML forms
Why don't you use another templating system instead of XSL?
How do you deal with pagination?
How do you sort the output by different columns?
How to make a normally writable field read-only or even invisible
Why don't you use hyperlinks to jump to child screens?
How can you enter ranges of values prior to a search?
How can I search for records with historic, current or future dates?
How do fields appear in the HTML output?
Why can't I bookmark pages?
Why don't the column hyperlinks sort the data as I expect?
Can I alter a screen's structure at runtime?
How can I make a single row in a multi-row area non-editable?
How can I make a single column in a multi-row area non-editable?
How can I remove the select box from a single row?
How can I hide/remove columns in a multi-row display?
How can I modify screen labels at runtime?
How can I remove the 'page created in ...' text from the bottom of each screen?
How can I replace a column and its label at runtime?
Why does the framework not have a responsive GUI?
Can I display a label on a screen without an associated field?
How can I add extra CSS files to a web page?
Menu and Navigation buttons
What is the difference between a 'menu bar' and a 'navigation bar'?
Can I change search criteria with a navigation button and without user dialog?
How do navigation buttons work?
How do entries appear in the navigation bar?
What happens when a button in the navigation bar is pressed?
How is context passed to a child task?
How is context passed to a child object in the same task?
Can I use images instead of text for the hyperlinks above the menu bar?
How can I remove the 'show nn' options from the navigation bar?
How can I remove the 'select all/unselect all' options from the navigation bar?
Can I remove navigation buttons at runtime?
Can I remove action buttons at runtime?
Can I have buttons in the data area?
HTML controls
How easy is it to dynamically change the HTML control for a field?
How can I change the display attributes for individual fields at runtime?
How can I make all the fields in a particular zone non-editable?
How can I add a hidden field to an HTML form?
How can I change how a field (or a column of fields) is displayed?
Dropdown lists, Radio Buttons and Checkboxes
How to incorporate dropdown lists or radio groups
How to incorporate a dropdown list with multiple selections
How are blank entries put into lookup arrays?
How to have different sets of dropdown/radio options for different database rows
Can I change the style of individual entries in a radio group?
How can I build a dropdown/radio group from a table with a compound key?
Can I change the style of an individual checkbox?
Can I change the style of individual entries in a dropdown?
Popup Buttons
What is a 'popup'?
How to incorporate a 'popup' control into a form
How does the processing of a 'popup' form actually work?
How can you switch a popup between accepting single and multiple selections?
How can I access a POPUP2 screen?
How can I enter a value before calling a POPUP form?
How can I call the same POPUP more than once in a form?
How can I return multiple rows from a popup into the current screen?
Role Based Access Control
Why, in the RBAC system, is task_id different from script_id?
How to hide menu options from certain users
Why can't I have anonymous users?
How can I turn on authentication via a RADIUS server?
How can I turn on authentication via an LDAP server?
Data Dictionary
Why doesn't the Data Dictionary use foreign key details in the database to identify relationships?
Why aren't primary key names in $where converted to foreign key names?
Workflow
How do I use the Workflow system?
Why doesn't the Workflow system have a facility for sending emails?
How can I customise the text of pending workflow items on the menu/home page?
How can I control the sequence in which the conditions on Explicit OR Splits are evaluated?
What happens in a workflow when a place contains more than one token?
How can I set the user_id of a workflow WORKITEM record?
How are workitems assigned to users in the Workflow system?
PDF Reports
Can I produce output in CSV or PDF format?
Can I add barcodes to my PDF documents?
How can I modify report labels at runtime?
How can I customise the printing of lines in the PDF List View?
How can I change the style of a field in a PDF report?
Can I change the fonts in a PDF document at runtime?
How can I draw a horizontal line in the title area of a PDF report?
How can I rotate column labels 90 degrees in PDF List View?

 


Introduction

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.


Frequently Asked Questions

1. Why don't you use javascript?

  1. It is not necessary to use javascript in any web page. All you need is HTML and CSS. Anyone who says you must use javascript is making a mistake, and anyone who creates a web application which cannot run without it is making an even bigger mistake. It is an option, not a requirement, and it is an option which some users choose to disable.
  2. Unlike HTML and CSS there is no W3C standard for javascript, so different browsers have different implementations and different proprietary extensions.
  3. Because there are so many different implementations it is often necessary to write different variations of the same code for different browsers. This requires even more code to detect which browser is being used so that the correct variation can be invoked, and even the browser detection techniques are not guaranteed to be foolproof as the results can often be spoofed.
  4. A significant number of users turn javascript off for security reasons, therefore any web page that cannot operate without javascript will become useless.
  5. It is only by using standards-compliant software that you can produce web pages which can be correctly rendered by as many browsers as possible. Writing web pages that rely on proprietary extensions is not considered to be 'user friendly'.

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.

2. Why don't you use a Front Controller?

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.

This can be represented by the structure shown in Figure 1:

Figure 1 - Diagram of a single Front Controller

infrastructure-faq-01 (3K)

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. Note that some people like to use the name index.php instead of controller.php, but the effect is the same.

Front Controllers are only necessary in languages which are compiled

In a compiled language you may have many different modules which are compiled separately from source files into binary object files which then have to be linked together to form a single executable program file. There are two types of module in this process:

When you run a program from the command line it will always begin its processing from the same place which is commonly known as the MAIN procedure. The command line may contain an argument which may be interpreted as "action=foobar" which can be used in the MAIN procedure to automatically call a subprogram which performs that particular action. In a web application the code which converts the contents of the "action" argument into a specific subroutine call is known as a ROUTER which is described as follows:

The router is the first component of the application to receive requests from users. When the application starts up, the router registers all the available endpoints (or routes) to an array. When a user sends a request to the server, the router first analyzes the URL provided in the request. It then compares this URL to the array of registered endpoints to determine whether there is a match. If the router finds a match, it knows which function or controller to execute in response to the request.

The big problem with this mechanism is that you have to hard code a list of endpoints in order to map each action to a particular subroutine call. You cannot simply add a new subroutine to the mix and have the router automagically detect it and call it. Compiled languages are statically typed, so control cannot be passed to a subroutine without an explicit call, and when the link process is run each subroutine call must be satisfied by a matching endpoint in a binary file otherwise the link process will fail.

PHP is not compiled, it is interpreted

PHP is dynamically typed, not statically typed. This means that any type checking is performed at run time and not compile/link time.

PHP does not require all the modules within an application to be compiled and linked into a single executable file. Each module can be a standalone script which can be activated directly by the web server from the URL provided by the browser. All sytax checking is performed at run time. So if I have an application with 4,000 different modules I do not need:

http://www.blah.com/controller.php?action=foobar  // with 4,000 variations of 'foobar'

as I can go directly to the relevant script using:

http://www.blah.com/path/to/script.php  // path to a file within the file system

Within program code it is possible to jump from the current script to a different script using the header() function, such as in the following example:

<?php
header("Location: path/to/script.php");
exit;
?>

If the specified script.php file cannot be found in the file system the web server will generate a "404 file not found" error.

You should be able to see here that all the processing of a front controller and a router is already handled by the web server, so writing code to do it yourself would be redundant and a violation of YAGNI. In the RADICORE framework each module within the application, which I refer to as a task (user transaction or use case), has its own script in the file system which can be accessed directly by the web server. This results in the structure shown in Figure 2:

Figure 2 - Diagram of multiple Transaction Controllers

infrastructure-faq-02 (2K)

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 which is available in PHP, 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 following:

3. Why don't you use SETTERS and GETTERS for individual database fields?

First, here are some definitions:

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. Note that it provides a classic example of tight coupling which should be avoided.

<?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. Note that it provides an example of loose coupling which is to be encouraged.

<?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, which is just a collection of rows and columns, 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.

This topic is also discussed in Getters and Setters are EVIL.

4. Why don't you use another templating system instead of XSL?

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 of 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 re-sequence the output without having to re-sequence 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.

5. Why does your infrastructure contain so many different components?

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.

If you think that the RADICORE framework contains a large number of components then you should be surprised to see what some of the popular front-end frameworks contain. It is not uncommon for these over-engineered bloated monsters frameworks to load dozens of files and instantiate dozen of classes just to build the index page. I have seen one particular big-name framework load and instantiate 100 classes just for the index page! WTF!!! I have even seen an open source library which does nothing but send emails, but requires over 100 classes to do so.

As far as I am concerned if you cannot draw a structure diagram - one that identifies all the classes - on a single A4 sheet of paper, then your structure is far too complicated and you should throw it away and start again.

An application must provide the ability for the user to run a component for each task (use case) in order to help him do his job. While some programmers prefer to create a small number of large components each of which can perform several tasks, ever since the mid-1980s I prefer to create a large number of small components each of which performs just one task. This is discussed further in Component Design - Large and Complex vs. Small and Simple. In my main ERP application, which now has over 4,000 tasks, each of these tasks has its own unique component script which is quite small as all it does is identify other sharable components which, when combined, perform the necessary processing for the user.

6. How do you deal with pagination?

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:

  1. When a multi-line screen is first invoked without a specific page number being requested it will start at page 1.
  2. After the data has been retrieved by my DML class the following pieces of information will be available as well as the data array:
  3. The values for $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.

  4. Each of the hyperlinks in the pagination area contains an absolute page number, not a reference to 'previous' or 'next', so whichever hyperlink is selected it will result in a URL similar to http://www.domain.com/script.php?page=3.
  5. When the controller script runs it looks for the existence of this parameter, and if found it passes it to the database object for action. This is done using code similar to the following:
    // obtain the required page number (optional)
    if (isset($_GET['page'])) {
        $dbobject->setPageNo($_GET['page']);
    } // if
    
  6. The whole cycle repeats from step (2).

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.

7. How do you sort the output by different columns?

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:

  1. When a multi-line screen is constructed the column headings will be constructed as hyperlinks which, when pressed, will result in a URL similar to http://www.domain.com/script.php?orderby=name1.
  2. When the controller script is run it looks for the existence of this parameter and if found it passes it to the database object for action. This is done using code similar to the following:
    // obtain the 'orderby' field (optional)
    if (isset($_GET['orderby'])) {
        $dbobject->setOrderBy($_GET['orderby']);
    } // if
    
  3. The setOrderBy() method will load the field name into variable $orderby, and the variable $order will toggle between 'ascending' and 'descending'.
  4. The values for $orderby and $order will be passed down to the DML object where, if not blank, will be built into the sql SELECT statement.
  5. The contents of $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.

8. How to extend the SQL SELECT statement

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:

  1. The generic table class contains a series of variables 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
       ...
    
  2. Within each component script it is possible to supply values for any of these variables using code similar to the following:
    // 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   = '';
    $sql_orderby = 'person_id';
    

    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.

  3. Within the controller script these values will be transferred into the database object using code similar to the following:
    $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);
    
  4. Within the database object the contents of $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
    
  5. Within the DML object these variables are used to construct an sql 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.
  6. It is also possible to bypass step (2) above and insert the changes in the _cm_pre_getData() method of the database table class.

9. How to incorporate dropdown lists or radio groups

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:

  1. Within the Data Dictionary use the Update Column task to set the control type to dropdown or radio group, and supply a value for option list.
  2. Within the Data Dictionary use the Export Table function to rebuild the <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:

  1. Within the database table class put some custom code inside the dummy _cm_getExtraData() method similar to 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 = RDCsingleton::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.

  2. Within the class for the foreign table create a method called _cm_getValRep() with contents similar to the following:-
       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:

  1. Within the database table class put some custom code inside the dummy _cm_getExtraData() method similar to 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 list for star_sign and insert into lookup array
            $array = $this->getValRep('star_sign');
            $this->lookup_data['star_sign'] = $array;
            
            return $fieldarray;
            
        } // _cm_getExtraData
    
  2. Within the same table class create a method called _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.

10. How to make a normally writable field read-only or even invisible

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:

  1. Within the Data Dictionary use the Update Column task to set the NOEDIT/NODISPLAY field to either noedit or nodisplay.
  2. Within the Data Dictionary use the Export Table function to rebuild the <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']);

11. What is the difference between a 'menu bar' and a 'navigation bar'?

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:

12. What do you mean by 'primary' and 'secondary' validation?

Primary validation is used to verify that when user input is inserted into an SQL query that it will not be rejected by the database. Each field or column in the database has a particular data type, such as date, time, number, et cetera, so this process checks that the value for a field conforms to its data type. If it is not then the value is returned to the user with a suitable error message and never sent to the database.

This type of validation is handled automatically in the data validation class by using the field specifications contained within the $fieldspec array within each database table class. This data is obtained from the contents of the the table structure file.

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.

13. How do you define 'secondary' validation?

Unlike primary validation which is carried out automatically by the framework, secondary validation is the processing of business rules which have to be manually defined by the developer. This is done by inserting code into the relevant customisable hook method which has been defined in the abstract table class but which can be overridden within any table subclass. These hook methods are part of the Template Method design pattern.

When a controller script accesses a database table class 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:

The 'before' and 'after' sets of data are for those situations where action need only be taken when a field value is actually changed, as shown in the following example:

        if ($fieldarray['foobar'] != $originaldata['foobar']) {
            if (<condition>) {
                $this->errors['whatever'] = 'Another error message';
            } // if
        } // if

Note that 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');

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.

Please also refer to FAQ14 to see how these error messages are handled.

14. How do you handle error messages?

Within each database table class there is a standard variable called $errors which should be used 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, as shown in FAQ13.

Error messages which are related to particular fields should be inserted as an associative array, as follows:

    $this->errors['fieldname'] = 'error message';

These messages will be shown in the screen body immediately below the associated fields, as shown in Figure 3. If the field name does not appear in the screen body the message will be shown in the general message area at the bottom of the screen, as shown in Figure 4.

Error messages which are not related to particular fields should be inserted as an indexed array, as follows:

    $this->errors[] = 'error message';

These messages will be shown in the general message area at the bottom of the screen, as shown in Figure 4.

If you access another object from within the top-level object you may wish to have any error messages clearly shown as having come from that internal object. This can be done using option (2) in the code sample below:

    $dbobject = RDCsingleton::getInstance('internal-class'); 
    $data = $dbobject->doStuff($data);
    if ($dbobject->errors) {
        either (1): $this->errors = array_merge($this->errors, $dbobject->errors);
            or (2): $this->errors[$dbobject->getClassName()] = $dbobject->errors;
    } // if

If you use option (1) above and the fieldname (array key) exists within the screen then it will appear in the screen area as shown in Figure 3. Note that this may overwrite an error message generated for the same fieldname within the top-level object. If the fieldname does not exist within the screen, or you use option (2) above, then the message will appear in the general message area as shown in Figure 4.

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 Figure 3.

Figure 3 - Error messages which are associated with fields in the screen

infrastructure-faq-03 (18K)

The contents of the general message area will be displayed at the bottom of the screen, as shown in Figure 4.

Figure 4 - Error messages which are not associated with fields in the screen.

infrastructure-faq-04 (9K)

15. How do you handle referential integrity?

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:
  • RESTRICTED - Cannot delete parent entry if any child entries exist.
  • CASCADE - Will delete all child entries as well as the parent entry.
  • NULLIFY - The foreign key on all child entries will be set to NULL (initialised).
  • IGNORE - Do nothing.

The CASCADE and NULLIFY selections have two options each: 'framework' (performed by the framework) or 'FK constraint' (performed by a Foreign Key Constraint in the database). The 'framework' option will be slower as it will read each child record, update it, and record the change in the AUDIT database.

The rules required for each relationship must be defined in the $child_relations array within each table structure file.

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:

  1. I have spent most of my career dealing with database engines that had no referential integrity, so I am used to dealing with it in my application code. Programmers who complain that the database engine should do it for them are demonstrating their own lack of ability.
  2. I prefer to keep all rules regarding each database table within the class for that database table. Anything else will break encapsulation.
  3. One advantage of having the rules processed within my code instead of the database engine is that I have complete control over how the rules are actually executed. For example, in a cascade delete I have the opportunity to read each child record before it is deleted so that I can enter its details into my audit log. This would not be possible if the delete were to be handled by the database engine.
  4. Another advantage of having the rules defined within my application code is that as well as hard-coding the rules within each database class I have the option to change any of those rules at runtime should I so desire. This is a powerful feature that would not be available if the rules were maintained within the database engine.

16. How do you handle candidate keys?

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:

17. What is a 'popup'?

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 popup (1K) 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.

17a. How to incorporate a 'popup' control into a form

In order to populate a field using the popup control you must perform the following steps:

  1. Within the Data Dictionary use the Add Relationship task to create a relationship between the foreign table (the parent) and the object table (the child), and supply a value for parent field. This may either be the name of an actual field, or it may be a calculated (derived) field as in:
    CONCAT(field1, ' ', field2) AS foreign_desc
    When 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'));
    
  2. Within the Data Dictionary use the Update Column task to set the target field's control type to popup, and supply a values for the following: When this information is exported from the Data Dictionary it will appear in the <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="task#<zone>#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#<zone>#task_identity" 
           src="images/popup.gif" 
           alt="Call popup form to obtain value"/>
  </div>

Note that <zone> will be replaced with the zone name within the current screen, which could be something like 'main', 'outer' or 'inner'.

17b. How does the processing of a 'popup' form actually work?

Whenever a popup button is pressed the following processing takes place:

  1. When the script receives the $_POST array it is merged with the current data within the object. The amended object is then saved in the $_SESSION array so that all data, saved or unsaved, can be displayed back to the user. This ensures that no unsaved data is lost and has to be re-entered.
  2. The script then examines the $_POST array looking for a field name which begins with 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 zone ('main', 'outer' or 'inner') and the task which is to be run.
  3. The task details are retrieved from the TASK table within the RBAC system, and the value for PATTERN_ID identifies whether the task is a popup or not. If it is a popup then custom method _cm_popupCall() is called in order to perform any pre-processing. This includes defining a $where string to be passed to the popup form, and defining any settings which can be passed to the popup form.
  4. The current form is suspended while the designated popup form is activated. The popup form will use whatever $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).
  5. If only a single row is retrieved from the database and the setting choose_single_row has been set then that row will automatically be selected without waiting for the user to press the CHOOSE button.
  6. When the CHOOSE button is pressed in the popup form all the records which were selected will have their primary keys inserted into the $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.

  7. The popup form then terminates, and the previous form is reactivated with the following variables set:
  8. The script then calls the popupReturn() method on the relevant object. This performs the following steps:
  9. The screen is then presented to the user with the value for foreign_field displayed in front of the popup button.

18. What is a JOIN and how is it handled?

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.

18a. How to handle a JOIN in the application

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.

18b. 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:

  1. Manually, as described in How to extend the sql SELECT statement.
  2. Automatically, as described in Using Parent Relations to construct sql JOINs. This uses the contents of the $parent_relations array which is constructed inside the Data Dictionary and then exported to the application.

19. Can you access tables from more than one database?

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.

20. Can you access tables from more than one database engine?

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']. 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 MySQL, PostgreSQL, Oracle and SQL 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:

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:

21. How can you handle the switch to the 'improved' MySQL extension?

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.

22. How can you handle the switch to PHP 5?

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.

23. What is the purpose of your generic table class?

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:

24. What is the purpose of your DML (DAO) class?

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:

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.

25. What is the purpose of each database table class?

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.

26. Aren't the MVC and 3-Tier architectures the same thing?

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

infrastructure-faq-05 (5K)

Here is another diagram which shows how the two patterns overlap:

Figure 5a - MVC plus 3 Tier Architecture

model-view-controller-03a (5K)

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.

27. How can you deal with a table that is related to itself?

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.

28. Why don't you use hyperlinks to jump to child screens?

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:

  1. The hyperlink has to be coded into the HTML page before it is sent, therefore the name of the target child screen has to be hard-coded and cannot be varied by the user. In my method I can display a choice of target child screens as buttons.
  2. The hyperlink for each entry contains the identifier for that entry, therefore the child screen can only process that single entry. The user will have to return to the parent screen in order to make another choice.
  3. A hyperlink will always use the GET method, which means that any arguments (such as the primary key) will be visible in the browser's address bar. This is felt by many to be a security risk.
  4. A hyperlink can only perform its action on a single object. This means that if you wish to perform the same action on several objects you have to keep returning to the screen in order to select another object.
  5. A hyperlink which causes a change in state, such as with an UPDATE or DELETE operation, breaks one of the rules of the internet that all GET requests be idempotent. If a search engine scans your page it will try to follow every hyperlink on that page, and if the page is full of hyperlinks which delete records then guess what? Those hyperlinks will be activated, and those records will be deleted.

My method has the following advantages:

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.

29. How easy is it to dynamically change the HTML control for a field?

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:

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?

30. Isn't every web application automatically 3-Tier?

The reason that I do not subscribe to this theory can be explained using the following diagram:

Figure 6 - The false 3-Tier Architecture

infrastructure-faq-06 (1K)

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

infrastructure-faq-07 (4K)

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

infrastructure-faq-08 (5K)

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.

A more extensive article on this subject can be found at What is the 3-Tier Architecture?

31. What are the benefits of the 3-Tier architecture?

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, Oracle or SQL Server 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 450+ 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?

32. Is it really possible to separate business logic from data access logic?

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?

My framework is built around the 3-Tier Architecture in which the application code is split into 3 areas of responsibility:

Note here that the term "logic" refers to program code and not the data on which that code operates. Data Access Logic is therefore that program code which references the APIs for the DBMS which is currently being used. You will not find any of these APIs, such as mysqli_query, pg_query, oci_execute or sqlsrv_query, in any business layer component, therefore there is no data access logic in the business layer. When the business layer wishes to communicate with the database it does not do it directly by calling the relevant API itself, instead it calls a separate Data Access Object to do it indirectly.

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. The components in the business layer are totally unaffected by this change.

All my table classes exist within the Business layer and do not have any direct contact with either the user or the database. Data is either input by the user and validated before it is sent to the DAO, or fetched via the DAO before it is passed back to the user. The Business layer is database-agnostic and contains absolutely no code which is tied to a particular DBMS. It is also UI-agnostic in that the requests may come from a variety of sources (a user via an HTML form, input from a CSV file, or a web service request) and output in a variety of different ways (HTML, CSV, PDF, XML or whatever). The fact that data passes through the Business layer, after being processed by code which enforces the business rules, and is then handled by separate components is not a new and peculiar concept that I have invented myself.

Here is a more detailed explanation of my implementation:

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', 'sqlsrv' 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. How's that for flexibility?

33. How can business logic and data access logic be separate if they exist in the same class?

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?

You clearly do not understand the difference between logic (code) and information (data). The fact that my business entity "knows" about the structure of the database table which it represents is not the same as data access logic. Data access logic is that program code which constructs SQL queries and sends them to the DBMS via the relevant API, and that code exists in a completely separate DAO component. The business entity contains code which validates the data and enforces any business rules. Data validation cannot be achieved without knowledge of what columns exist within that business entity, and what their data types are. For a more detailed explanation please read Information is not Logic just as Data is not Code.

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. This is meta-data, which is data, not code or logic. 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 meta-data and some user 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.

34. Why don't you put the table structure in a separate Mapper class?

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. Each table class "knows" the structure of the database table which it represents by means of metadata (data about data) which is loaded into the object from the contents of its associated <tablename>.dict.inc file when the object is instantiated. This file is exported from the Data Dictionary using data which is imported directly from the database schema. If the database structure changes for any reason I simply rerun the import/export functions and my software is automatically synchronised with the database. I could put the contents of the <tablename>.dict.inc file into a separate class, but what would be the point? What would I gain?

35. Why do you use your own DAO instead of an existing one, like PEAR?

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

infrastructure-faq-09 (5K)

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

infrastructure-faq-10 (3K)

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.

36. What do I do to build new components?

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:

  1. A script for the model which is the <tablename>.class.inc file created by the data dictionary export function.
  2. A script for the view which is a screen structure scripts. This identifies which XSL stylesheet is to be used, and which columns and labels need to be displayed, and in which order.
  3. A component script which identifies the combination of model, view and controller which are relevant for that task. Note that there is a different transaction controller script for each transaction pattern.

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:

Before you can actually run these scripts you will have to update the Menu and Security (RBAC) system as follows:

  1. Use the Add Task function to define each task in the menu system. These should all be added to the subsystem defined previously as described in FAQ 54.
  2. It may be a good idea to use a prefix on each task_id in order to avoid conflicts with task_id's from other subsystems. This prefix should be defined in the TASK_PREFIX field of the Update Subsystem screen. There is no need to include this prefix on the script_id as the scripts for each subsystem are kept in a separate subdirectory, thus avoiding any conflicts.
  3. Use the Maintain Menu Items function to add parent tasks to the subsystem menu created previously as described in FAQ 54. This will create a menu structure for the new subsystem.
  4. Use the Maintain Navigation Button function to add child tasks to parent tasks. These options will only appear when the parent task is active.
  5. Update the <subsys>/text/<language>/language_text.inc file to include the following details: Examples of the items marked with (*) are contained in file <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.

  1. Let us start with the following table definition:
    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;
    
  2. Import the details in the Data Dictionary, then export them to produce the following:
    1. An initial class definition in file 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.
    2. A set of table specifications in file 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.
  3. Create a LIST 1 screen:
    1. Create a component script called foobar(list1).php as follows:
      <?php
      $table_id = 'foobar';                   // table name
      $screen   = 'foobar.list.screen.inc';   // screen structure
      require 'std.list1.inc';                // activate controller
      ?>
      
    2. Create a screen structure script called foobar.list1.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');
      ?>
      
    3. Define an entry on the TASK table in the Menu and Security (RBAC) system database which points to this script.
    4. Add this TASK to a MENU in the Menu and Security (RBAC) system so that it can be chosen from an option in the menu bar.
  4. Create a series of detail screens which will be the children of the LIST 1 screen created previously:
    1. Create a component script of type ADD 1 called foobar(add1).php as follows:
      <?php
      $table_id = 'foobar';                   // table name
      $screen   = 'foobar.detail.screen.inc'; // screen structure
      require 'std.add1.inc';                 // activate controller
      ?>
      
    2. Create a component script of type DELETE 1 called foobar(del1).php as follows:
      <?php
      $table_id = 'foobar';                   // table name
      $screen   = 'foobar.detail.screen.inc'; // screen structure
      require 'std.delete1.inc';              // activate controller
      ?>
      
    3. Create a component script of type ENQUIRE 1 called foobar(enq1).php as follows:
      <?php
      $table_id = 'foobar';                   // table name
      $screen   = 'foobar.detail.screen.inc'; // screen structure
      require 'std.enquire1.inc';             // activate controller
      ?>
      
    4. Create a component script of type SEARCH 1 called foobar(search).php as follows:
      <?php
      $table_id = 'foobar';                   // table name
      $screen   = 'foobar.detail.screen.inc'; // screen structure
      require 'std.search1.inc';              // activate controller
      ?>
      
    5. Create a component script of type UPDATE 1 called foobar(upd1).php as follows:
      <?php
      $table_id = 'foobar';                   // table name
      $screen   = 'foobar.detail.screen.inc'; // screen structure
      require 'std.update1.inc';              // activate controller
      ?>
      
    6. Create a screen structure script called 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.

    7. Define entries on the TASK table in the Menu and Security (RBAC) system database which points to each of these scripts.
    8. Add these TASKS to the NAVIGATION BUTTON table in the Menu and Security (RBAC) system so that they can be chosen from options in the navigation bar from the parent LIST 1 screen defined previously in step (3).

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.

37. How do you deal with non-database fields?

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.

38. What is a subclass and how do you use it?

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

infrastructure-faq-11 (2K)

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.

38a. How do you use subclasses?

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 __construct ()
    {
        // save directory name of current script
        $this->dirname     = dirname(__file__);
        
        $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();
        
    } // __construct
    
// ****************************************************************************
} // 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:

38b. Using subclasses to provide alias names

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.

A subclass which provides an alias name can be constructed from the following template:

<?php
require_once '#tablename#.class.inc';
class #aliasname# extends #tablename#
{

    // ****************************************************************************
    // This is a subclass of #tablename#
    // ****************************************************************************

// ****************************************************************************
} // end class
// ****************************************************************************

?> 

38c. What is your naming convention for subclasses?

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:

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 table name without any suffix. This means that the table name that you use in the screen structure file must exclude any _snn suffix, but include any other variation.

39. Why is your design centered around data instead of functions?

The answer is because I write applications whose sole purpose is to process data. I do not develop software which directly manipulates real-world objects, such as process control, robotics, avionics, or missile guidance systems, so a lot of the properties and methods which apply to real-world objects are completely irrelevant in my software representation. In an enterprise application such as Sales Order Processing which deals with entities such as Products, Customers and Orders, I am only manipulating the information about those entities and not touching the actual entities themselves. In pre-computer days this information was held on paper documents, but nowadays it is held in a database in the form of tables, columns and relationships. An object in the real world may have many properties and methods, but in the software representation it may only need a small subset. For example, an organisation may sell many different products with each having different properties, but all that the software may require to maintain for each product is an identity, a description and a price. A real person may have operations such as stand, sit, walk, and run, but these operations would never be needed in an enterprise application. Regardless of the operations that can be performed on a real-world object, with a database table the only operations that can be performed are Create, Read, Update and Delete (CRUD). Following the process called data normalisation the information for an entity may need to be split across several tables, each with its own columns, constraints and relationships, and in these circumstances it may be wiser to create a separate class for each table instead of having a single class for a collection of tables.

As you can see I do not develop software which manipulates real-world objects, the software only manipulates the information on those objects which is held in a relational database. This has led me to the following design decisions:

In an enterprise application the data is often much more valuable than the software used to maintain it, and may even have a longer life than the software. This data usually has a large number of user interface screens as it may be viewed in different ways in different contexts.

Before the name was changed to Information Technology (IT) this profession used to be known as Data Processing, and what we developed were called Data Processing Systems. The definition of a "system" is "something which transforms input into output", as shown in Figure 45:

Figure 45 - a system

data-processing-system-1 (1K)

A system has two main parts - what gets processed and how it gets processed. A factory can be regarded as a "system" as raw materials go in, they are assembled or manufactured, and finished goods come out.

Software is a system as data goes in, is processed, and data comes out. Sometimes the "processing" part of the system is nothing more than saving the data in a high-speed high-capacity storage mechanism (a database) so that it can be be quickly retrieved and displayed to the user in more or less the same format that it went in. In other cases the data may be transformed or manipulated in some way before it is stored, and/or transformed or manipulated in some way before it is output. This would give rise to the situation shown in Figure 46:

Figure 46 - a data processing system

data-processing-system-2 (2K)

I learned a long time ago that in any database application the most important component is the database design - get this wrong and no amount of clever software will get you out of the hole you have dug for yourself. In the analysis phase of an application's development you must identify the data with which you need to work as well as the functions (user transactions) that need to be performed on that data. You then use the process of data normalisation to design a database that can hold that data in efficient structures, then you design each of the user transactions which carry out each unit of work or Use Case that allows the organisation to carry out its business. It may be necessary to tweak the database design so that each transaction can work as efficiently and effectively as possible.

By virtue of the fact that I design my database first, then design my application components to manipulate those data structures, I practice what is known as Table Oriented Programming (TOP) and totally avoid Object Oriented Design (OOD). This means that my software structure is always synchronised with my database structure, which means that I never suffer with Object-Relational Impedance Mismatch, which means in turn that I never need that abomination called an Object Relational Mapper (ORM).

Anybody who has experience of building database applications with large numbers of user transactions will be able to tell you that the operations that can actually be performed on a database table are strictly limited - Create, Read, Update and Delete (which is where the CRUD acronym comes from) so this is why the RADICORE framework was designed with the ability to quickly generate the tasks which perform those basic operations on any database table. This does not mean that RADICORE applications are restricted to these basic operations - they are merely the starting point. You can enhance and modify the basic tasks to perform whatever additional processing you want - you can format user data before it is written to the database; you can format database data before it is displayed to the user; you can read data from more than one database table; you can write data to more than one database table; you can process any business rule or task-specific behaviour that you are capable of coding; you can override default behaviour and replace it with something more specialised; you can alter the process flow by jumping to another task. You are only limited by what you can imagine and what you can code.

Every database application can be broken down into three basic components - a relational database, the user interface and the software which sits in the middle - and the effectiveness and efficiency of an application depends entirely on how these are constructed and how well they work together. A major problem arises when two components - the database and the software - are designed using totally different techniques as the result is invariably a set of incompatible structures. If the two major components of an application have incompatible structures then the effectiveness and efficiency of that application will undoubtedly 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!

Then you must be blind, as each class is full of operations known as "methods". Each class in the business/domain layer represents an entity, which makes it a noun. Each entity has operations which can be performed on it, so each operation represents a verb. Entities (classes) are nouns while methods (operations) are verbs.

Classes which represent entities have state, and these exist in the business/domain layer. In the presentation and data access layers there exist classes called services which do not have state, but which perform operations on the data which is passed to them. There are Controllers which call methods on objects in the business layer. There are Data Access Objects which send data to and from the database. There are View objects which extract data from objects in the business layer and send it to the user in the form of HTML, PDF or CSV documents.

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

infrastructure-faq-12 (1K)

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 initially contains is the table name and the table structure.

Figure 13 - Class hierarchy based on entity with multiple operations

infrastructure-faq-13 (1K)

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:

40. How do you validate user input?

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:

41. How do you provide dynamic selection criteria in the $where variable?

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:

  1. From a parent screen. The parent is typically a LIST screen which shows multiple occurrences. The user may select one or more of these occurrences by checking the 'select' box at the front of each row. When one of the navigation buttons is pressed the list component will take the primary key of each of the selected rows and construct a suitable WHERE string in the following format:
    (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.

  2. From a search screen. This shows a set of empty fields into which the user can put whatever combination of selection criteria is required, including wild card characters. When the SUBMIT button is pressed the search script will take the contents of the $_POST array and assemble a suitable WHERE clause such as the following:
    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.

42. What are the benefits of an infrastructure such as yours?

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:

These 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.

43. What parts of the infrastructure are case sensitive?

There are several different technologies used within this infrastructure, and each has its own attitude towards the difference between upper and lower case.

Operating System (OS)
This concerns the use of file names. Microsoft Windows is totally insensitive when dealing with file names, so it does not matter if you use 'filename.php', 'FileName.php' or FILENAME.PHP' - they all mean the same thing. Not so with Unix and Linux - different case means different objects.
SQL
This concerns the use of database names, table names and column names. SQL is totally insensitive when dealing with these entity names, so it does not matter whether you use uppercase, lowercase or mixed-case names - they all refer to the same entity. The only exception to this is where any database/table names also refer to file names within the OS, in which case it could cause a problem. The way to avoid any such problem is to always use lower case when referring to SQL entities within the application. This also makes the use of CamelCaps a waste of time, so stick to the traditional underscore separator instead (i.e. use 'field_name' instead of 'FieldName').
PHP
This has concerns in several areas:
XML and XSL
These are relatively new technologies, but for some reason the authors have decided to make them case-sensitive. This means that any element within an XML document cannot be accessed within an XSL stylesheet unless exactly the same case is used.
HTML
HTML is a mixture of various tags which are usually arranged in pairs, with an opening tag, some contents, and a closing tag. These tags are totally case-insensitive in that they can be written in whatever case you desire - either lower, upper or even mixed - and they will still work the same. It is not even necessary for a closing tag to use the same case as its corresponding opening tag.
XHTML
XHTML is a stricter version of HTML which rejects sloppy coding::
CSS
A Cascading Style Sheet is used to define the styles required for various elements within a range of HTML documents. These elements can be identified using a combination of:

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:

  1. What problems are SOLVED by having software which is case-sensitive?
  2. What problems are CREATED by having software which is case-sensitive?

End rant...

44. Does your infrastructure deal with Internationalisation (I18N)?

Yes. This is all documented in Internationalisation and the Radicore Development Infrastructure (Part 1) and Part 2.

45. How can you switch a popup between accepting single and multiple selections?

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;

46. Can you pass additional parameters to the XSL stylesheet?

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:

47. How do you deal with database transactions?

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.

For online tasks 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 obtains an optional list of tables to be locked from $this->lock_tables or, if this is empty, by calling the the $this->_cm_getDatabaseLock() method. If this list is not empty and $this->lock_standard_tables is TRUE the framework will automatically append a group of framework tables to the list.

The $this->row_locks variable can be used to specify the type of lock which is to be used on any table which is read during a database update. Depending on the DBMS you are using you may wish to also use $this->row_locks_supp.

The transaction isolation level may be adjusted by using $this->transaction_level.

When the subsequent call to the startTransaction() method within the DML class is made this will communicate those lock settings to the physical 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.

For background/batch tasks the calls to startTransaction(), commit() and rollback() must be handled manually.

48. How do you deal with database locking?

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.
{
    $this->transaction_level = null;
    //$this->transaction_level = 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED';
    //$this->transaction_level = 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED';
    //$this->transaction_level = 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ';  // *DEFAULT*
    //$this->transaction_level = 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE';
		
    $lock_array = array();

    // 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)
    // $lock_array['WRITE'][] = '...'      (for a WRITE lock)
		
    switch ($GLOBALS['mode']){
        case 'insert':
            // $lock_array[] = $this->tablename;
            break;
        case 'update':
            // $lock_array[] = $this->tablename;
            // $lock_array[] = 'x_tree_level AS t2';
            // $lock_array[] = 'x_tree_node';
            break;
        case 'delete':
            // $lock_array[] = $this->tablename;
            break;
        default:
            $lock_array = array();
    } // switch

    return $lock_array;

} // _cm_getDatabaseLock

To turn on table locking simply insert a list of tables into $this->lock_tables or alternatively use the _cm_getDatabaseLock() method. 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:

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:

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.

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.

49. How do you deal with task-specific behaviour?

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:

50. Why, in the RBAC system, is task_id different from script_id?

Because they represent different things. The script_id is the identity of a file in the file system while the task_id is the identity of a record in the MENU database. Although each task_id always has an associated script_id there is no reason why they should be the same, so in my framework they are not. 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_iddescriptionoptions
mnu_task(list1)List Task (All) 
mnu_task(list2)aList Task (Procedures)task_type='PROC'
mnu_task(list2)bList 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.
SettingsThe 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.

51. What debugging aids exist in this framework?

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:

  1. The XML document.

    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:

    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?

  2. The SQL query.

    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 several ways to see what SQL queries are being issued:

  3. The Audit Trail.

    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:

52. How can you enter ranges of values prior to a search?

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.

  1. In the table's _cm_changeConfig() method insert code similar to the following:
        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:

  2. Amend the task's screen structure script to remove the single DATE field and replace it with the DATE_FROM and DATE_TO fields. This will inform the XSL transformation process about those fields so it will know what to do with them. The following code will put both fields onto the same line:
    $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');
    
  3. In the table's _cm_pre_getData() method you will need to insert code to replace the output from the SEARCH screen, which looks something like this:
    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
    

53. How can I search for records with historic, current or future dates?

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:

  1. If the initialise() method detects that the current database table contains a pair of fields named START_DATE and END_DATE it will automatically call the setCurrentOrHistoric() function. If you include the field name curr_or_hist in your screen structure script then this dropdown list will magically appear.
  2. During the processing of the subsequent getData() method the currentOrHistoric() function will be called automatically to translate the curr_or_hist='C/H/F' string into valid SQL as follows:
  3. Instead of forcing the user to jump to the SEARCH screen to change this selection criteria you can offer these options as buttons on the navigation bar instead:

    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'.

  4. It is possible to force the task to limit its initial display to current records only by using the Update Task screen to insert 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.

54. What do I do to start a new application/project/subsystem?

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.

  1. Create a new subdirectory for the new application by copying the DEFAULT subdirectory. This will ensure that you start with the correct directory structure. This new application directory must be in exactly the same directory tree as all the existing application directories. You may add other sub directories if you wish, but do not delete any of the existing sub directories.
  2. Within the Menu and Security (RBAC) system use the Add Subsystem function to create an entry which points to the new subdirectory created in (1) above. This will allow the framework to locate each script when it needs to be activated.
  3. Also within the Menu and Security system you will need to use the Add Task function to create a top-level menu entry for the new subsystem. You should also use the Maintain Menu Items function to add this menu to one of the existing system menus so that it will actually appear when you run the framework.

    NOTE: The previous steps can now be performed automatically. Please refer to Radicore Tutorial - Initialisation Procedure.

  4. Create your database using the database administration tool of your choice.
  5. Import the database details into the Data Dictionary subsystem with the following steps:
    1. Use the Import Databases function to import the database name. Note that this imports just the database names and not any associated table or column details.
    2. Use the Import Tables function to import the table names for a selected database. Note that this imports just the table names and not any associated column details.
    3. Use the Import Columns function to import the column details for a selected table.
    4. Use the Add Relationship function to define the relationship between one table and another. As well as saying "table A is related to table B" these details can also include the names of the related fields and the name(s) of the field(s) on the parent (foreign) table so that the relevant JOIN statement can be automatically constructed within the framework. Note that this facility can deal with multiple relationships between the same two tables, or even where a table is related to itself, by allowing the use of alias names as described in How can you deal with a table that is related to itself?.
  6. Use the Update Column function within the Data Dictionary subsystem to include additional information for use within the application. This will include specifying which HTML control to be used if the default text box is inadequate.
  7. Use the Export Tables function within the Data Dictionary subsystem to create the following files for each database 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.

55. How can I make the system inaccessible during periods of maintenance?

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.

56. How can I run a batch job?

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:

  1. In the radicore directory create file batch.ini from batch.ini.default
  2. Amend the contents of batch.ini to provide values for the following: This will then be used by every batch job in any subsystem.
  3. Copy the contents of file 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.

  4. Each time you run the batch script it will echo its results to 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 = RDCsingleton::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.

57. How can I display data from a virtual table?

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

infrastructure-faq-14 (1K)

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

infrastructure-faq-15 (1K)

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:

  1. Create a class for the mythical database table. It does not matter whether you create a totally new class or extend an existing class. In the Classroom Scheduling prototype I created a subclass called crs_schedule_x01.
  2. Within this class create a _cm_changeConfig() method to contain the following:
    $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.
  3. Within the class create a _cm_initialise() method to contain code similar to the following:
    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.

  4. Within the class create a _cm_pre_getData() method to contain code similar to the following:
    $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.

  5. Within the class create a _cm_setScrollArray() method to contain code similar to the following:
    // 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.

58. How can I use a secure server in my application?

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:

  1. You must identify the address of the secure server. Simply swapping 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/
    
  2. You must identify those pages that should be served over HTTPS. This will enable those pages to detect which protocol is currently being used, and to issue a redirect from HTTP to HTTPS if necessary.

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:

59. How to incorporate a dropdown list with multiple selections

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 key="0" value="1" /> 
      <array key="1" value="3" /> 
      <array key="2" value="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

infrastructure-faq-16 (1K)

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

infrastructure-faq-17 (1K)

60. How does the HELP facility work?

This is described in the User Guide to the Menu and Security System - Appendix F.

61. Can I add javascript to my application?

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.

62. Can I produce output in CSV or PDF format?

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.

63. How do fields appear in the HTML output?

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:

  1. If the XSL stylesheet tries to place a named piece of data in its output document, but cannot find a piece of data with that name in the XML document, it will not output anything for that data item.
  2. If the XML document contains a piece of data without the XSL stylesheet containing instructions on what to do with that data, then that data will not appear in the output.

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:

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:

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:

Please note the following:

64. How to deal with ENUM fields

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');

65. How to hide menu options from certain users

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:

  1. Although the same menu can be seen by any number of users, any options on that menu which are not accessible to the current user will not be shown. This means that you do not have to create different menus for different user roles, and it also avoids the situation where the user see a menu option and selects it, only to be told "you do not have access to this option".
  2. The system may contain many levels of menu and sub-menu, and some users may only have access to options on a low-level menu. There is no need to force them to start at a top-level menu and have them navigate down to the low-level menu - simply tell the system to start those users at a specific menu. All you have to do is update the User Role record to set Start Task to the menu of your choice. Users of this role will then be taken straight to this menu after they log on.

66. How do I install RADICORE?

Installation instructions are available here. They are also available in the readme.txt file which is included in the downloadable zip file.

67. How do I use the Workflow system?

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-examples.html is a document which describes sample workflows which have been created, and which are shown in Figure 18:

Figure 18 - list Workflow entries

infrastructure-faq-18 (17K)

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.

68. Why can't I have anonymous users?

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.

69. Why can't I bookmark pages?

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:

70. How can I prevent simultaneous updates of the same database record?

Also known as: How can I maintain data consistency with concurrent updates?

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:

  1. Add a new field to the database table called rdcversion (Radicore Version Number). Give it the following settings:
  2. Import the changed table into the Data Dictionary.
  3. Use the Update Column function to turn on the AUTO-UPDATE option. This will cause the value to be automatically incremented by 1 during each update.
  4. Export the changed dictionary details to your application.

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.

71. How can I define preset/static search criteria for the $where variable?

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:

  1. Insert selection criteria in the component script, as in:
    <?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.

  2. From the Selection (fixed) field in the TASK data. This will be merged with any additional selection criteria provided by a parent component to provide the $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.

  3. From the Selection (temporary) field in the TASK data. This will be merged with any additional selection criteria provided by a parent component to provide the $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.

  4. From the INITIAL_VALUES_USER table. This can be used to provide fixed selection criteria whenever a particular user activates a particular task. This allows the same task to have different but pre-defined selection criteria depending on who accesses it.

    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.

  5. From the INITIAL_VALUES_ROLE table. This can be used to provide fixed selection criteria whenever users of a particular role activate a particular task. This allows the same task to have different but pre-defined selection criteria depending on who accesses it.

    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.

72. Can I change search criteria with a navigation button and without user dialog?

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.

73. How can I change the display attributes for individual fields at runtime?

By default all field values in the HTML output are displayed using the same attributes (as defined in the screen structure file) but it may be useful in certain circumstances to change the attributes for certain fields at runtime 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 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.

See also How can I change how a field (or a column of fields) is displayed?

74. What programming guidelines exist for RADICORE?

They can be found at RADICORE Programming Guidelines. There is also a document regarding Database Design.

75. How are blank entries put into lookup arrays?

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:

  1. If the lookup array has the same name as a field on the current screen, then it will be assumed that the array belongs to that field.
  2. If the lookup array does not have the same name as a field on the current screen then the framework will step through the contents of $this->fieldspec until it finds the entry where "optionlist" contains the name of that lookup array.

76. What data types are supported across the various databases?

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:

  1. MySQL:
  2. PostgreSQL:
  3. Oracle:

Boolean fields

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.

Date fields

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 fields

Auto-increment columns are available in all three databases, but with different implementations:

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.

77. Why do you ...?

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.

78. Why don't you ...?

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:

79. How can I access a POPUP2 screen?

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:

  1. If the primary key of an occurrence on the ONE table is known beforehand then it can be inserted into the WHERE string by code in the _cm_popupCall() method, which is processed before the popup is called, using code similar to the following:
        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
    
  2. If the primary key of an occurrence on the ONE table is not known beforehand then it is possible to wait until the POPUP2 screen has been activated, and then run a POPUP1 screen so that the user can provide the missing identity. This can be done by code in the _cm_initialise() method which is processed before the screen is displayed, using code similar to the following:
        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.

80. Why don't the column hyperlinks sort the data as I expect?

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.

81. How can I enter a value before calling a POPUP form?

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

infrastructure-faq-19 (1K)

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

infrastructure-faq-20 (1K)

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).

82. How can I call the same POPUP more than once in a form?

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 in the same screen zone the framework will always link it to the first field and completely ignore any others. Note that if the screen contains more than one zone (such as 'outer' and 'inner') then the same task_id can be used in both zones as the button name will include the zone name as described in FAQ17a.

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_idtask_idtask_description
location(popup1).phpxxx_location(popup1)Choose Location
location(popup1).phpxxx_location(popup1)fromChoose FROM Location
location(popup1).phpxxx_location(popup1)toChoose 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:

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:

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.

83. How do navigation buttons work?

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.

83a. How do entries appear in the navigation bar?

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.

83b. What happens when a button in the navigation bar is pressed?

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.

84. How to manually extend the automatically extended SQL SELECT statement

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:

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 = RDCsingleton::getInstance('whatever');
    $dbobject->sqlSelectDefault();
    $dbobject->sql_select .= '....';
    $dbobject->sql_from   .= '....';
    $data = $dbobject->getData('....');

85. How to perform a search on an aggregate or aliased column

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.

86. How can I make global environmental changes for individual subsystems?

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.

87. Can I add barcodes to my PDF documents?

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:

88. How can I implement Row Level Security (RLS)?

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.

89. What reserved words exist within the RADICORE framework?

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 WordMeaning
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:
rdc_fieldspec
rdc_fieldspecs
Refer to the following:
rdc_table_name Used when constructing WHERE strings from database tables whose primary key is a single column with the generic name of "id". This is used by the child form to identify the relevant primary key to foreign key translation from the relationship details in $this->parent_relations.
rdc_to_be_inserted
rdc_to_be_deleted
rdc_to_be_updated
rdc_to_be_ignored
Refer to the following:
rdc_to_be_copied If you have code in the _cm_getInitialData() method which manually creates part of a compound primary key, but you are in the process of copying records and you want to retain the current value instead of assigning a new one, then insert this switch into $fieldarray before you call the insertRecord() method.
rdc_no_foreign_data Used in the getExtraData() method to skip the call to getForeignData().
rdc_no_rollup If you have two tables in a parent-child relationship where values from the child table are accumulated into a value in the parent table then it is normal practice to perform the accumulation in the _cm_pre_updateRecord() method of the parent table. This then requires that whenever making a change to a child table it must include a call to the updateRecord() method of the parent table. However, when performing a copy of a document with many child records you may not want to update the parent after inserting each child as the parent record already contains the correct totals. In this case you can insert this switch into $fieldarray before you call the insertRecord() method in the task which is performing the copy.
rdc_skip_validation This will skip any custom validation for the current insert/update/delete operation. This is interchangeable with $this->skip_validation.
submit... Field names beginning with 'submit' should not be used as they could be confused with some of the buttons which exist in the action bar.

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

90. How to have different sets of dropdown/radio options for different database rows

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:

  1. If you use a pattern whose screen contains only a single database row (eg: ADD1, ADD2 or UPDATE1) then you do not need to construct more than one option list, so no special coding is necessary.
  2. If you use a pattern whose screen contains multiple database rows (eg: MULTI2, MULTI3 or MULTI4) then you must take the following steps: This will result in an XML document with contents similar to the following:
    <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

    infrastructure-faq-21 (2K)

91. Can I alter a screen's structure at runtime?

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 the following for specific details on how certain changes can be made:

92. How can I access different databases on different servers?

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', 'oracle' or 'sqlsrv'
$GLOBALS['dbusername'] = '??';
$GLOBALS['dbuserpass'] = '??';
$GLOBALS['dbprefix']   = '??';
$GLOBALS['dbport']     = '';
$GLOBALS['dbsocket']   = '';
// these are the database names used in the Data Dictionary
$GLOBALS['dbnames']    = '*';
// these are the database names used on the server
$GLOBALS['switch_dbnames'] = array('original_dbname' => 'different_dbname');

Note that the database engine can be switched between MySQL, PostgreSQL, Oracle or SQL Server 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.

The dbprefix option is to allow different copies of the same database to be accessed by adding a prefix, such as test_ or live_. Each of these databases uses the same dbschema with only the contents being different.

The switch_dbnames option is for those circumstances when a database name is changed, beyond the simple addition of a prefix, since being imported into the Data Dictionary. The 'original_dbname' is the name used within the Data Dictionary, while 'different_dbname' is the name being used on the current server. Note that this array can contain several entries.

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]['dbport']     = '';
    $servers[0]['dbsocket']   = '';
    $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]['dbport']     = '';
    $servers[1]['dbsocket']   = '';
    $servers[1]['dbnames']    = '*';
    $servers[1]['switch_dbnames'] = array('original_dbname' => 'different_dbname');
} 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:

It is also possible to use the switch_dbnames option to consolidate all the different Radicore databases into a single database name, as described in FAQ150.

93. How can I turn on authentication via a RADIUS server?

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:

  1. Ensure that the RADIUS extension is available in your copy of PHP. (Doh!)
  2. Create a 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
    
  3. Set Authentication? to RADIUS on the Menu Control Data screen.

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.

94. Can I have different initial values for different users?

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:

95. Can I restrict update and delete operations to a record's creator?

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.

96. Can I logon without seeing the LOGON screen?

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.

97. How is context passed to a child task?

This is documented in Appendix I of the Menu System User Guide, with additional notes in Associated/Related Rows.

98. How is context passed to a child object in the same task?

This is documented in Appendix I of the Menu System User Guide, with additional notes in Associated/Related Rows.

99. Can I use images instead of text for the hyperlinks above the menu bar?

By default the hyperlinks above the menu bar are plain text, as in the following:

Figure 22 - Menu Bar with text links

dialog-types-menu-bar (5K)

There is the option to change the text into images, as in the following:

Figure 23 - Menu Bar with image links

dialog-types-menu-bar2 (6K)

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';
$xsl_params['icon']['add-to-favourites'] = '/images/add-to-favourites.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']);
//unset($xsl_params['text']['add-to-favourites']);

?>

This file can be copied from radicore/css/xsl_params.inc.default.

Please note the following:

100. How can I add my company's logo to all web pages?

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 sample-logo (5K) and a background image sample-background (1K) 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

infrastructure-faq-25 (6K)

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 &lt;, &gt; and &amp; respectively. This will display the HTML tags as text instead of executing them as HTML.

101. How can I build a separate logon screen?

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:

1. Create a separate logon screen.

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.

2. Customise the look of this new screen.

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';

102. What are the CREATED_DATE/USER and REVISED_DATE/USER fields?

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:

103. What is the best way to perform a simple SQL aggregate function?

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.

104. How can I remove the 'show nn' options from the navigation bar?

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

105. How can I remove the 'select all/unselect all' options from the navigation bar?

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

106. How can I make a single row in a multi-row area non-editable?

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 them as editable to the user. So, instead of displaying the screen shown in Figure 26 to the user

Figure 26 - all rows editable

infrastructure-faq-26 (5K)

it would be better if it could be displayed as shown in Figure 27:

Figure 27 - all rows editable except one

infrastructure-faq-27 (5K)

This can be achieved with code similar to the following which uses the rdc_rowspecs pseudo-column:

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.

See also How can I make a single column in a multi-row area non-editable?

107. How can I remove the select box from a single row?

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

infrastructure-faq-28 (5K)

This can be achieved with code similar to the following which uses the rdc_rowspecs pseudo-column:

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.

108. How can I hide/remove columns in a multi-row display?

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

unsetColumnAttributes (3K)

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

setColumnAttributes (3K)

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.

See also How can I make a single column in a multi-row area non-editable?

109. How can I modify screen labels at runtime?

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

setColumnHeadings (3K)

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

For DETAIL screens (those which show one database record vertically with labels on the left and data on the right) the same result can be achieved by using the replaceScreenLabels() function.

110. How can I make all the fields in a particular zone non-editable?

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:

111. Why doesn't the Workflow system have a facility for sending emails?

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.

112. How can I remove the 'page created in ...' text from the bottom of each screen?

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;

113. What are the valid formats for the input and output of dates?

Dates are input and displayed according to the value in $GLOBALS['date_format'] in the CONFIG.INC file UNLESS separate formats are specified on the MNU_LANGUAGE record. This allows users with different languages to use date formats which are specific to that language.

The following formats are available:

PatternDescription
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
MMMddyyyy3 alpha, 1 or 2 digits, 4 digits
yyyy?m(m)?d(d)4 digits, separator, 1 or 2 digits, separator, 1 or 2 digits
ddmmyyyy2 digits, 2 digits, 4 digits
yyyymmdd4 digits, 2 digits, 2 digits
yyyy?MMM?d(d)4 digits, separator, 3 alpha, separator, 1 or 2 digits

In the above list MMM is the short month name which is defined in file sys.language_array.inc for the user's language code. The lookup will be case insensitive.

The separator is any character which is not alphabetic or numeric.

114. How can I turn on authentication via an LDAP server?

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:

  1. Ensure that the LDAP extension is available in your copy of PHP. (Doh!)
  2. Create an 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;
    
  3. Set Authentication? to LDAP on the Menu Control Data screen.

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.

115. How do I set up SSL encryption for a remote MySQL database?

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]['dbport']     = '';
    $servers[0]['dbsocket']   = '';
    $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.

116. How do I deal with multi-byte characters?

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:

  1. Change your PHP.INI or .htaccess file so that it contains the following directive:
    default_charset = "utf-8"
  2. Ensure that all HTML output contains the following entry:
    <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.

  3. Turn on MBSTRING function overloading so that the standard PHP string functions are replaced with their mb_* alternatives with the following directives in your PHP.INI or .htaccess file:
    mbstring.internal_encoding = utf-8
    mbstring.func_overload = 2
    
  4. Ensure that your database server is told to store all data in UTF-8, and that the client will send and receive data in UTF-8. For MySQL this will require the following: Also note that with MySQL it is possible to select a collation in addition to the character set, and different columns can have different collations. It is not a good idea to use a mixture of different collations in your MySQL database as this would require that every SQL statement be modified to deal with the differences. The RADICORE framework does not deal internally with collation issues, therefore the SQL statement may be rejected.

117. Can I change the style of individual entries in a radio group?

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:

118. How can I force a process to jump to another task?

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:

119. How can start a batch job from a web page?

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.

120. How do I perform a search on a related table?

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.

  1. By default the search screen for any table will only include fields from that table, so it will be necessary to modify the PRODUCT search to include the 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
    
  2. You must also amend the contents of the screen structure file to include this field otherwise it will not be displayed.
  3. After pressing the SUBMIT button in the SEARCH screen all search criteria will be combined into a single string and passed back to the calling program, which will usually be a LIST screen. This string will be made available in the $this->sql_search variable, but unless you include a JOIN to the FEATURE table in the subsequent sql SELECT statement any reference to the 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.

  4. As an alternative to using a JOIN you can use a subquery, as in 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);
        } // 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.

121. How can I modify report labels at runtime?

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

output-to-pdf-002 (9K)

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

You may also use the following code as an alternative:

function _cm_pre_output ($string)
// perform any processing required before the output operation
{
    if ($GLOBALS['mode'] == 'pdf-list') {
        // replace report headings
        $this->dynamic_column_headings['label_1'] = 'NEW label_1';
        $this->dynamic_column_headings['label_2'] = 'NEW label_2';
    } // if

    return $string;

} // _cm_pre_output

This options requires that the strings in the report structure file which are to be replaced are identified with the prefix '%%' as in the following example:

$structure['body']['fields'][] = array('subsys_id' => '%%label_1');
$structure['body']['fields'][] = array('subsys_desc' => '%%label_2');

122. What facilities are there for date processing?

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:

  1. If you have a DATE or DATETIME field which needs to be automatically set to the current date/time when a record is inserted or updated then modify its details in the Data Dictionary as follows:

    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.

  2. You may have a field called END_DATE, which identifies the date after which the record is deemed to be 'inactive'. If the END_DATE is not yet known by the user it is usually left blank, which is then stored as NULL in the database, but this can lead to problems when extracting records as the SQL SELECT statement would have to cater for both NULL and not-NULL values, as in:
    SELECT ... FROM ... WHERE end_date <= '$today' OR end_date IS NULL
    
    It 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'
    
  3. In a LIST task on a table which contains both START_DATE and END_DATE fields it may be useful to restrict the display to only those records which satisfy one of the following conditions:

    Any of these conditions can be incorporated into the SQL SELECT statement by adding one of the following to the $this->sql_search string:

    This is described in greater detail in FAQ53.
  4. When displaying a SEARCH screen for a table which contains both START_DATE and END_DATE fields the framework will automatically include a dropdown list called 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.
  5. It is possible to force a LIST task to show current records when it starts by inserting curr_or_hist='C' into the Selection (temporary) field using the Update Task screen.
  6. It is possible to allow the user to quickly switch between date ranges by adding the following pre-defined tasks to the navigation bar:
  7. If you have fields in a table which have the same processing as START_DATE and/or END_DATE, but with different names, you can identify these alias names in the Data Dictionary. This information will then appear in the $nameof area in the <tablename>.dict.inc file.

123. Why doesn't the Data Dictionary extract relationship details from the database?

The way that the Data Dictionary within RADICORE works is as follows:

  1. The details of all database tables are imported into the dictionary database.
  2. Any relationships are defined manually.
  3. The details are then exported to form part of the application.

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 defined in the database and checked automatically during any INSERT, UPDATE and DELETE queries. Foreign keys, as used in SELECT queries, are not defined in the database and used automatically, instead they have to be manually defined in any JOIN clauses.

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.

124. How can I use RADICORE components in my front-end web site?

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 as well as Why is RADICORE no good for building web sites?

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.

125. How can I alter times to be shown in the client's time zone?

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:

  1. As soon as any data is received from the client for an input or update operation all datetime and timestamp fields will be converted from the client time zone to the server time zone. All internal datetime handling will therefore be in the time zone of the server.
  2. Just before any data is exported to an XML document which will be transformed into XHTML for transmission to the client device all datetime and timestamp fields will be converted from the server time zone to the client time zone.

This also affects any messages which are displayed by the shutdown function.

126. How can I customise the printing of lines in the PDF List View?

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:

  1. Alternate rows can have a different background colour (known as 'candy stripes') by specifying the fillcolour attribute. If this is not specified then the background will be transparent. The example in Figure 35 also uses the default setting for border which is 'LR' (Left+Right).

    Figure 35 - List View with fillcolour = array(224,235,255)

    infrastructure-faq-35 (12K)
  2. A border can be specified for each cell in the row by specifying the border attribute. If this is not specified then the default will be 'LR' (Left+Right). The example in Figure 36 does not use a fillcolour.

    Figure 36 - List View with border => 'LRTB'

    infrastructure-faq-36 (14K)
  3. It is also possible to put a border around the page body instead of each cell. This is done by setting the border attribute to 'P' as shown in Figure 37.

    Figure 37 - List View with border => 'P'

    infrastructure-faq-37 (12K)

You can mix 'fillcolour' and 'border' settings to create whatever effect you desire.

127. How can I replace a column and its label at runtime?

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

infrastructure-faq-38 (23K)

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

infrastructure-faq-39 (19K)

This is achieved by calling the replaceScreenColumns() function in the _cm_pre_getData() method, as shown in the following 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

128. Why don't you store state-specific info in the 'context' field of a workflow case?

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:

When an application task which is part of a workflow is completed (fired) then the workflow engine will consume one token from each of the transition's input places and create a new token on each of the transition's output places. If one of these places is the input place for another transition then this other transition will be enabled (waiting to be fired). If one of these places is the end place then the workflow case is closed.

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.

129. Why aren't primary key names in $where converted to foreign key names?

NOTE: As of Radicore version 1.93.0 this answer is superseded by the use of new function getForeignKeyValues() which is described in Using Parent Relations to construct WHERE strings.

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:

130. How can I customise the text of pending workflow items on the menu/home page?

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)':
            $dbobject = RDCsingleton::getInstance('xample/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:

131. How can I control the sequence in which the conditions on Explicit OR Splits are evaluated?

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 progress 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-'.

132. How can I execute a task before the first menu screen is displayed?

If it is ever necessary to execute a task after the LOGON screen has been processed but before the first menu (Home Page) is displayed, it can be specified in the Initial Passthru field of the menu/home page task on the MNU_TASK table. This task will only be executed once, when the Home Page is first displayed, during which there will be no menu buttons so the only options will be either the SUBMIT or the CANCEL buttons in the Action Bar.

133. What options are there for hyperlinks and images?

There are several options for displaying either images or hyperlinks, or a hyperlink with an image, and these are described in the following paragraphs.

  1. An image which can be selected by the user.

    This uses the filepicker control. An example is available in the 'Person' screens of the 'Example' subsystem, which is included in the Radicore download. Using the Data Dictionary the details for the field can be specified as follows:

    $fieldspec['picture']           = array('type' => 'string',
                                            'size' => 40,
                                            'subtype' => 'image',
                                            'imagewidth' => 75,
                                            'imageheight' => 95,
                                            'control' => 'filepicker',
                                            'task_id' => 'x_person(filepicker)');
    

    This will produce the following in the HTML output:

    filepicker (4K)

    The field's value is the path to the image file, which in this example is relative to the current working directory. This value can be changed either directly, or by pressing the popup (1K) button which activates the specified filepicker task.

    If you do not wish the user to change the value directly you can remove the text field from the HTML output by adding the notext attribute to the field's specifications using code similar to the following in the _cm_changeConfig() method:

    $this->fieldspec['picture']['notext'] = 'y';
    
  2. An image which is defined programmatically.

    The image control is similar to the filepicker, but without the ability for the user to select which image to use. This is for those circumstances where the identity of the image is chosen programmatically, such as to indicate a record's status. Using the Data Dictionary the details for the field can be specified as follows:

    $fieldspec['status_icon']       = array('type' => 'string',
                                            'imagewidth' => 16,
                                            'imageheight' => 16,
                                            'control' => 'image');
    

    If the field containing the image does not actually exist in the database then its details will have to be specified manually in the _cm_changeConfig() method.

  3. An image with customised 'alt' text.

    By default an image's 'alt' text, which is displayed when the mouse is hovered over the image, is exactly the same as the path to the file which contains the image. It is possible to specify a customised string for this 'alt' text by appending [alt=...] to the path name, as shown in the following example:

    function _cm_formatData ($fieldarray, &$css_array)
    // perform custom formatting before values are shown to the user.
    {
        if (!empty($fieldarray['foobar'])) {
            $fieldarray['foobar'] .= '[alt=put your custom text here]';
        } // if
        
        return $fieldarray;
        
    } // _cm_formatData
    

    When the data is transferred to the XML document prior to the XSL transformation the [alt=...] string will be stripped from the field's value and inserted as an attribute. The remainder will be used as the path to the image file.

  4. A simple hyperlink.

    The hyperlink control is used when a field contains a URL in the format http://whatever... and when displayed it will be an actual hyperlink which the user can click on to jump to that URL. Using the Data Dictionary the details for the field can be specified as follows:

    $fieldspec['article_url']       = array('type' => 'string',
                                            'size' => 255,
                                            'control' => 'hyperlink');
    
  5. A hyperlink with a customised label.

    In some circumstances it may be desirable to display a shortened label to the user instead of the entire URL. This can be achieved programmatically by changing the field's value to specify the label and url as separate components, as shown in the following example:

    function _cm_formatData ($fieldarray, &$css_array)
    // perform custom formatting before values are shown to the user.
    {
        if (!empty($fieldarray['filename'])) {
            $fieldarray['filename'] .= '[url=put the url here]';
        } // if
        
        return $fieldarray;
        
    } // _cm_formatData
    

    When the data is transferred to the XML document prior to the XSL transformation the [url=...] string will be stripped from the field's value and inserted as an attribute. The remainder will be used as the label.

  6. A hyperlink around an image.

    The image-hyperlink control can be used in order to display a small version of an image (often known as a thumbnail) which is a hyperlink to the full-sized image. Using the Data Dictionary the details for the field can be specified as follows:

    $fieldspec['image_path']        = array('type' => 'string',
                                            'size' => 40,
                                            'subtype' => 'image',
                                            'imagewidth' => 75,
                                            'imageheight' => 95,
                                            'control' => 'imagehyper');
    

    When this image is displayed in the form it will use the dimensions specified in imagewidth and imageheight. By clicking anywhere in the image this will activate the hyperlink and display the full size image.

  7. An array of hyperlinks.

    Sometimes it may be necessary to display a list of hyperlinks instead of a single hyperlink, such as when an email contains several attachments. This is as simple as changing the string field into an array, as shown in the following:

    function _cm_formatData ($fieldarray, &$css_array)
    // perform custom formatting before values are shown to the user.
    {
        if (!empty($fieldarray['attachments'])) {
            // multiple attachments are separated by ';'
            $array = explode(';', $fieldarray['attachments']);
            if (count($array) > 1) {
                // more than 1 entry, so set field to an array
                $fieldarray['attachments'] = $array;
            } // if
        } // if
        
        return $fieldarray;
        
    } // _cm_formatData
    

    An array of hyperlinks will appear in the XML file similar to the following:

    <attachments noedit="y" control="hyperlink">
        <array url="email_attachment.php?id=154003&seq=1">att001.html</array>
        <array url="email_attachment.php?id=154003&seq=2">foobar.pdf</array>
        <array url="email_attachment.php?id=154003&seq=3">snafu.pdf</array>
    </attachments>
    

    Each entry may also contain the optional [url=...] as described in point 5 above.

    Here is an example showing how the original string field may be constructed:

    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_from    = null;
            $this->sql_groupby = null;
            $this->sql_having  = null;
            $this->sql_from    = $this->_sqlForeignJoin($this->sql_select, 
                                                        $this->sql_from, 
                                                        $this->parent_relations);
    
            if ($GLOBALS['mode'] == 'read') {
                // obtain a list of all attachments in format 'filename[url=...]; filename[url=...]; ...'
                $this->sql_select .= ",\n (SELECT GROUP_CONCAT(CONCAT(filename,'[url=...]')
                                           ORDER BY seq_no SEPARATOR ';') 
                                           FROM email_attachment 
                                           WHERE email_attachment.email_id=email.email_id
                                          ) AS attachments";
            } // if
        } // if
        
        return $where;
        
    } // _cm_pre_getData
    

    The [url=...] portion may point directly to the file on disk, or it may point to a PHP script which extracts the file's contents from a BLOB field in the database.

134. How can I change the style of a field in a PDF report?

Each PDF report has its own Report Structure File which defines the specific requirements for that report, such as what goes where, and which style to use for which element. All these styles must have been defined in the PDF Style File which exists in the main subsystem directory. This allows different subsystems to have their own set of PDF styles.

Sometimes it may be useful to change the style of a field at runtime, such as changing the colour of a negative value to be red, for example. This can be achieved by placing the relevant code in the _cm_formatData() method, as shown in the following:

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['value'] < 0) {
          $css_array['value'] = 'red';
      } // if

      return $fieldarray;

  } // _cm_formatData

In this example 'red' is the name of a style in the PDF Style File. This may contain all the attributes of a style, such as font, fillcolour, textcolour and drawcolour, or it may just specify the attributes which are to change, such as:

$style['red']['textcolour']   = array(255,0,0);
$style['green']['textcolour'] = array(0,255,0);
$style['blue']['textcolour']  = array(0,0,255);

135. Can I remove navigation buttons at runtime?

Under normal circumstances any navigation buttons which have been defined for a task will be displayed when that task is being executed, except those for which access by the current user has not been granted. However, there may be circumstances when a navigation button becomes inappropriate due to data values which can only be determined at runtime. While it is not possible to add a new button, it is possible to nominate a button for removal by adding the task's identity to the $GLOBALS['nav_buttons_omit'] array variable.

136. Can I remove action buttons at runtime?

The action buttons for each dialog type are hard-coded within each of the controller scripts. It is possible to remove any of these buttons at runtime by removing the relevant entry from the $GLOBALS['act_buttons'] variable using the unset() function..

137. Can I have buttons in the data area?

By default the only buttons allowed in the data area of a form are for fields which use the filepicker or popup controls as these help the user to choose values for those fields. Other buttons exist in the navigation bar and the action bar, but these perform actions which are not limited to a single field.

If you wish to have a button in the data area this can be done by amending the $fieldspec array using code similar to the following:

function _cm_changeConfig ($where, $fieldarray)
// Change the table configuration for the duration of this instance.
{
    if ($GLOBALS['mode'] == 'insert') {
        $this->fieldspec['add_attachment'] = array('type' => 'string',
                                                   'control' => 'input',
                                                   'subtype' => 'button',       <-- optional -->
                                                   'class' => '...',            <-- optional -->
                                                   'id' => '...'),              <-- optional -->
                                                   'task_id' => 'whatever');    <-- optional -->
    } // if
		
    ... or alternatively ...
		
    $this->fieldspec['some_button'] = array('type' => 'string',
                                            'control' => 'button',
                                            'subtype' => 'button',       <-- optional -->
                                            'class' => '...',            <-- optional -->
                                            'id' => '...'),              <-- optional -->
                                            'value' => '...'),           <-- optional -->
                                            'task_id' => 'whatever');    <-- optional -->
    
    return $fieldarray;
    
} // _cm_changeConfig

The output from the first option will look like the following:

<input name="..." class="..." id="..." value="..." type="[submit] or [button]" />

The output from the second option will look like the following:

<button name="..." class="..." id="..." value="..." type="[submit] or [button] or [reset]" >value</button>

The purpose of this control is to put a button in the data area. This can be used for one of the following reasons:

If no value for the subtype attribute is supplied the HTML type attribute will default to submit.

If no value for the class attribute is supplied it will default to button.

The name attribute will be set to one of the following:

Note that the input/button control cannot be set for any field via the data dictionary as that only deals with data fields which exist in the database, while custom buttons are non-database fields which represent actions and not data.

In order to have this button displayed in the HTML document you will need to add the relevant entry to the screen structure file, similar to the following:

$structure['main']['fields'][] = array('add_attachment' => '');

Unless you are in an ADD transaction you will also need to ensure that this non-database field appears in the data array otherwise it will not appear in the screen. You will need code similar to the following:

$this->fieldarray['add_attachment'] = '<button text>';

Here is an example showing how to display several buttons on the same line:

$structure['main']['fields'][11][] = array('label' => '');
$structure['main']['fields'][11][] = array('field' => 'add_attachment');
$structure['main']['fields'][11][] = array('field' => 'list_attachments', 'colspan' => 7);

Note here that the separate screen label is not used as the button will contain its own descriptive text, as shown in Figure 40:

Figure 40 - Example of Button controls

infrastructure-faq-40 (4K)

When the button is displayed in the HTML document the text displayed inside the button will be the field's value.

If the button's subtype is SUBMIT then when it is pressed it will call the current script using the POST method. If an optional 'task_id' has been specified then the nominated task will automatically be activated, otherwise the _cm_customButton() method on the object in which the button was defined will be activated instead. In order for the _cm_customButton() method to be called in other objects you will need to set $this->allow_buttons_all_zones=TRUE; in all those other objects. You will need to insert code into the _cm_customButton() method in all relevant objects to perform whatever actions are required otherwise nothing will happen at all.

Putting multiple buttons into a single cell

If you have several custom buttons in the same row on a screen then by default each button will be treated as a separate field and will require its own table cell. This can sometimes prove to be inconvenient, which is why I have enabled the option to put several buttons into a single table cell. The steps are as follows:

138. How can I build a dropdown/radio group from a table with a compound key?

FAQ09 gives an example of how to create the option list for a dropdown or radio group from a database table with a single-column key, but if the table in question has a compound key then a slightly different approach will be necessary. The option list which is used to populate the contents of a dropdown or radio group is a simple associative array, so it cannot handle compound keys without some assistance.

Here is an example which shows how to build the option list using a simple key:

    // get data from the database
    $this->sql_select      = 'id, name';
    $this->sql_orderby     = 'name';
    $this->sql_orderby_seq = 'asc';
    $data = $this->getData($where);
    
    // convert each row into 'id=name' in the output array
    foreach ($data as $row => $rowdata) {
        $array[$rowdata['id']] = $rowdata['name'];
    } // foreach
    
    return $array;

Here is an example which shows how to build the option list using a compound key:

    // get data from the database
    $this->sql_select      = 'idA, idB, idC, name';
    $this->sql_orderby     = 'name';
    $this->sql_orderby_seq = 'asc';
    $data = $this->getData($where);
    
    // convert each row into 'idA/ibB/idC=name' in the output array
    foreach ($data as $row => $rowdata) {
        $key = $rowdata['idA'].'/'.$rowdata['idB'].'/'.$rowdata['idC'];
        $array[$key] = $rowdata['name'];
    } // foreach
    
    return $array;

Here you see we have constructed the array key as 'idA/idB/idC', where '/' is the delimiter which separates the different values. You can use whatever delimiter you like, just ensure that it never appears in any of the values.

When the user selects one of these options it will be returned as a single string, but it will have to be split into its component parts before they can be used as individual values. This will require some additional code, in either the _cm_pre_insertRecord() or _cm_pre_updateRecord() methods, which is similar to the following:

    list($idA, $idB, $idC) = explode('/', $rowdata['compound_key']);
    $rowdata['idA'] = $idA;
    $rowdata['idB'] = $idB;
    $rowdata['idC'] = $idC;

139. Can I change the style of an individual checkbox?

By default all checkboxes 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 $fieldspec array, as shown in the following example:

140. Can I change the style of individual entries in a dropdown?

By default all entries in a dropdown list 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:

141. How can I add a hidden field to an HTML form?

A hidden field is one that use the 'hidden' control to make itself invisible to the user, but will still be sent to the client when a SUBMIT button is pressed. It will produce HTML output similar to the following:

    <td><input type="hidden" name="display_currency" value="GBP" /></td>

To add such a field to any HTML form you must perform the following:

  1. Ensure that the field has a value in the object's $fieldarray.
  2. Ensure that the field has an entry in the screen structure file.
  3. Ensure that the field has an entry in the object's $fieldspec array, such as:
        $this->fieldspec['display_currency'] = array('type' => 'string',
                                                     'control' => 'hidden');
    

If you ever want the field to be both 'hidden' and 'visible' at the same time (ie: can be seen by the user but not changed, yet still sent to the client when a SUBMIT button is pressed) just add the 'visible' attribute as follows:

    $this->fieldspec['display_currency'] = array('type' => 'string',
                                                 'control' => 'hidden',
                                                 'visible' => 'y');

This will produce HTML output similar to the following:

    <td><input type="hidden" name="display_currency" value="GBP" />GBP</td>

142. What happens in a workflow when a place contains more than one token?

In the Workflow system if a transition has a single input place then that place cannot hold more than one token as each time one lands on it the related transition is fired automatically and the token is instantly consumed. In this situation it is therefore impossible for a place to hold more than one token. If, however, a transition has multiple input places then it is possible for one of those places to hold more than one token at a time, as explained in the following example:

The remaining token on 'P1' just sits there, waiting for a token to be placed on 'P2' so that its transition can be fired and it can be consumed. If the workflow is completed before the token is consumed then it is never consumed as the workflow is no longer active.

143. What does 'Uncaught exception from DOMException, message = Invalid Character Error' mean?

This error is generated when creating the XML document prior to its transformation into HTML. It signifies that the element (field) name contains invalid characters. According to the XML specification an element name must conform to the following:

Among the excluded characters are '(' and ')', so the most common cause of this error is where a SELECT statement contains an aggregate without an alias name:

COUNT(...) This produces the name COUNT(...) which is invalid.
COUNT(...) AS count This produces the name count which is valid.

144. How do I include an aggregate in a SELECT statement?

Whenever you wish to modify a SELECT statement for a particular task the first step should *ALWAYS* be to try it out in your database client. In this way you will verify that the statement is syntactically correct and actually produces the results that you want. Then, and only then, should you modify your code to use this modified statement.

As an example I shall use the 'List Role' task in the MENU subsystem. In its original version the generated SQL was very simple:

SELECT mnu_role.role_id, start_task_id, global_access, role_desc
FROM mnu_role 
ORDER BY mnu_role.role_id asc 

This was produced by modifying the mnu_role(list1).php script to contain the following:

$sql_select = 'mnu_role.role_id, start_task_id, global_access, role_desc';
$sql_from   = '';
$sql_where  = '';
$sql_groupby = '';

When I wanted to include the count of users within each role I needed to generate the following statement:

SELECT mnu_role.role_id, start_task_id, global_access, role_desc, count(user_id) as count 
FROM mnu_role 
LEFT JOIN mnu_user ON (mnu_user.role_id=mnu_role.role_id)   
GROUP BY mnu_role.role_id  
ORDER BY mnu_role.role_id asc 

Note that this requires a GROUP BY clause.

This was produced by modifying the mnu_role(list1).php script to contain the following:

$sql_select = 'mnu_role.role_id, start_task_id, global_access, role_desc, count(user_id) as count';
$sql_from   = 'mnu_role LEFT JOIN mnu_user ON (mnu_user.role_id=mnu_role.role_id) ';
$sql_where  = '';
$sql_groupby = 'mnu_role.role_id';

An alternative statement, which does not require a GROUP BY clause, would be as follows:

SELECT mnu_role.role_id, start_task_id, global_access, role_desc
      ,(SELECT count(user_id) FROM mnu_user WHERE mnu_user.role_id=mnu_role.role_id) as count 
FROM mnu_role 
ORDER BY mnu_role.role_id asc 

145. How do I enable the QuickSearch Bar?

The QuickSearch Bar is a faster alternative to the use of a separate SEARCH screen. It is currently only supported in tasks of the LIST and POPUP patterns. It is not available by default, but can be enabled using one of the following methods:

  1. By populating the QuickSearch dropdown list using code similar to the following:
        function _cm_getExtraData ($where, $fieldarray)
        {
            $pattern_id = getPatternId();
    
            if (preg_match('/^(list1|popup1)$/i', $pattern_id)
            OR (preg_match('/^(list2|popup2)$/i', $pattern_id) AND $this->zone == 'inner')) {
                if (!array_key_exists('quicksearch_field', $this->lookup_data)) {
                    // set list of field names for QuickSearch option
                    $array = array('database_id' => 'Database Id',
                                   'database_desc' => 'Database Desc',
                                   'subsys_id' => 'Subsys Id');
                    $this->lookup_data['quicksearch_field'] = $array;
                    $this->xsl_params['quicksearch_default'] = 'database_id';  // optional
                } // if
            } // if
    
            return $fieldarray;
    
        } // _cm_getExtraData
    

    The quicksearch_default option will cause the named field to be pre-selected in the dropdown, otherwise it will be empty.

  2. From RADICORE version 1.95.0 onwards you can add a list of field names to the MNU-TASK-QUICKSEARCH table. Note that the first name in the list will always be used as the default selection.

Note that option #1 will still keep working for a task until option #2 is implemented. After this point the code for option #1 can be deleted as it will be redundant.

146. How can I change how a field (or a column of fields) is displayed?

One of the ways in which a screen can be customised is to alter the way in which a single field, or a whole column of fields, is displayed. This can be achieved by modifying the screen structure file to include one or more additional attributes, such as:

In list screens with a horizontal display these attributes can be applied to every field in a column using code similar to the following:

<?php
$structure['xsl_file'] = 'std.list1.xsl';

$structure['tables']['main'] = 'person';

$structure['main']['columns'][] = array('width' => 5);
$structure['main']['columns'][] = array('width' => 70);
$structure['main']['columns'][] = array('width' => 100);
$structure['main']['columns'][] = array('width' => 100);
$structure['main']['columns'][] = array('width' => 100, 'align' => 'center', 'nosort' => 'y');
$structure['main']['columns'][] = array('width' => '*', 'align' => 'right');

$structure['main']['fields'][] = array('selectbox' => 'Select');
$structure['main']['fields'][] = array('person_id' => 'ID');
$structure['main']['fields'][] = array('first_name' => 'First Name');
$structure['main']['fields'][] = array('last_name' => 'Last Name');
$structure['main']['fields'][] = array('star_sign' => 'Star Sign');
$structure['main']['fields'][] = array('pers_type_desc' => 'Person Type');
?>

In detail screens with a vertical display these attributes can be applied to individual fields using code similar to the following:

$structure['main']['fields'][1] = array('person_id' => 'ID',
                                        'colspan' => 5);

$structure['main']['fields'][2][] = array('label' => 'First Name');
$structure['main']['fields'][2][] = array('field' => 'first_name',
                                          'size' => 15);
$structure['main']['fields'][2][] = array('label' => 'Last Name');
$structure['main']['fields'][2][] = array('field' => 'last_name',
                                          'size' => 15);
$structure['main']['fields'][2][] = array('label' => 'Initials');
$structure['main']['fields'][2][] = array('field' => 'initials');

$structure['main']['fields'][4] = array('picture' => 'Picture',
                                        'colspan' => 5,
                                        'imagewidth' => 32,
                                        'imageheight' => 32);
....
$structure['main']['fields'][11] = array('value2' => 'Value 2',
                                         'colspan' => 5);

$structure['main']['fields'][12][] = array('label' => 'Start Date');
$structure['main']['fields'][12][] = array('field' => 'start_date');
$structure['main']['fields'][12][] = array('label' => 'End Date');
$structure['main']['fields'][12][] = array('field' => 'end_date',
                                           'colspan' => 3);

It is also possible to change the way a field is displayed at runtime, as described in FAQ73.

147. How can I update several tables in a single operation?

By default an insert, update or delete operation will only affect a single database table, the one which is accessed by the table class on which the operation is requested. However, it is possible to extend this default behaviour to include other tables by inserting the relevant custom code into any of the _cm_post_insertRecord(), _cm_post_updateRecord() and _cm_post_deleteRecord() methods. Here is an example:

function _cm_post_insertRecord ($rowdata)
// perform custom processing after database record has been inserted.
{
    $dbobject1 = RDCsingleton::getInstance('table_1');
    $new = $dbobject1->insertRecord($rowdata);
    if ($dbobject1->errors) {
        $this->errors[$dbobject1->getClassName()] = $dbobject1->errors;
        return $rowdata;
    } // if
		
    $dbobject2 = RDCsingleton::getInstance('table_2');
    $data['field_1']    = $rowdata['field_1'];
    $data['field_2']    = $rowdata['field_2'];
    $data = $dbobject2->updateRecord($data);
    if ($dbobject2->errors) {
        $this->errors[$dbobject2->getClassName()] = $dbobject2->errors;
        return $rowdata;
    } // if

    return $rowdata;

} // _cm_post_insertRecord

Note the following:

148. Why don't you use PDO instead of your own database abstraction class?

The simplest answer is because PDO was "too little, too late":

The limitation with PDO is that, although it can connect to a variety of different DBMS engines, it is only capable of issuing the same SQL query to each of those engines - it is not capable of modifying the query to deal with any peculiarities of a particular DBMS. This limitation is far too restrictive for me, so although I wrote my original Data Access Object (DAO) to deal with MySQL using the mysql_* extension, I later added a separate class for the mysqli_* extension. I used the same technique to create additional classes for the PostgreSQL, Oracle and SQL Server engines, and this enabled me to easily include the ability to deal with all the differences I found. The major ones are:

So you can see that switching to the PDO extension in the RADICORE framework would be a backward step, so it is one that I am not going to take.

149. What is the purpose of the Data Dictionary?

To keep the structure of the software objects synchronised with the structure of the database, thus avoiding the need for any sort of Object Relational Mapper. My approach is to design the database first, then construct my classes, one per table, from the database structure. This is done by importing the database structure into the data dictionary, then exporting from the dictionary to the application. If any database table is subsequently altered it is a simple process to re-import and then re-export. This will not touch the existing class file but will replace the structure file.

Please refer to A Data Dictionary for PHP Applications and Menu and Security System User Guide - Appendix N for more details.

150. How can I consolidate all the Radicore databases into a single database?

By default the four databases used by Radicore - AUDIT, DICT, MENU and WORKFLOW - have their own database names in the server instance. However, there are some Radicore users whose ISPs do not allow them to create or access more than one MySQL database, so it would be convenient if these four databases could be maintained under a single database name. This is now possible using the switch_dbnames option which is described in FAQ92. In the following sample from the CONFIG.INC file all the databases will be redirected at runtime from the names contained in the various class files to the consolidated name 'single':

    global $servers;
    // server 0
    $servers[0]['dbhost']     = 'localhost';
    $servers[0]['dbengine']   = 'mysql';
    $servers[0]['dbusername'] = '??';
    $servers[0]['dbuserpass'] = '??';
    $servers[0]['dbprefix']   = '';
    $servers[0]['dbport']     = '';
    $servers[0]['dbsocket']   = '';
    // these are the database names used in the Data Dictionary
    $servers[0]['dbnames']    = 'audit, dict, menu, workflow';
    // these are the database names used on the server
    $servers[0]['switch_dbnames'] = array('audit' => 'single',
					  'dict' => 'single',
					  'menu' => 'single',
					  'workflow' => 'single');

Instead of 'single' you may use any database name that you like.

151. How can I update a field using a database function instead of a literal?

There may be some cases where you wish to update a table column by using the result of a database function instead of supplying a literal value, so can this be done with Radicore? Imagine the following SQL statement which you wish to execute:

UPDATE ... SET field1=REPLACE(field1, '_','-') WHERE ...

During the normal process of converting the body of the statement from a string to array the following will be produced:

field1 = REPLACE(field1, '_','-')

By default when this array is converted back into a string it will result in the following:

UPDATE ... SET field1='REPLACE(field1, \'_\',\'-\')' WHERE ...

In order to prevent the function call to be treated as a string literal you must add an entry to the $this->allow_db_function array immediately before the update command as in the following:

$this->allow_db_function[] = 'field2';

This enables you to use this ability on any number of columns in a single SQL statement. Note also that the contents of this array will be cleared after it has been used.

Please note that this feature should not be used with any unique keys (primary or candidate) as the framework will test for uniqueness before the SQL query is executed, but the results of the function are not known until afterwards.

152. Why is RADICORE no good for building web sites?

If you think that the world revolves around a front-end website then you must be working with small websites which are so simple that they do not need a back-end administrative application. When writing business applications, such as e-commerce, there should be a separate back-end application which can be used by members of staff. The front-end website contains only a subset of the functionality and is only accessed by visitors and potential customers. This arrangement is shown in Figure 47:

Figure 47 - Front End web site, Back End administrative application

front-end-back-end-04 (3K)

The front-end website is nothing more than a gaudy order entry system while the back-end is responsible for everything involved with order fulfilment - pick lists, inventory, invoicing, shipments, purchase orders, suppliers, et cetera. The back-end application can sometimes be as much as 100 times bigger than the front-end.

RADICORE is built around the 3 Tier Architecture which provides separate components for the Presentation, Business and Data Access layers. While RADICORE's Presentation layer is built specifically for the needs of the back-end administrative application, the Business and Data Access layers can be shared by your front-end website (or even multiple front-end websites) with little effort. This arrangement is shown in Figure 48:

Figure 48 - A small Front End sharing the components in a larger Back End application

front-end-back-end-02 (5K)

This means that the bulk of the processing has already been provided by the back-end components, so all that is necessary to build a front-end website is a new light-weight Presentation layer. You can have as many different Presentation layers as you like, but they all share the same Business and Data Access layers. Because the Presentation layer is light-weight it can easily be re-styled or rebuilt without any effect on the other layers.

See also How can I use RADICORE components in my front-end web site?

153. Why re-invent the wheel by creating yet another framework?

By asking such a question you are exposing your own lack of experience. There is no such thing as a one-size-fits-all wheel just as there is no such thing as a one-size-fits-all framework. A wheel for a shopping trolley is unsuitable for a racing car just as a wheel for a tractor is unsuitable for a child's pram. Similarly a framework for building content management systems is unsuitable for building customer relationship management systems just as a framework for building blogging applications is unsuitable for building e-commerce applications. A framework for building front-end websites is also unsuitable for building back-end enterprise applications.

My background is in building database applications for the desktop such as order entry, order processing, warehousing and inventory, shipments, invoicing and accounting. This type of application is also known as Order Fulfilment, Order Processing, Supply Chain Management or Enterprise Resource Planning. In today's world of e-commerce a front-end website is used as a glamorous order entry mechanism, but that still leaves everything else to be handled by the back-end administrative application. Note here that the front-end is open to all visitors while the back-end is restricted to members of staff. Other requirements of back-end administrative applications include:

Another big failing with all these other frameworks is that they do not provide support for Rapid Application Development (RAD). The RADICORE framework implements the Model-View-Controller design pattern in the following ways:

It also contains an implementation of the 3-Tier Architecture, which is not the same thing, by taking all database access out of the Model and placing it in a separate Data Access Object (DAO).

All the frameworks I have seen have been concerned with the development of front-end websites and not back-end administrative applications (See FAQ152 for an explanation of the difference) and therefore don't offer the functionality that I expect, so they are all unsuitable. As I have already built back-end frameworks in two of my previous languages it was a simple exercise to build another in PHP rather than waste my time with someone else's pile of rubbish unsuitable offering. If you want to make a silk purse you don't start with a sow's ear.

154. How can you call your framework Object Oriented if it still contains procedural functions?

If you think that Object Oriented Programming (OOP) requires that absolutely everything be done using objects then you are sadly mistaken. It is a simple fact that there must be some procedural code somewhere, even if all it does is instantiate the first object. In this article you will see this simple definition of OOP:

Object Oriented Programming is programming which is oriented around objects, thus taking advantage of Encapsulation, Polymorphism, and Inheritance to increase code reuse and decrease code maintenance.

PHP is a great language because, just like C++, it supports both the procedural and OO approaches, so it is up to the individual developer to decide which approach works best depending on the circumstances. I will continue to use procedural functions unless there is a compelling reason, or a distinct advantage, in switching to objects. I'm afraid that "because that's how it is done" is not a compelling reason.

In my framework you will see that that the following components have been implemented as classes which can be turned into objects:

  1. All the Model (Business layer) components are classes so that:
  2. All the database (Data Access layer) components are classes so that:

All my page controllers are still procedural for the simple reason that there is no opportunity for either inheritance or polymorphism, so there would be no advantage in converting them into classes and objects. If there is no benefit in making a change, then how can you justify the effort in making that change?

Some programmers like to use static methods instead of procedural functions, but this is not Object Oriented as the classes are never instantiated into objects. Where is the inheritance? Where is the polymorphism?

Putting unrelated functions into the same class would be wrong as it would violate the principle of encapsulation. Putting each function in its own class, then following the convention of putting each class in its own file, would require an increase in the number of include statements which would be a logistical nightmare as well as increasing the processing overhead.

As far as I can see the only valid reason to take a group of procedural functions and place them in the same class is when those functions share common state.

In his article Why C++ is not just an Object Oriented Programming Language (PDF) the author Bjarne Stroustrup has this to say regarding language features:

Even when all the features required to support object oriented programming are available, you don't need to use them all the time. Some classes just don't belong in a hierarchy and some functions don't belong to any particular object.

You may also want to read the following article:

155. To where does the error log get written?

When there is a fatal error, such as when an SQL query fails, the standard error handler is invoked using the trigger_error() function in order to write the details of the error to a log file on disk as well as sending them via email to the system administrator. The address or file path which is used can be specified using the following constants in the CONFIG.INC file:

156. How can I dynamically add or remove rows from an ADD5 or MULTI2/3/4/5/6 screen?

The standard method of allowing the user to add new rows of data to the database is via an ADD1 or ADD2 screen which will process one row at a time. However, in the ADD5 screen the user is presented with a fixed number of blank rows which can be filled is as needed and added to the database with a single press of the SUBMIT button. For example, I have used this pattern in a timesheet entry screen as there are always seven days in a week, so the getInitialDataMultiple() method will always create seven new database rows for the designated week. But what if you have circumstances where the number of rows is not not fixed and you need to adjust them at runtime? There is no functionality within the standard ADD5 pattern to do this, but this functionality can be added by using custom buttons in the following manner:

  1. Define the 'Add Row' and 'Delete Row' buttons in the parent area:
    function _cm_changeConfig ($where, $fieldarray)
    // Change the table configuration for the duration of this instance.
    {
        if (!array_key_exists('add_row_button', $this->fieldspec)) {
            $this->fieldspec['add_row_button']          = array('type' => 'string',
                                                                'control' => 'button');
        } // if
        if (!array_key_exists('delete_row_button', $this->fieldspec)) {
            $this->fieldspec['delete_row_button']       = array('type' => 'string',
                                                                'control' => 'button');
        } // if
    		
    } // _cm_changeConfig
    
  2. Provide labels for these buttons:
    function _cm_getExtraData ($where, $fieldarray)
    {
        $fieldarray['add_row_button']    = 'ADD PACKAGE';
        $fieldarray['delete_row_button'] = 'DELETE PACKAGE';
    		
    } // _cm_getExtraData
    

    This is necessary as these two fields do not have any values which can be retrieved from the database, therefore by default would not be added to the XML output which is used to build the HTML form, which in turn means that they would not be displayed.

  3. Populate the _cm_customButton() method in the parent instance:
    function _cm_customButton ($fieldarray, $button)
    // user pressed a custom button.
    {
        $childOBJ = $this->getChildObject();
        if (!is_object($childOBJ)) return $fieldarray;  // no child, so exit now
        $child_data = $this->getChildData();
    		
        switch ($button) {
            case 'add_row_button':
                $child_data = $childOBJ->_add_package($child_data, $fieldarray);
                break;
            case 'delete_row_button':
                $child_data = $childOBJ->_delete_package($child_data, $_POST['select']);
                break;
            default:
                // do nothing
        } // switch
    		
        return $fieldarray;
    		
    } // _cm_customButton
    

    Note that in this example the actual processing for each button is contained in separate methods within the child object. It is also possible to modify the contents of $child_data within the parent object after which it must execute the following line in order to update the contents of the child object:

    $this->setChildData($child_data);
    
  4. Create the _add_package() method:

    This will add an empty row to the current screen after populating hidden fields with data from the parent.

    function _add_package ($fieldarray, $parent_data)
    // add a blank row for another package
    {
        if (!is_long(key($fieldarray))) {
            $fieldarray = array($fieldarray);  // convert from associative to indexed
        } // if
    
        $package['invoice_id'] = $parent_data['invoiceno'];
        $package['order_id']   = $parent_data['headerid'];
        $this->last_seq_no++;
        $package['seq_no']     = $this->last_seq_no;
        $package['box_id']     = null;
        $package['pkg_height'] = null;
        $package['pkg_width']  = null;
        $package['pkg_depth']  = null;
        $package['pkg_weight'] = null;
    
        $fieldarray[] = $package;
    
        return $fieldarray;
    
    } // _add_package
    
    Note here that I am using variable $this->last_seq_no to keep track of values for the seq_no column which is part of the primary key. The starting value for this variable can be obtained by putting code in the _cm_initialise() method similar to the following:
    $this->last_seq_no = $this->getCount("SELECT max(seq_no) FROM ... WHERE ...");
    
  5. Create the _delete_package() method:

    Existing rows are selected by using the checkbox in the 'Select' column before pressing the button. This will allow any number of rows to be selected for deletion.

    function _delete_package ($fieldarray, $select_array)
    // delete selected package(s)
    {
        if (empty($select_array)) {
            $this->errors[] = "No package has been selected - cannot delete";
            return $fieldarray;
        } // if
    
        foreach ($select_array as $ix => $selected) {
            // selections start from 1, but indexes start from zero
            $rownum = $ix-1;
            unset($fieldarray[$rownum]);
        } // foreach
    
        // resequence so that index numbers start at zero
        $fieldarray = array_merge($fieldarray);
    
        foreach ($fieldarray as $ix => &$rowdata) {
            $rowdata['seq_no'] = $ix+1;  // re-index all package numbers
        } // foreach
    
        return $fieldarray;
    
    } // _delete_package
    

The above procedure can be used in an ADD5 screen which does nothing but add new rows to the database. However, with a small modification it can also be used in a MULTI2, MULTI3, MULTI4, MULTI5 or MULTI6 screen which reads existing rows from the database and allows them to be updated using the updateMultiple() method. In order for this method to perform inserts and deletions as well as updates you must include the following in your custom code:

Note that a row containing rdc_to_be_deleted will still appear in the HTML output, but all the fields will be non-editable and the select checkbox at the start of the row will be missing. The row will also be displayed using the CSS class rdc_to_be_deleted which by default will show the row with text in yellow with line-through against a brown background, as shown in Figure 49 below:

Figure 49 - Result of using rdc_to_be_deleted

infrastructure-faq-49 (21K)

157. Can I execute an arbitrary SQL query?

By default in order to perform a database query you call one of the standard methods such as getData(), insertRecord(), updateRecord() or deleteRecord() which includes a large processing overhead due to the multiple steps which are taken. In some cases it would be useful to construct and execute a query without this additional overhead, and this can now be done using the executeQuery() method. Note that this method can be used to execute either a single query or a number of queries.

158. Can I execute multiple SQL queries in a single step?

It is possible to execute a series of pre-defined queries in a single operation by calling the executeQuery() method.

159. Can I change the fonts in a PDF document at runtime?

While the fonts defined in the pdf.styles.inc file will be satisfactory for most users, there may be circumstances when a different font is required to deal with unicode characters such as those found in languages such as Chinese, Japanese and Korean. This situation can now be dealt with by adding the $font_replacement array to the pdf.styles.inc such as in the following example:

// fonts will be replaced if the language changes
$font_replacement['ja']    = 'kozgopromedium';
$font_replacement['ko']    = 'hysmyeongjostdmedium';
$font_replacement['th']    = 'freeserif';
$font_replacement['zh-cn'] = 'stsongstdlight';
$font_replacement['zh-hk'] = 'msungstdlight';
$font_replacement['zh-mo'] = 'msungstdlight';
$font_replacement['zh-sg'] = 'msungstdlight';

Note that the format is $font_replacement['<language>'] = '<font>';

The font replacement will be triggered automatically by setting $GLOBALS['party_language'] to the desired language code. This value is initially set to the current user's language, but may be changed to the language of the party who will be receiving the PDF document. Only those styles in the pdf.styles.inc file which have the following attribute added to the style definition will be affected:

$style['hdg']['font'] = array('family' => 'Times',
                              'style' => 'B',
                              'size' => 12,
                              'height' => 7,
                              'draw' => .4,
                              'font_replacement' => 'y',
                              'halign' => 'center'); 

This will enable you to produce output such as the following:

font_replacement (5K)

160. How can I make a single column in a multi-row area non-editable?

FAQ106 shows how it is possible to make a single row in a multi-row area non-editable, but in some circumstances it may be a requirement for individual columns in individual rows to be made non-editable. For example, in Figure 50 the entry labelled 'Sick Leave' is valid only on Wednesday 27th and the entry labelled 'Vacation Leave' is valid only on the 28th and 29th.

Figure 50 - individual columns made non-editable

infrastructure-faq-50 (5K)

This can be achieved with code in the _cm_post_getData method similar to the following which uses the rdc_fieldspec pseudo-column:

function _cm_post_getData ($rows, &$where)
{
    foreach ($rows AS $rownum => $rowdata) {
        if (condition) {
            $rowdata['rdc_fieldspec']['field_name'] = array('noedit'] => 'y');
        } // if
        $rows[$rownum] = $rowdata;
    } // foreach
    
    return $rows;
    
} // _cm_post_getData

You may also use the name rdc_fieldspecs as an alias, and you may also put the code in the _cm_formatData method.

Note that pseudo-columns rdc_fieldspec and rdc_fieldspecs are reserved words.

Note also that you cannot unset a value that already exists in the $fieldspec array. You can only add new values or replace existing ones. Some features are enabled by having a 'keyword' = 'y' entry in the array, but it is the presence of the keyword which enables the feature, not the value 'y'. This means that if the feature is already enabled you cannot disable it by changing the value to 'n'.

161. How can I set the user_id of a workflow WORKITEM record?

By default when a new workflow case is created it will be assigned to a ROLE_ID so that it will appear in the Menu/Home page of all users within that role, and the first user to click on that workitem will have the entire case allocated to them. However, there may be circumstances where instead of assigning a new workitem to a role you wish to assign it immediately to a particular user. This can now be achieved using the following steps:

Whenever a new case is created from this workflow the first transition will be fired automatically, but it will do nothing but insert the value of $this->wf_user_id into the user_id field of the wf_workitem record. This will close that workitem record and then activate the next one which, provided that it's trigger is set to "manual", will then appear on the Menu/Home page for the designated user and no-one else.

162. Can I have a filepicker task which works with subdirectories?

By default a filepicker task will only show those files which exist in a single directory which is named in $this->picker_subdir. However, it is now possible to change the name of this directory dynamically by adding two new tasks to the navigation bar of the filepicker:

The Choose Directory task is similar to a standard filepicker but with the following differences:

The Up Directory task is a standard task in the "Miscellaneous" subsystem which executes script menu/directory_up.php, so all you need do is add this existing task to your filepicker's navigation buttons. This task contains code similar to the following:

<?php
require_once 'include.general.inc';

initSession();      // initialise session

if (!empty($where)) {
    $where_array = where2array($where);
    if (!empty($where_array['picker_subdir'])) {
        // strip last part of this path name
        $return_string = dirname($where_array['picker_subdir']);
    } // if
} // if

// send updated value back to the previous script
$prev_script = getPreviousScript();
$prev_task   = getPreviousTask($prev_script);
$_SESSION['pages'][$prev_script][$prev_task]['return_string'] = $where;
scriptPrevious(null, null, 'choose');
?>

You will also need to make the following changes to your filepicker task:

163. How does the 'singleton' class work?

When writing code there may be places where you wish to access another object, but if you have already loaded and instantiated that object you want to reuse that instance instead of creating another one. This is exactly the situation for which the Singleton design pattern was created. Unlike most implementations which require each class to have its own getInstance() method, the RADICORE framework has all the relevant code within a separate singleton class. The disadvantage of the first method is that you must load the class before you can access its getInstance() method. My approach has the ability to locate and load the class automatically.

Before you can use this class it must first be loaded, but that is taken care of within the statement require_once 'include.general.inc'; which is at the start of each page controller.

In order to obtain an instance of a class within your code this is all you need:

$object = RDCsingleton::getInstance('<classname>');

The reason that the class name is RDCsingleton and not just plain singleton is quite frustrating. In 2010 I was asked to provide some back-end functionality to a front-end website which was being developed by a separate design agency. I developed all the components using the RADICORE framework, but some of them had to be accessed from their code which was built using a totally different framework. This framework was not written according to accepted conventions, so they had an object interface called singleton which did not have the 'i' prefix which would have made it iSingleton. Due to a bug in PHP it was not possible to have both an interface and a class with the same name, so one of these two names had to change. Even though their use of the 'singleton' name was only referenced once in their entire application whereas my code contained multiple references, they insisted that their code was too precious to change, so muggins here had to bite the bullet and change the entire RADICORE framework.

The value passed as <classname> will be used to load the associated file containing the class definition as well as to instantiate the class with that name. It does this using code similar to the following:

static function &getInstance ($class, $arg1=null, $initialise=true)
// $class = name of class to be instantiated.
// $arg1 = an optional argument to pass to the class constructor (may be a string/array/whatever).
// $initialise = if set to FALSE an existing instance will not have its initialise() method called.
{
    static $instances = array();  // array of instance names

    if (substr_count($class, '/') == 1) {
        // use leading directory name as the subsystem name
        $subsystem = basename(dirname($class));     // only one directory allowed
        $classname = basename($class);              // strip leading directories
        $filename  = "../$subsystem/classes/$classname.class.inc";

    } elseif (substr_count($class, '/') > 1) {
        // use path name 'as-is'
        $classname = basename($class);              // strip leading directories
        $filename  = "$class.class.inc";
			
    } else {
        $classname = $class;
        $filename  = "classes/$classname.class.inc";
    } // if
		
    if (!empty($instances[$classname])) {
        // instance exists in array, so use it
        $instance =& $instances[$classname];
    } else {
        require_once $filename;
        $instances[$classname] = new $classname($arg1);
        $instance =& $instances[$classname];
    } // if
		
    if ($initialise === true) {
        if (method_exists($instance, 'initialise')) {
            // object has an 'initialise' method, so call it
            $null = $instance->initialise($arg1);
        } // if
    } // if

    return $instance;
}

The class file is loaded using the require_once command which searches through the directories specified in the include_path configuration directive. This should contain a list of all the relevant subsystem directories as it will automatically look for class files in the classes subdirectory. Note that both the name of the class, the class file and associated directories SHOULD be in lowercase. I do not like case sensitive software, so the simplest way to avoid mistakes is to define everything in lower case with underscore separators (known as snake case) - none of this camelCase crap for me.

Once a class has been loaded and instantiated it will be stored in the $instances array so that subsequent requests for the same class will be taken from this array.

Note that with the exception of my DML classes I do not use any arguments on the class constructor. For other classes I may use an argument on the separate initialise() method, and this argument may be a string or an array. This allows me to call the initialise() method as many times as I like, whereas the class constructor can only ever be called once.

By default the initialise() method will be called each time an instance is requested even if that instance was obtained from the $instances array. This is because it is my practice, within any object in the business layer, to extract the results of a method call and merge it with the current object's data. This means that if I reference the same object later on I can ignore its current state as it will always start afresh. This behaviour can be overridden by setting the $initialise argument to FALSE.

The argument which is passed to the RDCsingleton::getInstance() method can be in one of the following forms:

'foo' Will look for a file named 'classes/foo.class.inc' where the 'classes' directory will be a subdirectory in one of the entries on the include_path list. This is the default behaviour.
'foo/bar' Here the argument contains a single '/' character. The code will look for a file named 'foo/classes/bar.class.inc' and will cause the directory 'foo' to be added to the include_path list if it is not already there. This option was added in version 1.90.0
'foo/bar/snafu' Here the argument contains more than one '/' character. The code will look for a file named 'foo/bar/snafu.class.inc' without inserting a 'classes' directory. This option was added in version 2.04.2

164. How can I use a manual sequence?

Every DBMS has a mechanism for providing a technical or surrogate key from an automatic sequence, such as the AUTO_INCREMENT attribute in MySQL. Other databases, such as PostgreSQL, Oracle and SQL Server, have different mechanisms. With the exception of MySQL when using either the MyISAM or BDB storage engines, it is not possible to use an automatic sequence within a compound key. For example, in an order processing system I might have three types of order - Sales, Purchase and Transfer - and each order type requires its own sequence. The table schema looks something like the following:

CREATE TABLE `order_header` (
  `order_type` CHAR(1) NOT NULL,
  `order_id` INT(11) UNSIGNED NOT NULL,
  .....
  PRIMARY KEY (`order_type`, `order_id`)
)

If I set the order_id column to be AUTO_INCREMENT then each order_type would share the same sequence. The only solution to this is to assign the sequence numbers manually using code similar to the following:

function _cm_getInitialData ($fieldarray)
// Perform custom processing prior to insertRecord().
// $fieldarray contains data from the initial $where clause.
{
    // set order_id to next available number
    if (!empty($fieldarray['order_type'])) {
        $where = "order_type='{$fieldarray['order_type']}'";
        $query = "SELECT MAX(order_id) FROM $this->tablename WHERE $where";
        $count = $this->getCount($query);
        $fieldarray['order_id'] = $count + 1;
        $this->retry_on_duplicate_key = 'order_id';
    } // if
		
    return $fieldarray;
		
} // _cm_getInitialData

The line $this->retry_on_duplicate_key = 'order_id'; tells the DML object that if the current value should produce a duplicate key error then it should increment the specified column's value and try the INSERT again. This should get around the issue when the code above is executed at the same time by different processes, thus providing several processes with the same number. The INSERT statement will succeed for the first process, but will fail for the others.

165. How can I draw a horizontal line in the title area of a PDF report?

This requires two simple steps:

  1. Amend your PDF style file (pdf.styles.inc) to include a definition for the horizontal line, which I have called 'rule' in the following example:
    $style['rule']['font'] = array('family' => 'Times',     // Courier, Helvetica, Times
                                   'style' => '',           // blank=Regular, B=Bold, I=Italic, U=Underline
                                   'size' => 1.75,          // size in points
                                   'height' => 0.75,        // line height in units
                                   'draw' => 0);            // width of drawn lines
    
    $style['rule']['fillcolour'] = array(113,113,113);   // colour for background
    $style['rule']['textcolour'] = array(0,0,0);         // colour for foreground
    $style['rule']['drawcolour'] = array(0,0,0);         // colour for line drawing
    

    Note here that the font is actually irrelevant. The important factors are 'height' and 'fillcolour'.

  2. Amend the structure file for the relevant report as follows:
    $structure['title'][] = array('text' => '',
                                  'style' => 'rule',
                                  'width' => '100%',
                                  'y_relative' => 2.5,
                                  'newline' => 'y');
    

    This will output a blank line (no text) across the full width of the page using 'fillcolour' as the background colour and with a height of 'height'.

166. How are workitems assigned to users in the Workflow system?

When a workflow case is created the workitem(s) which come out of the START place are initially assigned to a ROLE, not a particular USER. This role can be specified on the transition record, but if blank will default to the primary role of the user who started the case. These workitem(s) will then appear as hyperlinks on the Home Page for all users who share that role. The first user to click on one of these hyperlinks will then have that workitem assigned to himself/herself, and by default all remaining workitems in that case will be assigned to that user.

However, during the execution of a transition/workitem it is possible to assign the subsequent workitems to a different user by loading the relevant user's Id into $this->wf_user_id.

167. Can I specify javascript globally instead of per class?

This is possible as of version 2.01.0 with the file includes/custom_javascript.class.inc. This class contains the following methods:

For more details please refer to Radicore for PHP - Inserting optional JavaScript.

168. Can I filter records in the _cm_post_getData method?

By far the easiest way to filter records when reading from the database is to construct an appropriate WHERE clause for use in an SQL "select" query. This will provide the following information which will be used in the Navigation Bar and the Pagination Area:

However, in some circumstances it may be necessary to filter records AFTER they have been read from the database, possibly because the filtering criteria are too complex to put into the SQL query. This post-query filtering can now be achieved by using code similar to the following in the _cm_post_getData() method:

function _cm_post_getData ($rows, &$where)
{
    foreach ($rows as $rownum => $rowdata) {
    if ( ..condition..) {
       unset($rows[$rownum]);
    } // foreach
		
    return $rows;
		
} // _cm_post_getData

The framework will detect when rows have been removed by noticing that the row count coming out of that method is less that the row count going in, and will take the following steps:

This means that where the SQL query returns 100 rows at 10 rows per page, but 5 of the rows are removed from each page, the row count in the Navigation Bar will still show 100 (not 50), the Pagination Area will still show 10 pages, but some page numbers will be skipped. In this example the first page will be #2, and all odd numbers will be skipped.

169. Why does the framework not have a responsive GUI?

Responsive Web Design (RWD) is an approach to web design aimed at allowing desktop web pages to be viewed in response to the size of the screen or web browser with which it is being viewed. This enables the same website to be viewed on a desktop, tablet or smartphone without the page being shrunk to such a point that it is unreadable.

The RADICORE framework does not support this concept 'out of the box' for the simple reason that it was designed to aid the building of business-facing administrative web applications and not public-facing web sites. This means that it designed to be used by an organisation's members of staff who work in their offices, or perhaps from home, using full sized screens. The idea of performing administrative tasks on a mobile device with a miniature screen does not appear to be a practical proposition as too many of the screens contain vast amounts of data which would be awkward to view on anything other than a full sized screen.

Should a different user interface be required then the fact that the framework is built using the 3-Tier Architecture should indicate that a new Presentation layer could be built to your own specifications which could reuse the existing Business and Data Access layers.

UPDATE: The ability to change each classic web page into a responsive web page is now available in the commercial version which uses a set of drop-in files which use the BOOTSTRAP library. This enables a responsive GUI to be turned on or off for individual pages as well as individual users.

170. How can you have foreign keys without foreign key constraints?

Simple. Foreign keys and foreign key constraints are different animals.

Foreign Keys These are used in SELECT queries where you use a JOIN to link one table with another. The columns that are used in the JOIN must be identified explicitly as they are never taken from any foreign key constraint which is defined within the database schema.
Foreign Key Constraints These are used in INSERT, UPDATE or DELETE queries and will either allow or disallow the operation, or propagate it through all related records on the child/many table. A constraint is never used in a SELECT query.

Please also refer to:

171. Can I display a label on a screen without an associated field?

Yes you can, by using the label-only option described in the Screen Structure file. An example of how this looks is shown as the orphan label in Figure 51:

Figure 51 - example of an orphan label

infrastructure-faq-51 (6K)

You may have more than one of these labels on a single line, and you may align each label differently.

172. How can I provide different SQL queries for different database engines?

While other users of the RADICORE framework may only develop their applications for a single DBMS system which will never change during the lifetime of that application, the RADICORE framework has to provide support for several popular database engines and generate valid queries regardless of which DBMS is actually being used. Although in theory each DBMS follows the same SQL standard, in practice they all contain their own unique deviations from the standard, which means that an SQL query which works on one DBMS may fail on another. By default all the queries which are generated by the framework use MySQL syntax, and where there are differences in another DBMS the syntax conversion is handled by specific code within the relevant dml.???.class.inc file where '???' is the mnemonic for that DBMS. MySQL is the preferred syntax simply because all my original development was done on a MySQL database, and I only offered support for other database engines when their vendors provided free community editions for the Windows operating system.

Unlike most other developers who work on an application which is used only by a single customer, I have developed an ERP package which can be used by any number of customers, and each customer is able to deploy this package on a DBMS of their choice. While the queries for the standard CRUD operations can be handled silently by the framework, there may be occasions where some specialist queries are required in order to deal with non-standard circumstances, such as the one provided as an example in this->executeQuery(). Where the queries need to work on more than one DBMS, and the syntax is different for each DBMS, it will be necessary to identify the current DBMS and then use this to construct a valid query. This can be done by using the $dbengine variable which is provided by the findDBConfig() function, as shown in the following example:

    $date_from            = $where_array['date_from'];
    $date_to              = $where_array['date_to'];
    $sold_not_sold        = $where_array['sold_not_sold'];

    list($dbname, $dbprefix, $dbengine) = findDBConfig($this->dbname);

    $productDB = findDBName('product');

    $product_id_size = $this->fieldspec['product_id']['size'];

    // step 1: create a copy of the PRODUCT table
    $query[] = "DROP TEMPORARY TABLE IF EXISTS temp_product;";
    if ($dbengine == 'sqlsrv') {
        $query[] = "CREATE TEMPORARY TABLE IF NOT EXISTS temp_product (product_id NVARCHAR($product_id_size) PRIMARY KEY, 
                                                                       product_name NVARCHAR(80) );";
    } else {
        $query[] = "CREATE TEMPORARY TABLE IF NOT EXISTS temp_product (product_id VARCHAR($product_id_size), 
                                                                       product_name VARCHAR(80), 
                                                                       PRIMARY KEY (product_id) );";
    } // if
    $query[] = "INSERT INTO temp_product (product_id, product_name)"
                                ." SELECT product_id, product_name"
                                ." FROM {$productDB}product"
                                ." WHERE date_intro <= '{$date_to} 23:59:59' AND end_date_sales >= '{$date_from} 00:00:00'"
                                  ." AND NOT EXISTS(SELECT 1 FROM {$productDB}prod_cat_class 
                                                    WHERE product_id=product.product_id 
                                                      AND prod_cat_id LIKE 'NOTFORSALE%' 
                                                      AND start_date<='{$date_to} 23:59:59' AND end_date>='{$date_to} 00:00:00');";

    // step 2: create a second table containing items which have been ordered during this period
    $query[] = "DROP TEMPORARY TABLE IF EXISTS temp_ordered;";
    if ($dbengine == 'sqlsrv') {
        $query[] = "CREATE TEMPORARY TABLE temp_ordered (product_id NVARCHAR($product_id_size) PRIMARY KEY );";
    } else {
        $query[] = "CREATE TEMPORARY TABLE IF NOT EXISTS temp_ordered (product_id VARCHAR($product_id_size), 
                                                                       PRIMARY KEY (product_id) );";
    } // if
    $query[] = "INSERT INTO temp_ordered (product_id)"
                   ." SELECT product_id FROM order_item"
                   ." WHERE order_item.order_type='S'"
                     ." AND order_item.created_date>='{$date_from}' AND order_item.created_date<='{$date_to}'"
                     ." AND order_item.order_item_status_type_id NOT IN ('PEND','CNCL','CNRG','HOLD','SAM1','SAM2','SAM3','SAM4')"
                     ." GROUP BY product_id;";

    // step 3: remove from TEMP_PRODUCT anything which exists in TEMP_ORDERED
    $query[] = "DELETE FROM temp_product WHERE product_id IN (SELECT product_id FROM temp_ordered);";

    $result = $this->executeQuery($query);

    return $result;

173. How can I add extra CSS files to a web page?

Just add the path to the CSS file to the $this->css_files array in the current object.

174. How can I find out the version number of the current DBMS?

This is useful when you want to make use of a feature which only became available in a later release of the DBMS, such as Common Table Expressions which did not exist in MySQL until version 8. The code to obtain the necessary values is as follows:

    $db_version = $this->findDBVersion();

This method is described in findDBVersion.

175. How can I use a recursive Common Table Expression for traversing a hierarchy?

See FAQ180 if you want a non-recursive query with multiple expressions.

When traversing a hierarchy of records such as for a TREE 1 or TREE 2 pattern the most efficient method is to use a Common Table Expression (CTE) which can retrieve the entire hierarchy in a single query. However, CTEs were not available in MySQL until version 8 which was released in May 2018, so for earlier versions I had to perform the extract using a separate query for each level in the hierarchy. For small hierarchies this is manageable, but for very large hierarchies it is not. As of version 2.10.0 of the RADICORE framework I have included code which allows the use of CTEs in all the database drivers - MySQL, Postgresql, Oracle and SQL Server - and have provided a working example in the XAMPLE subsystem. Select the 'Tree Type' menu option, select a type, then press the 'Tree Structure' navigation button. The code itself can be found in script xample/classes/x_tree_node_jnr.class.inc.

The CTE requires a special query structure which is as follows:

WITH <cte_name> (<cte_select>)
-- end of CTE declaration --
AS (
   <cte_anchor>
   -- end of CTE anchor --
   UNION ALL
   <cte_recursive>
   -- end of CTE recursion --
)
-- end of CTE --
<outer_query>

The following properties have been added to the abstract table class:

Note that the contents of $this->sql_CTE_select can be appended to $this->sql_CTE_name in which case the enclosing parentheses must be defined.

The <outer_query> does not require a new property as it makes use of the existing properties.

The script xample/classes/x_tree_node_jnr.class.inc provides code (shown below) which shows how the CTE can be constructed for each of the supported DBMS engines. Note that there are small variations in the CTE syntax for each DBMS. So much for following a single SQL standard!

Here is the PHP code:

$this->sql_CTE_name   = 'RECURSIVE cte';
$this->sql_CTE_select = 'node_depth, node_id, node_desc, node_id_snr, sort_seq';

$this->sql_CTE_anchor = "SELECT 1 AS node_depth, x_tree_node.node_id, x_tree_node.node_desc, x_tree_node.node_id_snr
, CAST(LPAD(ROW_NUMBER() OVER (ORDER BY x_tree_node.node_id_snr ASC), 4, '0') AS CHAR(4000)) AS sort_seq
FROM x_tree_node
LEFT JOIN x_tree_level ON (x_tree_level.tree_type_id=x_tree_node.tree_type_id AND x_tree_level.tree_level_id=x_tree_node.tree_level_id)
WHERE {$where}";

$expanded_list = "'0'";  // do not expand any nodes
if (is_array($expanded) AND !empty($expanded)) {
    $container_list = array_keys($expanded);
    $expanded_list = "'".implode("','", $container_list) ."'";  // expand this node
} elseif ($expanded == 'ALL') {
    $expanded_list = null;  // expand all nodes
} // if
$collapsed_list = '';
if (is_array($collapsed) AND !empty($collapsed)) {
    $container_list = array_keys($collapsed);
    $collapsed_list = "'".implode("','", $container_list) ."'";  // collapse this node
} // if

$this->sql_CTE_recursive = "SELECT node_depth+1, x_tree_node.node_id, x_tree_node.node_desc, x_tree_node.node_id_snr
, CONCAT(cte.sort_seq, '/',  LPAD(x_tree_node.node_id, 4, '0')) AS sort_seq
FROM x_tree_node
INNER JOIN cte ON (x_tree_node.node_id_snr = cte.node_id)";

$recursive_where_array = array();
if (!empty($expanded_list)) {
    $recursive_where_array[] = "x_tree_node.node_id_snr IN ($expanded_list)";
} // if
if (!empty($collapsed_list)) {
    $recursive_where_array[] = "x_tree_node.node_id_snr NOT IN ($collapsed_list)";
} // if
if (!empty($recursive_where_array)) {
    $recursive_where = array2where($recursive_where_array);
    $this->sql_CTE_recursive .= "\nWHERE $recursive_where";
} // if

$count_expression = "SELECT count(node_id) FROM x_tree_node AS child WHERE child.node_id_snr=cte.node_id";

$this->sql_select = "cte.*"
                   ."\n, ($count_expression) AS child_count";

if (!empty($expanded_list) OR !empty($collapsed_list)) {
    // some nodes are expanded while others are not
    $condition = '';
    if (!empty($collapsed_list)) {
        $condition .= "WHEN cte.node_id IN ($collapsed_list) THEN 'N' ";
    } // if
    if (!empty($expanded_list)) {
        $condition .= "WHEN cte.node_id IN ($expanded_list) THEN 'Y' ";
    } else {
        $condition .= "WHEN ($count_expression) > 0 THEN 'Y'";
    } // if
    $this->sql_select .= "\n, CASE $condition ELSE 'N' END AS expanded";
} else {
    // every node with children is automatically expanded
    $this->sql_select .= "\n, CASE WHEN ($count_expression) > 0 THEN 'Y' ELSE 'N' END AS expanded";
} // if

$this->sql_from    = "cte";
$this->sql_orderby = 'sort_seq';
$this->sql_groupby = '';
$this->sql_having  = '';

$this->CTE_in_use = true;  // see fetchRowChild() for details

$this = null;

return $where;

Note that this code does not automatically expand every node in the tree. It starts by displaying only the node(s) at level #1. If a node has children a plus button will be displayed which, when pressed, will display the children of that node. The button will then change to plus which will hide the children of that node.

Here is the SQL query which that code generates:

WITH RECURSIVE cte (node_depth, node_id, node_desc, node_id_snr, sort_seq)
AS (
SELECT 1 AS node_depth, x_tree_node.node_id, x_tree_node.node_desc, x_tree_node.node_id_snr
, CAST(LPAD(ROW_NUMBER() OVER (ORDER BY x_tree_node.node_id_snr ASC), 4, '0') AS CHAR(4000)) AS sort_seq
FROM x_tree_node
LEFT JOIN x_tree_level ON (x_tree_level.tree_type_id=x_tree_node.tree_type_id AND x_tree_level.tree_level_id=x_tree_node.tree_level_id)
WHERE x_tree_node.tree_type_id= 'ORG' AND tree_level_seq= '1' AND x_tree_node.node_id_snr IS  NULL
  UNION ALL
  SELECT node_depth+1, x_tree_node.node_id, x_tree_node.node_desc, x_tree_node.node_id_snr
, CONCAT(cte.sort_seq, '/',  LPAD(x_tree_node.node_id, 4, '0')) AS sort_seq
FROM x_tree_node
INNER JOIN cte ON (x_tree_node.node_id_snr = cte.node_id)
)
SELECT cte.*
, (SELECT count(node_id) FROM x_tree_node AS child WHERE child.node_id_snr=cte.node_id) AS child_count
, CASE WHEN (SELECT count(node_id) FROM x_tree_node AS child WHERE child.node_id_snr=cte.node_id) > 0 THEN 'Y' ELSE 'N' END AS expanded
FROM cte   
ORDER BY sort_seq asc 

176. How can I return multiple rows from a popup into the current screen?

By default when a popup button is pressed in a screen it is used to identify the primary key of a single row in a foreign/parent table which can then be used as a foreign key in the current row of the child table. It is simply not possible for a foreign key to link to multiple rows in the parent table. However, in the ADD 3 pattern it is possible to select multiple rows in the popup form as this task does not have a visible screen. It can therefore add multiple rows to the database, one for each entry selected in the popup, before returning control to the previous screen, which is usually a MULTI 2 pattern.

In an ADD 7 task which allows multiple child rows, and each row contains a popup button, instead of adding new rows one at a time in order to use the popup button on each row it would be faster to call the popup task from a single row, make multiple selections, then have each of those selections appear in their own rows which are automatically added to the screen. This is now possible by using the $rows_to_be_appended variable. This is always empty by default, but can be populated in the _cm_popupReturn() method using code similar to the following:

if ($return_from == 'whatever') {
    if (is_long(key($select_array))) {
        // multiple rows selected, so start at current row then add more as necessary
        foreach ($select_array as $rownum => $rowdata) {
            if ($rownum == 0) {
                $fieldarray['field1'] = $rowdata['field1'];
                $fieldarray['field2'] = $rowdata['field2'];
            } else {
                $append = $fieldarray;
                $append['field1'] = $rowdata['field1'];
                $append['field2'] = $rowdata['field2'];
                $append = $this->getForeignData($append);
                $this->rows_to_be_appended[] = $append;
            } // if
        } // foreach
        $select_array = null;
    } // if
} // if

If $select_array were to contain just a single selection then it would be an associative array, and that selection would be added to $fieldarray which represents the current row in the screen. If multiple selections are made then $select_array would be an indexed array containing a series of associative arrays indexed by row number. The first selection is added to the current row while additional selections are added to $this->to_be_appended which will be processed by the framework code later. This processing will include calling $this->getExtraData() on each new row.

177. How can I rotate column labels 90 degrees in a PDF List View?

In some cases a PDF List View may have a large number of short columns where the length of the column labels is wider than the column values. In such cases it may be better to display the labels vertically instead of horizontally. This can be achieved in one of two ways, or a combination of both:

Note that this method will not actually rotate the label text through 90 degrees so that you have to tilt your head to read them. The characters will be displayed one under the other instead of one next to the other, so instead of
label
you will see
l
a
b
e
l
.

178. Is it really possible to separate business logic from presentation logic?

This question is similar to Is it really possible to separate business logic from data access logic? which means that the answer is also similar. Every database application contains a mixture of presentation logic, business logic and data access logic which, while they could be combined into a single monolithic component, would be more difficult to read and maintain. A better solution would be to have a separate component which deals with only one of those areas of logic, which is precisely the purpose of the 3-Tier Architecture which has separate components for each area of logic as follows:

You should note that I have merged the 3-Tier Architecture with the Model-View-Controller (MVC) design pattern by splitting the Presentation Layer into two smaller components, a Controller and a View. This also means that I have taken all database access out of the Model and placed it into a separate Data Access Object. This means that each Model (business layer) component contains nothing but business rules and the way that the data is presented to the user is handled in a totally separate View object. The only data which comes out of a Model is raw application data in the form of a PHP array which is then given to a View object to transform it into the desired type of output, which is usually HTML, but could be CSV or PDF. There is absolutely no code, such as an echo or print statement, in any Model which writes to the output stream.

Note that I do not have a separate version of the View object for each table in the database as I have been able to create a single View object for each of the HTML, CSV and PDF formats which can handle any data from any database table. In the case of HTML output I have a single component which takes the array of raw data out of the Model, inserts it into to an XML document, then performs a transformation using a set of reusable XSL stylesheets. My reasons for using XML and XSL to generate all HTML output is explained in Why don't you use another templating system instead of XSL?

179. How can I use Subqueries and Derived Tables?

SQL (Structured Query Language) is a powerful language for manipulating data inside a relational database. While INSERTS, UPDATES and DELETES are quite straightforward in that they mainly operate on one table at a time, SELECT queries, on the other hand, can retrieve and combine data from multiple tables into a single result set. As well as retrieving values which exist in table columns they can perform functions such as CONCAT() which will combine several strings into a single column or aggregate functions such as AVG(), COUNT(), MIN(), MAX() and SUM() which will accumulate a single result by accumulating values from multiple rows.

As well as simple queries you can have queries within queries, which are known as subqueries. As well as retrieving columns from a physical table it is also possible to retrieve columns from a derived table. These will be described below. Another form of complicated query is the Common Table Expression which is described separately.

The following examples use tables in the MENU database.

Example 1: a simple query.

SELECT subsys_id, subsys_name, subsys_dir, task_prefix
FROM mnu_subsystem

Example 2: a simple query with an aggregate.

SELECT mnu_subsystem.subsys_id, subsys_name, subsys_dir, task_prefix
, count(task_id) AS task_count
FROM mnu_subsystem
LEFT JOIN mnu_task ON (mnu_task.subsys_id=mnu_subsystem.subsys_id)
GROUP BY task_id
HAVING task_count > 50
WHERE ...
ORDER BY task_count, subsys_id

The primary key subsys_id of table mnu_subsystem is also a foreign key on table mnu_task, and this query will include the count of associated rows in the mnu_task table which is identified in a JOIN. Note that I have to qualify the column name subsys_id in the SELECT string otherwise I would get an error telling me that "Column xxx in field list is ambiguous" as that column exists in both tables in that query.

The GROUP BY string will return a separate row for each different task_id. Without it the result would contain only a single row with a total count for all entries on the mnu_task table instead of a different count for each task_id. Note that MySQL will allow you to specify only a single column from those which are identified in the SELECT list whereas other databases have failed to implement the SQL 1999 standard and still operate in ONLY FULL GROUP BY mode.

The WHERE clause is entirely optional, but note that you wish to refer to an aggregated column you must put it in the HAVING clause instead of the WHERE clause. If you forget to do this the framework will do it for you. If it detects a column name in the WHERE clause which is the alias for an expression in the SELECT list it will automatically move that column reference from the WHERE clause to the HAVING clause. This is because the WHERE clause restricts the result set before returning rows and HAVING restricts the result set after bringing all the rows. You cannot restrict the selection on an aggregated value until after the rows which need to be aggregated have been selected. Note that MySQL will allow you to reference the aggregated column by its alias name task_count while other databases will insist that you repeat the aggregate expression.

The ORDER BY clause can reference any column whether it be an aggregate or not. Note that MySQL will allow you to reference the aggregated column by its alias name task_count while other databases will insist that you repeat the aggregate expression.

Example 3: moving the aggregate to a subquery.

SELECT subsys_id, subsys_name, subsys_dir, task_prefix
, (SELECT count(task_id) FROM mnu_task WHERE mnu_task.subsys_id=mnu_subsystem.subsys_id) AS task_count
FROM mnu_subsystem
-- LEFT JOIN mnu_task ON (mnu_task.subsys_id=mnu_subsystem.subsys_id)
-- GROUP BY task_id
HAVING task_count > 50
WHERE ...
ORDER BY task_count, subsys_id

This example contains two queries, an outer query and an inner (nested) subquery. In this example it is known as a correlated or synchronised subquery as it contains a reference to a table contained in the outer query. Note the differences with Example 2:

The rules for HAVING and ORDER BY are the same as in Example 2.

Example 4: using a derived table.

SELECT xyz.* 
FROM (
  SELECT subsys_id, subsys_name, subsys_dir, task_prefix
  , (SELECT count(task_id) FROM mnu_task WHERE mnu_task.subsys_id=mnu_subsystem.subsys_id) AS task_count
  FROM mnu_subsystem
) AS xyz    
WHERE task_count > 50
ORDER BY task_count, subsys_id  

A derived table is an expression that generates a table within the scope of a query FROM clause. This comes in handy when the use of column aliases is not possible because another clause is processed by the SQL translator before the alias name is known. Please note the following:

In order to make it easy to use derived tables some extra variables have been added to the framework as follows:

$this->sql_derived_table;    // the alias name for the expression
$this->sql_derived_select;   // the SELECT list for the expression
$this->sql_derived_from;     // the FROM clause for the expression

Here is the code from mnu_subsystem.class.inc which uses these variables:

if (preg_match('/^(LIST1)$/i', $pattern_id)) {
    $this->sql_derived_table = 'xyz';
    $tablename =& $this->sql_derived_table;
    $this->sql_select  = "$tablename.*";

    $this->sql_having  = null;
    $this->sql_groupby = null;

    // construct nested subquery for a derived table
    $this->sql_derived_select  = 'subsys_id, subsys_name, subsys_dir, task_prefix';
    $this->sql_derived_select .= "\n, (SELECT count(task_id) FROM mnu_task WHERE mnu_task.subsys_id=mnu_subsystem.subsys_id) AS task_count";

    $this->sql_derived_from  = $this->tablename;

    $this->sql_from  = $tablename;
    $this->sql_from  = $this->_sqlForeignJoin($this->sql_select, $this->sql_from, $this->parent_relations);
} // if

Here is another example of a more complicated query with a derived table:

SELECT xyz.*, mnu_user.user_name, mnu_user.user_password, mnu_user.rdcaccount_id, mnu_user.pswd_change_datetime, mnu_user.pswd_count
            , mnu_user.force_pswd_chg, mnu_user.in_use, mnu_user.is_disabled, mnu_user.logon_datetime, mnu_user.language_id
            , mnu_user.start_date, mnu_user.end_date, mnu_user.ip_address, mnu_user.email_addr, mnu_user.external_id
            , mnu_user.is_external_auth_off, mnu_user.party_id, mnu_user.user_timezone, mnu_user.allow_responsive_gui
            , mnu_user.created_date, mnu_user.created_user, mnu_user.revised_date, mnu_user.revised_user, mnu_account.account_name
FROM (
SELECT user_id
, (SELECT GROUP_CONCAT(role_name ORDER BY sort_seq ASC, mnu_user_role.role_id ASC SEPARATOR ', ') 
     FROM mnu_user_role 
     LEFT JOIN mnu_role ON (mnu_role.role_id=mnu_user_role.role_id)
    WHERE user_id=mnu_user.user_id 
      AND start_date<='2021-05-25 23:59:59' 
      AND end_date>='2021-05-25 00:00:00'
  ) AS role_list
, (SELECT role_name 
     FROM mnu_user_role 
     LEFT JOIN mnu_role ON (mnu_role.role_id=mnu_user_role.role_id) 
    WHERE user_id=mnu_user.user_id 
      AND sort_seq=(SELECT MIN(sort_seq) 
                      FROM mnu_user_role 
                     WHERE user_id=mnu_user.user_id 
                       AND start_date<='2021-05-25 23:59:59' 
                       AND end_date>='2021-05-25 00:00:00'
                   ) 
   ) AS primary_role
FROM mnu_user
) AS xyz
LEFT JOIN mnu_user ON (mnu_user.user_id=xyz.user_id)
LEFT JOIN mnu_account ON (mnu_account.rdcaccount_id=mnu_user.rdcaccount_id)
ORDER BY user_id asc  

This was produced using the following code in mnu_user.class.inc:

if (empty($this->sql_from)) {
    $this->sql_derived_table = 'xyz';
    $tablename =& $this->sql_derived_table;
    $this->sql_select  = "$tablename.*, mnu_user.*";
    $this->sql_select .= ', mnu_account.account_name';
		
    $this->drop_from_sql_select[] = 'mnu_user.user_id';  // this is contained in the inner query

    $this->sql_having  = null;
    $this->sql_groupby = null;

    // construct nested subquery for a derived table
    $this->sql_derived_select  = 'user_id';
    $this->sql_derived_select .= "\n, (SELECT GROUP_CONCAT(role_name ORDER BY sort_seq ASC, mnu_user_role.role_id ASC SEPARATOR ', ') 
                                         FROM mnu_user_role 
                                         LEFT JOIN mnu_role ON (mnu_role.role_id=mnu_user_role.role_id) 
                                        WHERE user_id=mnu_user.user_id 
                                          AND start_date<='$today 23:59:59' 
                                          AND end_date>='$today 00:00:00'
                                      ) AS role_list";
    $this->sql_derived_select .= "\n, (SELECT role_name 
                                         FROM mnu_user_role 
                                         LEFT JOIN mnu_role ON (mnu_role.role_id=mnu_user_role.role_id) 
                                        WHERE user_id=mnu_user.user_id 
                                          AND sort_seq=(SELECT MIN(sort_seq) 
                                                          FROM mnu_user_role 
                                                         WHERE user_id=mnu_user.user_id 
                                                           AND start_date<='$today 23:59:59' 
                                                           AND end_date>='$today 00:00:00'
                                                       ) 
                                      ) AS primary_role";

    $this->sql_derived_from  = $this->tablename;

    $this->sql_from  = $tablename;
    $this->sql_from .= "\nLEFT JOIN mnu_user ON (mnu_user.user_id=$tablename.user_id)";
    $this->sql_from  = $this->_sqlForeignJoin($this->sql_select, $this->sql_from, $this->parent_relations);
} // if

Here the derived table xyz will return three columns called user_id, role_list and primary_role. The outer query will return every column from table xyz plus every column from table mnu_user. As this would automatically include a duplicate of user_id I put an entry into the $this->drop_from_sql_select array to exclude it when the string menu_user.* is expanded into a full list of columns.

How can I use a non-recursive Common Table Expression (CTE)?

Unlike the example shown in FAQ175 which uses a recursive query to traverse a hierarchy this version can use multiple CTEs where the results of one CTE can be carried forward into another CTE.

WITH <cte_name[0]>
-- end of CTE declaration --
AS (
   <cte_anchor[0]>
   -- end of CTE anchor --
), <cte_name[1]>
AS (
   <cte_anchor[1]>
   -- end of CTE anchor --
), <cte_name[2]>
AS (
   <cte_anchor[2]>
   -- end of CTE anchor --
.....
   -- end of CTE anchor --
)
-- end of CTE --
<outer_query>

The following properties have been added to the abstract table class:

Note here that both $this->sql_CTE_name and $this->sql_CTE_anchor are arrays, not strings, in order to allow multiple sets of CTE names and their associated queries.


References


Amendment History

01 Nov 2022 Updated Why does the framework not have a responsive GUI?
01 Jul 2022 Added How can I use a non-recursive Common Table Expression (CTE)?
01 Jun 2021 Added Is it really possible to separate business logic from presentation logic?
Added How to use Subqueries and Derived Tables?
06 Aug 2020 Amended How to incorporate a 'popup' control into a form to show that the XML/HTML output includes the screen zone for each popup.
Amended How can I call the same POPUP more than once in a form? to show that the XML/HTML output includes the screen zone for each popup.
03 Jun 2020 Amended How do you deal with database transactions?
Amended How do you deal with database locking?
09 Jan 2020 Amended How can I modify report labels at runtime?
Added How can I rotate column labels 90 degrees in a PDF List View?
02 Feb 2019 Added How can I return multiple rows from a popup into the current screen?
01 Aug 2018 Added How can I find out the version number of the current DBMS?
Added How can I use a Common Table Expression (CTE) for traversing a hierarchy?
01 Mar 2018 Added How can I add extra CSS files to a web page?
25 Oct 2017 Added How can I provide different SQL queries for different database engines?
01 Sep 2017 Added Can I display a label on a screen without an associated field?
01 Jul 2017 Amended How does the 'singleton' class work?
Added Why does the framework not have a responsive GUI?
Added How can you have foreign keys without foreign key constraints?
02 Apr 2017 Added Can I filter records in the _cm_post_getData() method?
01 Nov 2016 Added How are workitems assigned to users in the Workflow system?
Added Can I specify javascript globally instead of per class?
Amended How can I run a batch job?
01 Oct 2016 Amended Can I have buttons in the data area? to allow several buttons to be grouped into a single cell instead of having each button in its own cell.
Amended What debugging aids exist in this framework? by changing the SQL logging option:
  • Log files will now be written to the /sql/logs directory instead of the /sql directory.
  • Log file names will be in the format <script_id>.<user_id>.sql to provide separate files for each user.
  • Each query logged will also include the start, finish and elapsed times.
01 Jul 2016 Amended What reserved words exist within the RADICORE framework? by adding rdc_to_be_inserted and rdc_to_be_deleted.
Amended How can I dynamically add or remove rows from an ADD5 or MULTI2/3/4/5/6 screen? by adding references to rdc_to_be_inserted and rdc_to_be_deleted
19 Jun 2016 Added How can I draw a horizontal line in the title area of a PDF report?
01 Jun 2016 Updated How do I enable the QuickSearch Bar? which can now be defined in the database instead of with program code.
01 May 2016 Updated Can I have buttons in the data area? to deal with the $allow_buttons_all_zones variable.
30 Jan 2016 Updated Why aren't primary key names in $where converted to foreign key names?
01 Nov 2015 Added How does the 'singleton' class work?
Added How can I use a manual sequence?
01 Oct 2015 Updated Can I have buttons in the data area? to include the option to use a <button> control instead of an <input> control.
01 Jul 2015 Added Can I have a filepicker task which works with subdirectories?
01 Dec 2014 Updated Can I change the fonts in a PDF document at runtime?
01 Sep 2014 Added How can I make a single column in a multi-row area non-editable?
Updated What reserved words exist within the RADICORE framework?.
Added How can I set the user_id of a workflow WORKITEM record?
01 Jun 2014 Updated How can I modify screen labels at runtime? to allow labels in DETAIL screens to be changed programmatically.
Added Can I change the fonts in a PDF document at runtime?
01 May 2014 Updated What are the valid formats for the input and output of dates? to allow different input and output formats to be defined for different languages.
07 Mar 2014 Added Can I execute an arbitrary SQL query?
Added Can I execute multiple SQL queries in a single step?
03 Dec 2013 Added How can I dynamically add or remove rows from an ADD5 or MULTI2/3/4/5/6 screen?
01 Dec 2013 Amended How do you handle referential integrity? to include the IGNORE and framework/FK constraint options.
03 Sep 2013 Added To where does the error log get written?
12 Aug 2013 Added Why re-invent the wheel by creating yet another framework?
Added How can you call your framework Object Oriented if it still contains procedural functions?
07 Jul 2013 Added Why is RADICORE no good for building web sites?
29 May 2013 Added How can I consolidate all the Radicore databases into a single database?
Added How can I update a field using a database function instead of a literal?
24 May 2013 Reorganised the index by topic type.
Added What is the purpose of the Data Dictionary?
17 Mar 2013 Added Why don't you use PDO instead of your own database abstraction class?
06 Jan 2013 Added How can I update several tables in a single operation?
23 Sep 2012 Amended How can I access different databases on different servers? to include the switch_dbnames option.
01 Aug 2011 Added How can I change how a field (or a column of fields) is displayed?
Amended How do you handle error messages?
01 Jun 2011 Added How do I enable the QuickSearch Bar?
29 Apr 2011 Added What does 'Uncaught exception from DOMException, message = Invalid Character Error' mean?
Added How do I include an aggregate in a SELECT statement?
12 Dec 2010 Added What happens in a workflow when a place contains more than one token?
01 Nov 2010 Added How can I add a hidden field to an HTML form?
01 Sep 2010 Added Can I change the style of an individual checkbox?
Added Can I change the style of individual entries in a dropdown?
01 Aug 2010 Modified FAQ99 to include the add to favourites option.
16 Jul 2010 Added How can I build a dropdown/radio group from a table with a compound key?
01 Jun 2010 Added Can I remove navigation buttons at runtime?
Added Can I remove action buttons at runtime?
Added Can I have buttons in the data area?
Amended FAQ89 to include field names which begin with 'submit'.
01 May 2010 Added How can I execute a task before the first menu screen is displayed?
Added What options are there for hyperlinks and images?
Added How can I change the style of a field in a PDF report?
01 Mar 2010 Added How can I customise the text of pending workflow items on the menu/home page?
Added How can I control the sequence in which the conditions on Explicit OR Splits are evaluated?
30 Oct 2009 Added Why aren't primary key names in $where converted to foreign key names?
18 Oct 2009 Added Why don't you store state-specific info in the 'context' field of a workflow case?
08 Jul 2009 Added How can I replace a column and its label at runtime?
01 Jul 2009 Added How can I alter times to be shown in the client's time zone?
Amended FAQ55 to incorporate different time zones.
Added How can I customise the printing of lines in the PDF List View?
01 Jun 2009 Added Why doesn't the Data Dictionary extract relationship details from the database?
Added How can I use RADICORE components in my front-end web site?
01 May 2009 Added What facilities are there for date processing?
12 Jan 2009 Added a note to How can I enter a value before calling a POPUP form?
01 Jan 2009 Amended How can I modify screen labels at runtime?
Added How can I modify report labels at runtime?
01 Dec 2008 Added How do I set up SSL encryption for a remote MySQL database?
Added How do I deal with multi-byte characters?
Added Can I change the style of individual entries in a radio group?
Added How can I force a process to jump to another task?
Added How can start a batch job from a web page?
Added How do I perform a search on a related table?
01 Oct 2008 Amended How to manually extend the automatically extended SQL SELECT statement.
Added What are the valid formats for the input and output of dates?
Added How can I turn on authentication via an LDAP server?
01 Aug 2008 Added How can I remove the 'page created in ...' text from the bottom of each screen?
01 Jul 2008 Amended What reserved words exist within the RADICORE framework?
01 May 2008 Added How can I remove the 'show nn' options from the navigation bar?
Added How can I remove the 'select all/unselect all' options from the navigation bar?
Added How can I make a single row in a multi-row area non-editable?
Added How can I remove the select box from a single row?
Added How can I hide/remove columns in a multi-row display?
Added How can I modify screen labels at runtime?
Added How can I make all the fields in a particular zone non-editable?
Added Why doesn't the Workflow system have a facility for sending emails?
13 Mar 2008 Added What is the best way to perform a simple SQL aggregate function?
07 Mar 2008 Added What are the CREATED_DATE/USER and REVISED_DATE/USER fields?
06 Mar 2008 Added How can I add my company's logo to all web pages?
Added How can I build a separate logon screen?
01 Mar 2008 Modified core code to make FAQ 85 redundant.
Added How is context passed to a child task?
Added How is context passed to a child object in the same task?
Added Can I use images instead of text for the hyperlinks above the menu bar?
01 Feb 2008 Added How can I access different databases on different servers?
Added How can I turn on authentication via a RADIUS server?
Added Can I have different initial values for different users?
Added Can I restrict update and delete operations to a record's creator?
Added Can I logon without seeing the LOGON screen?
Modified How do you provide dynamic selection criteria in the $where variable?
Modified How can I define preset/static search criteria for the $where variable?
20 Dec 2007 Added How to have different sets of dropdown/radio options for different database rows
Added Can I alter a screen's structure at runtime?
01 Oct 2007 Added How can I implement Row Level Security (RLS)?
Added What reserved words are used within the RADICORE framework?
01 Sep 2007 Added Can I add barcodes to my PDF documents?
25 Jul 2007 Added How can I enter a value before calling a POPUP form?
Added How can I call the same POPUP more than once in a form?
Added How do navigation buttons work?
Added How to manually extend the automatically extended sql SELECT statement
Added How to perform a search on an aggregate or aliased column
Added How can I make global environmental changes for individual subsystems?
09 Jun 2007 Added Why don't the column hyperlinks sort the data as I expect?
27 Jan 2007 Added How can I access a POPUP2 screen?
Amended Can you access tables from more than one database engine?
15 Jan 2007 Added How can I change the display attributes for individual fields at runtime?
Added What programming guidelines exist for RADICORE?
Added How are blank entries put into lookup arrays?
Added What data types are supported across the various databases?
Added Why do you ...?
Added Why don't you ...?
28 Dec 2006 Added How can I define preset/static search criteria for the $where variable?
Added Can I change search criteria with a navigation button and without user dialog?
05 Dec 2006 Added How can I prevent simultaneous updates of the same database record?
29 Nov 2006 Added Why can't I have anonymous users?
Added Why can't I bookmark pages?
18 Nov 2006 Added How to deal with ENUM fields.
Added How to hide menu options from certain users.
Amended How do you deal with database locking? to change the default behaviour with database reads during the processing of a database transaction.
Added How do I install RADICORE?.
Added How do I use the Workflow system?.
01 Nov 2006 Added Using subclasses to provide alias names
18 Oct 2006 Amended What do I do to start a new application/project/subsystem?
Amended What do I do to build new components?
19 Sep 2006 Amended How to incorporate a 'popup' control into a form
Added How does the processing of a 'popup' form actually work?
28 Aug 2006 Added Can I produce output in CSV or PDF format?
Added How do fields appear in the HTML output?
09 Aug 2006 Added How does the HELP facility work?.
Added Can I add javascript to my application?.
03 Aug 2006 Added How to incorporate a dropdown list with multiple selections.
03 Jun 2006 Added How can I use a secure server in my application?.
21 Apr 2006 Added How can I make the system inaccessible during periods of maintenance?.
Added How can I run a batch job?.
Added How can I display data from a virtual table?
9 Apr 2006 Amended FAQ 36 by moving What do I do to start a new application/project/subsystem? into a separate section.
7 Apr 2006 Amended FAQ 51 to include a reference to task Update Session data.
22 Mar 2006 Added What debugging aids exist in this framework?.
Added How can you enter ranges of values prior to a search?
Added How can I search for records with historic, current or future dates?
10 Mar 2006 Updated several references to point to contents of Functions, Methods and Variables.
27 Dec 2005 Added How do you deal with task-specific behaviour?
Added Why, in the RBAC system, is task_id different from script_id?
Updated FAQ 36 to include references to the RBAC system and Data Dictionary.
23 Nov 2005 Amended What is a 'popup'? to include more details.
Added How do you deal with database transactions?
Added How do you deal with database locking?
27 Jul 2005 Added What parts of the infrastructure are case sensitive?
Added Does your infrastructure deal with Internationalisation (I18N)?
Added Can a popup be made to accept a single selection rather than multiple selections?
Added Can you pass additional parameters to the XSL stylesheet?
21 Jun 2005 Amended FAQ 36 to include a reference to the revised format for screen structure scripts which follows the provision of additional options.
17 Mar 2005 Added What are the benefits of an infrastructure such as yours?
29 Jan 2005 Amended Why do you use your own DAO instead of an existing one, like PEAR? to contain more details.
Added How do you provide dynamic selection criteria in the $where variable?
10 Dec 2004 Added How do you deal with non-database fields?
Added What is a subclass and how do you use it?
Added Why is your design centered around data instead of functionality?
Added How do you validate user input?
19 Nov 2004 Added What effort is actually involved in building new components?
11 Nov 2004 Added How can business logic and data access logic be separate if they exist in the same class?
Added Why don't you put the table structure in a separate Mapper class?
Added Why do you use your own DAO instead of an existing one, like Pear?
06 Nov 2004 Added What are the benefits of the 3-Tier architecture?
Added Is it really possible to separate business logic from data access logic?
11 Oct 2004 Added Isn't every web application automatically 3-Tier?
06 Oct 2004 Added How easy is it to dynamically change the HTML control for a field?
Amended Why don't you use a Front Controller?
02 Oct 2004 Added Why don't you use hyperlinks to jump to child screens?
25 Sep 2004 Added Aren't the MVC and 3-Tier architectures the same thing?
Added How can you deal with a table that is related to itself?
06 Sep 2004 Added What is the purpose of your generic table class?
Added What is the purpose of your DML class?
Added What is the purpose of each database table class?
28 Aug 2004 Added What is a 'popup'?
Added What is a JOIN and how is it handled?
Added Can you access tables from more than one database?
Added Can you access tables from more than one database engine?
Added How can you handle the switch to the 'improved' MySQL extension?
Added How can you handle the switch to PHP 5?
21 Aug 2004 Added What is the difference between a 'menu bar' and a 'navigation bar'?
Added What do you mean by 'primary' and 'secondary' validation?
Added How do you define 'secondary' validation?
Added How do you handle error messages?
Added How do you handle referential integrity?
Added How do you handle candidate keys?
20 Aug 2004 Added Why don't you use javascript?
Added Why don't you use a Front Controller?
Added Why don't you use SETTERS and GETTERS for individual database fields?
Added Why don't you use another templating system instead of XSL?
Added Why does your infrastructure contain so many different components?
Added How do you deal with pagination?
Added How do you sort the output by different columns?
10 Aug 2004 Extracted FAQ's out of A Development Infrastructure for PHP to make a completely separate document.

Back to TOP.

counter