Расширение функциональности модулей: различия между версиями
Olga (обсуждение | вклад) |
Olga (обсуждение | вклад) |
||
(не показаны 52 промежуточные версии этого же участника) | |||
Строка 7: | Строка 7: | ||
Находим там кусок кода: | Находим там кусок кода: | ||
− | < | + | <syntaxhighlight lang="php"> |
$line_arr = []; | $line_arr = []; | ||
$line_arr['attribute:id'] = $element_id; | $line_arr['attribute:id'] = $element_id; | ||
Строка 14: | Строка 14: | ||
$line_arr['xlink:href'] = 'upage://' . $element_id; | $line_arr['xlink:href'] = 'upage://' . $element_id; | ||
$line_arr['void:header'] = $lines_arr['name'] = $element->getName(); | $line_arr['void:header'] = $lines_arr['name'] = $element->getName(); | ||
− | </ | + | </syntaxhighlight> |
Там и правда не добавляется анонс. Поэтому вставляем одну строчку: | Там и правда не добавляется анонс. Поэтому вставляем одну строчку: | ||
− | < | + | <syntaxhighlight lang="php"> |
$line_arr['attribute:anons'] = $element->anons; | $line_arr['attribute:anons'] = $element->anons; | ||
− | </ | + | </syntaxhighlight> |
Все эти ''node:'' и ''attribute:'' предназначены для других шаблонизаторов, но лучше их сохранить для общности. | Все эти ''node:'' и ''attribute:'' предназначены для других шаблонизаторов, но лучше их сохранить для общности. | ||
− | + | Открываем главную страницу нашего сайта - и видим ошибку "Ошибка (Error): Class 'Service' not found"! Правда, с этим понятно, что делать, надо в начале файла customMacros.php вставить "use UmiCms\Service;", вот так: | |
− | + | <syntaxhighlight lang="php"> | |
+ | <?php | ||
+ | use UmiCms\Service; | ||
+ | |||
+ | /** Класс пользовательских макросов */ | ||
+ | class NewsCustomMacros { | ||
+ | |||
+ | /** @var news $module */ | ||
+ | public $module; | ||
+ | ... | ||
+ | } | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | |||
+ | Больше ошибок не возникнет, и теперь при вызове метода lastlist() получим также и элемент массива 'anons'. Если изменим в файле content/home/news.phtml код цикла вот так: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | if (!empty($newsList['items'])): | ||
+ | foreach ($newsList['items'] as $item): | ||
+ | ?> | ||
+ | <li class="list-group-item"> | ||
+ | <em><?= date("d.m.Y", $item['publish_time']) ?></em> | ||
+ | <a href="<?= $item['link'] ?>"><h3 umi:element-id="<?= $item['id'] ?>" umi:field-name="name" umi:empty="<?= $this->translate('empty_page_name') ?>"><?= $item['name'] ?></h3></a> | ||
+ | <div umi:element-id="<?= $item['id'] ?>" umi:field-name="anons" umi:empty="<?= $this->translate('empty_news_anons') ?>"><?= $item['anons'] ?></div> | ||
+ | </li> | ||
+ | <?php | ||
+ | endforeach; | ||
+ | endif; | ||
+ | ?> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | то увидим также и текст анонсов. Можно просто набрать в браузере http://umi.example.com/udata://news/lastlist/7/.json и увидеть изменения. | ||
Что будет, если внести исправления сразу в файл macros.php? Очевидно, что все исправления будут потеряны при очередном обновлении системы. | Что будет, если внести исправления сразу в файл macros.php? Очевидно, что все исправления будут потеряны при очередном обновлении системы. | ||
+ | |||
+ | Сразу замечу, что переопределять стандартные методы - '''ОЧЕНЬ ДУРНАЯ ПРАКТИКА'''. Если с сайтом будет работать другой программист, ему придется потратить кучу сил и времени на поиск ошибки, ведь он будет уверен, что вызывается стандартный метод. Поэтому '''ВСЕГДА''', скопировав код стандартного метода, '''ПЕРЕИМЕНОВЫВАЙТЕ ЕГО'''! Пусть этот метод будет целиком и полностью кастомным, и это будет видно уже при вызове. И тогда возникает следующая задача - настройка разрешений. | ||
+ | |||
+ | === Настройка разрешений для выполнения метода === | ||
+ | |||
+ | Да, переопределение метода - нехороший поступок, но когда мы так делаем, автоматически избегаем множества проблем. Дело в том, что в UMI CMS есть сложная система разрешений, для каждого модуля существует собственный набор, посмотреть его можно в папке модуля в файле permissions.php. | ||
+ | |||
+ | Когда мы создаем метод с новым именем, то он не будет выполняться, т к отсутствует в файле permissions.php. Но это можно исправить, создав в той же папке модуля файл permissions.custom.php. | ||
+ | |||
+ | Предположим, по аналогии с методом lastlist() мы создали метод newslist(), куда внесли все необходимые нам изменения. Если посмотреть содержимое файла permissions.php, увидим, что это массив: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | /** Группы прав на функционал модуля */ | ||
+ | $permissions = [ | ||
+ | /** Права на просмотр новостей */ | ||
+ | 'view' => [ | ||
+ | 'lastlist', | ||
+ | 'listlents', | ||
+ | 'rubric', | ||
+ | /** ... и так далее, не буду приводить его целиком */ | ||
+ | ] | ||
+ | ]; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | По логике, нам надо добавить один элемент в массив $permissions['view'], поэтому в файле permissions.custom.php пишем: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | $permissions['view'][] = 'newslist'; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Интересный факт: даже если название метода содержит буквы с разной капитализацией, в этом массиве все нужно писать маленькими буквами. Не спрашивайте, почему. | ||
+ | |||
+ | Пока всё просто? Это только кажется. В каждом модуле структура и ключи массива разрешений разные, и в них нет никакой системы. Хорошо, если метод должен быть общедоступным, как правило, можно угадать, в какой именно массив его добавлять. Сложнее, если доступ можно давать только авторизоваанным пользователям, и то не всем. Так что - удачи! | ||
+ | |||
+ | === Использование кастомного метода в шаблона === | ||
+ | |||
+ | Теперь меняем в файле content/home/news.phtml имя метода: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | $newsList = $this->macros('news', 'newslist', [$newsPageId]); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | и увидим то, что планировали. И даже есть вызовем этот метод просто в строке URL, на пример, так: | ||
+ | |||
+ | http://umi.example.com/udata://news/newslist/7/.json | ||
+ | |||
+ | (где 7 - это id ленты новостей, на других сайтах может быть другой), то получим json-массив с элементом anons. | ||
+ | |||
+ | Иногда добавленные таким способом методы выдают ошибку, с одним таким случаем только что столкнулись, второй из возможных - это когда в копируемом методе используются другие методы из файла macros.php в виде $this->название_метода(). При вызове из другого класса метод не обнаруживается.. В этом случае надо заменить вызов на $this->module->название_метода(), не зря же переменная $module была добавлена в кастомный класс. | ||
== Кастомные методы внутри шаблона == | == Кастомные методы внутри шаблона == | ||
Строка 34: | Строка 116: | ||
Описанный выше способ хорош, но добавленный код находится вне нашего шаблона default. А нам бы хотелось, во-первых, при установке системы на другом хостинге просто закинуть туда готовый шаблон и получить готовый сайт, больше ничего не трогая, особенно системные директории. И во-вторых, хотелось бы для разных сайтов на разных щаблонах использовать разные кастомные методы. | Описанный выше способ хорош, но добавленный код находится вне нашего шаблона default. А нам бы хотелось, во-первых, при установке системы на другом хостинге просто закинуть туда готовый шаблон и получить готовый сайт, больше ничего не трогая, особенно системные директории. И во-вторых, хотелось бы для разных сайтов на разных щаблонах использовать разные кастомные методы. | ||
− | И что приятно, это можно сделать! Для этого создаем внутри нашего шаблона папку classes | + | И что приятно, начиная с версии 2.8.5 системы [http://api.docs.umi-cms.ru/razrabotka_nestandartnogo_funkcionala/razrabotka_sobstvennyh_makrosov_i_modulej/novyj_format_rasshireniya_funkcionala/ это можно сделать]! Для этого создаем внутри нашего шаблона папку classes, внутри неё папку modules, в ней папку с именем модуля, например, news, а там - файл class.php, где создаем отдельный класс. Имя класса должно быть 'имямодуля_custom', а имена методов не должны совпадать с именами стандартных методов модуля. Таким образом, получим файл classes/modules/news/class.php с кодом: |
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | |||
+ | /** Класс пользовательских методов */ | ||
+ | class news_custom extends def_module { | ||
+ | |||
+ | } | ||
+ | ?> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | В этот файл можно добавлять свои методы, которые могут быть использованы в шаблоне при помощи вызова $this->macros('news', 'имя_метода', [массив_параметров_метода] ). Что приятно, в class.php уже не нужно подключать 'use UmiCms\Service;', его уже подключает def_module. | ||
+ | |||
+ | === Добавление разрешения на выполнение метода === | ||
+ | |||
+ | Тут тоже понадобится добавлять разрешения. К счастью, для этого достаточно в той же директории classes/modules/news/ создать файл permissions.php, куда внести тот же код, что ранее использовали в permissions.custom.php: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | $permissions['view'][] = 'newslist'; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Создание кастомного метода === | ||
+ | |||
+ | Перенесём наш метод newslist() из кастомных макросов в этот новый класс. Не буду приводить тут его код, он в точности тот же. | ||
+ | |||
+ | Из кастомных макросов этот метод можно убрать, можно также удалить файл /classes/components/news/permissions.custom.php, ведь мы уже добавили разрешения в классы шаблона. В нашем фрагменте шаблона content/home/news.phtml ничего не меняем. | ||
+ | |||
+ | Если набрать в браузере ту же ссылку http://umi.example.com/udata://news/newslist/7/.json (не забываем поменять id ленты новостей, это только у меня он 7), то увидим массив новостей с анонсом, да и на сайте все прекрасно выводится. | ||
+ | |||
+ | === Создание кастомных событий внутри шаблона === | ||
+ | |||
+ | До создания собственных событий ещё довольно далеко. Просто для общности надо отметить, что события тоже можно создать внутри шаблона, в той же папке /classes/components/news/ в файле events.php. | ||
+ | |||
+ | === Вызов метода из "чужого" шаблона === | ||
+ | |||
+ | Шаблонов на сайте может быть несколько, и в одном шаблоне сайта получить доступ к методу из другого шаблона, вообще говоря, нельзя. Но есть хитрый хак. Если запрашивать данные, например, AJAXом через udata:, то можно в строке запроса в конце добавить параметр '?template_id={id шаблона}', и всё получится. Не знаю, кому и зачем это может понадобиться, разве только чтобы в дальнейшем получше запутать себя и коллег. | ||
+ | |||
+ | == Собственный класс сайта == | ||
+ | |||
+ | До сих пор мы рассматривали очень правильный с точки зрения MVC путь создания своих методов. Однако есть более простой и быстрый, но немного менее "канонический" способ, доступный только для PHP-шаблонизатора. Это создание класса расширения шаблонизатора. | ||
+ | |||
+ | Для начала создадим в директории php папку library. В данном случае название может быть любым. А в ней файл PhpExtension.php с вот таким кодом: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | |||
+ | /** Сервисные классы, которые понадобятся для выполнения методов */ | ||
+ | use UmiCms\Classes\System\Utils\Captcha\Strategies\GoogleRecaptcha; | ||
+ | use UmiCms\Service; | ||
+ | |||
+ | /** Расширение php шаблонизатора для шаблона default */ | ||
+ | class PhpExtension extends ViewPhpExtension { | ||
+ | |||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Далее, открываем файл config.ini, который перед этим создали в папке шаблона, там уже есть настройки для "причёсывания" отдаваемых методами массивов, и добавляем туда 2 строчки: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | [php-templater] | ||
+ | extensions[] = "/templates/default/php/library/PhpExtension" | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | '''Важно!''' Имя класса в принципе может быть любым, но имя файла, содержащего этот класс, а также путь к файлу, прописанный в конфиге, должны в точности его повторять, вплоть до капитализации. | ||
+ | |||
+ | Поскольку extensions - это массив, понятно, что таких классов расширений можно добавить сколько угодно. Пока нам хватит и одного. Но иногда полезно расширения функциональности конкретных модулей группировать в отдельные классы, чтобы проще было ориентироваться в коде. | ||
+ | |||
+ | Все публичные методы этого класса теперь будут доступны в шаблоне при помощи волшебной переменной $this. Но лучше это посмотреть на примере. | ||
+ | |||
+ | === Добавление глобальных переменных === | ||
+ | |||
+ | Ранее мы сделали одну не очень красивую вещь - добавили полученные прямо в шаблоне настройки к массиву $variables, и вынуждены были и дальше передавать их в методе render(), чтобы не потерялись. Значительно лучше было бы создать какие-то глобальные переменные, которые были бы доступны в любом фрагменте нашего шаблона. | ||
+ | |||
+ | Итак, добавляем в наш класс расширения следующие функции: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | |||
+ | /** Сервисные классы, которые понадобятся для выполнения методов */ | ||
+ | use UmiCms\Classes\System\Utils\Captcha\Strategies\GoogleRecaptcha; | ||
+ | use UmiCms\Service; | ||
+ | |||
+ | /** Расширение php шаблонизатора для шаблона default */ | ||
+ | class PhpExtension extends ViewPhpExtension { | ||
+ | |||
+ | /** | ||
+ | * Инициализирует общие переменные для шаблонов. | ||
+ | * @param array $variables глобальные переменные запроса | ||
+ | */ | ||
+ | public function initializeCommonVariables($variables) { | ||
+ | $templateEngine = $this->getTemplateEngine(); | ||
+ | $templateEngine->setCommonVar('domain', $variables['domain']); | ||
+ | $templateEngine->setCommonVar('lang', $variables['lang']); | ||
+ | $templateEngine->setCommonVar('settings', $this->requestSettingsContainer($variables)); | ||
+ | } | ||
+ | |||
+ | |||
+ | /** | ||
+ | * Запрашивает актуальный объект настроек и возвращает его | ||
+ | * @param array $variables глобальные переменные запроса | ||
+ | * @return bool|iUmiObject | ||
+ | */ | ||
+ | public function requestSettingsContainer($variables) { | ||
+ | $set = new selector('objects'); | ||
+ | $set->types('object-type')->name('umiSettings', 'settings'); | ||
+ | $set->where('domain_id')->equals($variables['domain-id']); | ||
+ | $set->where('lang_id')->equals($variables['lang-id']); | ||
+ | $set->limit(0, 1); | ||
+ | $settings = $set->result(); | ||
+ | if (is_array($settings)) return $settings[0]; | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Здесь использовали методы базового класса ViewPhpExtension для работы с глобальными переменными. Вставим вызов метода initializeCommonVariables() в файл common.phtml: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | $this->initializeCommonVariables($variables); | ||
+ | ?> | ||
+ | <!DOCTYPE html> | ||
+ | <html> | ||
+ | <?= $this->render($variables, 'layout/head') ?> | ||
+ | <body> | ||
+ | <?= $this->render($variables, 'layout/header') ?> | ||
+ | <?= $this->render($variables, 'layout/main') ?> | ||
+ | <?= $this->render($variables, 'layout/footer') ?> | ||
+ | |||
+ | <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script> | ||
+ | <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script> | ||
+ | </body> | ||
+ | </html> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Теперь для получения значения этих переменных в любом фрагменте шаблона достаточно написать $this->getCommonVar('имя переменной'). Например, файл footer.phtml приобретет следующий вид: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | $settings = $this->getCommonVar('settings'); | ||
+ | ?> | ||
+ | <footer> | ||
+ | <div class="container"> | ||
+ | <span umi:object-id="<?= $settings->id ?>" umi:field-name="copyright"><?= $settings->copyright ?></span> <?= date("Y") ?> | ||
+ | </div> | ||
+ | </footer> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Аналогичным образом меняем остальные фрагменты шаблона, где используются настройки. Теперь их не надо добавлять к массиву $variables. | ||
+ | |||
+ | === Получение блоков для главной страницы === | ||
+ | |||
+ | При помощи класса PhpExtension можем улучшить код ещё одного фрагмента - content/home/index.phtml. Добавляем в класс метод getHomePageBlocks(): | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | /** | ||
+ | * Возвращает информацию для главной страницы по блокам | ||
+ | * @param array $variables глобальные переменные запроса | ||
+ | * @return bool|array | ||
+ | */ | ||
+ | public function getHomePageBlocks($variables) { | ||
+ | $blocks = []; | ||
+ | $hierarchy = umiHierarchy::getInstance(); | ||
+ | $children = $hierarchy->getChildrenTree($variables['pageId'], false, false, 1); | ||
+ | foreach ($children as $id => $val) { | ||
+ | $element = $hierarchy->getElement($id); | ||
+ | if ($element instanceof umiHierarchyElement) { | ||
+ | $blocks[$id]['id'] = $element->id; | ||
+ | $blocks[$id]['name'] = $element->name; | ||
+ | $blocks[$id]['h1'] = $element->h1; | ||
+ | $blocks[$id]['altName'] = $element->altName; | ||
+ | $blocks[$id]['content'] = $element->content; | ||
+ | } | ||
+ | } | ||
+ | return $blocks; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Следовало бы ещё cmsController::getInstance()->getResourcesDirectory(); убрать в PhpExtension, для красоты, вот так: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | /** | ||
+ | * Возвращает путь до директории шаблона | ||
+ | * @return bool|string | ||
+ | */ | ||
+ | public function getTemplateDirectory() { | ||
+ | return cmsController::getInstance()->getResourcesDirectory(); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | В результате код content/home/index.phtml станет вполне приличным: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | /** | ||
+ | * Главная страница: | ||
+ | * - Блок с вступительным текстом | ||
+ | * - Блок "Новости" | ||
+ | * - Блок "Контакты" | ||
+ | * Для удаления блока достаточно снять | ||
+ | * у соответствующего раздела признак "Отображать в меню" | ||
+ | * Для добавления блока надо добавить блок в структуру | ||
+ | * раздела и создать в папке content/home файл шаблона | ||
+ | * с именем как в поле "Псевдостатический адрес" | ||
+ | * | ||
+ | */ | ||
+ | $blocks = $this->getHomePageBlocks($variables); | ||
+ | $resourcesDir = $this->getTemplateDirectory(); | ||
+ | |||
+ | foreach ($blocks as $page) { | ||
+ | if (isset($page['altName'])) { | ||
+ | if ($resourcesDir && file_exists($resourcesDir . 'php/content/home/' . $page['altName'] . '.phtml')) echo $this->render($page, 'content/home/' . $page['altName']); | ||
+ | else echo '<div class="alert alert-danger">Отсутствует шаблон: ', $resourcesDir, 'content/home/', $page['altName'], '.phtml</div>'; | ||
+ | } | ||
+ | } | ||
+ | ?> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Организация повторяющегося кода === | ||
+ | |||
+ | На главной странице мы вывели новости с анонсами, но совсем забыли про собственно раздел новостей, где всё ещё используем макрос lastlist(). Было бы правильно добавить в PhpExtension метод: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | /** | ||
+ | * Возвращает список последних новостей | ||
+ | * @param bool|int $newsPageId id ленты новостей | ||
+ | * @return bool|array | ||
+ | */ | ||
+ | public function getNewsList($newsPageId = false) { | ||
+ | if (!$id) { | ||
+ | $templateEngine = $this->getTemplateEngine(); | ||
+ | $newsList = $templateEngine->getCommonVar('settings')->newslist; | ||
+ | if (!empty($newsList)) $newsPageId = $newsList[0]->id; | ||
+ | else return false; | ||
+ | } | ||
+ | return $this->macros("news", "newslist", [$newsPageId]); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Если передан id ленты новостей, то возвращаем массив, если нет - пытаемся найти его в настройках. После этого можем в файле content/home/news.phtml написать: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | $newsList = $this->getNewsList(); | ||
+ | ?> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | а в файле news/rubric.phtml: | ||
+ | |||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | $page = $variables['page']; | ||
+ | $newsList = $this->getNewsList($page->id); | ||
+ | ?> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Когда надо добавлять методы в в класс шаблонизатора? === | ||
+ | |||
+ | Даже на этих примерах видно, насколько понятней становится код шаблонов. По сути, это все равно обращение к базе в процессе рендеринга. Но выглядит лучше, и код обращений собран в одном файле. Поэтому лучше создавать методы класса, если надо: | ||
+ | |||
+ | * Организовать глобальны переменные | ||
+ | * Убрать из шаблона большие куски кода, особенно содержащие обращения к базе данных | ||
+ | * Ликвидировать повторяющиеся в разных фрагментах шаблона куски кода | ||
+ | |||
+ | == Вместо заключения == | ||
+ | |||
+ | За рамками этой инструкции остались события, это довольно большая тема, и, к счастью, [http://api.docs.umi-cms.ru/razrabotka_nestandartnogo_funkcionala/sobytijnaya_model_umicms/ хорошо документированная]. | ||
+ | |||
+ | Следующим этапом можно было также рассмотреть методы работы с формами, это, например, раздел личного кабинета пользователя, методы регистрации и авторизации. Но во-первых, на сайте не всегда нужен такой раздел, во-вторых, делать странички с формами, в то время как это давно везде реализовано при помощи модальных окон и AJAX-запросов, мне просто лень. | ||
+ | |||
+ | Надеюсь, инструкция помогла на примерах понять архитектуру UMI CMS и принципы построения новых элементов. Самое время [[Что дальше?|перейти к практике]]! |
Текущая версия на 11:28, 6 ноября 2020
Некоторые методы UMI CMS оставляют желать лучшего. Например, метод lastlist модуля news не возвращает значение поля anons новости, который может понадобиться в списке новостей. Можно ли с этим что-то сделать?
Кастомные методы стандартных модулей
Открываем директорию /classes/components/news и видим там файлы macros.php и customMacros.php. В файле macros.php находим функцию lastlist(), копируем её и целиком вставляем в customMacros.php после строки public $module;.
Находим там кусок кода:
$line_arr = [];
$line_arr['attribute:id'] = $element_id;
$line_arr['node:name'] = $element->getName();
$line_arr['attribute:link'] = $umiLinksHelper->getLinkByParts($element);
$line_arr['xlink:href'] = 'upage://' . $element_id;
$line_arr['void:header'] = $lines_arr['name'] = $element->getName();
Там и правда не добавляется анонс. Поэтому вставляем одну строчку:
$line_arr['attribute:anons'] = $element->anons;
Все эти node: и attribute: предназначены для других шаблонизаторов, но лучше их сохранить для общности.
Открываем главную страницу нашего сайта - и видим ошибку "Ошибка (Error): Class 'Service' not found"! Правда, с этим понятно, что делать, надо в начале файла customMacros.php вставить "use UmiCms\Service;", вот так:
<?php
use UmiCms\Service;
/** Класс пользовательских макросов */
class NewsCustomMacros {
/** @var news $module */
public $module;
...
}
Больше ошибок не возникнет, и теперь при вызове метода lastlist() получим также и элемент массива 'anons'. Если изменим в файле content/home/news.phtml код цикла вот так:
<?php
if (!empty($newsList['items'])):
foreach ($newsList['items'] as $item):
?>
<li class="list-group-item">
<em><?= date("d.m.Y", $item['publish_time']) ?></em>
<a href="<?= $item['link'] ?>"><h3 umi:element-id="<?= $item['id'] ?>" umi:field-name="name" umi:empty="<?= $this->translate('empty_page_name') ?>"><?= $item['name'] ?></h3></a>
<div umi:element-id="<?= $item['id'] ?>" umi:field-name="anons" umi:empty="<?= $this->translate('empty_news_anons') ?>"><?= $item['anons'] ?></div>
</li>
<?php
endforeach;
endif;
?>
то увидим также и текст анонсов. Можно просто набрать в браузере http://umi.example.com/udata://news/lastlist/7/.json и увидеть изменения.
Что будет, если внести исправления сразу в файл macros.php? Очевидно, что все исправления будут потеряны при очередном обновлении системы.
Сразу замечу, что переопределять стандартные методы - ОЧЕНЬ ДУРНАЯ ПРАКТИКА. Если с сайтом будет работать другой программист, ему придется потратить кучу сил и времени на поиск ошибки, ведь он будет уверен, что вызывается стандартный метод. Поэтому ВСЕГДА, скопировав код стандартного метода, ПЕРЕИМЕНОВЫВАЙТЕ ЕГО! Пусть этот метод будет целиком и полностью кастомным, и это будет видно уже при вызове. И тогда возникает следующая задача - настройка разрешений.
Настройка разрешений для выполнения метода
Да, переопределение метода - нехороший поступок, но когда мы так делаем, автоматически избегаем множества проблем. Дело в том, что в UMI CMS есть сложная система разрешений, для каждого модуля существует собственный набор, посмотреть его можно в папке модуля в файле permissions.php.
Когда мы создаем метод с новым именем, то он не будет выполняться, т к отсутствует в файле permissions.php. Но это можно исправить, создав в той же папке модуля файл permissions.custom.php.
Предположим, по аналогии с методом lastlist() мы создали метод newslist(), куда внесли все необходимые нам изменения. Если посмотреть содержимое файла permissions.php, увидим, что это массив:
/** Группы прав на функционал модуля */
$permissions = [
/** Права на просмотр новостей */
'view' => [
'lastlist',
'listlents',
'rubric',
/** ... и так далее, не буду приводить его целиком */
]
];
По логике, нам надо добавить один элемент в массив $permissions['view'], поэтому в файле permissions.custom.php пишем:
<?php
$permissions['view'][] = 'newslist';
Интересный факт: даже если название метода содержит буквы с разной капитализацией, в этом массиве все нужно писать маленькими буквами. Не спрашивайте, почему.
Пока всё просто? Это только кажется. В каждом модуле структура и ключи массива разрешений разные, и в них нет никакой системы. Хорошо, если метод должен быть общедоступным, как правило, можно угадать, в какой именно массив его добавлять. Сложнее, если доступ можно давать только авторизоваанным пользователям, и то не всем. Так что - удачи!
Использование кастомного метода в шаблона
Теперь меняем в файле content/home/news.phtml имя метода:
$newsList = $this->macros('news', 'newslist', [$newsPageId]);
и увидим то, что планировали. И даже есть вызовем этот метод просто в строке URL, на пример, так:
http://umi.example.com/udata://news/newslist/7/.json
(где 7 - это id ленты новостей, на других сайтах может быть другой), то получим json-массив с элементом anons.
Иногда добавленные таким способом методы выдают ошибку, с одним таким случаем только что столкнулись, второй из возможных - это когда в копируемом методе используются другие методы из файла macros.php в виде $this->название_метода(). При вызове из другого класса метод не обнаруживается.. В этом случае надо заменить вызов на $this->module->название_метода(), не зря же переменная $module была добавлена в кастомный класс.
Кастомные методы внутри шаблона
Описанный выше способ хорош, но добавленный код находится вне нашего шаблона default. А нам бы хотелось, во-первых, при установке системы на другом хостинге просто закинуть туда готовый шаблон и получить готовый сайт, больше ничего не трогая, особенно системные директории. И во-вторых, хотелось бы для разных сайтов на разных щаблонах использовать разные кастомные методы.
И что приятно, начиная с версии 2.8.5 системы это можно сделать! Для этого создаем внутри нашего шаблона папку classes, внутри неё папку modules, в ней папку с именем модуля, например, news, а там - файл class.php, где создаем отдельный класс. Имя класса должно быть 'имямодуля_custom', а имена методов не должны совпадать с именами стандартных методов модуля. Таким образом, получим файл classes/modules/news/class.php с кодом:
<?php
/** Класс пользовательских методов */
class news_custom extends def_module {
}
?>
В этот файл можно добавлять свои методы, которые могут быть использованы в шаблоне при помощи вызова $this->macros('news', 'имя_метода', [массив_параметров_метода] ). Что приятно, в class.php уже не нужно подключать 'use UmiCms\Service;', его уже подключает def_module.
Добавление разрешения на выполнение метода
Тут тоже понадобится добавлять разрешения. К счастью, для этого достаточно в той же директории classes/modules/news/ создать файл permissions.php, куда внести тот же код, что ранее использовали в permissions.custom.php:
<?php
$permissions['view'][] = 'newslist';
Создание кастомного метода
Перенесём наш метод newslist() из кастомных макросов в этот новый класс. Не буду приводить тут его код, он в точности тот же.
Из кастомных макросов этот метод можно убрать, можно также удалить файл /classes/components/news/permissions.custom.php, ведь мы уже добавили разрешения в классы шаблона. В нашем фрагменте шаблона content/home/news.phtml ничего не меняем.
Если набрать в браузере ту же ссылку http://umi.example.com/udata://news/newslist/7/.json (не забываем поменять id ленты новостей, это только у меня он 7), то увидим массив новостей с анонсом, да и на сайте все прекрасно выводится.
Создание кастомных событий внутри шаблона
До создания собственных событий ещё довольно далеко. Просто для общности надо отметить, что события тоже можно создать внутри шаблона, в той же папке /classes/components/news/ в файле events.php.
Вызов метода из "чужого" шаблона
Шаблонов на сайте может быть несколько, и в одном шаблоне сайта получить доступ к методу из другого шаблона, вообще говоря, нельзя. Но есть хитрый хак. Если запрашивать данные, например, AJAXом через udata:, то можно в строке запроса в конце добавить параметр '?template_id={id шаблона}', и всё получится. Не знаю, кому и зачем это может понадобиться, разве только чтобы в дальнейшем получше запутать себя и коллег.
Собственный класс сайта
До сих пор мы рассматривали очень правильный с точки зрения MVC путь создания своих методов. Однако есть более простой и быстрый, но немного менее "канонический" способ, доступный только для PHP-шаблонизатора. Это создание класса расширения шаблонизатора.
Для начала создадим в директории php папку library. В данном случае название может быть любым. А в ней файл PhpExtension.php с вот таким кодом:
<?php
/** Сервисные классы, которые понадобятся для выполнения методов */
use UmiCms\Classes\System\Utils\Captcha\Strategies\GoogleRecaptcha;
use UmiCms\Service;
/** Расширение php шаблонизатора для шаблона default */
class PhpExtension extends ViewPhpExtension {
}
Далее, открываем файл config.ini, который перед этим создали в папке шаблона, там уже есть настройки для "причёсывания" отдаваемых методами массивов, и добавляем туда 2 строчки:
[php-templater]
extensions[] = "/templates/default/php/library/PhpExtension"
Важно! Имя класса в принципе может быть любым, но имя файла, содержащего этот класс, а также путь к файлу, прописанный в конфиге, должны в точности его повторять, вплоть до капитализации.
Поскольку extensions - это массив, понятно, что таких классов расширений можно добавить сколько угодно. Пока нам хватит и одного. Но иногда полезно расширения функциональности конкретных модулей группировать в отдельные классы, чтобы проще было ориентироваться в коде.
Все публичные методы этого класса теперь будут доступны в шаблоне при помощи волшебной переменной $this. Но лучше это посмотреть на примере.
Добавление глобальных переменных
Ранее мы сделали одну не очень красивую вещь - добавили полученные прямо в шаблоне настройки к массиву $variables, и вынуждены были и дальше передавать их в методе render(), чтобы не потерялись. Значительно лучше было бы создать какие-то глобальные переменные, которые были бы доступны в любом фрагменте нашего шаблона.
Итак, добавляем в наш класс расширения следующие функции:
<?php
/** Сервисные классы, которые понадобятся для выполнения методов */
use UmiCms\Classes\System\Utils\Captcha\Strategies\GoogleRecaptcha;
use UmiCms\Service;
/** Расширение php шаблонизатора для шаблона default */
class PhpExtension extends ViewPhpExtension {
/**
* Инициализирует общие переменные для шаблонов.
* @param array $variables глобальные переменные запроса
*/
public function initializeCommonVariables($variables) {
$templateEngine = $this->getTemplateEngine();
$templateEngine->setCommonVar('domain', $variables['domain']);
$templateEngine->setCommonVar('lang', $variables['lang']);
$templateEngine->setCommonVar('settings', $this->requestSettingsContainer($variables));
}
/**
* Запрашивает актуальный объект настроек и возвращает его
* @param array $variables глобальные переменные запроса
* @return bool|iUmiObject
*/
public function requestSettingsContainer($variables) {
$set = new selector('objects');
$set->types('object-type')->name('umiSettings', 'settings');
$set->where('domain_id')->equals($variables['domain-id']);
$set->where('lang_id')->equals($variables['lang-id']);
$set->limit(0, 1);
$settings = $set->result();
if (is_array($settings)) return $settings[0];
return false;
}
}
Здесь использовали методы базового класса ViewPhpExtension для работы с глобальными переменными. Вставим вызов метода initializeCommonVariables() в файл common.phtml:
<?php
$this->initializeCommonVariables($variables);
?>
<!DOCTYPE html>
<html>
<?= $this->render($variables, 'layout/head') ?>
<body>
<?= $this->render($variables, 'layout/header') ?>
<?= $this->render($variables, 'layout/main') ?>
<?= $this->render($variables, 'layout/footer') ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>
</html>
Теперь для получения значения этих переменных в любом фрагменте шаблона достаточно написать $this->getCommonVar('имя переменной'). Например, файл footer.phtml приобретет следующий вид:
<?php
$settings = $this->getCommonVar('settings');
?>
<footer>
<div class="container">
<span umi:object-id="<?= $settings->id ?>" umi:field-name="copyright"><?= $settings->copyright ?></span> <?= date("Y") ?>
</div>
</footer>
Аналогичным образом меняем остальные фрагменты шаблона, где используются настройки. Теперь их не надо добавлять к массиву $variables.
Получение блоков для главной страницы
При помощи класса PhpExtension можем улучшить код ещё одного фрагмента - content/home/index.phtml. Добавляем в класс метод getHomePageBlocks():
/**
* Возвращает информацию для главной страницы по блокам
* @param array $variables глобальные переменные запроса
* @return bool|array
*/
public function getHomePageBlocks($variables) {
$blocks = [];
$hierarchy = umiHierarchy::getInstance();
$children = $hierarchy->getChildrenTree($variables['pageId'], false, false, 1);
foreach ($children as $id => $val) {
$element = $hierarchy->getElement($id);
if ($element instanceof umiHierarchyElement) {
$blocks[$id]['id'] = $element->id;
$blocks[$id]['name'] = $element->name;
$blocks[$id]['h1'] = $element->h1;
$blocks[$id]['altName'] = $element->altName;
$blocks[$id]['content'] = $element->content;
}
}
return $blocks;
}
Следовало бы ещё cmsController::getInstance()->getResourcesDirectory(); убрать в PhpExtension, для красоты, вот так:
/**
* Возвращает путь до директории шаблона
* @return bool|string
*/
public function getTemplateDirectory() {
return cmsController::getInstance()->getResourcesDirectory();
}
В результате код content/home/index.phtml станет вполне приличным:
<?php
/**
* Главная страница:
* - Блок с вступительным текстом
* - Блок "Новости"
* - Блок "Контакты"
* Для удаления блока достаточно снять
* у соответствующего раздела признак "Отображать в меню"
* Для добавления блока надо добавить блок в структуру
* раздела и создать в папке content/home файл шаблона
* с именем как в поле "Псевдостатический адрес"
*
*/
$blocks = $this->getHomePageBlocks($variables);
$resourcesDir = $this->getTemplateDirectory();
foreach ($blocks as $page) {
if (isset($page['altName'])) {
if ($resourcesDir && file_exists($resourcesDir . 'php/content/home/' . $page['altName'] . '.phtml')) echo $this->render($page, 'content/home/' . $page['altName']);
else echo '<div class="alert alert-danger">Отсутствует шаблон: ', $resourcesDir, 'content/home/', $page['altName'], '.phtml</div>';
}
}
?>
Организация повторяющегося кода
На главной странице мы вывели новости с анонсами, но совсем забыли про собственно раздел новостей, где всё ещё используем макрос lastlist(). Было бы правильно добавить в PhpExtension метод:
/**
* Возвращает список последних новостей
* @param bool|int $newsPageId id ленты новостей
* @return bool|array
*/
public function getNewsList($newsPageId = false) {
if (!$id) {
$templateEngine = $this->getTemplateEngine();
$newsList = $templateEngine->getCommonVar('settings')->newslist;
if (!empty($newsList)) $newsPageId = $newsList[0]->id;
else return false;
}
return $this->macros("news", "newslist", [$newsPageId]);
}
Если передан id ленты новостей, то возвращаем массив, если нет - пытаемся найти его в настройках. После этого можем в файле content/home/news.phtml написать:
<?php
$newsList = $this->getNewsList();
?>
а в файле news/rubric.phtml:
<?php
$page = $variables['page'];
$newsList = $this->getNewsList($page->id);
?>
Когда надо добавлять методы в в класс шаблонизатора?
Даже на этих примерах видно, насколько понятней становится код шаблонов. По сути, это все равно обращение к базе в процессе рендеринга. Но выглядит лучше, и код обращений собран в одном файле. Поэтому лучше создавать методы класса, если надо:
- Организовать глобальны переменные
- Убрать из шаблона большие куски кода, особенно содержащие обращения к базе данных
- Ликвидировать повторяющиеся в разных фрагментах шаблона куски кода
Вместо заключения
За рамками этой инструкции остались события, это довольно большая тема, и, к счастью, хорошо документированная.
Следующим этапом можно было также рассмотреть методы работы с формами, это, например, раздел личного кабинета пользователя, методы регистрации и авторизации. Но во-первых, на сайте не всегда нужен такой раздел, во-вторых, делать странички с формами, в то время как это давно везде реализовано при помощи модальных окон и AJAX-запросов, мне просто лень.
Надеюсь, инструкция помогла на примерах понять архитектуру UMI CMS и принципы построения новых элементов. Самое время перейти к практике!