通常,我们认为事情是理所当然的。 如果某件事情按预期工作,那么我们就不必理会它的内部工作原理以了解其基本机制。 或者换句话说,除非遇到麻烦,否则我们不会深入研究!
同样,我一直想知道OpenCart中在底层框架中使用过的几个概念,其中一个是Proxy类。 我花了一些时间来理解它,我认为与您分享它是一件很棒的事情,因为了解新事物总是很有趣。
什么是代理类?
尽管您会在网上找到定义“代理”一词的各种材料,但Wikipedia的定义引人注目且易于理解。
在最一般的形式上,代理是一个类,充当与其他对象的接口。
因此,代理将控件委派给它打算使用的对象,并因此代表实例化的实际类。 实际上,代理设计模式是一种非常流行的模式,流行的框架会根据需要使用该模式。 考虑到代理方法本身的讨论是一个广泛的话题,不在本文的讨论范围之内,因此我将快速总结一下在大多数情况下使用的方法:
- 充当包装器以提供其他功能。
- 延迟昂贵对象的实例化,也称为延迟加载。
在OpenCart的上下文中,我们可以说代理模式用于向基本代理类添加功能。 话虽如此,基本代理类本身只提供了几种魔术方法,什么也没有提供! 正如我们将在下一节中看到的那样,代理类在运行时通过将属性附加到其上而得到了丰富。
在进入下一部分之前,让我们快速看一下代理类。 它位于system/engine/proxy.php
。
<?php
class Proxy {
public function __get($key) {
return $this->{$key};
}
public function __set($key, $value) {
$this->{$key} = $value;
}
public function __call($key, $args) {
$arg_data = array();
$args = func_get_args();
foreach ($args as $arg) {
if ($arg instanceof Ref) {
$arg_data[] =& $arg->getRef();
} else {
$arg_data[] =& $arg;
}
}
if (isset($this->{$key})) {
return call_user_func_array($this->{$key}, $arg_data);
} else {
$trace = debug_backtrace();
exit('<b>Notice</b>: Undefined property: Proxy::' . $key . ' in <b>' . $trace[1]['file'] . '</b> on line <b>' . $trace[1]['line'] . '</b>');
}
}
}
如您所见,它实现了三种魔术方法: __get()
, __set()
和__call()
。 其中, __call()
方法实现是一个重要的实现,我们将很快恢复。
代理类如何与模型一起使用
在本节中,我将说明如何像$this->model_catalog_category->getCategory($category_id)
这样的调用$this->model_catalog_category->getCategory($category_id)
工作。
实际上,故事始于以下陈述。
$this->load->model('catalog/category');
在引导过程中,OpenCart框架将所有通用对象存储到Registry
对象,以便可以随意获取它们。 结果, $this->load
调用从注册表中返回Loader
对象。
Loader
类提供了各种方法来加载不同的组件,但是我们在这里感兴趣的是模型方法。 让我们快速从system/engine/loader.php
model
方法的代码片段。
public function model($route) {
// Sanitize the call
$route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route);
// Trigger the pre events
$this->registry->get('event')->trigger('model/' . $route . '/before', array(&$route));
if (!$this->registry->has('model_' . str_replace(array('/', '-', '.'), array('_', '', ''), $route))) {
$file = DIR_APPLICATION . 'model/' . $route . '.php';
$class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', $route);
if (is_file($file)) {
include_once($file);
$proxy = new Proxy();
foreach (get_class_methods($class) as $method) {
$proxy->{$method} = $this->callback($this->registry, $route . '/' . $method);
}
$this->registry->set('model_' . str_replace(array('/', '-', '.'), array('_', '', ''), (string)$route), $proxy);
} else {
throw new \Exception('Error: Could not load model ' . $route . '!');
}
}
// Trigger the post events
$this->registry->get('event')->trigger('model/' . $route . '/after', array(&$route));
}
考虑上述示例, $route
参数的值是catalog/category
。 首先,价值$route
变量消毒,并按照它的触发before
的事件,让其他模块听众改变的价值$route
变量。
接下来,它检查注册表中请求的模型对象是否存在。 如果注册表包含请求的对象,则无需进一步处理。
如果找不到该对象,则将按照一个有趣的过程对其进行加载,这是我们在本文中所要查找的片段。
首先,它准备所请求模型的文件路径,并在存在时加载它。
...
$file = DIR_APPLICATION . 'model/' . $route . '.php';
$class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', $route);
if (is_file($file)) {
include_once($file);
...
}
...
之后,它实例化Proxy
对象。
$proxy = new Proxy();
现在,注意下一个for
循环-它的作用远不止看起来。
foreach (get_class_methods($class) as $method) {
$proxy->{$method} = $this->callback($this->registry, $route . '/' . $method);
}
在我们的例子中, $class
的值应为ModelCatalogCategory
。 get_class_methods($class)
片段加载ModelCatalogCategory
类的所有方法并循环遍历。 它在循环中做什么? 让我们仔细看看。
在循环中,它调用相同类的callback
方法。 有趣的是,回调方法返回以键为方法名分配给$proxy
对象的可调用函数。 当然,代理对象没有任何此类属性。 它将使用__set()
魔术方法即时创建!
接下来,将$proxy
对象添加到注册表中,以便以后可以在需要时获取它。 仔细研究set
方法的关键组成部分。 在我们的情况下,它应该是model_catalog_category
。
$this->registry->set('model_' . str_replace(array('/', '-', '.'),
array('_', '', ''), (string)$route), $proxy);
最后,它将调用after
事件,以允许其他模块侦听器更改$route
变量的值。
那是故事的一部分。
让我们来看看当您在控制器中使用以下命令时会发生什么情况。
$this->model_catalog_category->getCategory($category_id);
$this->model_catalog_category
代码段尝试在注册表中找到与model_catalog_category
项匹配的项。 如果您想知道如何操作,只需查看system/engine/controller.php
文件中的Controller
类定义-它提供了实现该功能的__get()
魔术方法。
正如我们刚刚讨论的那样,这应该返回分配给该特定键的$proxy
对象。 接下来,它尝试在该对象上调用getCategory
方法。 但是Proxy类没有实现这种方法,那怎么办呢?
__call()
魔术方法可以解救! 每当您调用类中不存在的方法时,控件就会转移到__call()
魔术方法中。
让我们详细研究它以了解发生了什么。 打开代理类文件,并注意该方法。
$key
包含正在调用的函数的名称,即getCategory
。 另一方面, $args
包含传递给方法的参数,它应该是一个数组,其中包含一个要传递的类别ID的元素。
接下来,有一个数组$arg_data
,用于存储参数的引用。 坦白说,我不确定$arg instanceof Ref
代码是否有意义。 如果有人知道它为什么存在,我将很高兴学习。
此外,它尝试检查$proxy
对象中$key
属性的存在,并导致类似这样的情况。
if (isset($this->getCategory)) {
回想一下,我们之前使用for
循环将ModelCatalogCategory
类的所有方法分配为$proxy
对象的属性。 为了方便起见,我将再次粘贴该代码。
...
foreach (get_class_methods($class) as $method) {
$proxy->{$method} = $this->callback($this->registry, $route . '/' . $method);
}
...
因此它应该在那里,并且还应该向我们返回可调用的函数! 最后,它通过传递可调用函数本身和方法参数来使用call_user_func_array
函数调用该可调用函数。
现在,让我们将注意力转移到函数可调用定义本身。 我将从system/engine/loader.php
定义的callback
方法中system/engine/loader.php
。
...
function($args) use($registry, &$route) {
static $model = array();
$output = null;
// Trigger the pre events
$result = $registry->get('event')->trigger('model/' . $route . '/before', array(&$route, &$args, &$output));
if ($result) {
return $result;
}
// Store the model object
if (!isset($model[$route])) {
$file = DIR_APPLICATION . 'model/' . substr($route, 0, strrpos($route, '/')) . '.php';
$class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', substr($route, 0, strrpos($route, '/')));
if (is_file($file)) {
include_once($file);
$model[$route] = new $class($registry);
} else {
throw new \Exception('Error: Could not load model ' . substr($route, 0, strrpos($route, '/')) . '!');
}
}
$method = substr($route, strrpos($route, '/') + 1);
$callable = array($model[$route], $method);
if (is_callable($callable)) {
$output = call_user_func_array($callable, $args);
} else {
throw new \Exception('Error: Could not call model/' . $route . '!');
}
// Trigger the post events
$result = $registry->get('event')->trigger('model/' . $route . '/after', array(&$route, &$args, &$output));
if ($result) {
return $result;
}
return $output;
};
...
由于它是一个匿名函数,因此它以$registry
和$route
变量的形式保存了值,这些值早先已传递给回调方法。 在这种情况下, $route
变量的值应为catalog/category/getCategory
。
除此之外,如果我们查看该函数中的重要代码段,它将实例化ModelCatalogCategory
对象并将其存储在静态$model
数组中。
...
// Store the model object
if (!isset($model[$route])) {
$file = DIR_APPLICATION . 'model/' . substr($route, 0, strrpos($route, '/')) . '.php';
$class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', substr($route, 0, strrpos($route, '/')));
if (is_file($file)) {
include_once($file);
$model[$route] = new $class($registry);
} else {
throw new \Exception('Error: Could not load model ' . substr($route, 0, strrpos($route, '/')) . '!');
}
}
...
这是一个片段,其中包含需要使用$route
变量调用的方法名称。
$method = substr($route, strrpos($route, '/') + 1);
因此,我们有一个对象引用和一个方法名称,使我们可以使用call_user_func_array
函数对其进行调用。 以下代码片段就是这样做的!
...
if (is_callable($callable)) {
$output = call_user_func_array($callable, $args);
} else {
throw new \Exception('Error: Could not call model/' . $route . '!');
}
...
在方法的最后,通过$output
变量返回结果。 是的,那是故事的另一部分!
我有意忽略了前置和后置事件的代码,使您可以覆盖核心Opencart的类的方法。 使用它,您可以覆盖任何核心类方法,并根据需要进行调整。 但是,让我们将其保留一天,因为它太过复杂而无法写一篇文章!
这就是它的全部工作方式。 我希望您对那些速记OpenCart调用及其内部工作方式更有信心。
结论
我们今天刚刚讨论的是OpenCart中有趣且模棱两可的概念之一:在框架中使用Proxy方法来支持用于调用模型方法的速记约定。 我希望本文足够有趣,并丰富您的OpenCart框架知识。
我很想知道您对此的反馈,如果您觉得我应该在我的后续文章中介绍此类主题,请不要犹豫。
翻译自: https://code.tutsplus.com/tutorials/decoding-the-proxy-class-in-the-opencart--cms-28196