Tutorials

InitSystem

initSystem Introduction

Why initSystem ??

The term initSystem, apart from being a tag used to group certain PHP files together in the new documentation, is meant to embrace all of those files that are automatically included/initialised before any 'command' scripts can be run.

Zen Cart® v1.x uses a (non Object Oriented) page controller pattern to decide the scripts to run, based on HTTP_GET parameters. The most important of these is the 'main_page' HTTP_GET parameter. Depending on that parameter, a command script is then run. Each commmand script resides in a directory in /includes/modules/pages.

For example if main_page=login the command script would be taken from the /includes/modules/pages/login/ directory. However the first thing every command script always does is require() the /includes/application_top.php file. This is the heart of the initSystem.

It is application_top.php that is responsible for initialising basic subsystems (database abstraction/sessions/languages etc.) and loading global configuration data. In the past this was done using a hard-coded script. From v1.3.0 on, however, Zen Cart® now uses a control array to decide which functions/classes/data files are to be included and initialised. This allows contribution authors and third party developers to gain access to and extend the initSystem without compromising upgradeability.

In the following sections we will work through exactly how the Zen Cart® engine uses application_top.php to initialise core systems.

application_top.php - A little bit of history

In terms of its osCommerce roots, application_top.php was the file included on every page/script needed to invoke and handle basic core sub-systems. Any function/class that was needed globally by any page needed to be initialised here.

From a customisation perspective this was a bad thing. If third party code (contributions) needed to access a new global function/class then application_top.php would need to be 'hacked'. This would obviously cause problems on upgrades, when application_top.php would be overwritten, and any customisations would be lost.

Zen Cart® attempted to mitigate this by providing certain override directories where extra data/functions files could be placed that would be automatically included when application_top.php was run.

The problem with this system is that it only provides for a very few places within the running order of application_top.php where new code can be introduced. It also did not provide at all for the addition of new classes. What was required was an application_top.php that allowed for the placing of any new function/class/script that was completely under the developer's control. Futhermore, some method of loading and invoking classes was also required.

Since v1.3, Zen Cart® achieves this by abstracting the code run by application_top.php into a control array. This array stores details of functions/classes/init scripts that need to be run, and the order in which they are run in a special PHP array. Given this it is now possible for third party developers to 'hook' into application_top.php and be confident that any future code upgrades will not normally overwrite their own code.

application_top.php - Breakpoints

In Zen Cart® there is now almost no procedural code in application_top.php. The small amount that is there will be discussed later. The bulk of procedural code in application_top.php is now given over to handling breakpoints. Breakpoints can simply be described as points of importance. We currently have approximately 20 breakpoints in application_top.php. At each breakpoint something important happens - we may load a function or class, initialise a class, load a script fragment, and so on. The important point is to recognise that at each breakpoint, third party code can, by adding to the control array, also load functions, load classes, initialise classes, run a class method or load (require) a script fragment.

The Control Array

Control arrays are automatically loaded from the directory /includes/auto_loaders. Every *.php file within that directory is expected to have a certain structure. In v1.3 we use a file called config.core.php as the main file for governing application_top.php. Third party developers can add their own control array files. The structure of each file should look like this:

$autoLoadConfig[0] = array();

The value after $autoLoadConfig (in this case [0]) represents the order in which the actions happen (e.g. the Breakpoint), such that $autoLoadConfig[0] will occur before $autoLoadConfig[1]. Note also that any two entries where the breakpoint is the same will occur in the order they appear within the file. The actual contents of the array() part depends upon what effect is needed. Let's consider a number of different scenarios.

First I just want to require a file to be loaded. For this, the control array entry would be:

We then have a special type of 'require'. The initSystem introduces a special class of .php files called init_ scripts. These are stored in the includes/init_includes directory. Each of these contains a small amount of procedural code that can be run as part of the initSystem process. The reason for separating them out into a special directory is to allow for those init_scripts to be overridden, more of which will be discussed later. For now, to load an init_script we use the following control array structure.

Where the auto_loader system comes into its own is in the handling of class files. With a class file we want to load the class file definition, then instantiate the class, and finally possibly run a class method (all running thus within the scope of application_top.php)

In terms of the control array we have the following entries to help us.

Where autotype=>'class' all we are really doing here is 'including' the 'loadFile'. However, in this case we draw the file from the includes/classes (DIR_WS_CLASS) directory.

Where autotype=>'classInstantiate' executes code of the form objectName = new className();

An example based on the code above is

$zc_cache = new cache();

One corollary to this is that we may need to instantiate a class that is bound to a session, like the shopping_cart class.
In this case as from the example above we get

$_SESSION['cart'] = new shoppingCart();

and in fact we take that one step further, Normally we only want to instantiate a session object if it is not already a session object. In this case we take advantage of the 'checkInstantiated' property, which would generate code:

if (!$_SESSION['cart']) {
$_SESSION['cart'] = new shoppingCart();
}

The final example, where autotype-'objectMethod' shows how to run a class method within application_top.php. At the time of writing there is no provision for passing method parameters, so the code generated would be (based on the example above):

$navigation->add_current_page();

Notes on admin autoloaders

The goal of v1.3.x is to eventually remove and refactor all functions into classes. Further, these classes will be common between admin and catalog code.

However this presents a problem with the autoloading of class files. Currently the autoloader code, will default to loading a class from the catalog includes/classes directory rather than admin/includes/classes.

To provide for that interim period where we still need to to load admin class files from admin/includes/classes, we provide an extra option to the 'autotype'=>class option.

if this is used in an admin autoloader it will load the class from the catalog classes directory. For the base class this is fine as the code can be shared between catalog and admin. However, at the moment the split_page_results_class is different between admin and catalog. So in order to load an admin-specific class we use:

Overriding/Extending the system autoloader

There are two ways of overriding/extending the inbuilt auto_loader and thus affecting what happens during the loading of application_top.php.

The usual method would be to simply add a new file to the /includes/auto_loader/ directory. The file you add here should have start with config and have a .php extension (ie: config.your_app_name.php), and should contain one or more control array definitions. This is the recommended method to use for adding code to be executed within application_top.php, and allows contribution authors to customise the code here in a way that will be generally unaffected by system upgrades.

Additionally, within the /includes/auto_loader/ directory is another directory called overrides. This can be used to override any autoloader file in the /includes/auto_loader/ directory. For example, the main autoloader file used in Zen Cart® is config.core.php. If a file called config.core.php is placed in the overrides directory, this will be used instead of the original.

init_scripts and application_top.php

Introduction

The initSystem allows you to automate the including/requiring of files and to automate the loading/instantiating of classes. However we still also need to be able to run some procedural code. We also want to allow 3rd parties to override that procedural code. init_scripts allow us to do this.

init_scripts

There are currently 18 init_scripts in the base 1.3.0 release. These init_scripts are in the includes/init_includes directory.

init_templates.php (Responsible for initialising the template System and activating template-specific language-content defines)

init_tlds.php (Responsible for setting Top Level Domain Variables)

Overriding init_scripts

It is very simple to override a core init script. The directory includes/init_incudes contains a directory called overrides. If I wanted to override the incudes/init_includes/init_sessions.php script then I would simply create a file called init_sessions.php in the includes/init_includes/overrides directory.

Procedural code in application_top.php

Despite the use of the autoloader system, there is still a little procedural code left in application_top.php; although most of this procedural code is given over to processing autoloaders themselves.

Below is the code from the catalog includes/application_top.php. Note: I have removed all documentation tags for clarity

Observer Class

Introduction

One of the many goals of the Zen Cart® project has always been to make it easier for third party developers to add functionality to the core code in an easy and unobtrusive manner. To do this we have in the past relied on the override and auto inclusion systems. However these still do not give developers an easy method of hooking into many areas of core code, without 'hacking' core files themselves.

The observer/notifier system was introduced to give developers unprecedented access to core code, without the need to touch any core files at all. Although ostensibly written for an object-oriented code base, we will see later how it can be used with general procedural code as well.

Extending All Classes

In order to implement the observer/notifier system, some structural changes have been made to Zen Cart®. Firstly two new classes have been introduced: the base class (class.base.php) and the notifier class (class.notifier.php).

The base class contains the code that is used to implement the observer/notifier system. However to make it effective all other Zen Cart® classes now have to be declared as children of the base class. You will see this if you look at the source of any of the Zen Cart® classes.

class currencies extends base {

The notifier class will be discussed later, when we look at extending the observer/notifier system (ONS) into procedural code.

Notifiers: Big Brother is watching

So, what is all the fuss about?

The point of the ONS is that developers can write code that wait for certain events to happen, and then when they do, have their own code executed.

So, how are events defined, where are they triggered?

Events are triggered by code added to the core for v1.3 (with more to be added over time). In any class that wants to notify of an event happening we have added:

$this->notify('EVENT_NAME');

An example would probably help here:

In the shopping cart class after an item has been added to the cart this event is triggered:

$this->notify('NOTIFIER_CART_ADD_CART_END');

There are many other events that have notifiers in Zen Cart® v1.3 and newer; for a list see here.

All of this notifying is all well and good, but how does this help developers?

Observe and Prosper

To take advantage of notifiers, developers need to write some code to watch for them. Observers need to be written as a class. There's even a nice directory, includes/classes/observers, where developers can put these classes.

Lets take an example. Using the notifier mentioned above (NOTIFIER_CART_ADD_CART_END), how would I write a class that watched for that event?

As you can see we have defined a new class called myObserver and in the constructor function for that class (function myObserver) have attached this myObserver class to the event NOTIFIER_CART_ADD_CART_END.

"Fine," I hear you saying, "but how do I actually do anything useful?"

Ok, good question. Whenever an event occurs, the base class looks to see if any other observer class is watching that event. If it is, the base class executes a method in that observer class. Remember the $this->notify('EVENT_NAME') from above? Well, when that event occurs, the base class calls the update method of all observers. Lets see some more code:

Now, whenever the NOTIFIER_CART_ADD_CART_END occurs, our myObserver::update method will be executed. Note that attach() may be called as a method of whatever class you want to listen to ($_SESSION['cart'], in this case) or by the internal class variable $this. Both are available since each are part of the class base, where the attach method resides.

Some notes about the parameters...the attach method has two parameters:

&$observer - Reference to the observer class, used to generated a unique ID for the new listener

$eventIDArray - An array of notifiers that this observer is listening for

The update method is passed three parameters. These are:

&$callingClass - This is a reference to the class in which the event occurred, allowing you access to that class's variables

$notifier - The name of the notifier that triggered the update (It is quite possible to observe more than one notifier)

$paramsArray - Not Used Yet (for future)

NB! The observer/notifier system is written for an OOP-based application, as the observer expects to attach to a class that has notifiers within its methods. However a lot of the code within Zen Cart® is still procedural in nature and not contained within a class.

To work around this, we added the 'stub' notifier class. So if you want to create an observer for a notifier that lies within procedural code (like in page headers) you should add the notifier into your myObserver class like this:

Including observers into your code

Please note that the includes/classes/observers directory is not an autoload directory, so you will need to arrange for application_top.php to autoload your observer class as was described above (add a new config.xxxxx.php file in the auto_loaders folder, etc). Let's assume you are using the freeProduct class (see the example below), and you have saved this in includes/classes/observers/class.freeProduct.php.

You now need to arrange for this class to be loaded and instantiated. To do this you need to use the application_top.php autoload system.

In includes/auto_loaders create a file called config.freeProduct.php containing

Note: 10 has been chosen to cause the observer class to be loaded before the session is started.
Note: 90 has been chosen as the offset since the observer needs to attach to the $SESSION['cart'] class (see the freeProduct example below), which is instantiated at offset 80.

To tie this all together, let's look at a real world example.

A Real World Example

One of the most-often requested features is the ability for the store to automatically add a free gift to the Shopping Cart if the customer spends more than a certain amount.

The code has to be intelligent: it has to not only add the free gift when the shopper spends over a certain amount, but also remove the gift if the user changes the contents of the shopping cart, such that the total falls below the threshold.

Traditionally, although the code for this is not particularly difficult, it would have meant 'hacking' the core shoppingCart class in a number of places. With the ONS, this can be achieved with one very small custom class and absolutely no hacking whatsoever.

so we are watching for the NOTIFIER_CART_ADD_CART_END and NOTIFIER_CART_REMOVE_END of the shopping_cart class.

The update class is extremely simple but in its simplicity manages to do all the work we require of it. It first tests to see if the total in the cart is over the threshold and, if it hasn't already, adds the free product to the cart.

It then tests to see if the cart total has dropped below the threshold and, if the free product is in the cart, removes it.

Now that was cool, how about something a little more difficult.

Another Real World Example

Again we return to the Shopping Cart and promotions. Another oft-requested feature is the BOGOF promotion, or Buy One Get One Free.
This is a little more difficult to achieve than our previous example, as there is some manipulation needed of the cart totals. However as you will see it is still pretty much a breeze.

<?php
/**
* Observer class used apply a Buy One Get One Free(bogof) algorithm to the cart
*
*/
class myBogof extends base {
/**
* an array of ids of products that can be BOGOF.
*
* @var array
*/
var $bogofsArray = array(10,4); //Under Siege2-Dark Territory & The replacement Killers
/**
* Integer number of bogofs allowed per product
*
* For example if I add 4 items of product 10, that would suggest that I pay for 2 and get the other 2 free.
* however you may want to restrict the customer to only getting 1 free regardless of the actual quantity
*
* @var integer
*/
var $bogofsAllowed = 1;
/**
* constructor method
*
* Watches for 1 notifier event, triggered from the shopping cart class.
*/
function __construct() {
$this->attach($this, array('NOTIFIER_CART_SHOW_TOTAL_END'));
}
/**
* Update Method
*
* Called by observed class when any of our notifiable events occur
*
* This is a bit of a hack, but it works.
* First we loop through each product in the bogof Array and see if that product is in the cart.
* Then we calculate the number of free items. As it is buy one get one free, the number of free items
* is equal to the total quantity of an item/2.
* Then we have to hack a bit (would be nice if there was a single cart method to return a product's in-cart price)
* We loop thru the cart until we find the bogof item, get its final price, calculate the saving
* and adjust the cart total accordingly.
*
* @param object $class
* @param string $eventID
*/
function update(&$class, $eventID) {
$cost_saving = 0;
$products = $_SESSION['cart']->get_products();
foreach ($this->bogofsArray as $bogofItem) {
if ($_SESSION['cart']->in_cart($bogofItem)) {
if (isset($_SESSION['cart']->contents[$bogofItem]['qty']) && $_SESSION['cart']->contents[$bogofItem]['qty'] > 1) {
$numBogofs = floor($_SESSION['cart']->contents[$bogofItem]['qty'] / 2);
if ($numBogofs > $this->bogofsAllowed) $numBogofs = $this->bogofsAllowed;
if ($numBogofs > 0) {
for ($i=0, $n=sizeof($products); $i<$n; $i++) {
if ($products[$i]['id'] == $bogofItem) {
$final_price = $products[$i]['final_price'];
break;
}
}
$cost_saving .= $final_price * $numBogofs;
}
}
}
}
$_SESSION['cart']->total -= $cost_saving;
}
}
?>

NB: There are still some weaknesses here...

First although the adjust total is correctly shown on the shopping cart page and sidebox, the line total is not adjusted.