Статьи

Расшифровка прокси-класса в OpenCart

Чаще всего мы принимаем вещи как должное. Если что-то работает так, как ожидалось, мы не беспокоимся о его внутренней работе, чтобы понять механизм, лежащий в основе. Или, иначе говоря, мы не копаемся во что-то, пока у нас не возникнут какие-то проблемы!

Точно так же мне всегда было интересно о паре концепций в 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() , и мы скоро к ней вернемся.

В этом разделе я объясню, как именно вызов типа $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.

Я хотел бы знать ваши отзывы по этому вопросу, и если вы считаете, что я должен освещать такие темы в моих будущих статьях, не стесняйтесь написать об этом!