От автора: чаще всего мы принимаем что-то как должное. Если это что-то работает, как нужно, мы не будем беспокоиться о его внутренней работе и пытаться понять лежащий в основе механизм. Говоря другими словами, мы не придаём значения, пока не начинаются проблемы! Мне всегда было интересно узнать о нескольких концепциях OpenCart, которые использовались во фреймворке, и одним из них был прокси класс. Именно про прокси классы OpenCart мы сегодня и поговорим.
Что такое прокси-класс?
Вы найдете различные материалы в Интернете, которые определяют термин прокси, но лучше всего даёт определение этому термину Википедия: Прокси-сервер в его самой общей форме — это класс, функционирующий как интерфейс к чему-то другому.
Таким образом, прокси делегирует элемент управления объекту, который он намеревается использовать, и действует от имени фактического класса, который был создан. Фактически, шаблон прокси-дизайна является очень популярным шаблоном, который по мере необходимости используется фреймворками. Учитывая, что обсуждение прокси-метода само по себе является такой широкой темой и выходит за рамки этой статьи, я быстро объясню, почему он так часто используется:
Действуйте как обертка, чтобы обеспечить дополнительную функциональность.
Задерживает создание дорогостоящих объектов так называемой ленивой загрузкой.
В контексте OpenCart можно сказать, что шаблон прокси используется для добавления функциональности в базовый прокси-класс. Сам по себе базовый прокси-класс не предоставляет ничего, кроме нескольких магических методов! В следующем разделе мы увидим, как прокси-класс обогащается во время выполнения за счёт добавления к нему какого-либо свойства.
Прежде чем перейти к следующему разделу, давайте мельком взглянем на прокси-класс. Он находится в system/engine/proxy.php.
1 2 3 4 5 6 7 8 9 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() наиболее важна, и довольно скоро мы вернемся к ней.
Как работает прокси-класс с моделью
В этом разделе я объясню, как именно вызов $this->model_catalog_category->getCategory($category_id) работает из коробки. Фактически, история начинается со следующего утверждения.
1 |
$this->load->model('catalog/category'); |
Во время начальной загрузки фреймворк OpenCart хранит все общие объекты в объекте Registry, чтобы по желанию их можно было получить. В результате этого $this->load вызов возвращает Loader объект из реестра.
Класс Loader предоставляет различные методы для загрузки различных компонентов, но то, что нас здесь интересует — это модель метода. Давайте извлечём фрагмент model метода system/engine/loader.php.
1 2 3 4 5 6 7 8 9 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 . '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.
Затем он проверяет наличие запрошенного объекта модели в реестре. Если реестр содержит запрошенный объект, дальнейшая обработка не требуется.
Если объект не найден, следует загрузить интересную процедуру, и это тот фрагмент, который мы ищем в контексте этой статьи. Для начала он подготавливает путь к файлу запрашиваемой модели и загружает ее, если она существует.
1 2 3 4 5 6 7 8 9 10 |
... $file = DIR_APPLICATION . 'model/' . $route . '.php'; $class = 'Model' . preg_replace('/[^a-zA-Z0-9]/', '', $route); 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 на этом объекте. Но прокси-класс не реализует такой метод, тогда как это будет работать?
Магический метод __call() приходит на помощь! Всякий раз, когда вы вызываете метод, которого не существует в классе, элемент управления передается методу __call().
Давайте рассмотрим его подробно, чтобы понять, что происходит. Откройте файл прокси-класса и обратите внимание на этот метод.
$key содержит имя функции, которая будет вызвана — 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 функции, передавая саму вызываемую функцию и аргументы метода.
Теперь давайте обратим наше внимание на само определение вызываемое функцией. Я возьму фрагмент из callback метода, определенного в system/engine/loader.php.
1 2 3 4 5 6 7 8 9 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 . '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 массиве.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... // 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 переменной.
1 |
$method = substr($route, strrpos($route, '/') + 1); |
Таким образом, мы имеем ссылку на объект и имя метода, которое позволяет нам называть его с помощью 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 переменную. И это еще одна часть истории!
Я намеренно игнорировал код событий pre и post, который позволяет переопределять методы основных классов OpenCart. Используя его, можно переопределить любой метод основного класса и настроить его в соответствии с потребностями. Но давайте сделаем это в течение несколько позже, так как это будет слишком много, чтобы вписаться в одну статью!
Так вот как это вообще работает. Я надеюсь, что вы должны быть более уверенны в вызовах OpenCart и их внутренней работе.
Заключение
То, что мы только что обсуждали сегодня, является одной из интересных и неоднозначных концепций в OpenCart: использование прокси-метода в рамках поддержки сокращенных соглашений для вызова методов модели. Надеюсь, статья была достаточно интересна и обогатила ваши знания OpenCart.
Автор: Sajal Soni
Источник: //code.tutsplus.com/
Редакция: Команда webformyself.