Difference between revisions of "Developers API Tutorials"

From Zen Cart(tm) Wiki
Jump to: navigation, search
m (Introduction)
(Change constructor to __construct in examples)
 
(44 intermediate revisions by 9 users not shown)
Line 6: Line 6:
 
Why initSystem ??
 
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.
+
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.
  
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.
+
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.
  
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 ===
 
=== 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.
+
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 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.
+
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.
  
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.
+
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 ===
 
=== 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.
+
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 ===
 
=== 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.
+
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.
 +
 
  
$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:
  
First I just want to require a file to be loaded. for this the control array entry would be
 
  
 
   $autoLoadConfig[0][] = array('autoType'=>'require', 'loadFile'=> DIR_WS_INCLUDES . 'somefile.php');  
 
   $autoLoadConfig[0][] = array('autoType'=>'require', 'loadFile'=> DIR_WS_INCLUDES . 'somefile.php');  
 +
 
   
 
   
The autotype parameter tells us that we just want to require a file, the loadFile parameter tells us which file we want to load.
+
The '''autotype''' parameter tells us that we just want to require a file
 +
 
 +
 
 +
The '''loadFile''' parameter tells us which file we want to load.
 +
 
  
 
Loading function files can also obviously be done using the above.
 
Loading function files can also obviously be done using the above.
Similarly if we want to 'include' a file.
+
 
 +
 
 +
Similarly if we want to 'include' a file:
 +
 
  
 
   $autoLoadConfig[0][] = array('autoType'=>'include', 'loadFile'=> DIR_WS_INCLUDES . 'somefile.php');  
 
   $autoLoadConfig[0][] = array('autoType'=>'include', 'loadFile'=> DIR_WS_INCLUDES . 'somefile.php');  
  
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.
 
  
   $autoLoadConfig[] = array('autoType'=>'init_script', 'loadFile'=> 'init_database.php');  
+
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.
 +
 
 +
 
 +
   $autoLoadConfig[] = array(array('autoType'=>'init_script', 'loadFile'=> 'init_database.php'));
 +
 
 +
 
 +
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'')
  
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.
 
In terms of the control array we have the following entries to help us.
Line 75: Line 105:
 
                                 'objectName'=>'navigation',
 
                                 'objectName'=>'navigation',
 
                                 'methodName' => 'add_current_page');
 
                                 'methodName' => 'add_current_page');
 +
  
 
Taking these options one by one
 
Taking these options one by one
  
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  
+
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
  
objectName = new className();
 
an example based on the code above is
 
  
 
   $zc_cache = new cache();
 
   $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.
 
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
 
In this case as from the example above we get
 +
  
 
   $_SESSION['cart'] = new shoppingCart();
 
   $_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
+
 
 +
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']) {
 
   if (!$_SESSION['cart']) {
Line 98: Line 135:
 
   }   
 
   }   
  
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).
+
 
 +
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();
 
   $navigation->add_current_page();
 +
  
 
==== Notes on admin autoloaders ====
 
==== 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.
+
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.  
 
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.
 
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.
 +
  
 
consider this
 
consider this
 +
  
 
   $autoLoadConfig[0][] = array('autoType'=>'class',
 
   $autoLoadConfig[0][] = array('autoType'=>'class',
 
                               'loadFile'=>'class.base.php');
 
                               'loadFile'=>'class.base.php');
  
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
+
 
 +
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:
 +
 
  
 
   $autoLoadConfig[0][] = array('autoType'=>'class',
 
   $autoLoadConfig[0][] = array('autoType'=>'class',
 
                               'loadFile'=>'split_page_results.php',
 
                               'loadFile'=>'split_page_results.php',
 
                               'classPath'=>DIR_WS_CLASSES);
 
                               'classPath'=>DIR_WS_CLASSES);
 +
  
 
=== Overriding/Extending the system autoloader ===
 
=== 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.
+
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.
  
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.
+
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.
  
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 ===
 
=== init_scripts and application_top.php ===
Line 135: Line 182:
  
 
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.
 
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 ====
 
==== 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.
+
There are currently 18 init_scripts in the base 1.3.0 release. These init_scripts are in the ''includes/init_includes'' directory.
  
 
* init_add_crumbs.php (Responsible for initialising the Breadcrumb)
 
* init_add_crumbs.php (Responsible for initialising the Breadcrumb)
Line 143: Line 191:
 
* init_category_path.php (Reponsible for initialising Category Paths)
 
* init_category_path.php (Reponsible for initialising Category Paths)
 
* init_currencies.php (Responsible for initialising the Currencies Sub-System)
 
* init_currencies.php (Responsible for initialising the Currencies Sub-System)
* init_customer.php (Responsible for checking customer status, either thru Down for Maintenance or the Approval level)
+
* init_customer_auth.php (Responsible for checking customer status, either thru Down for Maintenance or the Approval level)
 
* init_database.php (Responsible for initialising the DB layer)
 
* init_database.php (Responsible for initialising the DB layer)
* init_db_config_read.php (Responsible for reading SB configuration data)
+
* init_db_config_read.php (Responsible for reading configuration data from database)
 
* init_file_db_names.php (Responsible for loading File and Database tablename Defines)
 
* init_file_db_names.php (Responsible for loading File and Database tablename Defines)
* init_general_funcs.php (Resposible for loading general functions from the includes/functions directory)
+
* init_general_funcs.php (Resposible for loading general functions from the includes/functions directory as well as the extra_functions folder)
* init_gzip.php (Responsible for loading Gzip functions)
+
* init_gzip.php (Responsible for loading Gzip output-buffering functions)
* init_languages.php (Responsible for loading language based defines)
+
* init_header.php (Responsible for running page-header procedures)
* init_sanitize.php (Responsible for loading inout sanitising code)
+
* init_languages.php (Responsible for loading multiple-language support sub-system)
* init_sefu.php (Responsible for loading code to provide Search engine friendly urls)
+
* init_sanitize.php (Responsible for loading input-sanitising code)
 +
* init_sefu.php (Responsible for loading code to provide search-engine-friendly URLs)
 
* init_sessions.php (Responsible for loading Session code)
 
* init_sessions.php (Responsible for loading Session code)
* init_special_funcs.php (Responsible for loading specialized funcions)
+
* init_special_funcs.php (Responsible for loading specialized but necessary functions)
* init_templates.php (Responsible for initialising the template System)
+
* 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)
 
* 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 ===
 
=== 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.
+
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''
  
Below is the code from the catalog includes/application_top.php.  ''Note: I have removed all documentation tags for clarity''
 
  
 
  define('DEBUG_AUTOLOAD', false);
 
  define('DEBUG_AUTOLOAD', false);
Line 218: Line 274:
 
  }
 
  }
  
==== 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 ==
 
== Observer Class ==
 
=== Introduction ===
 
=== 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.  
+
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 ===
  
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.
+
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'').
  
=== 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.  
  
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 {
 
   class currencies extends base {
  
  
The notifier class will be discussed later, when we look at extending the obsever/notifier system(ONS) into procedural code.
+
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.
 +
 
  
=== Notifiers, Big Brother is watching ===
+
So, how are events defined, where are they triggered?
  
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:
  
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');
 
   $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
+
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');
 
   $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 [[#Notifiers_currently_set_in_Zen_Cart|see here]].
  
  
There are many other events that have notifiers in Zen Cart v1.3.0, for a list [[#Notifiers_currently_set_in_v1.3.0|see here]].
+
All of this notifying is all well and good, but how does this help developers?
  
All of this notifying is all well and good, but how does this help developers.
 
  
 
=== Observe and Prosper ===  
 
=== 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.  
+
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?
  
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.
 
  
 
   <?php
 
   <?php
 
   class myObserver extends base {
 
   class myObserver extends base {
     function myObserver() {
+
     function __construct() {
       $this->attach('NOTIFIER_CART_ADD_CART_END');
+
       $this->attach($this, array('NOTIFIER_CART_ADD_CART_END'));
 
     }
 
     }
 
  ...
 
  ...
 
   }
 
   }
 +
  
 
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.  
 
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.
+
"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:
 +
 
  
  <?php
 
 
   class myObserver extends base {
 
   class myObserver extends base {
     function myObserver() {
+
     function __construct() {
       $this->attach('NOTIFIER_CART_ADD_CART_END');
+
       $this->attach($this, array('NOTIFIER_CART_ADD_CART_END'));
 
     }
 
     }
     function update(&$callingClass. $notifier, $paramsArray) {
+
     function update(&$callingClass, $notifier, $paramsArray) {
 
       ... do some stuff
 
       ... do some stuff
 
     }
 
     }
 
   }
 
   }
  
Now, whenever the  NOTIFIER_CART_ADD_CART_END occurs, our myObserver::update method will be executed.
 
  
some notes about the parameters. As you can see the update method is passed 3 parameters. These are
+
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 classes variables
+
* &$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 observer more than one notifier.)
+
* $notifier - The name of the notifier that triggered the update (It is quite possible to observe more than one notifier)
* $paramsArray - Not Used (for future)
+
* $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&reg; 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:
 +
 
 +
 
 +
  class myObserver extends base {
 +
    function __construct() {
 +
      global $zco_notifier;
 +
      $zco_notifier->attach($this, array('NOTIFY_HEADER_END_CHECKOUT_CONFIRMATION'));
 +
    }
 +
 
 +
=== 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
 +
 
 +
 
 +
<?php
 +
$autoLoadConfig[10][] = array('autoType'=>'class',
 +
                              'loadFile'=>'observers/class.freeProduct.php');
 +
$autoLoadConfig[90][] = array('autoType'=>'classInstantiate',
 +
                              'className'=>'freeProduct',
 +
                              'objectName'=>'freeProduct');
 +
?>
 +
 
 +
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, lets look at a real world example.
+
To tie this all together, let's look at a real world example.
  
 
=== 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.
+
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.
  
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.
 
  
Heres the code.
+
Here's the code.
  
 
  <?php
 
  <?php
Line 326: Line 450:
 
     * The id of the free product.
 
     * The id of the free product.
 
     *  
 
     *  
     * Note. This must be a true free product. e.g. price = 0 also make sure tht if you don't want the customer
+
     * Note. This must be a true free product. e.g. price = 0 Also make sure that if you don't want the customer
 
     * to be charged shipping on this, that you have it set correctly.
 
     * to be charged shipping on this, that you have it set correctly.
 
     *
 
     *
Line 337: Line 461:
 
     * Attaches our class to the $_SESSION['cart'] class and watches for 2 notifier events.
 
     * Attaches our class to the $_SESSION['cart'] class and watches for 2 notifier events.
 
     */
 
     */
   function freeProduct() {
+
   function __construct() {
 
     $_SESSION['cart']->attach($this, array('NOTIFIER_CART_ADD_CART_END', 'NOTIFIER_CART_REMOVE_END'));
 
     $_SESSION['cart']->attach($this, array('NOTIFIER_CART_ADD_CART_END', 'NOTIFIER_CART_REMOVE_END'));
 
   }
 
   }
Line 348: Line 472:
 
     * @param string $eventID
 
     * @param string $eventID
 
     */
 
     */
   function update(&$class, $eventID) {
+
   function update(&$class, $eventID, $paramsArray = array()) {
     if ($_SESSION['cart']->show_total() >= $this->freeAmount && !$_SESSION['cart']->in_cart($this->freeProductID) ) {
+
 
 +
  if ($eventID == 'NOTIFIER_CART_REMOVE_END' && (isset($_SESSION['freeProductInCart']) && $_SESSION['freeProductInCart'] == TRUE ))
 +
  {
 +
    if (!$_SESSION['cart']->in_cart($this->freeProductID))
 +
    {
 +
      $_SESSION['userRemovedFreeProduct'] = TRUE;
 +
    }
 +
  }
 +
 
 +
  if (!isset($_SESSION['userRemovedFreeProduct']) || $_SESSION['userRemovedFreeProduct'] != TRUE)
 +
  {
 +
     if ($_SESSION['cart']->show_total() >= $this->freeAmount && !$_SESSION['cart']->in_cart($this->freeProductID) )  
 +
    {
 
       $_SESSION['cart']->add_cart($this->freeProductID);
 
       $_SESSION['cart']->add_cart($this->freeProductID);
 +
      $_SESSION['freeProductInCart'] = TRUE; 
 
     }
 
     }
    if ($_SESSION['cart']->show_total() < $this->freeAmount && $_SESSION['cart']->in_cart($this->freeProductID) ) {
 
      $_SESSION['cart']->remove($this->freeProductID);
 
    }
 
    if ($_SESSION['cart']->in_cart($this->freeProductID)) {
 
      $_SESSION['cart']->contents[$this->freeProductID]['qty'] = 1;
 
    }
 
 
   }
 
   }
 +
 
 +
  if ($_SESSION['cart']->show_total() < $this->freeAmount && $_SESSION['cart']->in_cart($this->freeProductID) )
 +
  {
 +
    $_SESSION['cart']->remove($this->freeProductID);
 +
  }
 +
 +
  if ($_SESSION['cart']->in_cart($this->freeProductID))
 +
  {
 +
    $_SESSION['cart']->contents[$this->freeProductID]['qty'] = 1;
 +
  }
 +
   
 +
  } 
 
  }
 
  }
 
  ?>
 
  ?>
 +
 +
 +
A couple notes:
 +
  
 
First, I have set the options for the system in the class itself. This is obviously a bad idea, and it would be much better to have an admin module to set these options.
 
First, I have set the options for the system in the class itself. This is obviously a bad idea, and it would be much better to have an admin module to set these options.
  
Second, notice that we are actually watching for 2 events in the one class.
+
 
 +
Second, notice that we are actually watching for two events in the one class.
 +
 
  
 
     $_SESSION['cart']->attach($this, array('NOTIFIER_CART_ADD_CART_END', 'NOTIFIER_CART_REMOVE_END'));
 
     $_SESSION['cart']->attach($this, array('NOTIFIER_CART_ADD_CART_END', 'NOTIFIER_CART_REMOVE_END'));
  
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.
+
so we are watching for the NOTIFIER_CART_ADD_CART_END and NOTIFIER_CART_REMOVE_END of the shopping_cart class.
It then tests to see if the cart total has dropped below the threshold, and if the free product in the cart, removes it.
+
 
 +
 
 +
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.
 
Now that was cool, how about something a little more difficult.
 +
  
 
=== Another Real World Example ===  
 
=== 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.
+
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.  
+
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
 
  <?php
 
  /**
 
  /**
   * Observer class used apply a Buy One Get One Free(bogof) algorithim to the cart
+
   * Observer class used apply a Buy One Get One Free(bogof) algorithm to the cart
 
   *
 
   *
 
   */
 
   */
Line 405: Line 560:
 
     * constructor method
 
     * constructor method
 
     *  
 
     *  
     * Attaches our class to the $_SESSION['cart'] class and watches for 1 notifier event.
+
     * Watches for 1 notifier event, triggered from the shopping cart class.
 
     */
 
     */
   function myBogof() {
+
   function __construct() {
     $_SESSION['cart']->attach($this, array('NOTIFIER_CART_SHOW_TOTAL_END'));
+
     $this->attach($this, array('NOTIFIER_CART_SHOW_TOTAL_END'));
 
   }
 
   }
 
   /**
 
   /**
Line 416: Line 571:
 
     *  
 
     *  
 
     * This is a bit of a hack, but it works.  
 
     * 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.
+
     * First we loop through 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
+
     * 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.
 
     * 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)
+
     * 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 till we find the bogof item, and get its final price
+
     * We loop thru the cart until we find the bogof item, get its final price, calculate the saving
     * calculate the saving, and adjust the cart total accordingly.
+
     * and adjust the cart total accordingly.
 
     *
 
     *
 
     * @param object $class
 
     * @param object $class
Line 436: Line 591:
 
           if ($numBogofs > 0) {
 
           if ($numBogofs > 0) {
 
             for ($i=0, $n=sizeof($products); $i<$n; $i++) {
 
             for ($i=0, $n=sizeof($products); $i<$n; $i++) {
               if ($products[$i]['id'] = $bogofItem) {
+
               if ($products[$i]['id'] == $bogofItem) {
 
                 $final_price = $products[$i]['final_price'];
 
                 $final_price = $products[$i]['final_price'];
 
                 break;
 
                 break;
Line 451: Line 606:
 
  ?>
 
  ?>
  
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
+
 
 +
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.  
 +
 
 +
 
 +
Secondly this will probably produce a confusing output at checkout.  
  
  
=== Notifiers currently set in v1.3.0 ===
+
Third: Have not tested for tax compliance yet ( @TODO )
Notifier points for Zen Cart 1.3.0
 
  
==== Shopping Cart class ====
+
=== Notifiers currently set in Zen Cart ===
  
* NOTIFIER_CART_INSTANTIATE_START
+
Notifier points for Zen Cart 1.3.7
* NOTIFIER_CART_INSTANTIATE_END
 
  
* NOTIFIER_CART_RESTORE_CONTENTS_START
 
* NOTIFIER_CART_RESTORE_CONTENTS_END
 
  
* NOTIFIER_CART_RESET_START
 
* NOTIFIER_CART_RESET_END
 
  
* NOTIFIER_CART_ADD_CART_START
+
==== Shopping Cart class ====
* NOTIFIER_CART_ADD_CART_END
+
 
+
*NOTIFIER_CART_INSTANTIATE_START
* NOTIFIER_CART_UPDATE_QUANTITY_START
+
*NOTIFIER_CART_INSTANTIATE_END
 +
*NOTIFIER_CART_RESTORE_CONTENTS_START
 +
*NOTIFIER_CART_RESTORE_CONTENTS_END
 +
*NOTIFIER_CART_RESET_START
 +
*NOTIFIER_CART_RESET_END
 +
*NOTIFIER_CART_ADD_CART_START
 +
*NOTIFIER_CART_ADD_CART_END
 +
*NOTIFIER_CART_UPDATE_QUANTITY_START
 
*NOTIFIER_CART_UPDATE_QUANTITY_END
 
*NOTIFIER_CART_UPDATE_QUANTITY_END
 
 
*NOTIFIER_CART_CLEANUP_START
 
*NOTIFIER_CART_CLEANUP_START
 
*NOTIFIER_CART_CLEANUP_END
 
*NOTIFIER_CART_CLEANUP_END
 
 
*NOTIFIER_CART_COUNT_CONTENTS_START
 
*NOTIFIER_CART_COUNT_CONTENTS_START
 
*NOTIFIER_CART_COUNT_CONTENTS_END
 
*NOTIFIER_CART_COUNT_CONTENTS_END
 
 
*NOTIFIER_CART_GET_QUANTITY_START
 
*NOTIFIER_CART_GET_QUANTITY_START
 
*NOTIFIER_CART_GET_QUANTITY_END_QTY
 
*NOTIFIER_CART_GET_QUANTITY_END_QTY
 
*NOTIFIER_CART_GET_QUANTITY_END_FALSE
 
*NOTIFIER_CART_GET_QUANTITY_END_FALSE
 
 
*NOTIFIER_CART_IN_CART_START
 
*NOTIFIER_CART_IN_CART_START
 
*NOTIFIER_CART_IN_CART_END_TRUE
 
*NOTIFIER_CART_IN_CART_END_TRUE
 
*NOTIFIER_CART_IN_CART_END_FALSE
 
*NOTIFIER_CART_IN_CART_END_FALSE
 
 
*NOTIFIER_CART_REMOVE_START
 
*NOTIFIER_CART_REMOVE_START
 
*NOTIFIER_CART_REMOVE_END
 
*NOTIFIER_CART_REMOVE_END
 
 
*NOTIFIER_CART_REMOVE_ALL_START
 
*NOTIFIER_CART_REMOVE_ALL_START
 
*NOTIFIER_CART_REMOVE_ALL_END
 
*NOTIFIER_CART_REMOVE_ALL_END
 
 
*NOTIFIER_CART_GET_PRODUCTS_START
 
*NOTIFIER_CART_GET_PRODUCTS_START
 
*NOTIFIER_CART_GET_PRODUCTS_END
 
*NOTIFIER_CART_GET_PRODUCTS_END
 
 
*NOTIFIER_CART_SHOW_TOTAL_START
 
*NOTIFIER_CART_SHOW_TOTAL_START
 
*NOTIFIER_CART_SHOW_TOTAL_END
 
*NOTIFIER_CART_SHOW_TOTAL_END
 
 
*NOTIFY_CART_USER_ACTION
 
*NOTIFY_CART_USER_ACTION
 
 
*NOTIFY_HEADER_START_SHOPPING_CART
 
*NOTIFY_HEADER_START_SHOPPING_CART
 
*NOTIFY_HEADER_END_SHOPPING_CART
 
*NOTIFY_HEADER_END_SHOPPING_CART
 +
  
 
==== Order Class: ====
 
==== Order Class: ====
Line 527: Line 681:
 
*NOTIFY_MODULE_START_META_TAGS
 
*NOTIFY_MODULE_START_META_TAGS
 
*NOTIFY_MODULE_END_META_TAGS
 
*NOTIFY_MODULE_END_META_TAGS
 +
*NOTIFY_MODULE_START_CREATE_ACCOUNT
 +
*NOTIFY_FAILURE_DURING_CREATE_ACCOUNT
 +
*NOTIFY_LOGIN_SUCCESS_VIA_CREATE_ACCOUNT
 +
*NOTIFY_MODULE_END_CREATE_ACCOUNT
  
==== Individual Pages (Header scripts): ====
 
  
* NOTIFY_HEADER_START_ACCOUNT_HISTORY
+
==== Checkout: ====
* NOTIFY_HEADER_END_ACCOUNT_HISTORY
+
*NOTIFY_CHECKOUT_PROCESS_BEGIN
 +
*NOTIFY_CHECKOUT_PROCESS_BEFORE_ORDER_TOTALS_PRE_CONFIRMATION_CHECK
 +
*NOTIFY_CHECKOUT_PROCESS_BEFORE_ORDER_TOTALS_PROCESS
 +
*NOTIFY_CHECKOUT_PROCESS_AFTER_ORDER_TOTALS_PROCESS
 +
*NOTIFY_CHECKOUT_PROCESS_AFTER_PAYMENT_MODULES_BEFOREPROCESS
 +
*NOTIFY_CHECKOUT_PROCESS_AFTER_ORDER_CREATE
 +
*NOTIFY_CHECKOUT_PROCESS_AFTER_PAYMENT_MODULES_AFTER_ORDER_CREATE
 +
*NOTIFY_CHECKOUT_PROCESS_AFTER_ORDER_CREATE_ADD_PRODUCTS
 +
*NOTIFY_CHECKOUT_PROCESS_AFTER_SEND_ORDER_EMAIL
 +
*NOTIFY_CHECKOUT_PROCESS_HANDLE_AFFILIATES
 +
 
 
*NOTIFY_HEADER_START_CHECKOUT_CONFIRMATION
 
*NOTIFY_HEADER_START_CHECKOUT_CONFIRMATION
 
*NOTIFY_HEADER_END_CHECKOUT_CONFIRMATION
 
*NOTIFY_HEADER_END_CHECKOUT_CONFIRMATION
 
*NOTIFY_HEADER_START_CHECKOUT_PAYMENT
 
*NOTIFY_HEADER_START_CHECKOUT_PAYMENT
 
*NOTIFY_HEADER_END_CHECKOUT_PAYMENT
 
*NOTIFY_HEADER_END_CHECKOUT_PAYMENT
 +
*NOTIFY_HEADER_START_CHECKOUT_PAYMENT_ADDRESS
 +
*NOTIFY_HEADER_END_CHECKOUT_PAYMENT_ADDRESS
 +
*NOTIFY_HEADER_START_CHECKOUT_PROCESS
 +
*NOTIFY_HEADER_END_CHECKOUT_PROCESS
 +
*NOTIFY_HEADER_START_CHECKOUT_SHIPPING
 +
*NOTIFY_HEADER_END_CHECKOUT_SHIPPING
 
*NOTIFY_HEADER_START_CHECKOUT_SHIPPING_ADDRESS
 
*NOTIFY_HEADER_START_CHECKOUT_SHIPPING_ADDRESS
 
*NOTIFY_HEADER_END_CHECKOUT_SHIPPING_ADDRESS
 
*NOTIFY_HEADER_END_CHECKOUT_SHIPPING_ADDRESS
 
*NOTIFY_HEADER_START_CHECKOUT_SUCCESS
 
*NOTIFY_HEADER_START_CHECKOUT_SUCCESS
 
*NOTIFY_HEADER_END_CHECKOUT_SUCCESS
 
*NOTIFY_HEADER_END_CHECKOUT_SUCCESS
 +
*NOTIFY_MODULE_START_CHECKOUT_NEW_ADDRESS
 +
*NOTIFY_MODULE_END_CHECKOUT_NEW_ADDRESS
 +
 +
 +
==== Individual Pages (Header scripts): ====
 +
 +
*NOTIFY_HEADER_START_ACCOUNT
 +
*NOTIFY_HEADER_END_ACCOUNT
 +
*NOTIFY_HEADER_START_ACCOUNT_EDIT
 +
*NOTIFY_HEADER_ACCOUNT_EDIT_UPDATES_COMPLETE
 +
*NOTIFY_HEADER_END_ACCOUNT_EDIT
 +
*NOTIFY_HEADER_START_ACCOUNT_HISTORY
 +
*NOTIFY_HEADER_END_ACCOUNT_HISTORY
 +
*NOTIFY_HEADER_START_ACCOUNT_HISTORY_INFO
 +
*NOTIFY_HEADER_END_ACCOUNT_HISTORY_INFO
 +
*NOTIFY_HEADER_START_ACCOUNT_NOTIFICATION
 +
*NOTIFY_HEADER_END_ACCOUNT_NOTIFICATION
 +
*NOTIFY_HEADER_START_ACCOUNT_PASSWORD
 +
*NOTIFY_HEADER_END_ACCOUNT_PASSWORD
 +
*NOTIFY_HEADER_START_ADDRESS_BOOK
 +
*NOTIFY_HEADER_END_ADDRESS_BOOK
 +
*NOTIFY_HEADER_START_ADDRESS_BOOK_PROCESS
 +
*NOTIFY_HEADER_ADDRESS_BOOK_DELETION_DONE
 +
*NOTIFY_HEADER_ADDRESS_BOOK_ENTRY_UPDATE_DONE
 +
*NOTIFY_HEADER_ADDRESS_BOOK_ADD_ENTRY_DONE
 +
*NOTIFY_HEADER_END_ADDRESS_BOOK_PROCESS
 +
 
*NOTIFY_HEADER_START_CREATE_ACCOUNT
 
*NOTIFY_HEADER_START_CREATE_ACCOUNT
 
*NOTIFY_FAILURE_DURING_CREATE_ACCOUNT
 
*NOTIFY_FAILURE_DURING_CREATE_ACCOUNT
 
*NOTIFY_LOGIN_SUCCESS_VIA_CREATE_ACCOUNT
 
*NOTIFY_LOGIN_SUCCESS_VIA_CREATE_ACCOUNT
 
*NOTIFY_HEADER_END_CREATE_ACCOUNT
 
*NOTIFY_HEADER_END_CREATE_ACCOUNT
 +
*NOTIFY_HEADER_START_CREATE_ACCOUNT_SUCCESS
 +
*NOTIFY_HEADER_END_CREATE_ACCOUNT_SUCCESS
  
 
*NOTIFY_MAIN_TEMPLATE_VARS_START_DOCUMENT_GENERAL_INFO
 
*NOTIFY_MAIN_TEMPLATE_VARS_START_DOCUMENT_GENERAL_INFO
Line 597: Line 799:
 
*NOTIFY_PRODUCT_TYPE_VARS_END_PRODUCT_FREE_SHIPPING_INFO
 
*NOTIFY_PRODUCT_TYPE_VARS_END_PRODUCT_FREE_SHIPPING_INFO
  
 +
*NOTIFY_HEADER_START_PRODUCT_INFO
 +
*NOTIFY_HEADER_END_PRODUCT_INFO
 
*NOTIFY_MAIN_TEMPLATE_VARS_START_PRODUCT_INFO
 
*NOTIFY_MAIN_TEMPLATE_VARS_START_PRODUCT_INFO
 
*NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_PRODUCT_INFO
 
*NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_PRODUCT_INFO
Line 617: Line 821:
  
  
==== PayPal: ====
+
==== PayPal&trade;: ====
  
 
*NOTIFY_PAYMENT_PAYPAL_RETURN_TO_STORE
 
*NOTIFY_PAYMENT_PAYPAL_RETURN_TO_STORE
Line 623: Line 827:
 
*NOTIFY_PAYMENT_PAYPAL_INSTALLED
 
*NOTIFY_PAYMENT_PAYPAL_INSTALLED
 
*NOTIFY_PAYMENT_PAYPAL_UNINSTALLED
 
*NOTIFY_PAYMENT_PAYPAL_UNINSTALLED
 +
  
 
==== Sideboxes: ====
 
==== Sideboxes: ====
Line 637: Line 842:
  
 
*NOTIFY_HEADER_START_ADVANCED_SEARCH_RESULTS
 
*NOTIFY_HEADER_START_ADVANCED_SEARCH_RESULTS
*NOTIFY_HEADER_END_ADVANCED_SEARCH_RESULTS
 
 
*NOTIFY_SEARCH_COLUMNLIST_STRING
 
*NOTIFY_SEARCH_COLUMNLIST_STRING
 
*NOTIFY_SEARCH_SELECT_STRING
 
*NOTIFY_SEARCH_SELECT_STRING
Line 643: Line 847:
 
*NOTIFY_SEARCH_WHERE_STRING
 
*NOTIFY_SEARCH_WHERE_STRING
 
*NOTIFY_SEARCH_ORDERBY_STRING
 
*NOTIFY_SEARCH_ORDERBY_STRING
 +
*NOTIFY_HEADER_END_ADVANCED_SEARCH_RESULTS
 +
 +
 +
==== EZ-Pages: ====
 +
*NOTIFY_START_EZPAGES_FOOTERBAR
 +
*NOTIFY_END_EZPAGES_FOOTERBAR
 +
*NOTIFY_START_EZPAGES_HEADERBAR
 +
*NOTIFY_END_EZPAGES_HEADERBAR

Latest revision as of 15:32, 2 July 2016

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:


 $autoLoadConfig[0][] = array('autoType'=>'require', 'loadFile'=> DIR_WS_INCLUDES . 'somefile.php'); 


The autotype parameter tells us that we just want to require a file


The loadFile parameter tells us which file we want to load.


Loading function files can also obviously be done using the above.


Similarly if we want to 'include' a file:


 $autoLoadConfig[0][] = array('autoType'=>'include', 'loadFile'=> DIR_WS_INCLUDES . 'somefile.php'); 


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.


 $autoLoadConfig[] = array(array('autoType'=>'init_script', 'loadFile'=> 'init_database.php'));


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.


 $autoLoadConfig[0][] = array('autoType'=>'class',
                               'loadFile'=>'shopping_cart.php');
 $autoLoadConfig[30][] = array('autoType'=>'classInstantiate',
                               'className'=>'cache',
                               'objectName'=>'zc_cache');
 $autoLoadConfig[80][] = array('autoType'=>'classInstantiate',
                               'className'=>'shoppingCart',
                               'objectName'=>'cart',
                               'checkInstantiated'=>true,
                               'classSession'=>true);
 $autoLoadConfig[120][] = array('autoType'=>'objectMethod',
                               'objectName'=>'navigation',
                               'methodName' => 'add_current_page');


Taking these options one by one


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.


consider this


 $autoLoadConfig[0][] = array('autoType'=>'class',
                              'loadFile'=>'class.base.php');


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:


 $autoLoadConfig[0][] = array('autoType'=>'class',
                              'loadFile'=>'split_page_results.php',
                              'classPath'=>DIR_WS_CLASSES);


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_add_crumbs.php (Responsible for initialising the Breadcrumb)
  • init_cart_handler.php (Responsible for handling Cart actions)
  • init_category_path.php (Reponsible for initialising Category Paths)
  • init_currencies.php (Responsible for initialising the Currencies Sub-System)
  • init_customer_auth.php (Responsible for checking customer status, either thru Down for Maintenance or the Approval level)
  • init_database.php (Responsible for initialising the DB layer)
  • init_db_config_read.php (Responsible for reading configuration data from database)
  • init_file_db_names.php (Responsible for loading File and Database tablename Defines)
  • init_general_funcs.php (Resposible for loading general functions from the includes/functions directory as well as the extra_functions folder)
  • init_gzip.php (Responsible for loading Gzip output-buffering functions)
  • init_header.php (Responsible for running page-header procedures)
  • init_languages.php (Responsible for loading multiple-language support sub-system)
  • init_sanitize.php (Responsible for loading input-sanitising code)
  • init_sefu.php (Responsible for loading code to provide search-engine-friendly URLs)
  • init_sessions.php (Responsible for loading Session code)
  • init_special_funcs.php (Responsible for loading specialized but necessary functions)
  • 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


define('DEBUG_AUTOLOAD', false);
define('IS_ADMIN_FLAG', false);
define('PAGE_PARSE_START_TIME', microtime());
@ini_set("arg_separator.output","&");
if (file_exists('includes/local/configure.php')) {
  include('includes/local/configure.php');
}
if (defined('STRICT_ERROR_REPORTING') && STRICT_ERROR_REPORTING == true) {
  error_reporting(E_ALL);
} else {
  error_reporting(E_ALL & ~E_NOTICE);
}
if (file_exists('includes/configure.php')) {
  include('includes/configure.php');
} else {
  header('location: zc_install/index.php');
}
if (!is_dir(DIR_FS_CATALOG.'/includes/classes'))  header('location: zc_install/index.php');
if ($za_dir = @dir(DIR_WS_INCLUDES . 'extra_configures')) {
  while ($zv_file = $za_dir->read()) {
    if (preg_match('/\.php$/', $zv_file) > 0) {
      include(DIR_WS_INCLUDES . 'extra_configures/' . $zv_file);
    }
  }
} 
$loader_file = 'config.core.php';
$base_dir = DIR_WS_INCLUDES . 'auto_loaders/';
if (file_exists(DIR_WS_INCLUDES . 'auto_loaders/overrides/' . $loader_file)) {
  $base_dir = DIR_WS_INCLUDES . 'auto_loaders/overrides/';
}
include($base_dir . $loader_file);
if ($loader_dir = dir(DIR_WS_INCLUDES . 'auto_loaders')) {
  while ($loader_file = $loader_dir->read()) {
    if ((preg_match('/^config\./', $loader_file) > 0) && (preg_match('/\.php$/', $loader_file) > 0)) {
      if ($loader_file != 'config.core.php') {
        $base_dir = DIR_WS_INCLUDES . 'auto_loaders/';
        if (file_exists(DIR_WS_INCLUDES . 'auto_loaders/overrides/' . $loader_file)) {
          $base_dir = DIR_WS_INCLUDES . 'auto_loaders/overrides/';
        }
        include($base_dir . $loader_file);
      }
    }
  }
}
if (( (!file_exists('includes/configure.php') && !file_exists('includes/local/configure.php')) ) || (DB_TYPE == ) ||    (!file_exists('includes/classes/db/' .DB_TYPE . '/query_factory.php'))) {
  header('location: zc_install/index.php');
  exit;
}
require('includes/autoload_func.php');
require(DIR_WS_INCLUDES . 'counter.php');
$customers_ip_address = $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION['customers_ip_address'])) {
  $_SESSION['customers_ip_address'] = $customers_ip_address;
}


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?


 <?php
 class myObserver extends base {
   function __construct() {
     $this->attach($this, array('NOTIFIER_CART_ADD_CART_END'));
   }
...
 }


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:


 class myObserver extends base {
   function __construct() {
     $this->attach($this, array('NOTIFIER_CART_ADD_CART_END'));
   }
   function update(&$callingClass, $notifier, $paramsArray) {
     ... do some stuff
   }
 }


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:


 class myObserver extends base {
   function __construct() {
     global $zco_notifier;
     $zco_notifier->attach($this, array('NOTIFY_HEADER_END_CHECKOUT_CONFIRMATION'));
   }

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


<?php

$autoLoadConfig[10][] = array('autoType'=>'class',
                              'loadFile'=>'observers/class.freeProduct.php');
$autoLoadConfig[90][] = array('autoType'=>'classInstantiate',
                              'className'=>'freeProduct',
                              'objectName'=>'freeProduct');

?>

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.


Here's the code.

<?php
/**
 * Observer class used to add a free product to the cart if the user spends more than $x
 *
 */
class freeProduct extends base {
  /**
   * The threshold amount the customer needs to spend.
   * 
   * Note this is defined in the shops base currency, and so works with multi currency shops
   *
   * @var decimal
   */
  var $freeAmount = 50;
  /**
   * The id of the free product.
   * 
   * Note. This must be a true free product. e.g. price = 0 Also make sure that if you don't want the customer
   * to be charged shipping on this, that you have it set correctly.
   *
   * @var integer
   */
  var $freeProductID = 57;
  /**
   * constructor method
   * 
   * Attaches our class to the $_SESSION['cart'] class and watches for 2 notifier events.
   */
  function __construct() {
    $_SESSION['cart']->attach($this, array('NOTIFIER_CART_ADD_CART_END', 'NOTIFIER_CART_REMOVE_END'));
  }
  /**
   * Update Method
   * 
   * Called by observed class when any of our notifiable events occur
   *
   * @param object $class
   * @param string $eventID
   */
  function update(&$class, $eventID, $paramsArray = array()) {
  if ($eventID == 'NOTIFIER_CART_REMOVE_END' && (isset($_SESSION['freeProductInCart']) && $_SESSION['freeProductInCart'] == TRUE ))
  {
    if (!$_SESSION['cart']->in_cart($this->freeProductID))
    {
      $_SESSION['userRemovedFreeProduct'] = TRUE;
    }
  }
  if (!isset($_SESSION['userRemovedFreeProduct']) || $_SESSION['userRemovedFreeProduct'] != TRUE) 
  {
    if ($_SESSION['cart']->show_total() >= $this->freeAmount && !$_SESSION['cart']->in_cart($this->freeProductID) )   
    {
      $_SESSION['cart']->add_cart($this->freeProductID);
      $_SESSION['freeProductInCart'] = TRUE;  
    }
  }
 
  if ($_SESSION['cart']->show_total() < $this->freeAmount && $_SESSION['cart']->in_cart($this->freeProductID) ) 
  {
    $_SESSION['cart']->remove($this->freeProductID);
  }
  if ($_SESSION['cart']->in_cart($this->freeProductID)) 
  {
    $_SESSION['cart']->contents[$this->freeProductID]['qty'] = 1;
  }
   
  }  
}
?>


A couple notes:


First, I have set the options for the system in the class itself. This is obviously a bad idea, and it would be much better to have an admin module to set these options.


Second, notice that we are actually watching for two events in the one class.


   $_SESSION['cart']->attach($this, array('NOTIFIER_CART_ADD_CART_END', 'NOTIFIER_CART_REMOVE_END'));


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.


Secondly this will probably produce a confusing output at checkout.


Third: Have not tested for tax compliance yet ( @TODO )

Notifiers currently set in Zen Cart

Notifier points for Zen Cart 1.3.7


Shopping Cart class

  • NOTIFIER_CART_INSTANTIATE_START
  • NOTIFIER_CART_INSTANTIATE_END
  • NOTIFIER_CART_RESTORE_CONTENTS_START
  • NOTIFIER_CART_RESTORE_CONTENTS_END
  • NOTIFIER_CART_RESET_START
  • NOTIFIER_CART_RESET_END
  • NOTIFIER_CART_ADD_CART_START
  • NOTIFIER_CART_ADD_CART_END
  • NOTIFIER_CART_UPDATE_QUANTITY_START
  • NOTIFIER_CART_UPDATE_QUANTITY_END
  • NOTIFIER_CART_CLEANUP_START
  • NOTIFIER_CART_CLEANUP_END
  • NOTIFIER_CART_COUNT_CONTENTS_START
  • NOTIFIER_CART_COUNT_CONTENTS_END
  • NOTIFIER_CART_GET_QUANTITY_START
  • NOTIFIER_CART_GET_QUANTITY_END_QTY
  • NOTIFIER_CART_GET_QUANTITY_END_FALSE
  • NOTIFIER_CART_IN_CART_START
  • NOTIFIER_CART_IN_CART_END_TRUE
  • NOTIFIER_CART_IN_CART_END_FALSE
  • NOTIFIER_CART_REMOVE_START
  • NOTIFIER_CART_REMOVE_END
  • NOTIFIER_CART_REMOVE_ALL_START
  • NOTIFIER_CART_REMOVE_ALL_END
  • NOTIFIER_CART_GET_PRODUCTS_START
  • NOTIFIER_CART_GET_PRODUCTS_END
  • NOTIFIER_CART_SHOW_TOTAL_START
  • NOTIFIER_CART_SHOW_TOTAL_END
  • NOTIFY_CART_USER_ACTION
  • NOTIFY_HEADER_START_SHOPPING_CART
  • NOTIFY_HEADER_END_SHOPPING_CART


Order Class:

  • NOTIFY_ORDER_PROCESSING_STOCK_DECREMENT_BEGIN
  • NOTIFY_ORDER_PROCESSING_STOCK_DECREMENT_END
  • NOTIFY_ORDER_PROCESSING_CREDIT_ACCOUNT_UPDATE_BEGIN
  • NOTIFY_ORDER_PROCESSING_ATTRIBUTES_BEGIN
  • NOTIFY_ORDER_PROCESSING_ONE_TIME_CHARGES_BEGIN


Email:

  • NOTIFY_EMAIL_AFTER_EMAIL_FORMAT_DETERMINED
  • NOTIFY_EMAIL_BEFORE_PROCESS_ATTACHMENTS
  • NOTIFY_EMAIL_AFTER_PROCESS_ATTACHMENTS
  • NOTIFY_EMAIL_AFTER_SEND (individual email)
  • NOTIFY_EMAIL_AFTER_SEND_ALL_SPECIFIED_ADDRESSES (full batch)


Modules:

  • NOTIFY_MODULE_START_META_TAGS
  • NOTIFY_MODULE_END_META_TAGS
  • NOTIFY_MODULE_START_CREATE_ACCOUNT
  • NOTIFY_FAILURE_DURING_CREATE_ACCOUNT
  • NOTIFY_LOGIN_SUCCESS_VIA_CREATE_ACCOUNT
  • NOTIFY_MODULE_END_CREATE_ACCOUNT


Checkout:

  • NOTIFY_CHECKOUT_PROCESS_BEGIN
  • NOTIFY_CHECKOUT_PROCESS_BEFORE_ORDER_TOTALS_PRE_CONFIRMATION_CHECK
  • NOTIFY_CHECKOUT_PROCESS_BEFORE_ORDER_TOTALS_PROCESS
  • NOTIFY_CHECKOUT_PROCESS_AFTER_ORDER_TOTALS_PROCESS
  • NOTIFY_CHECKOUT_PROCESS_AFTER_PAYMENT_MODULES_BEFOREPROCESS
  • NOTIFY_CHECKOUT_PROCESS_AFTER_ORDER_CREATE
  • NOTIFY_CHECKOUT_PROCESS_AFTER_PAYMENT_MODULES_AFTER_ORDER_CREATE
  • NOTIFY_CHECKOUT_PROCESS_AFTER_ORDER_CREATE_ADD_PRODUCTS
  • NOTIFY_CHECKOUT_PROCESS_AFTER_SEND_ORDER_EMAIL
  • NOTIFY_CHECKOUT_PROCESS_HANDLE_AFFILIATES
  • NOTIFY_HEADER_START_CHECKOUT_CONFIRMATION
  • NOTIFY_HEADER_END_CHECKOUT_CONFIRMATION
  • NOTIFY_HEADER_START_CHECKOUT_PAYMENT
  • NOTIFY_HEADER_END_CHECKOUT_PAYMENT
  • NOTIFY_HEADER_START_CHECKOUT_PAYMENT_ADDRESS
  • NOTIFY_HEADER_END_CHECKOUT_PAYMENT_ADDRESS
  • NOTIFY_HEADER_START_CHECKOUT_PROCESS
  • NOTIFY_HEADER_END_CHECKOUT_PROCESS
  • NOTIFY_HEADER_START_CHECKOUT_SHIPPING
  • NOTIFY_HEADER_END_CHECKOUT_SHIPPING
  • NOTIFY_HEADER_START_CHECKOUT_SHIPPING_ADDRESS
  • NOTIFY_HEADER_END_CHECKOUT_SHIPPING_ADDRESS
  • NOTIFY_HEADER_START_CHECKOUT_SUCCESS
  • NOTIFY_HEADER_END_CHECKOUT_SUCCESS
  • NOTIFY_MODULE_START_CHECKOUT_NEW_ADDRESS
  • NOTIFY_MODULE_END_CHECKOUT_NEW_ADDRESS


Individual Pages (Header scripts):

  • NOTIFY_HEADER_START_ACCOUNT
  • NOTIFY_HEADER_END_ACCOUNT
  • NOTIFY_HEADER_START_ACCOUNT_EDIT
  • NOTIFY_HEADER_ACCOUNT_EDIT_UPDATES_COMPLETE
  • NOTIFY_HEADER_END_ACCOUNT_EDIT
  • NOTIFY_HEADER_START_ACCOUNT_HISTORY
  • NOTIFY_HEADER_END_ACCOUNT_HISTORY
  • NOTIFY_HEADER_START_ACCOUNT_HISTORY_INFO
  • NOTIFY_HEADER_END_ACCOUNT_HISTORY_INFO
  • NOTIFY_HEADER_START_ACCOUNT_NOTIFICATION
  • NOTIFY_HEADER_END_ACCOUNT_NOTIFICATION
  • NOTIFY_HEADER_START_ACCOUNT_PASSWORD
  • NOTIFY_HEADER_END_ACCOUNT_PASSWORD
  • NOTIFY_HEADER_START_ADDRESS_BOOK
  • NOTIFY_HEADER_END_ADDRESS_BOOK
  • NOTIFY_HEADER_START_ADDRESS_BOOK_PROCESS
  • NOTIFY_HEADER_ADDRESS_BOOK_DELETION_DONE
  • NOTIFY_HEADER_ADDRESS_BOOK_ENTRY_UPDATE_DONE
  • NOTIFY_HEADER_ADDRESS_BOOK_ADD_ENTRY_DONE
  • NOTIFY_HEADER_END_ADDRESS_BOOK_PROCESS
  • NOTIFY_HEADER_START_CREATE_ACCOUNT
  • NOTIFY_FAILURE_DURING_CREATE_ACCOUNT
  • NOTIFY_LOGIN_SUCCESS_VIA_CREATE_ACCOUNT
  • NOTIFY_HEADER_END_CREATE_ACCOUNT
  • NOTIFY_HEADER_START_CREATE_ACCOUNT_SUCCESS
  • NOTIFY_HEADER_END_CREATE_ACCOUNT_SUCCESS
  • NOTIFY_MAIN_TEMPLATE_VARS_START_DOCUMENT_GENERAL_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_DOCUMENT_GENERAL_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_EXTRA_DOCUMENT_GENERAL_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_END_DOCUMENT_GENERAL_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_START_DOCUMENT_GENERAL_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_END_DOCUMENT_GENERAL_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_START_DOCUMENT_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_DOCUMENT_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_EXTRA_DOCUMENT_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_END_DOCUMENT_PRODUCT_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_START_DOCUMENT_PRODUCT_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_END_DOCUMENT_PRODUCT_INFO
  • NOTIFY_HEADER_START_DOWNLOAD
  • NOTIFY_DOWNLOAD_VIA_SYMLINK___BEGINS
  • NOTIFY_DOWNLOAD_WITHOUT_REDIRECT___COMPLETED
  • NOTIFY_DOWNLOAD_WITHOUT_REDIRECT_VIA_CHUNKS___COMPLETED
  • NOTIFY_HEADER_END_DOWNLOAD
  • NOTIFY_HEADER_START_GV_FAQ
  • NOTIFY_HEADER_END_GV_FAQ
  • NOTIFY_HEADER_START_GV_SEND
  • NOTIFY_HEADER_END_GV_SEND
  • NOTIFY_HEADER_START_INDEX
  • NOTIFY_HEADER_END_INDEX
  • NOTIFY_HEADER_START_INDEX_MAIN_TEMPLATE_VARS
  • NOTIFY_HEADER_INDEX_MAIN_TEMPLATE_VARS_RELEASE_PRODUCT_TYPE_VARS
  • NOTIFY_HEADER_END_INDEX_MAIN_TEMPLATE_VARS
  • NOTIFY_HEADER_START_LOGIN
  • NOTIFY_LOGIN_SUCCESS
  • NOTIFY_LOGIN_FAILURE
  • NOTIFY_HEADER_END_LOGIN
  • NOTIFY_HEADER_START_LOGOFF
  • NOTIFY_HEADER_END_LOGOFF
  • NOTIFY_HEADER_START_EZPAGE
  • NOTIFY_HEADER_END_EZPAGE
  • NOTIFY_HEADER_START_PAGE_NOT_FOUND
  • NOTIFY_HEADER_END_PAGE_NOT_FOUND
  • NOTIFY_HEADER_START_PASSWORD_FORGOTTEN
  • NOTIFY_HEADER_END_PASSWORD_FORGOTTEN
  • NOTIFY_MAIN_TEMPLATE_VARS_START_PRODUCT_FREE_SHIPPING_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_PRODUCT_FREE_SHIPPING_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_EXTRA_PRODUCT_FREE_SHIPPING_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_END_PRODUCT_FREE_SHIPPING_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_START_PRODUCT_FREE_SHIPPING_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_END_PRODUCT_FREE_SHIPPING_INFO
  • NOTIFY_HEADER_START_PRODUCT_INFO
  • NOTIFY_HEADER_END_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_START_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_EXTRA_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_END_PRODUCT_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_START_PRODUCT_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_END_PRODUCT_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_START_PRODUCT_MUSIC_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_PRODUCT_TYPE_VARS_PRODUCT_MUSIC_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_EXTRA_PRODUCT_MUSIC_INFO
  • NOTIFY_MAIN_TEMPLATE_VARS_END_PRODUCT_MUSIC_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_START_PRODUCT_MUSIC_INFO
  • NOTIFY_PRODUCT_TYPE_VARS_END_PRODUCT_MUSIC_INFO
  • NOTIFY_HEADER_START_SITE_MAP
  • NOTIFY_HEADER_END_SITE_MAP
  • NOTIFY_HEADER_START_UNSUBSCRIBE
  • NOTIFY_HEADER_END_UNSUBSCRIBE


PayPal™:

  • NOTIFY_PAYMENT_PAYPAL_RETURN_TO_STORE
  • NOTIFY_PAYMENT_PAYPAL_CANCELLED_DURING_CHECKOUT
  • NOTIFY_PAYMENT_PAYPAL_INSTALLED
  • NOTIFY_PAYMENT_PAYPAL_UNINSTALLED


Sideboxes:

  • NOTIFY_SIDEBOX_START_EZPAGES_SIDEBOX
  • NOTIFY_SIDEBOX_END_EZPAGES_SIDEBOX
  • NOTIFY_HEADER_START_EZPAGES_HEADER
  • NOTIFY_HEADER_END_EZPAGES_HEADER
  • NOTIFY_FOOTER_START_EZPAGES_FOOTER
  • NOTIFY_FOOTER_END_EZPAGES_FOOTER


Search:

  • NOTIFY_HEADER_START_ADVANCED_SEARCH_RESULTS
  • NOTIFY_SEARCH_COLUMNLIST_STRING
  • NOTIFY_SEARCH_SELECT_STRING
  • NOTIFY_SEARCH_FROM_STRING
  • NOTIFY_SEARCH_WHERE_STRING
  • NOTIFY_SEARCH_ORDERBY_STRING
  • NOTIFY_HEADER_END_ADVANCED_SEARCH_RESULTS


EZ-Pages:

  • NOTIFY_START_EZPAGES_FOOTERBAR
  • NOTIFY_END_EZPAGES_FOOTERBAR
  • NOTIFY_START_EZPAGES_HEADERBAR
  • NOTIFY_END_EZPAGES_HEADERBAR