Все основные элементы системы MODX, такие как чанки, шаблоны, сниппеты и т.д, хранятся в БД, из этого появляется проблема осуществления контроля версий за этими элементами, а также сложности с разделением на development и production версии сайта.

Приведу основные требования, чего я хочу от своего рабочего процесса на MODX Revo:
  • контроль версий везде, где пишу какой-либо код (html, css, js, php),
  • иметь отдельную dev-версию сайта, на которой ведётся текущая разработка, а после — деплоить все изменения в продакшн, причём, желательно, автоматизировать этот процесс,
  • минимум копипаста при разработке и деплое.



Содержание


  1. Введение
  2. Development и Production версии сайта
  3. Контроль версий
  4. Схема работы
  5. Деплой
  6. Заключение
  7. Ссылки



Введение


На момент написания этой статьи я имел опыт веб-разработки чуть более года. Мне повезло, я начал свой путь в этой сфере именно с MODX Revo, но несмотря на все плюсы этого фреймворка, со временем я начал сталкиваться и с минусами. Пока что я не работал в команде над большими сложными проектами и не знаю как обычно люди справляются с указанными выше сложностями. В интернетах я не нашёл какого-либо конкретного решения, и это поспособствовало созданию своего workflow, который я хочу представить в этой статье. Сразу оговорюсь, я не придумал ничего принципиально нового, а просто собрал разбросанные по интернету куски информации в одно руководство как устроить свой рабочий процесс при работе с MODX Revolution.

Что нам понадобится.
— Git
— npm + Gulp
Для работы я использую IDE PHPStorm.


Development и Production версии сайта


Первое что приходит на ум, когда думаешь как организовать рабочую (тестовую, dev) и релизную (боевой, prod) версии сайта, это просто иметь две его копии. Сначала ведём разработку на тестовом сайте, а после одобрения менеджером или клиентом, переносим всё на боевой. Просто и ясно, только вот этот процесс может превратиться в кошмар, т.к. всё делается вручную и существует немалая вероятность ошибки при переносе, потом придётся проверять, что ты не так сделал, а потом ещё окажется, что боевой сайт не вполне соответствует тестовому и нужно что-то ещё дополнительно править и т.д. и т.п. Нет, такого нам не надо.

Не буду долго рассуждать и скажу сразу, я предлагаю использовать один экземпляр сайта, в котором мы воспользуемся механизмом контекстов MODX Revo. Стандартный контекст web будет содержать релизную версию сайта. Плюс мы создадим ещё один контекст dev, работу с которым вынесем в поддомен. Таким образом, нам нужно:
— домен example.com, работающий в контексте web
— поддомен dev.example.com, работающий в контексте dev
При деплое мне нужно, чтобы изменения сделанные в контексте dev, каким бы то ни было образом сливались в web.

Создаём контекст dev


Эта часть работы начинается с того момента, когда у вас уже установлен движок и необходимые компоненты и настроены доменные имена, т.е. при обращении по обоим адресам example.com и dev.example.com открывается одна и та же стартовая страница MODX (корневая директория обоих доменов общая).

Теперь заходите в админ-панель → системные настройки → контексты и создайте новый контекст. Ключ укажите dev, имя — как угодно, я назвал Development. В контекстном меню слева у вас появится новый контекст. Первым делом создайте для него ресурс, а потом зайдите в настройки контекста (правой кнопкой по контексту → редактировать → настройки контекста) и задайте настройки
site_start
error_page
unauthorized_page,
указав в них ID созданного ресурса. Это нужно, чтобы система не падала с ошибкой, если при работе в контексте dev не будет найдена какая-либо страница.


прим. Я создал контекст dev (Development) и переименовал стандартный контекст Website на Production, ключ стандартного контекста остался прежним — web.

Теперь в папке core/elements/common/plugins/ (создайте необходимые папки) создайте файл switchContext.php со следующим содержимым
<?php
/**
 * @var modX $modx
 */

/* don't execute if in the Manager */
if ($modx->context->get('key') == 'mgr') {
    return;
}

switch ($_SERVER['HTTP_HOST']) {
    case 'dev.example.com':
        $modx->switchContext('dev');
        break;
    case 'example.com':
        break;
    default:
        $modx->log(modX::LOG_LEVEL_ERROR, 'Check this plugin! May be your headache coming from here.');
        break;
}
return;

Поясню, что делает плагин. При работе с сайтом через админку, т.е. в контексте mgr, ничего не делает. При работе с фронтальной частью сайта на домене dev.example.com переключает контекст на dev. При работе на фронт-енде на основном домене тоже ничего не делает. Но в случае, если исполнение скрипта каким-то образом попадает в default, то выводит ошибку в лог. Такое может случиться, например, при переносе сайта на новое доменное имя, и это сообщение призвано облегчить жизнь тому разработчику, который будет разбираться, почему после переноса ничего не работает. Когда я проверял описанную здесь схему работы на удалённом сервере, я как раз забыл исправить доменные имена в этом плагине и по ошибке в логе сразу понял что не так. В очередной раз сам себе сказал «Спасибо».

Обратите внимание, в самом конце файла плагина необходимо поставить оператор return, если плагин не должен ничего возвращать, это необходимо для того, чтобы переписать возв��ащающее значение оператора include, который мы используем при подключении файла плагина.

Через админ-панель создайте новый плагин, который будет подтягивать созданный файл
<?php
$filepath = 'plugins/switchContext.php';
$context = 'common';
$plugin = $modx->getOption('pdotools_elements_path') . "$context/$filepath";
if (file_exists($plugin)) {
    return include $plugin;
}

повесьте запуск плагина на событие OnHandleRequest.

Теперь у нас есть два контекста, как их использовать в дальнейшей разработке будет показано ниже.


Контроль версий


Создаём репозиторий проекта, например на GitHub, при этом в рабочем каталоге будут только файлы и папки, не относящиеся к движку. Примерно так
assets/
|-- dev/
    |-- css/
    |-- js/
    |-- img/
    \-- scss/
|-- web/
    |-- css/
    |-- js/
    \-- img/
core/
\-- elements/
    |-- common/
        |-- plugins/
    |-- dev/
        |-- chunks/
        |-- plugins/
        |-- snippets/
        \-- templates/
    |-- web/
        |-- chunks/
        |-- plugins/
        |-- snippets/
        \-- templates/
    \-- plugins/
.gitignore
gulpfile.js
package.json

При этом, мы будем также контролировать файлы, получаемые в результате сборки (assets/web/ и core/elements/web/), чтобы иметь возможность откатиться после неудачного деплоя. Такая структура папок будет объяснена ниже.

Разработчики, как обычно, работают в своих локальных репозиториях и пушат коммиты в удалённый репозиторий. Кроме того, должен существовать репозиторий в корне сайта.
Пример файла .gitignore
    # IntelliJ project files
    .idea/
    *.iml
    out
    gen

    # ignore MODX files
    /assets/*
    /connectors/*
    /core/*
    /manager/*
    /setup/*
    /config.core.php
    /index.php
    /.htaccess

    # ignore node modules
    /node_modules/

    # do not ignore
    !/assets/dev/
    !/assets/web/
    !/core/elements/




Схема работы


Когда я выше говорил о Develpoment и Production версиях, я предложил использовать один экземпляр сайта. Это также предполагает отсутствие его локальной копии (не путать с локальным git-репозиторием проекта). Мы будем править файлы на локальной машине, но при этом в нашем локальном рабочем каталоге будут только файлы и папки, не относящиеся к движку. Мы будем вносить правки в имеющиеся файлы, затем делать upload этих файлов на сайт. После выполнения какой-либо задачи делаем коммит в локальном репозитории, после чего, если нужно, пушим изменения на GitHub, или где вы там собираетесь держать проект.

Теперь я расскажу, как я предлагаю устроить версионирование основных элементов системы — шаблонов, чанков, сниппетов и плагинов, и описать рабочий процесс через IDE PHPStorm в связке с панелью администрирования MODX. В этом нам поможет легендарный pdoTools и используемый им шаблонизатор Fenom.

Как известно, парсер pdoTools позволяет подключать внешние файлы прямо в теле чанка, и именно эта фича лежит в основе всего устройства контроля версий.
Необходимо выставить настройке pdotools_fenom_parser (Использовать Fenom на страницах) значение Да

Компонент pdoTools имеет системную настройку pdotools_elements_path со значением по умолчанию {core_path}elements/. Что ж, соответственно, создаём папку elements/ (если ещё не создана) в папке core/ движка и внутри создаём следующую структуру папок:
elements/
|-- common/
    |-- plugins/
|-- dev/
    |-- chunks/
    |-- plugins/
    |-- snippets/
    \-- templates/
|-- web/
    |-- chunks/
    |-- plugins/
    |-- snippets/
    \-- templates/
\-- plugins/

Собственно разработка ведётся в папке dev/ при этом действует важное правило для всей команды: «Никто и никогда не должен проводить никакие правки в папке web/». При деплое всё содержимое папки dev/ копируется в web/ с помощью Gulp. Папка common/ содержит общие для обоих контекстов файлы, например, плагин переключающий текущий контекст, описанный выше.

Такая структура папок позволяет при подключении внешних файлов при помощи шаблонизатора Fenom использовать значение плейсхолдера [[*context_key]] примерно так
{set $ctx = $_modx->resource.context_key}
{include "file:$ctx/chunks/common/head.tpl"}

В первой строке мы складываем в переменную $ctx ключ текущего контекста (dev или web), а во второй строке используем это значение в пути к файлу. Это и позволяет иметь две версии сайта на одном движке.


прим. Подключения чанков в файле шаблона

Помимо файлов, содержащих основные элементы системы, нам также нужно вести контроль версий файлов вёрстки и js. Как правило, эти файлы расположены в папке assets/ следующим образом.
assets/
|-- css/
|-- js/
\-- img/

По старой схеме предлагаю создать следующую структуру
assets/
|-- dev/
    |-- css/
    |-- js/
    |-- img/
    \-- scss/
|-- web/
    |-- css/
    |-- js/
    \-- img/

Работая с файлами вёрстки и скриптов следует учесть следующий момент. Например, в папке dev/js/ может лежать несколько файлов .js (к примеру, calendar.module.js и search-form.module.js), но при деплое в продакшн эти файлы следует объединить в один и минифицировать (например, main.bundle.min.js), так что в чанках нельзя просто взять и вписать в путь файла необходимое звено [[*context_key]], следует поступить примерно следующим образом и развернуть такую конструкцию
[[*context_key:is=`dev`:then=`
  <script src="[[++assets_url]][[*context_key]]/js/calendar.module.js"></script>
  <script src="[[++assets_url]][[*context_key]]/js/search-form.module.js"></script>
  <!-- остальные скрипты -->
`]]
[[*context_key:is=`web`:then=`
  <script src="[[++assets_url]][[*context_key]]/js/main.bundle.min.js"></script>
`]]



Связываем ресурсы с шаблонами


Шаблоны создаём в папке core/elements/dev/templates/, в которых подключаем чанки из папки core/elements/dev/chunks, используя синтаксис шаблонизатора Fenom. При этом, создавая файл шаблона, мы также должны создать соответствующий ему шаблон в админ-панели. Только мы не будем создавать статический шаблон, так как не можем указать конкретный путь к файлу, потому что он зависит от контекста, в котором шаблон используется. Вместо этого в теле шаблона мы пропишем одну единственную строку
{include 'file:[[*context_key]]/templates/base.tpl'}

Таким образом, этот шаблон послужит своеобразным переключателем файла шаблона для ресурсов в различных контекстах.


прим. Подключение файла шаблона

Создаём плагины


При таком подходе разработки следует отдельно оговорить внедрение в систему новых плагинов.
Аналогично ситуации с шаблонами, плагины создаём в папке core/elements/dev/plugins/, в которых пишем логику работы плагина. Далее, заходим в админ-панель и создаём соответствующий ему плагин (НЕ статический!), в котором подключаем файл плагина через include следующим образом
<?php
$filepath = 'plugins/somePlugin.php';
$context = $modx->context->get('key');
$plugin = $modx->getOption('pdotools_elements_path') . "$context/$filepath";
if (file_exists($plugin)) {
    return include $plugin;
}

Не забываем, конечно, в админ-панели задать события, на которые вешается плагин. Если нам нужен плагин, общий для обоих контекстов, в переменную context складываем значение «common», как это было сделано в плагине switchContext.


Деплой


Вот мы и подошли к самому ответственному моменту. Выкатываем тестовую версию сайта на продакшн. Предварительно необходимо выполнить кое-какие действия с репозиторием на сервере, а именно создать SSH ключ, зарегистрировать его и прописать в настройках репозитория на GitHub. Описывать здесь этот процесс я не буду. Ссылка на иснтуркцию по генерации и регистрации ключа я оставлю внизу.
Итак, город засыпает, разработчики прекращают свою работу, все файлы закоммичены и запушены в удалённый репозиторий. Просыпается тимлид, его задача — максимально быстро выкатить всю произведённую работу в релиз. Предлагаю такой порядок действий.
  1. На своей локальной машине нужно сделать синхронизацию файлов с удалённым репозиторием, чтобы убедиться, что в локальном репозитории все файлы в актуальном состоянии.
  2. Создаём метку, например «v1.0.666-pre», обозначая таким образом коммит, предшествующий сборке версии v1.0.666, для быстрого и удобного отката в случае неудачного деплоя.
    $ git tag v1.0.666-pre

  3. С помощью Gulp собираем новую версию пр��екта.
    $ gulp modx:build

    Приводу пример gulp файла. Здесь я описал лишь одну задачу для копирования файлов из core/elements/dev в core/elements/web/, остальные таски, наверняка, сможете написать и сами.
        'use strict';
    
        var gulp = require('gulp');
    
        var paths = {
            dist: {
                //  output
                js: 'assets/web/js/',
                css: 'assets/web/css/',
                img: 'assets/web/img/',
                fonts: 'assets/web/fonts/',
                modx: 'core/elements/web/'
            },
            src: {
                //  sources
                js: 'assets/dev/js/**/*.js',
                style: 'assets/dev/sass/style.scss',
                img: 'assets/dev/img/**/*.*',
                fonts: 'assets/dev/fonts/**/*.*',
                modx: 'core/elements/dev/**/*.*'
            },
            watch: {
                //  files watch to
                js: 'assets/dev/js/**/*.js',
                style: 'assets/dev/sass/**/*.scss',
                img: 'assets/dev/img/**/*.*',
                fonts: 'assets/dev/fonts/**/*.*',
                modx: 'core/elements/dev/**/*.*'
            }
        };
    
        gulp.task('modx:build', function () {
            return gulp.src(paths.src.modx)
                .pipe(gulp.dest(paths.dist.modx))
        });
        

  4. В результате сборки будут добавлены новые или изменены старые файлы в папках /assets/web/ и /core/elements/web/. Делаем коммит, назначем ему метку и делаем push с флагом --tags, чтобы отправить в удалённый репозиторий созданные метки.
    
        $ git add . 
        $ git commit -m "Build v1.0.666" 
        $ git tag v1.0.666
        $ git push --tags
        

  5. Через SSH подключаемся к серверу и переходим в корневую директорию сайта, подтягиваем данные из удалённого репозитория и делаем hard reset, чтобы привести все файлы сайта к тому виду, в котором они хранятся в репозитории.
    
        $ git fetch --all
        $ git reset --hard origin/master
        

  6. Смотрим результат в продакшене. Если я правильно представляю как устроен мир разработки и программирования, то примерно в 10 из десяти случаев что-то пойдёт не так. Поэтому переходим обратно в консоль, выполняем команду
    $ git checkout v1.0.666-pre

    т.е. переводим продакшн-файлы в состояние до сборки и начинаем разбираться что не так.



прим. Пример дерева коммитов.

Выше описанное относится к деплою шаблонов, чанков и сниппетов, а также файлов вёрстки, но что же у нас с ресурсами?

А что с ресурсами?


Рассмотрим такую задачу. На сайте есть раздел Новости, со временем количество публикаций стало очень большим и было принято решение создать новый раздел сайта Архив новостей.

В dev контексте вы создаёте соответствующий контейнер, добавляете пару экземпляров архивных новостей, создаёте чанки, если нужно шаблон и т.п. В результате на домене dev.example.com появляется новый раздел. После одобрения заказчиком, вы производите деплой, описанный выше, но нового раздела на продакшене, конечено, не появится, хотя все файлы будут приведены к необходимому виду. Конечно, это произойдёт, потому что созданный раздел (имеется в виду совокупность созданных ресурсов) будет находиться в контексте dev и не будет доступна в контексте web.
— И что делать?
— Копипаст, товарищи, увы.
А точнее перенос. Мы просто переносим созданный раздел в контекст web, а после повторно создаём его копию в dev, чтобы структура dev-версии сайта соответствовала продакшену.
Да, полностью избавиться от копипаста у меня не получилось.


Заключение


Таким образом, мне удалось организовать рабочий процесс, который бы устроил, в первую очередь, меня. В реальной работе эта схема ещё не применялась, поэтому подводных камней пока не обнаружено. Может быть более профессиональный взгляд читателей увидит существенные недостатки, поэтому прошу написать об этом в комментариях. И кстати, это моя первая статья, и я бы хотел узнать насколько доступно и ясно мне удалось изложить свои мысли, буду благодарен за конструктивную критику.

Шаблон для разработки, созданный по ходу написания статьи, доступен на Github https://github.com/vanyaraspopov/start-code-modx.


Ссылки


pdoTools. Парсер
pdoTools. Файловые элементы
Синтаксис Fenom
Переключение контекстов в зависимости от URL
Contexts | MODX Revolution
Using One Gateway Plugin to Manage Multiple Domains
Настройка IDE PHPStorm
Приятная сборка frontend проекта
8 двухколёсных советов по MODX Revolution
Generating a new SSH key and adding it to the ssh-agent
GitHub. vanyaraspopov/start-code-modx