Чаще всего мы принимаем вещи как должное. Если что-то работает так, как ожидалось, мы не беспокоимся о его внутренней работе, чтобы понять механизм, лежащий в основе. Или, иначе говоря, мы не копаемся во что-то, пока у нас не возникнут какие-то проблемы!
Точно так же мне всегда было интересно о паре концепций в OpenCart, которые использовались в базовой структуре, и одним из них был класс Proxy. Мне потребовалось некоторое время, чтобы понять это, и я подумал, что с вами приятно поделиться, потому что всегда интересно узнать что-то новое.
Что такое прокси-класс?
Хотя в Интернете вы найдете различные материалы, в которых дается определение термина «прокси», определение из Википедии поразительно и легко для понимания.
Прокси, в его наиболее общем виде, является классом, функционирующим как интерфейс к чему-то другому.
Таким образом, прокси-сервер делегирует управление объекту, который он намеревается использовать, и, таким образом, действует от имени фактического класса, который был создан. На самом деле, шаблон прокси-дизайна является очень популярным шаблоном, который используется популярными фреймворками по мере необходимости. Учитывая тот факт, что обсуждение прокси-метода само по себе является такой широкой темой и выходит за рамки этой статьи, я быстро подведу итог, что он используется в большинстве случаев:
- Выступать в качестве оболочки, чтобы обеспечить дополнительную функциональность.
- Задержка создания дорогих объектов, также называемая отложенной загрузкой.
В контексте OpenCart можно сказать, что шаблон прокси используется для добавления функциональности в базовый класс прокси. При этом сам базовый прокси-класс не предоставляет ничего, кроме пары магических методов! Как мы увидим в следующем разделе, прокси-класс обогащается во время выполнения за счет добавления к нему свойств.
Прежде чем перейти к следующему разделу, давайте кратко рассмотрим прокси-класс. Он находится в system/engine/proxy.php
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<?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()
реализация метода __call()
, и мы скоро к ней вернемся.
Как класс Proxy работает с моделью
В этом разделе я объясню, как именно вызов типа $this->model_catalog_category->getCategory($category_id)
работает из коробки.
На самом деле история начинается со следующего утверждения.
1
|
$this->load->model(‘catalog/category’);
|
Во время начальной загрузки среда OpenCart сохраняет все общие объекты в объекте Registry
чтобы их можно было выбирать по желанию. В результате этого вызов $this->load
возвращает объект Loader
из реестра.
Класс Loader
предоставляет различные методы для загрузки различных компонентов, но здесь нас интересует метод модели . Давайте быстро system/engine/loader.php
фрагмент метода model
из system/engine/loader.php
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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 .
$class = ‘Model’ .
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
.
Затем он проверяет наличие запрошенного объекта модели в Реестре. Если Реестр содержит запрошенный объект, дальнейшая обработка не требуется.
Если объект не найден, для его загрузки следует интересная процедура, и именно этот фрагмент мы ищем в контексте этой статьи.
Для начала он подготавливает путь к файлу запрашиваемой модели и загружает его, если он существует.
01
02
03
04
05
06
07
08
09
10
|
…
$file = DIR_APPLICATION .
$class = ‘Model’ .
if (is_file($file)) {
include_once($file);
…
}
…
|
После этого он создает экземпляр объекта Proxy
.
1
|
$proxy = new Proxy();
|
Теперь обратите внимание на следующий цикл for
— он делает намного больше, чем кажется.
1
2
3
|
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
.
1
2
|
$this->registry->set(‘model_’ . str_replace(array(‘/’, ‘-‘, ‘.’),
array(‘_’, », »), (string)$route), $proxy);
|
В конце он вызовет событие after
чтобы позволить другим слушателям модуля изменить значение переменной $route
.
Это одна часть истории.
Давайте рассмотрим, что именно происходит, когда вы используете следующее в вашем контроллере.
1
|
$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
функции — getCategory
. С другой стороны, $args
содержит аргументы, переданные методу, и это должен быть массив из одного элемента, содержащий идентификатор категории, который передается.
Далее, есть массив $arg_data
котором хранятся ссылки на аргументы. Честно говоря, я не уверен, имеет ли смысл код $arg instanceof Ref
или нет. Если кто-нибудь знает, почему это там, я был бы рад узнать.
Кроме того, он пытается проверить наличие свойства $key
объекте $proxy
, и это приводит к чему-то вроде этого.
1
|
if (isset($this->getCategory)) {
|
Напомним, что ранее мы присваивали все методы класса ModelCatalogCategory
как свойства объекта $proxy
с использованием цикла for
. Для вашего удобства я снова вставлю этот код.
1
2
3
4
5
|
…
foreach (get_class_methods($class) as $method) {
$proxy->{$method} = $this->callback($this->registry, $route . ‘/’ . $method);
}
…
|
Так оно и должно быть, и оно также должно возвращать нам вызываемую функцию! И, наконец, он вызывает эту функцию с call_user_func_array
функции call_user_func_array
, передавая call_user_func_array
функцию call_user_func_array
и аргументы метода.
Теперь давайте обратим наше внимание на само определение вызываемой функции. Я возьму фрагмент из метода callback
определенного в system/engine/loader.php
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
…
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 .
$class = ‘Model’ .
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
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
…
// Store the model object
if (!isset($model[$route])) {
$file = DIR_APPLICATION .
$class = ‘Model’ .
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
.
1
|
$method = substr($route, strrpos($route, ‘/’) + 1);
|
Таким образом, у нас есть ссылка на объект и имя метода, которые позволяют нам вызывать его с call_user_func_array
функции call_user_func_array
. Следующий фрагмент делает именно это!
1
2
3
4
5
6
7
|
…
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.
Я хотел бы знать ваши отзывы по этому вопросу, и если вы считаете, что я должен освещать такие темы в моих будущих статьях, не стесняйтесь написать об этом!