很多时候,我们认为事情是理所当然的。如果某些东西按预期工作,我们就不必担心它的内部运作来理解底层机制。或者换一种说法,在遇到某种麻烦之前,我们不会深入研究!
同样,我一直想知道 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()
方法实现是一个重要的部分,我们很快就会回到它。
代理类如何与模型一起使用
在本节中,我将解释调用 like 是如何$this->model_catalog_category->getCategory($category_id)
开箱即用的。
事实上,故事从以下陈述开始。
$this->load->model('catalog/category');
在引导期间,OpenCart 框架将所有通用对象存储到 Registry
对象中,以便可以随意获取它们。结果,调用从注册表$this->load
返回对象。Loader
该类Loader
提供了各种方法来加载不同的组件,但我们在这里感兴趣的是 模型方法。让我们快速 model
从system/engine/loader.php
.
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
变量的值。
接下来,它检查注册表中请求的模型对象是否存在。如果 Registry 持有请求的对象,则不需要进一步处理。
如果找不到对象,它会按照一个有趣的过程来加载它,这就是我们在本文上下文中寻找的片段。
首先,它准备所请求模型的文件路径并加载它(如果存在)。
... $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
在注册表中查找键的匹配项。如果您想知道如何做,只需查看文件Controller
中的类定义 system/engine/controller.php
——它提供了执行此 操作的__get()
魔术方法。
正如我们刚刚讨论的那样,这应该返回$proxy
分配给该特定键的对象。接下来,它尝试调用该 getCategory
对象的方法。但是 Proxy 类没有实现这样的方法,那么它是如何工作的呢?
神奇的__call()
方法来拯救!每当您调用类中不存在的方法时,控制权就会转移到 __call()
魔术方法。
让我们详细探索它以了解发生了什么。打开 Proxy 类文件并注意该方法。
$key
包含正在调用的函数的名称—— getCategory
。另一方面, $args
包含传递给方法的参数,它应该是一个包含一个元素的数组,其中包含正在传递的类别 id。
接下来,有一个存储参数引用的数组$arg_data
。坦率地说,我不确定代码$arg instanceof Ref
是否有意义。如果有人知道它为什么在那里,我会很乐意学习。
此外,它会尝试检查对象中是否存在 $key
属性, $proxy
结果是这样的。
if (isset($this->getCategory)) {
回想一下,之前我们使用循环将ModelCatalogCategory
类的所有方法分配为对象的属性 。为了您的方便,我将再次粘贴该代码。$proxy
for
... foreach (get_class_methods($class) as $method) { $proxy->{$method} = $this->callback($this->registry, $route . '/' . $method); } ...
所以它应该在那里,它也应该返回我们可调用的函数!最后,它 call_user_func_array
通过传递可调用函数本身和方法参数来调用可调用函数。
现在,让我们将注意力转移到函数可调用定义本身。我将从中 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 框架知识。