One of the many goals of the Zen-Cart project has always been to make it easier for 3rd 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.

One of the many goals of the Zen-Cart project has always been to make it easier for 3rd 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 unprecendented 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 also be used with general procdural code as well.

+

The observer/notifier system was introduced to give developers unprecendented 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 also be used with general procedural code as well.

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 system, is meant to embrace all of those files that are automatically included/initialised before any 'command' scripts can be run.

Zen Cart uses a (non Object Oriented) page controller pattern to decide the scripts run, based on HTTP_GET parameters. The most important of these being the 'main_page' HTTP_GET parameter. Dependant 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 /inlcudes/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 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 3rd party developers to gain access to 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 neeeded globally by any page needed to be initialised here.

From a customisation perspective this was a bad thing. If 3rd 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 over written, and any customisations would be lost.

Zen Cart attempted to mitigate this by providing certain override directories where extra data files/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 addittion 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 developers control. Futhermore some method of loading and invoking classes was also required.

v1.3.0 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 3rd party developers to 'hook' into application_top.php and be confident that any future code upgrades will not affect their own code.

application_top.php - Breakpoints

In Zen Cart v1.3.0, 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. The important point is to recognise that at each beakpoint 3rd party code, by adding to the control array, can also load functions, load classes, initialise classes, run a class method or load a (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 1.3.0 we use a file called config.core.php as the main file for governing application_top.php, 3rd party developers can add their own control arrays. 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 2 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. lets 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_scripts directory. Each of these contain 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 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 within 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 we, in this case draw the file from the includes/classes (DIR_WS_CLASS) directory.

Where autotype='instantiate' execute 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, Noramlly 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.0 is to eventually remove and refactor all function 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 a admin specific class we do

Overriding/Extending the system autoloader

There are 2 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 a .php extension and should contain 1 or more control array definitions.

This is the recommended method to use for adding code to be executed witin application_top.php, and allows contribution authors to customise the code here in a way that will be unaffected by system upgrades.

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 17 init_scripts in the base 1.3.0 release. These init_scripts are in the includes/init_includes direcrtory.

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.

Observer Class

Introduction

One of the many goals of the Zen-Cart project has always been to make it easier for 3rd 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 unprecendented 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 also 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.0, 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. Theres 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 here you saying, but how do I actually doing any thing useful.

Ok, good question. Whenever an event occurs, the base class looks to see if any other observer class is whatching 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.

some notes about the parameters. As you can see the update method is passed 3 parameters. These are

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

$notifier - the name of the notifier that triggered the update (it is quite possible to observer more than one notifier.)

$paramsArray - Not Used (for future)

To tie this all together, lets look at a real world example.

A Real World Example

One of the often requested features, is the abilty for the store to automatically add a free gift to the Shopping Cart if the customers spends over a certain amount. The code has to be intelligent. It not only has to add the free gift, when the shopper spends over a certain amount, but also remove the gift if the users 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 absolutley 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 extemely simple, but in its simplicity, manages to do all the work wwe 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 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 difficut 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) algorithim 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
*
* Attaches our class to the $_SESSION['cart'] class and watches for 1 notifier event.
*/
function myBogof() {
$_SESSION['cart']->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 thru each product in the bogof Array and see if that product is in the cart.
* Then we calcualte 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 their was a single cart method to return a products in cart price)
* we loop thru the cart till we find the bogof item, and 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 showm on the shopping cart page and sidebox. the line total is not adjusted. Secondly this will probably producee a confusling output at checkout. Have not tested for tax compliance yet @TODO