Обновил модуль, который разрабатывал для alplager.kz: по чертежам Кирилла Белоцерковского (инструктор лагеря и администратор сайта) переделал список программ и форму бронирования, добавил подробный просмотр и ещё больше разных улучшений в бэкенд и под капот системы. Теперь программы красиво выводятся плиткой, по ним есть удобная навигация и возможность сразу забронировать место в лагере на смену, которая понравилась. Кирилл подробно написал об этом на сайте.
В этой статье я опишу интересные моменты в разработке плагина под WordPress
, который интегрирован с системой оплаты processing.kz. Плагин (далее — модуль) управляет сменами и заявками на бронирование мест в лагере, рассылает письма, архивирует прошедшие смены и старые заявки, и помогает автоматизировать работу инструкторов лагеря с регистрацией участников и онлайн-оплатой.
В статье будут примеры с разбором URL-тегов, шорткодами и тестированием проекта с PHPUnit
.
До обновления список программ выглядел так:
С тех пор, как я в 2013-м начал разработку этого модуля, прошло порядочно времени, и благодаря скудному API вордпресса и большим перерывам между обновлениями и введением новых функций, проект к началу нового витка разработки представлял собой окаменелые наслоения кода различной степени качества и технологичности. Каждый раз, как я открывал исходники, чтобы добавить что-то новое, я с тоской смотрел на низкоуровневый код: в вордпрессе нет ORM, сервисов, не было удобных гридов с поддержкой AJAX (сортировка, пагинация), это всё пришлось писать самому.
Рефакторинг архитектуры и файловой системы
Я решил наконец навести порядок, и принялся за обновление с самого конца TODO-листа: «визуально незаметные» изменения я откладывал на потом.
Совсем махнув рукой на стандарты оформления кода WP, я ввёл автозагрузку классов с именованием и файловой структурой, как мне нравится (как в Symfony
).
Из одного главного файла с заголовком модуля я выделил стопку контроллеров, каждый для своей задачи: настройки, админка, ajax, список программ, бронирование и так далее. В итоге, остался один «заголовочный» файл, который теперь чистый бутстраппер: подключает необходимые файлы и запускает автозагрузку классов.
Для гридов в админке я когда-то дописал абстрактный класс DataTable
, который расширял стандартный WP_List_Table
и который наследовали все остальные сущности-условные модели. Из этих моделей я выделил сервисы, и структура модуля стала проясняться.
Все скрипты, стили и картинки и вынес в отдельную директорию с ресурсами, разделил пухлую директорию шаблонов для страниц на поддиректории-пространства имён.
Миграция Bazaar в git
Все остальные мои проекты давно переехали на гит, и каждый коммит альпмодуля выглядел так:
git add . bzr commit
Мигрировать на гит оказалось делом пары команд. Без проблем перенеслись все коммиты, история и тэги.
Переход на SCSS-стили
Взял, и конвертировал весь чистый CSS
в SCSS
, потому что писать стили так намного удобней. IDE позволяет настроить автотрансляцию в пару кликов (раньше надо было вручную запустить вотчер в терминале, не перепутав параметры), это тоже ускорило и упростило разработку.
Публичные страницы
Кирилл решил, что программа должна иметь свою отдельную страницу, с описанием и выбором, на какие даты забронировать место.
Так как программы — это сущности модуля, то плодить дополнительный тип материала было бы лишним. Вместо этого я добавил в модель программы фото для превью и адрес страницы. Для описания выводится WYSIWYG-редактор.
Добавить визуальный редактор в указанное поле в админке оказалось просто:
1 2 3 4 5 | // В шаблоне, который выводит страницу редактирования программы wp_editor($programm->description, 'description', [ 'textarea_rows' => 12, 'textarea_name' => 'description', ]); |
Так как поле с описанием программы теперь выводится как содержимое страницы, логично было бы иметь в нём поддержку шорткодов. Это тоже занимает полминуты:
1 2 3 4 5 | <!-- В шаблоне, который выводит подробный вид программы --> <header> <h3><?= $programm->title ?></h3> </header> <article class="description"> <?= do_shortcode($programm->description) ?> </article> |
Работа с URL
Каждая программа имеет свой URL-тег, по которому её можно открыть: alplager.kz/programms/beginner
.
Я решил, что легче всего программы отдельно выводить через шорткод, который будет выполнять роль фронт-контроллера. Шорткод «слушает» адрес страницы, и если есть заданный URL-тег (слаг), то модуль проверяет, существует ли такая программа, и показывает её.
Реализовать разбор параметров в URL в шорткоде можно так:
1 2 | // Регулярка без разделителей в начале и конце const RE_PROGRAMM_SLUG = '^\/?programms\/([^\/]*)\/?'; |
У роутинга есть один скрытый нюанс: после добавления или обновления тега в add_rewrite_tag
обязательно нужно сбросить кэш постоянных ссылок в админке на странице wp-admin/options-permalink.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 | global $wp_query; // Вывод полного просмотра if (isset($wp_query->query_vars['programm_slug'])) { $slug = sanitize_text_field($wp_query->query_vars['programm_slug']); $content = null; if ($slug) { // Загрузить программу по её URL-тегу // $content = ... } // Показать ошибку 404 if (!$content) { $wp_query->set_404(); // Обычно нужно послать заголовки, но они уже отправлены //status_header(404); //nocache_headers(); // Загрузить шаблон со страницей 404 из текущей темы. // Так как вызов происходит внутри страницы, когда уже отрисованы // шапка и скорее всего, сайдбары, то можно сломать разметку. // Вместо этого, лучше определить отдельный шаблон с ошибкой // специально для этого случая. include( get_query_template( '404' ) ); die; } } // Список всех программ else {} |
Поменять странице заголовок для соответствующей программы можно с помощью фильтра pre_get_document_title
:
1 2 3 4 5 6 7 8 | public static function process_title() { global $wp_query; // Логика такая же, как в предыдущем примере } // Заголовки для подробного вида программ add_filter('pre_get_document_title', [$this, 'process_title']); |
Да, адрес проверяется в двух местах, но на сайте настроено кэширование, и пользователю отдаются уже скомпилированные страницы, так что лишний запрос в БД будет происходить редко.
Парсинг Markdown и сборка с Phing
Сделал человеческую документацию в интерфейсе модуля. Чтобы вывести справку по категориям, я разбираю Markdown-файл с доками библиотекой Parsedown + ParsedownExtra, и получаю таким образом HTML-разметку для вывода. При этом в читаемом (ну, почти) виде остаётся и оригинальный текстовый файл, который можно открыть в терминале, если потребуется.
1 2 3 4 5 6 7 8 | require_once 'vendor/erusev/parsedown/Parsedown.php'; require_once 'vendor/erusev/parsedown-extra/ParsedownExtra.php'; $docs = self::get_text_from('Documentation/README.md'); $parser = new \ParsedownExtra(); $parser->setBreaksEnabled(true); // учитывать переводы строк $layout = $parser->text($docs); |
Давно хотел настроить Phing для сборки produce-версии модуля, и когда сделал это, пожалел, что не пользовался раньше.
Теперь с помощью одной команды можно получить produce-версию модуля: с реальными, а не тестовыми адресами веб-сервисов processing.kz; с отключенными отладочными проверками; без директорий VCS, тестов и прочих системных файлов.
Тесты
Модуль стал таким большим и сложным, что разрабатывать проект без тестов было бы безумием.
Структура тестов организована так:
1 2 3 4 5 6 7 8 9 10 11 | # tree Test Test/ ├── PHPUnit │ └── SimpleUnitTest.php ├── Selenium │ ├── SeleniumTest.php │ └── phpunit-selenium.xml └── WP ├── WpTest.php ├── bootstrap-wp.php └── phpunit-wp.xml |
В директории Test обитает целый зоопарк:
- обычные юнит-тесты в PHPUnit, которые тестируют методы-хэлперы вроде форматтеров строк или денежных сумм;
- функциональные тесты, о которых я писал в статье про тестирование с Selenium;
- тесты с использованием API WordPress в WP, про это на блоге тоже есть отдельная статья: тестирование с WP_UnitTestCase.
Скрипт run-tests.sh запускает всё по очереди:
1 2 3 4 5 6 7 8 | # Загрузить перед тестом API.php с инклюдами и константами модуля phpunit --bootstrap API.php Test/PHPUnit/HelperTest.php # Тесты с селеном phpunit -c Test/Selenium/phpunit-selenium.xml # Тесты с WP_UnitTestCase и WP_Ajax_UnitTestCase phpunit -c Test/WP/phpunit-wp.xml |
Перед тестами нужно запустить сервер селена в другой вкладке терминала, иначе в консоль будут постоянно сыпать сообщения от его процесса.
1 | java -jar /usr/local/bin/selenium-server.jar -port 4445 & |
Целая стопка тестов проверяет разные части модуля в разных ситуациях: открываются ли страницы админки на списки и редактирование сущностей, доступны ли публичные страницы модуля со сменами и бронированием, выводится ли на них верная информация, работает ли заполнение формы и отправка на processing.kz, отвечают ли AJAX-сервисы.
Чем больше тестов, тем спокойнее — ввёл новую функциональность, запустил тесты, закомиттил код. Больше никакой паранойи: если фронтенд или оплата отвалится, разработчик узнаёт об этом сразу, а не после десятка пропущенных звонков. Кроме того, с помощью тестов удаётся вылавливать баги на ранней стадии, и не пускать их в продакшен. Красиво, технологично.
Отладка модуля с Selenium выглядит так:
Оптимизировать тесты и вычистить возможные баги можно, установив модуль на другой сайт с вордпрессом, и запустив тесты там. Если есть, то сразу вылезают зависимости от темы или сторонних плагинов.
Ещё один бонус: поменяв строчку с URL, который должен открываться тестами селена, можно тестировать и мониторить produce-сайт.
В общем
В статье я разобрал процесс рефакторинга модуля для WordPress и рецепты:
- Обработку шорткодов для кастомного содержимого
- Тестирование плагинов
- Роутинг или маршрутизацию URL
- Добавление визуального редактора
- Разбор Markdown-разметки и конвертирование в HTML