• Блог
  • Zoomx 3.0. Полноценный парсер

Ну что, друзья, встречайте новую версию ZoomX с полноценной шаблонизацией и ресурсов и чанков и шаблонов. Т.е. Smarty теперь можно использовать и в обычных шаблонах MODX, а не только в роутах. Ну и решён вопрос с pdoTools, который парсит чанки исключительно шаблонизатором Fenom без какой-либо альтернативы. Также добавлена полноценная поддержка привычных файловых элементов. Ещё добавлены пара функций хелперов для более удобной работы в роутах, событие инициализации ZoomX, собственный перехватчик исключений, возможность расширения главного класса сервиса через механизм макросов (подсмотрено у Laravel), безопасный режим Smarty, режим конкретных виртуальных страницы. Далее разберём эти изменения подробнее.

Внимание!

Версия 3.0.0 требует PHP >= 7.1.

Важно!

Версия 3.0.0 предназначена только для новой установки! Обновление может сломать сайт.

Полноценная шаблонизация

Начиная в этой версии шаблонизатор Smarty может быть использован не только в режиме фреймворка, т.е. только для ресурсов, для которых указаны роуты, но и в обычном режиме, т.е. прямо в шаблонах MODX. Казалось бы, чего сложного было добавить это раньше. Но для этого решения требовалось время. Необходимо было продумать и обсудить один очень важный момент — принцип работы шаблонизатора Smarty несовместим с шаблонизотором MODX. Его нельзя просто указать в настройке parser_class, как в случае с pdoParser. Новый шаблонизатор должен быть наследником modParser. Что, как вы понимаете, для любого PHP шаблонизатора условие невыполнимое. А значит вариант с системной настройкой отпадает. Потребовалось время обдумать другие варианты внедрения. Ну и в итоге я остановился на таком — основной парсер указывается в системной настройке zoomx_parser_class, а тот, что указан в настройке parser_class, используется для парсинга MODX тегов (например, в блоке modx).

Давайте теперь осмыслим всё сказанное. Раньше это работало так — в смешанном режиме (когда работает и ZoomX и MODX), ресурсы, для которых указаны роуты, обрабатывались Smarty, а остальные передавались в MODX и обрабатывались указанным в настройке parser_class шаблонизатором (с многократным парсингом). Это было сделано для гибкости (где хочешь — используй ZoomX, где нет — пусть MODX работает как обычно). Теперь ресурсы, для которых не настроены роуты, можно передать на обработку шаблонизатору Smarty (с однократным парсингом). Но, опять же, для сохранения гибкости добавлена системная настройка zoomx_use_zoomx_parser_as_default, которая может отключать эту возможность.

Но и тут есть своя тонкость. Я сторонник принципа из мира фреймворков — логика в шаблонах, контент в ресурсах. Поэтому шаблонизатор будет парсить только шаблон. Это не значит, что вы не сможете парсить контент ресурса. Просто вам придётся делать это самостоятельно, например через модификатор parse — {'content'|resource|parse}.

Раз можно использовать Smarty в обычных шаблонах, то становятся доступны все его фишки. В том числе и расширение шаблонов. Для этого нужно создать базовый шаблон в папке шаблонов Smarty. Сделать это можно прямо в админке — или через вкладку «Файлы» или создать статический шаблон MODX. А следующие шаблоны расширять с указанием имени файла базового шаблона —

{extends "base.tpl"}

{block "styles"}
  <link rel="stylesheet" href="styles.css">
{/block}

{block "content"}
  <h1>{'longtitle'|resource:'pagetitle'}</h1>
  <section>
    <p>Без контента ресурса.</p>
  </section>
{/block}

Поддержка pdoTools сниппетов

Несмотря на то, что поддержка pdoTools перешла ко мне, кашерно подсунуть в него Smarty или любой другой PHP шаблонизатор не получается. Проблема — нельзя указать парсер, не наследующий базовый класс modParser. На самом деле решение есть — можно создать адаптер и указать его в настройке parser_class. Но в этом случае теряется вышеописанная возможность использования разных шаблонизаторов в разных режимах, так как непонятно, какой парсер использовать в режиме MODX — modParser, pdoParser или ещё какой другой. Поэтому я пошёл другим путём — создал свои классы, заменяющие стандартные pdoTools и pdoFetch. В них для парсинга чанков вызывается шаблонизатор Smarty. Ну и по традиции управлять этим можно с помощью настройки zoomx_enable_pdotools_adapter. Таким образом, теперь смело можно отключать шаблонизатор Fenom и использовать сниппеты pdoTools только со Smarty.

Файловые элементы

Как я уже говорил, в PHP шаблонизаторах все шаблоны и подшаблоны (чанки по-нашему) — файловые. Поэтому достаточно написать в шаблоне {include 'chunk'} и файл chunk.tpl подключится и обработается. Но так как есть особенный модыксовский подход — подготовка контента в сниппетах с использованием чанков, то простого include недостаточно. Нужно в параметрах сниппета передавать файловые чанки. С точки зрения фреймворковсой разработки это мягко говоря странно — находясь в разметке формировать кусок нужной разметки где-то в другом месте (в сниппете). Но это специфика MODX, придуманная для своего собственного шаблонизатора. Не будешь же переписывать все компоненты под PHP шаблонизаторы. Хотя в последней версии pdoTools я доработал механизм возврата данных из сниппетов как раз для использования в сторонних шаблонизаторах. Но этим пользуются исключительно опытные и давно работающие с MODX разработчики. Новички даже Fenom'а бояться, а многие просто уходят в поисках более современного стека разработки. Ну ладно, это лирика. Ближе к теме.

Файловые чанки

Сначала разберёмся с чанками. В разметке это выглядит так

<!-- Обычный Smarty синтаксис -->
{include 'article.tpl'}
<!-- Использование байндинга @FILE -->
{'@FILE article.tpl'|chunk}

Расширение .tpl можно не указывать — к имени файла будет автоматически добавлено расширение, указанное в системной настройке zoomx_template_extension.

В php коде нужно использовать метод getChunk() сервиса ZoomX —

$content = zoomx()->getChunk('@FILE article', $properties);

Файлы чанков должны находится в папке шаблонов Smarty.

Файловые сниппеты

Сниппеты работают похоже. Вызывать можно несколькими способами.

<!-- Обычный Smarty синтаксис -->
{run file='file_snippet.php' params=['foo' => 'bar', 'tpl' => '@INLINE <span>{$pagetitle}</span>']}
<!-- Использование байндинга @FILE -->
{'@FILE file_snippet'|snippet:['foo' => 'bar', 'tpl' => '@FILE file_chunk.tpl']}
<!-- Использование объекта $zoomx -->
{$zoomx->runFileSnippet('file_snippet', ['foo' => 'bar', 'tpl' => 'modx_chunk'])}

Информация!

Функция run может быть использована и для вызова обычных сниппетов. Для этого вместо атрибута file нужно указать атрибут snippet.

В php коде вызывать так

// Вызов файлового сниппета
$content = zoomx()->runFileSnippet('file_snippet', $properties);
// Вызов обычного сниппета
$content = zoomx()->runSnippet('usual_snippet', $properties);

Расширение .php можно не указывать — оно добавится автоматически.

Новые хелперы

В новой версии добавлены 2 новых фукнции — redirectx() и filex(). Первая для более удобной переадресации. Вторая для отдачи файлов.

Функция redirectx($url, $status = 302, array $headers = [])

  • (string) $url — новый URL для переадресации.
  • (int) $code — код переадресации. По-умолчанию, 302.
  • (array) $headers — массив HTTP заголовков.
$router->get('first.html',  function () use ($modx) {
    // Вместо 
    // $modx->sendRedirect('second.html', ['responseCode' => $_SERVER['SERVER_PROTOCOL'] . ' 301 Moved Permanently']);
    
    return redirectx('second.html', 301);
});

Функция filex($path, $isAttachment = false, $deleteFileAfterSend = false)

  • (string) $path — полный путь к файлу.
  • (bool) $isAttachment — вернуть файл как вложение (attachment). Или вывести его на странице или открыть диалог для скачивания.
  • (bool) $deleteFileAfterSend — удалить файл после ответа.
$router->get('files/{file}',  function ($file) use ($modx) {
    // Отключить автозагрузку ресурса
    zoomx()->autoloadResource(false);
    // Проверить права
    if (!$modx->user->isMember('Subscribers')) {
        abortx(403, 'Только пользователи группы "Subscribers" могут скачивать файлы.');
    }

    return filex(MODX_CORE_PATH . 'path/to/users/files/' . basename($file), true);
});

Собственный перехватчик исключений

В современной разработке давно уже используют исключения для возврата ошибки. Теперь и в MODX можно просто выбросить исключение и в ответ он выведет страницу ошибки. Причем или в полном формате или в сокращённом в зависимости от системной настройки zoomx_show_error_details.

Самый простой вариант — использование функции abortx(), где первым параметром передаётся код, а вторым сообщение. Про коды и исключения можно прочитать в документации.

Отдельные виртуальные страницы

С помощью настройки zoomx_autoload_resource можно отключить автозагрузку ресурсов для всех роутов. А если нужна только одна виртуальная страница или несколько? Тогда можно отключить загрузку только в конкретном роуте.

$router->get('users/{id}/profile',  function ($user) {
    // Отключаем автозагрузку ресурса
    zoomx()->autoloadResource(false);  // === $modx->setOption('zoomx_autoload_resource', false); 
    $user = $modx->getObject('modUser', ['id' => (int)$id])
    if (!user) {
        abortx(404, 'User not found');
    } 
    return viewx('profile.tpl', ['user' => $user]);
});

Автоматический детектор Content-Type

Когда вы используете виртуальные страницы, то ресурс не подгружается. А именно он определяет Content-Type ответа. Соответственно, нужно или вручную в роуте создать пустой ресурс с нужным Content-Type — $modx->resource = $modx->newObject('modDocument', ['content_type' => 2]); или положится на автоматический детектор, который по расширению в URI может выявить нужный тип.

$router->get('sitemap.xml',  function () {
    // Отключаем автозагрузку ресурса
    zoomx()->autoloadResource(false);  // === $modx->setOption('zoomx_autoload_resource', false); 
    // Автоматический детектор по URI определит Content-Type = "text/xml".
    return viewx('sitemap.xml');
});

Если расширения в URI нет, то можно отправить заголовок прямо в роуте — header('Content-Type: text/xml; charset=UTF-8');.

Безопасный режим Smarty

Про то, что это такое, можно прочитать в документации Smarty. Если коротко, то можно отключать функции PHP, глобальные переменные, модификаторы, теги и т.д. Для этого нужно создать класс как указано в документации и положить его в папку core/components/zoomx/smarty/security/. Название класса и файла должны совпадать. Далее нужно прописать его в настройку zoomx_smarty_security_class и включить режим с помощью настройки zoomx_smarty_security_enable.

Заключение

Доработки очень серъёзные, поэтому я не советую обновлять уже установленный компонент. Если ничего особо не трогали, то может и не сломает сайт. Но лучше не рисковать. Следующий шаг — обновление документации. Это ничуть не легче, чем писать код, и требует немало времени. После этого обычно вносятся какие-нибудь правки в код. Пока пишу документацию, надесь, ещё придёт обратная связь с возможными предожениями и ошибками. В общем, пробуйте, пишите. Сделаем MODX лучше :)

1   3097

Комментарии ()

  1. Семён 02 октября 2021 # 0
    Сергей, приветствую!
    Уже писал в комментах в телеге, но на всякий случай и сюда напишу:
    при установке, в файлах компонента отсутствует всё, что касается адаптера pdoTools, просто нет самой папки с файлами и соответственно получаем ошибки в консоли при включении настройки замены парсера Fenom
    Видимо забыл положить в упакованный компонент
    1. Сергей Шлоков 02 октября 2021 # 0
      Я тебе в телеге уже ответил и файлы скинул.
    2. Борис 16 октября 2021 # 0
      Использую конструкцию типа {include 'article.tpl'}, в чанке вызов [[pdoResources? &includeTVs=`page-img` &tpl=`@INLINE… ]] в итоге на странице выводится заголовок и tv.картинка и нет например introtext.
      Стандартный способ выводит всё нормально.

      Включение zoomx_enable_pdotools_adapter выпадает в 500 ошибку.
      1. Сергей Шлоков 12 ноября 2021 # 0
        Хм. Пропустил сообщение — почему-то не было письма о нём. Поэтому прошу прощения за поздний ответ.

        Если ещё актуально, можете поподробнее рассказать что как и где выводите? У меня сайт частично уже переведён на ZoomX, ошибок нет.
      2. Sergusnet 14 ноября 2021 # 0
        День добрый, Сергей
        А как решается проблема с привязкой TV к шаблону? У меня есть сайт, где есть несколько шаблонов с достаточно большим количество своих TV. Как быть? Создать пустые шаблоны в базе и использовать их только для привязки TV?
        1. Сергей Шлоков 02 декабря 2021 # 0
          Добрый день!
          Ну пока да, нужно создавать пустой шаблон. Других интерфейсов для TV нет. Подумаю на эту тему!

          П.С. Что-то с почтой опять. Письма не приходят. Наверно и это не придёт.
        2. traf.spb 15 ноября 2021 # 0
          Сергей, добрый день.
          Вопрос по доступ к тв в шаблонах smarty при вызове снипета pdoResource.
          Столкнулся со странной проблемой
          есть вызов снипета
          {'pdoResources'|snippet:[
          	'parents' => 0,
          	'includeTVs' => 'offices',
          	'tpl' => '@FILE templates/chunks/main/zavod.tpl'
          ]}
          
          с шаблоне zavod.tpl пытаюсь обращаться к TV
          {if $offices = ${'tv.offices'}}
          ТУТ какой то код
          {/if}
          
          в момент когда писал код это все работало)), но прошли выходные и перестало:)

          Может подскажешь как правильно, нужно обращаться к ТВ свойствам в pdo-сниппетах?
          Хочется полностью отказаться от fenom-а
          1. Сергей Шлоков 02 декабря 2021 # 0
            Привет!
            {'tv_name'|tv}
            // или так
            {'tv_name'|resource}
            
            В конструкциях так
            {if !empty('tv_name'|tv)}
                <div>{'tv_name'|tv}</div>
            {else}
                <div>Пустое значение.</div>
            {/if}
            
            П.С. Задержка с ответом по причине какой-то проблемы с почтой — письма не приходят.
            1. Макс 24 февраля 2022 # 0
              Присоединяюсь. Думаю traf.spb имеет ввиду, как выводить TV в чанках через includeTVs.
              $item['tv.image_preview'] → modxSmarty
              $_pls['tv.image_preview'] → pdoTools Fenom
              ${'tv.image_preview'} → pdoToolsAdapter zoomX
              
              Вопрос, нет ли другой конструкции с синтаксисом массива, как у первых двух примеров. Ошибки нет никакой конечно, но на конструкцию {${'tv.image_preview'}} ругается подсветка, исключительно визуальное неудобство.

              1. Сергей Шлоков 28 февраля 2022 # 0
                А плейсхолдер ph не подходит?
          2. traf.spb 15 ноября 2021 # 0
            Разобрался с проблемой почему перестало обрабатывать код
            ${'tv.offices'}
            сам отключил zoomx_enable_pdotools_adapter.
            Но вопрос про как правильно обращаться к TV остался, можешь сказать верно ли я делаю вызов к этим значениям.
            1. Sergusnet 15 ноября 2021 # 0
              С одним контекстом никаких проблем. Второй контекст Zoomx не видит, за исключением первой страницы (site_start). Modx с нуля на локалке. Никаких плагинов, кроме переключателя контекста и Zoomx. Переключатель контекста работает без изменения htaccess.
              zoomx_routing_mode = 0,
              zoomx_use_zoomx_parser_as_default = нет
              Роуты и парсер отключил, все равно ошибка 404. Структура URL сайта: site.com, site.com/ua/
              В контекстах определены:
              base_url
              cultureKey
              http_host
              site_start
              site_url
              Очень хочу внедрить Zoomx, но вот такой затык. День убил:(. Отключаешь переключатель языков — вместо ошибки загружается страница, к примеру, site.com/ua/page-1, но с русским содержанием.
              Куда копать, кто-то подскажет?
              1. Sergusnet 17 ноября 2021 # 0
                Сам себе и отвечаю, может, пригодится кому-то.
                В файле\core\components\zoomx\src\Request.php в строке
                $this->modx->resourceIdentifier = $this->handler->getResourceIdentifier();
                определяется идентификатор в виде URL для поиска страницы. В случае с префиксом контекста он выглядит так: ru/cat1/page1, без префикса: cat1/page1
                Вырезал префикс и все стало на свои места. Определение контекста происходит в отдельном плагине.
                После
                $this->modx->resourceIdentifier = $this->handler->getResourceIdentifier();
                вставил
                $base_url = ltrim($this->modx->getOption('base_url', null, MODX_BASE_URL),'/');
                $resource_url = $this->handler->getResourceIdentifier();
                if($_REQUEST['q'] != ''){
                if (0 == strncmp($resource_url, $base_url, strlen($base_url))) {
                $this->modx->resourceIdentifier = substr($resource_url, strlen($base_url) );
                }

                }
                Если что не так, пусть профессиональные программисты поправят. Автору, на мой взгляд, стоит продумать подобные ситуации. В виде настройки что ли… А вообще спасибо за разработку парсера.:)
                1. Сергей Шлоков 02 декабря 2021 # 0
                  Обязательно продумаю. Постараюсь в ближайшей версии решить.

              Вы должны авторизоваться, чтобы оставлять комментарии.

              Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.