Decoding the Proxy Class in OpenCart

More often than not, we take things for granted. If something is working as expected, we don't bother about the inner workings of it to understand the underlying mechanism. Or to put it another way, we don't dig into something until we're in some sort of trouble!

Similarly, I was always wondering about a couple of concepts in OpenCart that were used in the underlying framework, and one of them was the Proxy class. It took me a while to understand it, and I thought it's great stuff to share with you as it's always to fun to know something new.

What Is a Proxy Class?

Although you'll find various materials online that define the term proxy, the definition from Wikipedia is striking and easy to understand.

A proxy, in its most general form, is a class functioning as an interface to something else.

So the proxy delegates the control to the object it intends to use, and thus acts on behalf of the actual class that's instantiated. In fact, the proxy design pattern is a very popular pattern that's used by popular frameworks as needed. Considering the fact that a discussion of the proxy method in itself is such a wide topic and out of the scope of this article, I'll quickly summarize what it's used for most of the time:

• Act as a wrapper to provide additional functionality.
• Delay the instantiation of expensive objects, also referred as lazy loading.

In the context of OpenCart, we could say that the proxy pattern is used to add functionality to the base proxy class. Having said that, the base proxy class itself doesn't provide anything except a couple of magic methods! As we'll see in the next section, the proxy class is enriched at run-time by attaching properties to it.

Before we move into the next section, let's have a quick look at the proxy class. It resides in system/engine/proxy.php.

 1 {$key};  5  }  6   7  public function __set($key, $value) {  8 $this->{$key} =$value;  9  }  10   11  public function __call($key,$args) {  12  $arg_data = array();  13   14 $args = func_get_args();  15   16  foreach ($args as$arg) {  17  if ($arg instanceof Ref) {  18 $arg_data[] =& $arg->getRef();  19  } else {  20 $arg_data[] =& $arg;  21  }  22  }  23   24  if (isset($this->{$key})) {  25  return call_user_func_array($this->{$key},$arg_data);  26  } else {  27  $trace = debug_backtrace();  28   29  exit('Notice: Undefined property: Proxy::' .$key . ' in ' . $trace[1]['file'] . ' on line ' .$trace[1]['line'] . '');  30  }  31  }  32 } 

As you can see, it implements three magic methods: __get(), __set(), and __call(). Among them, the __call() method implementation is an important one, and we'll get back to it pretty soon.

How the Proxy Class Works With the Model

In this section, I'll explain how exactly a call like $this->model_catalog_category->getCategory($category_id) works out of the box.

In fact, the story begins with the following statement.

 1 $this->load->model('catalog/category');  During bootstrapping, the OpenCart framework stores all generic objects to the Registry object so that they can be fetched at will. As a result of that, the $this->load call returns the Loader object from the Registry.

The Loader class provides various methods to load different components, but what we're interested in here is the model method. Let's quickly pull the snippet of the model method from system/engine/loader.php.

 1 public function model($route) {  2  // Sanitize the call  3 $route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route);  4   5  // Trigger the pre events  6 $this->registry->get('event')->trigger('model/' . $route . '/before', array(&$route));  7   8  if (!$this->registry->has('model_' . str_replace(array('/', '-', '.'), array('_', '', ''),$route))) {  9  $file = DIR_APPLICATION . 'model/' .$route . '.php';  10  $class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '',$route);  11   12  if (is_file($file)) {  13  include_once($file);  14 15  $proxy = new Proxy();  16   17  foreach (get_class_methods($class) as $method) {  18 $proxy->{$method} =$this->callback($this->registry,$route . '/' . $method);  19  }  20   21 $this->registry->set('model_' . str_replace(array('/', '-', '.'), array('_', '', ''), (string)$route),$proxy);  22  } else {  23  throw new \Exception('Error: Could not load model ' . $route . '!');  24  }  25  }  26   27  // Trigger the post events  28 $this->registry->get('event')->trigger('model/' . $route . '/after', array(&$route));  29 } 

Considering the aforementioned example, the value of the $route argument is catalog/category. First, the value of the $route variable is sanitized, and following that it triggers the before event to allow other module listeners to change the value of the $route variable. Next, it checks the existence of the requested model object in the Registry. If the Registry holds the requested object, no further processing is needed. If the object is not found, it follows an interesting procedure to load it, and it's the snippet that we're looking for in the context of this article. To start with, it prepares the file path of the requested model and loads it if it exists.  1 ...  2 $file = DIR_APPLICATION . 'model/' . $route . '.php';  3 $class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', $route);  4 5 if (is_file($file)) {  6  include_once($file);  7 8  ...  9 }  10 ...  Following that, it instantiates the Proxy object.  1 $proxy = new Proxy(); 

Now, pay attention to the next for loop—it does a lot more than it seems.

 1 foreach (get_class_methods($class) as$method) {  2  $proxy->{$method} = $this->callback($this->registry, $route . '/' .$method);  3 } 

In our case, the value of $class should be ModelCatalogCategory. The get_class_methods($class) snippet loads all methods of the ModelCatalogCategory class and loops through it. What does it do in the loop? Let's look closely.

In the loop, it calls the callback method of same class. It's interesting to note that the callback method returns the function callable that's assigned to the $proxy object with the key as the method name. Of course, the proxy object doesn't have any such properties; it'll be created on the fly using the __set() magic method! Next, the $proxy object is added to the Registry so that it can be fetched later when needed. Have a close look at the key component of the set method. In our case, it should be model_catalog_category.

 1 $this->registry->set('model_' . str_replace(array('/', '-', '.'),  2  array('_', '', ''), (string)$route), $proxy);  At the end, it'll call the after event to allow other module listeners to change the value of the $route variable.

That's one part of the story.

Let's go through what happens exactly when you use the following in your controller.

 1 $this->model_catalog_category->getCategory($category_id); 

The $this->model_catalog_category snippet tries to find a match for the model_catalog_category key in the Registry. If you're wondering how, just look into the Controller class definition in the system/engine/controller.php file—it provides the __get() magic method that does it. As we've just discussed, that should return the $proxy object that's assigned to that particular key. Next, it tries to call the getCategory method on that object. But the Proxy class doesn't implement such a method, so how is that going to work?

The __call() magic method comes to the rescue! Whenever you call a method that doesn't exist in the class, the control is transferred to the __call() magic method.

Let's explore it in detail to understand what's going on. Open the Proxy class file and pay attention to that method.

The $key contains the name of the function that's being called—getCategory. On the other hand, $args contains arguments passed to the method, and it should be an array of one element holding the category id that's being passed.

Next, there's an array $arg_data that stores references of arguments. Frankly, I'm not sure if the code $arg instanceof Ref makes any sense or not there. If anyone knows why it's there, I would be happy to learn.

Further, it tries to check the existence of the $key property in the $proxy object, and it results in something like this.

 1 if (isset($this->getCategory)) {  Recall that earlier we assigned all the methods of ModelCatalogCategory class as properties of the $proxy object using a for loop. For your convenience, I'll paste that code again.

 1 ...  2 foreach (get_class_methods($class) as$method) {  3  $proxy->{$method} = $this->callback($this->registry, $route . '/' .$method);  4 }  5 ... 

So it should be there, and it should also return us the function callable! And finally, it calls that function callable using the call_user_func_array function by passing the function callable itself and the method arguments.

Now, let's divert our attention to the function callable definition itself. I'll grab the snippet from the callback method defined in system/engine/loader.php.

 1 ...  2 function($args) use($registry, &$route) {  3  static$model = array();  4   5  $output = null;  6   7  // Trigger the pre events  8 $result = $registry->get('event')->trigger('model/' .$route . '/before', array(&$route, &$args, &$output));  9   10  if ($result) {  11  return $result;  12  }  13   14  // Store the model object  15  if (!isset($model[$route])) {  16 $file = DIR_APPLICATION . 'model/' . substr($route, 0, strrpos($route, '/')) . '.php';  17  $class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', substr($route, 0, strrpos($route, '/')));  18 19  if (is_file($file)) {  20  include_once($file);  21   22 $model[$route] = new$class($registry);  23  } else {  24  throw new \Exception('Error: Could not load model ' . substr($route, 0, strrpos($route, '/')) . '!');  25  }  26  }  27 28 $method = substr($route, strrpos($route, '/') + 1);  29   30  $callable = array($model[$route],$method);  31 32  if (is_callable($callable)) {  33 $output = call_user_func_array($callable,$args);  34  } else {  35  throw new \Exception('Error: Could not call model/' . $route . '!');  36  }  37   38  // Trigger the post events  39 $result = $registry->get('event')->trigger('model/' .$route . '/after', array(&$route, &$args, &$output));  40   41  if ($result) {  42  return $result;  43  }  44   45  return$output;  46 };  47 ... 

As it's an anonymous function, it has preserved the values in the form of $registry and $route variables that were passed earlier to the callback method. In this case, the value of the $route variable should be catalog/category/getCategory. Apart from that, if we look at the important snippet in that function, it instantiates the ModelCatalogCategory object and stores it in a static $model array.

 1 ...  2 // Store the model object  3 if (!isset($model[$route])) {  4  $file = DIR_APPLICATION . 'model/' . substr($route, 0, strrpos($route, '/')) . '.php';  5 $class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', substr($route, 0, strrpos($route, '/')));  6 7  if (is_file($file)) {  8  include_once($file);  9   10  $model[$route] = new $class($registry);  11  } else {  12  throw new \Exception('Error: Could not load model ' . substr($route, 0, strrpos($route, '/')) . '!');  13  }  14 }  15 ... 

And here's a snippet that grabs the method name that needs to be called using the $route variable.  1 $method = substr($route, strrpos($route, '/') + 1); 

So we have an object reference and a method name that allows us to call it using the call_user_func_array function. The following snippet does just that!

 1 ...  2 if (is_callable($callable)) {  3 $output = call_user_func_array($callable,$args);  4 } else {  5  throw new \Exception('Error: Could not call model/' . $route . '!');  6 }  7 ...  At the end of the method, the resulting outcome is returned via the $output variable. And yes, that's another part of the story!

I've intentionally ignored the pre and post events code that allows you to override methods of the core OpenCart classes. Using that, you could override any core class method and tweak it according to your need. But let's keep that for some other day, as it'll be too much to fit into a single article!

So that's how it works altogether. I hope that you should be more confident about those shorthand OpenCart calls and about their inner workings.

Conclusion

What we've just discussed today is one of the interesting and ambiguous concepts in OpenCart: the use of the Proxy method in a framework to support shorthand conventions to call model methods. I hope the article was interesting enough and enriched your OpenCart framework knowledge.

I would love to know your feedback on this, and if you feel I should cover such topics in my upcoming articles, don't hesitate to drop a line about it!