Tony Marston's Blog About software development, PHP and OOP

Using Radicore components in a front-end website

Posted on 1st June 2009 by Tony Marston

Amended on 9th November 2009

Introduction
Separate Directories
Include Files
- include.front.inc
- include.session.inc
- include.singleton.php4/5.inc
Accessing the database
Retrieving Text
Alternative solution - Web Services
Conclusion
Amendment History
Comments

Introduction

The Radicore framework was designed specifically to aid in the development of back office web applications, not front-end websites. If you do not understand the differences between the two then please read Web Site vs Web Application.

The User Interface (UI) of a Radicore application is constructed using a standard library of page controllers and XSL stylesheets, which is usually too inflexible for a front-end website which is supposed to be sexy and slick and full of gizmos and fancy widgets. A website 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 website. 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 website which has its own set of components in the presentation layer, but which uses the Radicore components for the business and data access layers. As you can see in Figure 1 the only common resource which is shared by the front and back ends is the database, so it would make sense to share all the database components.

Figure 1 - Separation of Front Office and Back Office applications

front-end-back-end-01 (2K)

The effect of the front-end and back-end applications which each have their own Presentation layer but share the same Business and Data Access layers is shown in Figure2:

Figure 2 - Different Presentation layers sharing the same Business & Data Access layers

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

Note that with this implementation the front-end application requires direct access to the business and data access layer components of the back-end application. This means that either they are both installed on the same server, or the front-end server contains a copy of the relevant components from the back-end server.

This article is based on my experiences of using Radicore to develop an ERP application for a company which sells its products through a website, then modifying the code for the website to use the ERP components to access the databases.

Separate Directories

The first thing to state is that the front-end website and the back-end ERP application are separate applications, therefore their files should be kept in separate directories, but under the same web root. All the files for the front-end can be under the web root, but all the files for the back-end should be under its own subdirectory. This should lead to a directory structure similar to the following:

/home/account/                                   <-- account directory
/home/account/includes                           <-- include files for the front-end
/home/account/includes_erp                       <-- include files for the back-end
/home/account/public_html                        <-- web root
........................./index.php              <-- home page
........................./subdir1                <-- front-end subdirectory
........................./subdir2                <-- front-end subdirectory
........................./.......                <-- front-end subdirectory
........................./subdirN                <-- front-end subdirectory

........................./radicore               <-- back-end subdirectory
........................./radicore/audit         <-- Radicore subsystem
........................./radicore/menu          <-- Radicore subsystem
........................./radicore/soap          <-- Radicore subsystem
........................./radicore/workflow      <-- Radicore subsystem
........................./radicore/xmlrpc        <-- Radicore subsystem

........................./radicore/inventory     <-- ERP subsystem
........................./radicore/order         <-- ERP subsystem
........................./radicore/party         <-- ERP subsystem
........................./radicore/product       <-- ERP subsystem
........................./radicore/shipment      <-- ERP subsystem

Notice here that all the front-end code resides under /home/account/public_html while all the back-end code resides under /home/account/public_html/radicore. This includes both the standard radicore subsystems as well as whatever additional subsystems were created for the back-end application. You can change the name radicore to anything you like, such as erp or even foobar, just as long as it separates the back-end files from the front-end.

Notice also that the include files for the front and back ends are in separate directories both of which are outside the web root. This means that none of the files can be accessed via a URL, so is a security measure. The directory names are totally arbitrary, so you can change them to whatever you want.

Files in one application can be accessed by the other by means of modifications to the include_path in htaccess files. The front-end should use an include_path which specifies directories in the following order:

/home/account/includes
/home/account/includes_erp
/home/account/public_html/radicore/audit
/home/account/public_html/radicore/menu
/home/account/public_html/radicore/workflow
/home/account/public_html/radicore/inventory
/home/account/public_html/radicore/order
/home/account/public_html/radicore/party
/home/account/public_html/radicore/product

The back-end should use an include_path such as the following:

/home/account/includes_erp
/home/account/includes

Include Files

It is possible for a component in the back-end to require access to a function in the front-end, which is why the include_path for the back-end contains a reference to the front-end 'includes' directory. When the front-end accesses a database table class it needs to search through the subsystem directories in the back-end. Each of these database table classes uses functions that reside in the back-end 'includes' directory.

include.front.inc

While it is necessary for the front-end to access the include.general.inc file which resides in the back-end 'includes' directory, it may be necessary to override certain functions with alternative behaviour. This can be done by creating a file with a name such as include.front.inc with contents such as the following:

<?php

// This file contains generic functions for the front-end website

if (isset($_SERVER['SERVER_PROTOCOL'])) {
    $protocol = 'HTTP://';
    require_once 'include.session.inc';
} else {
    // this is being run in batch/cli mode, not from a web server
    $protocol = '';
} // if

$transaction_has_started = false;
$use_HTTPS = false;

// ****************************************************************************
function append2ScriptSequence ($next=null, $prepend=false)
{
    // empty function, do nothing
    return;

} // append2ScriptSequence

// ****************************************************************************
function getPatternId ($script_id=null)
// internet scripts do not have a pattern, so use default value.
{
    $pattern_id = 'INTERNET';

    return $pattern_id;

} // getPatternId

// ****************************************************************************
function removeFromScriptSequence ($task_id=null)
{
    // empty function, do nothing
    return;

} // removeFromScriptSequence

// ****************************************************************************
function scriptNext ($task_id, $where=null, $selection=null, $task_array=array())
{
    // empty function, do nothing
    return;

} // scriptNext

// ****************************************************************************
function scriptPrevious($errors=null, $messages=NULL, $action=NULL, $instruction=NULL)
{
    // empty function, do nothing
    return;

} // scriptPrevious

require 'include.general.inc';

?>

Notice that the last line refers to the back-end include.general.inc file. Every function in this file is surrounded with

if (!function_exists('whatever')) {
    function whatever ()
    // blah blah blah
    {
        ....
    } // whatever
} // if

This will therefore not attempt to re-define a function that has already been defined, thus avoiding a fatal error.

Every page in the front-end should therefore include the contents of include.front.inc (or whatever name you call it). You can also use this file to define any new functions, or include other files which you may have created for your front-end.

include.session.inc

This will contain an alternative to the back-end initSession() function which does not contain the checks which force the user to pass through the logon screen before being allowed to access the application. It does, however, need to initialise some variables which are referenced by some of the back-end functions:

<?php

function initSession()
// initialise session data
{
    global $errors;             // array of error messages
    global $messages;           // optional array of non-error messages

    $errors   = array();
    $messages = array();

    // continue with existing session, or start a new one
    if (!isset($_SESSION)) {
        session_start();             // open/reopen session
    } // if

    $_SESSION['logon_user_id']    = 'INTERNET';
    $_SESSION['role_id']          = 'INTERNET';
    $GLOBALS['task_id']           = 'INTERNET';
    $GLOBALS['mode']              = 'INTERNET';
    $_SESSION['default_language']      = 'en';    // default is 'English'
    $_SESSION['default_currency_code'] = 'GBP';   // default is GB pounds (sterling)
    
    // everything on this server is in English
    $server_locale = 'en_gb';
    
    // populate language array using specified locale
    $_SESSION['user_language_array'] = get_languages($server_locale);
    
    $_SESSION['user_language'] = $server_locale;
    
    // set language for reading database data and text files
    $GLOBALS['party_language'] = $_SESSION['user_language'];
    
    // store locale data based on user's preferred language
    $_SESSION['locale_name'] = saveLocaleFormat($server_locale);
    $_SESSION['localeconv'] = $GLOBALS['localeconv'];
    
    return;

} // initSession

?>

This is the basic code which you should use as a starting point. You may modify it if you wish, such as to set the language and currency codes to whatever is appropriate for your website. This has been used, for example, to have copies of the front-end on different servers in different countries, and to set the language and currency according to the server name.

If you wish to retrieve text from the database in different languages, as documented in Internationalisation and the Radicore Development Infrastructure (Part 2), then all you need to is insert the correct value into $server_locale.

include.singleton.php4/5.inc

By default any updates made to the database are recorded in the AUDIT database, but this may be an unnecessary overhead in the front-end website. As all instances of database table classes are created using the getInstance() function, it would be possible to provide an alternative version of this function which sets the audit_logging variable to FALSE using code similar to the following:

<?php

class RDCsingleton
// ensure that only a single instance exists for each class.
{
    function &getInstance ($class, $arg1=null, $initialise=true)
    // implements the 'singleton' design pattern.
    {
        static $instances = array();  // array of instance names
        
        .....
        
        $instance->audit_logging = false;  // turn audit logging OFF for the front-end
        
        return $instance;
        
    } // getInstance
        
} // RDCsingleton

?>

Note that there are separate versions of this function in files which are specific to either PHP 4 or PHP 5.

Accessing the database

When it is required to access the shared database in the scripts for your front-end pages you will need to go through the database table classes which were created by Radicore. It may be useful to put the necessary code in functions which can then be called from many different places.

function get_product_array($where, $orderby, $pageno=1, $rows_per_page=12)
// this returns an array containing any number of products
{
    $dbobject = RDCsingleton::getInstance('product');
    
    $dbobject->sql_select = 'whatever';
    $dbobject->sql_from   = 'product'
                          .' LEFT JOIN .... AS .. ON (...)';
    $dbobject->sql_search = "curr_or_hist='C'";
    
    $dbobject->sql_orderby = $orderby;
    $dbobject->setPageNo($pageno);
    $dbobject->setRowsPerPage($rows_per_page);
    $array = $dbobject->getData($where);
    
    return $array;
    
} // get_product_array

Notice that the $dbobject->sql_* variables can be set to change the generated sql SELECT statement to a customised string instead of the default string. This function can be accessed using code similar to the following:

    $where    = 'whatever';
    $orderby  = 'whatever';
    $pageno   = 1;
    $pagesize = 12;
    $product_array = get_product_array($where, $orderby, $pageno, $pagesize);
    foreach ($product_array as $rownum => $product) {
        echo "<tr>";
        echo "<td>" .$product['product_id'] ."</td>";
        echo "<td>" .$product['product_desc'] ."</td>";
        echo "<td>" .$product['product_price'] ."</td>";
        echo "</tr>";
    } // foreach

The get_product_array() function returns all its products in a single array, but if you wished to return them one at a time you could use a different function, such as:

function get_product_serial($where, $orderby, $pageno=1, $rows_per_page=12)
// this returns a result set which can be accessed using $dbobject->fetchRow($result)
{
    $dbobject = RDCsingleton::getInstance('product');
    
    // first, get the count of available products
    $dbobject->sql_select = 'count(product.product_id) AS count';
    $dbobject->sql_from   = 'product'
                          .' LEFT JOIN .... AS .. ON (...)';
    $dbobject->sql_search = "curr_or_hist='C'";
    
    $data = $dbobject->getData($where);
    if (empty($data)) {
        $count = 0;
    } else {
        $count = $data[0]['count'];
    } // if
    
    // second, get the data
    $dbobject->sql_select = 'whatever';
    $dbobject->sql_from   = 'product'
                          .' LEFT JOIN .... AS .. ON (...)';
    $dbobject->sql_search = "curr_or_hist='C'";
    
    $dbobject->sql_orderby = $orderby;
    $dbobject->setPageNo($pageno);
    $dbobject->setRowsPerPage($rows_per_page);
    $result = $dbobject->getData_serial($where);
    
    return array($result, $dbobject, $count);
    
} // get_product_serial

This function can be accessed using code similar to the following:

    $where    = 'whatever';
    $orderby  = 'whatever';
    $pageno   = 1;
    $pagesize = 12;
    list($result, $dbobject, $count) = get_product_serial($where, $orderby, $pageno, $pagesize);
    $num_rows = $dbobject->numrows;  // the number of rows in this page
    while ($product = $dbobject->fetchRow($result)) {
        echo "<tr>";
        echo "<td>" .$product['product_id'] ."</td>";
        echo "<td>" .$product['product_desc'] ."</td>";
        echo "<td>" .$product['product_price'] ."</td>";
        echo "</tr>";
    } // while

The previous functions can retrieve any number of records, but if you only wish to retrieve a single record using its primary key you can use a function similar to the following:

function get_product_data($product_id)
// this returns the details of a single specified product
{
    $dbobject = RDCsingleton::getInstance('product');
    
    $dbobject->sql_select = 'whatever';
    $dbobject->sql_from   = 'product'
                          .' LEFT JOIN .... AS .. ON (...)';
        
    $array = $dbobject->getData("product_id='$where'");
    if (empty($array)) {
        $result = false;
    } else {
        $result = $array[0]; // return 1st row as an associative array
    } // if
    
    return $result;
    
} // get_product_data

Those are some examples of reading from the database, but what about writing? This can be achieved with a function similar to the following:

function createCustomer ($input)
// create a new record on the CUSTOMER table
{
    $dbobject = RDCsingleton::getInstance('customer');
    
    $result = $dbobject->insertRecord($input);
    if ($dbobject->errors) {
        $result['errors']   = $dbobject->errors;
        $result['FILE']     = __FILE__;
        $result['FUNCTION'] = __FUNCTION__;
        $result['LINE']     = __LINE__;
    } // if

    return $result;

} // createCustomer

Here the $input array will usually be the $_POST array. Note that the errors field, if it is created, will be an associative array in the format field1 = error1, field2 = error2, etc. The calling script can detect the existence of this field and use it to associate each error message with its related field.

Retrieving Text

It may be that your front-end application needs to be multi-lingual, in which case you may wish to use the getLanguageText() function. This will require that you create a text and a menu/text directory with separate subdirectories for each supported language. Each of these subdirectories should contain the following files:

text/
  en/                       <= English
    language_array.inc
    language_text.inc
    
  en_us/                    <= US English
    ...
  fr/                       <= French
    ...
  de/                       <= German
    ...
  es/                       <= Spanish
    ...
menu/text/
  en/                       <= English
    sys.language_array.inc
    sys.language_text.inc
    
  en_us/                    <= US English
    ...
  fr/                       <= French
    ...
  de/                       <= German
    ...
  es/                       <= Spanish
    ...

With this arrangement the existing getLanguageText() function can be used without modification as it will be able to locate the correct file when called from any module in either the front-end or back-end application.

Alternative solution - Web Services

The above solution assumes that the front-end application has direct access to the table classes in the back-end application, which means one of the following:

There is, however, another solution which removes the direct access between the two applications. By implementing web services (such as via XML-RPC or SOAP) the only code the front-end application would need would be for generating web service requests and dealing with the responses. All the business logic and data access logic would be confined to the web services server in the back-end application. This will produce the structure shown in Figure 3:

Figure 3 - Linking the front and back ends with Web Services

front-end-back-end-03 (12K)

Here you can see that the front-end is located on server 'A' while the back-end is located on server 'B', but there is no reason why they cannot both be on the same server. This would only require a one-line change in the front-end application to identify the address of the web services server.

Conclusion

This article has shown that a back-end application developed using Radicore can easily share its data access logic and business logic with a front-end application. Although the front-end application will have its own presentation layer and therefore not use any pages that were generated by Radicore, it can still use the business layer and data access layer components, either by direct access or via web services.


Amendment history:

09 Nov 2009 Added Alternative solution - Web Services.

counter