Events (Script Notifications) in OpenCart 2.2.x. What Has Changed?

The event system in OpenCart was first introduced in version 2.0.0.0. It was one of the major changes in OpenCart’s core. In OpenCart 2.2.0.0, the event system evolved, and it has a more powerful and cleaner implementation now. However, the changes are not really backwards compatible, so the extensions which make use of it, will have to be updated.

This article aims to explain the changes and help the developers. We will be doing a simple extension which replaces the ?route=* part of a url and encodes the rest of the GET parameters as /key:value. At the end, our extension will be rewriting links like:

domain.com/index.php?route=product/category&path=25_28 to

domain.com/product/category/path:25_28, which look prettier. The extension is called “Event SEO”, which is not the best name probably, but hey, this is just an example. :)

What has changed

In the first implementation, the events triggers were explicitly placed on the appropriate places in OpenCart’s code. In 2.2.x this is changed to a more general approach, using the interceptors pattern. Events are triggered before and after each controller and model methods, on loading of views, config file loading and language file loading. Since this is true for all controllers and models the event names have also changed. They look pretty much like routes now. For example, when the home page is being loaded via the common/home controller, the following 2 events are emitted: common/home/before and common/home/after.

Key takeouts:

The old events (e.g post.admin.product.add) are no longer being fired. (Click to Tweet)

The new OpenCart 2.2.x events have a route-like style. (Click to Tweet)

The model methods for adding/removing an event handler are the same as before. (Click to Tweet)

Building our extension

We will need 3 files for this extension:

  • 1 admin controller

  • 1 catalog controller

  • 1 language file for the admin

The file structure looks like this:

*Notice the change in the default language. Languages no longer have a separate directory setting, so the language code is used for its directory name. The code for the default language is “en-gb”, so this is why we put our language file in that directory.

Let’s start with the simplest file - the language one. It looks like this:

<?php

$_['heading_title'] = "Event SEO";

There is nothing much going on here. We just set the display name for the extension. This is what will be displayed on the modules page.

Moving on to the admin controller file. It will be used only to register our event handlers when the extension is installed and remove them when it is uninstalled. Here is the code for this controller:

<?php

class ControllerModuleEventSeo extends Controller {
   public function index() {
       $this->response->redirect($this->request->server['HTTP_REFERER']);
   }

   public function install() {
       $this->load->model('extension/event');
       $this->model_extension_event->addEvent('event_seo', 'catalog/controller/*/before', 'module/event_seo/decode');
       $this->model_extension_event->addEvent('event_seo', 'catalog/model/*/before', 'module/event_seo/add_url_rewrite');
   }

   public function uninstall() {
       $this->load->model('extension/event');
       $this->model_extension_event->deleteEvent('event_seo');
   }
}

Let’s take a quick look at the code. The index method has a single line of code:

$this->response->redirect($this->request->server['HTTP_REFERER']);

Since we have no config panel for this extension, this just redirects the user back to where he was, before trying to edit the extension. This is usually the modules page.

The install method is where we register our event handlers. In this case there are 2 of them. The first one:

$this->model_extension_event->addEvent('event_seo', 'catalog/controller/*/before', 'module/event_seo/decode');

is the one for our URL decoding function.

We need the second one:

$this->model_extension_event->addEvent('event_seo', 'catalog/model/*/before', 'module/event_seo/add_url_rewrite');

in order to register our URL rewriter object.

Notice the change in the event name notation. The ‘/’ separator is used instead of ‘.’ now. The general form of an event handler is as follows: {app}/{route}/{position}.

{app} can be either admin or catalog.

{route} is the route to the position where you would like to listen for events. It can be basically anything that can be loaded using OpenCart’s loader. For example, if you call $this->load->model(‘catalog/product’), the base route for this will be model/catalog/product.

{position} can be either before or after.

There are 2 more very important things to note about the routes:

  1. You can use a wildcard (*) to match more than one routes. For example model/catalog/* will match all events for the catalog models.

  2. You can match against specific methods. For example model/catalog/product/addProduct allows you to execute code either before or after a product has been added. This is actually what you need to pay attention to, because this is how you replace the old style events, with the new style. Suppose you had an event handler for the “post.admin.product.add”. In order to make it compatible with the new OpenCart version you need to change this to “admin/model/catalog/product/addProduct/after”. The rest of your code should be fine.

Back to our example. Maybe you are wondering why we have chosen exactly these event triggers? Well we need to add our URL rewriter as early as possible, so we register our function, which does this, to the catalog/model/*/before event. This way our function will execute on whatever model event happens. We will apply some simple logic in the catalog controller, to make sure we add our rewriter only once. So far so good. We found the best place to register our rewriter function. We now need to find a way to decode these URL upon request. Luckily we can apply almost the same logic here. We will invoke the decode function on the first catalog/controller/*/before event. It will try to parse the URL, and if can’t do it a page not found will be displayed. This is all happening in the catalog controller of our extension, so here is how it looks:

<?php

class ControllerModuleEventSeo extends Controller {
   public static $is_route_decoded = false;
   public static $is_rewrite_added = false;

   public function add_url_rewrite($route, $args) {
       if (!self::$is_rewrite_added) {
           $this->url->addRewrite($this);
           self::$is_rewrite_added = true;
       }
   }

   public function decode($route, $args) {
       if (self::$is_route_decoded || !isset($this->request->get['_route_']) || (isset($this->request->get['route']) && $this->request->get['route'] != 'error/not_found')) return;

       $parts = preg_split('@(?<!/fs)/(?!fs/)@', $this->request->get['_route_']);
       $route = array();
       $is_route_set = false;

       foreach ($parts as $part) {
           if (!$is_route_set && strpos($part, ':') !== false) {
               $this->request->get['route'] = implode('/', $route);
               $is_route_set = true;
           }

           if ($is_route_set) {
               $query_var_parts = explode(':', str_replace('/fs/', '/', $part));
               $query_var_name = array_shift($query_var_parts);
               $this->request->get[$query_var_name] = implode(':', $query_var_parts);
           } else {
               $route[] = $part;
           }
       }

       if (!$is_route_set) {
           $this->request->get['route'] = implode('/', $route);
           $is_route_set = true;
       }

       self::$is_route_decoded = true;
       $action = new Action($this->request->get['route']);
       $result = $action->execute($this->registry, array($args));

       if ($result) {
           return $result;
       } else if ($this->response->getOutput()) {
           $this->response->output();exit;
       }
   }

   public function rewrite($link) {
       if (strpos($link, 'index.php?route=') === false) return $link;

       $url_info = parse_url(str_replace('&amp;', '&', $link));
       if (empty($url_info['query'])) return $link;

       $url = '';
       $data = array();
       parse_str($url_info['query'], $data);

       foreach ($data as $key=>$value) {
           if ($key == 'route') {
               $url .= '/' . $value;
               continue;
           }

           $url .= '/' . $key . ':' . str_replace('/', '/fs/', $value);
       }

       return $url_info['scheme'] . '://' . $url_info['host'] . (isset($url_info['port']) ? ':' . $url_info['port'] : '') . str_replace('/index.php', '', $url_info['path']) . $url;
   }
}

A lot is happening here, but we will only make a quick overview, since the actual URL rewrite and decode logic is not important here. So there are only 3 methods here.

The first one, add_url_rewrite($route, $args), is used to register our object as a URL rewriter. We use static properties of the class make sure we are only executing this function’s logic once. If we were to use standard class properties, we would not be able to achieve this effect, since OpenCart creates a new instance of this class, each time it has to invoke our event handlers. This means that if the event catalog/model/*/before is fired 10 times, for example, there will be 10 separate objects of our class and none of them will be able to tell whether the function has already been executed or not.

Note: In order for an object to be able to work as a rewriter, it must have a rewrite($link) method. This is the third method in our object. It is called internally by the Url::link() function. So every time there is $this->url->link() call in the code, our rewriter will be invoked. It receives the generated URL and is expected to return the rewritten form of it.

The second one, decode($route, $args), is responsible for understanding the URL and invoking the appropriate OpenCart controller. The interesting part of this method are the final 4 lines of code:

       if ($result) {
           return $result;
       } else if ($this->response->getOutput()) {
           $this->response->output();exit;
       }

It does not look interesting at first, but notice that there is a return statement there. This is interesting because your event handler functions can now return strings and if they do, this will be used as the output of the actual controller. The controller will not even be invoked. This way you can override entire modules, or default pages like product/product for example. In order to make this more clear, take a look at the method which executes a controller according to a route:

I have separated this into 3 parts.

The first part is where the “before” event handlers are being invoked. There are 2 interesting moments here.

  1. The arguments $route and $data are passed by reference. This means that we can modify them in our event handlers.

  2. In case some of the event handlers returns anything, everything will stop here and the $result will be returned to the parent function. Note that it is expected that your event handlers return strings here.

The second part is where the controller for the $route is executed. At this point our “before” event handler may have changed that $route from its original value, so the controller for the new $route will be invoked instead of the one which was intended to be executed at the beginning. There will be a small example on how to swap column left and column right at the end of the article.

Lastly the third part is where the “after” event handlers are being invoked. The thing to note here is that these event handlers receive a third argument, which is the $output of the controller executed in step 2. Your event handlers can modify this. This is very useful, especially if you are creating themes. You can alter the default modules and widgets in a non-obtrusive way. There will be a small example on how to remove the “Powered By” text at the end of the article.

The third method rewrite($link), is responsible for rewriting the URLs. It simply takes a link in the form of a string as its only argument, and is expected to return a string which will be the new version of this link. It may as well return the very same link if no changes can be made.

This is it. Using the new events system we were able to create an extension, which generates better looking URLs, without any vQmod or OCMOD involved. This is really something we are excited about. It makes your extensions easier to maintain and less likely to fail. It is the preferred method over OCMOD or vQmod, because in the case of lots of extensions present in the system, the chance that some of them will fail to work is really small. On the other hand, when using OCMOD or vQmod, this chance is very high. The code for this extension can be found in GitHub https://github.com/iSenseLabs/demo-event-seo. There is also an installable ZIP ready for the Extension Installer, so you can easily give the extension a try.

Fun with the events

Swapping column left and column right

We can swap the 2 column really easy by registering 2 simple event handlers, one for catalog/controller/common/column_left/before and one for catalog/controller/common/column_right/before. Suppose our module is called “mymodule”. The code to register these event handlers on installation will look like this:

$this->model_extension_event->addEvent('mymodule', 'catalog/controller/common/column_left/before', 'module/mymodule/use_column_right');
$this->model_extension_event->addEvent('mymodule, 'catalog/controller/common/column_right/before', 'module/mymodule/use_column_left');

Then the catalog controller will have the following 2 methods:

public function use_column_left(&$route, $args) {
   $route = 'common/column_left';
}

public function use_column_right(&$route, $args) {
   $route = 'common/column_right';
}

As easy as that, column left and column right will be swapped in the front-end of the store.

Removing the Powered By string

We can also use the event system to get rid of the Powered By text in the footer. All we need is an event handler after the footer. Suppose that we are building an extension called “mymodule”, then the code to register the event handler will look like this:

$this->model_extension_event->addEvent('mymodule', 'catalog/controller/common/footer/after', 'module/mymodule/remove_powered_by');

And the catalog controller will have the following method:

public function remove_powered_by($route, $args, &$output) {
   $output = str_replace('Powered By <a href="http://www.opencart.com">OpenCart</a><br /> ', '', $output);
}

Again, it is really simple and clean and the end result is much more manageable compared to doing the same using OCMOD or vQmod.

Let us know if this has been helpful and if you have any questions, feel free to post them in the comments section below. Happy coding! :)

Join 11,000+ subscribers receiving actionable E-commerce advice

* Unsubscribe any time

Trending blogs

comments powered by Disqus