Дорогой друг, если ты уже знаешь, что WP — это “CMS для домохозяек“, “Движок для простеньких блогов” и вовсе никакой не framework. Что он тормозит из-за ужасной структуры БД, что большой и сложный сайт, интернет-магазин, и тем более какой-нибудь веб-сервис на нем сделать нельзя (а если и можно то все будет очень криво), и что профи предпочитают Laravel, Symfony, Yii и CodeIgniter для решения абсолютно всех задач. То призываю тебя остаться с этими знаниями и дальше не читать, то что будет написано ниже скорее всего тебе не понравится.
Не MVC или почему разработчики не любят WP
При первом знакомстве с WP у многих разработчиков, знающих какой-либо MVC-фреймворк, возникает чувство смятения ввиду того что этой простой концепции в WordPress они распознать не могут. Структура системы выглядит для них сумбурно и нелогично, и как следствие в большинстве случаев на WP вешают ярлык “говн… кхм… плохого архитектурного решения”, после благополучно с ним прощаются не потрудившись разобраться в деталях. Также масла в огонь подливает распространенность системы и низкий порог входа в ее использование. Отсюда растут ноги у тонн статей от псевдоразработчиков в духе “Как правильно сделать меню и загрузить его через FTP” или “Создаем шорткод для вывода чего-то там в сайдбаре без знаний PHP при помощи плагина такого-то”, при взгляде на содержание которых хочется залезть под подушку и рыдать, равно как и при лицезрении того получилось в итоге чтения подобных материалов. Еще WP не любят за “плохой” код, обычно критикующий описывает это так: “Я открыл исходники, а там global, я чуть смузи себе на свитшот не пролил, когда это увидел! Как можно использовать global? Это ужасно!” Действительно, глобальные переменные в WP присутствуют по причине “так исторически сложилось”. Это тянется еще с самых первых версий и присутствует по большей части для обеспечения обратной совместимости, которая в WP крайне хорошая. Это единственная CMS, в которой можно обновить ядро без переживаний о том, что все сломается, или переписывания половины существующего кода перед обновлением. Более того, разрабатывая что-либо на WordPress, вы можете вообще никогда их не использовать напрямую.
Немного теории никому не повредит
WordPress построен на базе двух паттернов: EDA (Event Driven Architecture) и EAV (Entity Attribute Value). Не буду сильно вдаваться в подробности реализации, скажу лишь, что первый обеспечивает взаимодействие между слабосвязанными между собой частями системы и дает гибкость в разработке куда большую, чем MVC. С помощью этого паттерна реализованы так называемые фильтры (filters) и действия (actions), которые в WP принято называть “хуки” (hooks). Разработчик может создавать собственные точки срабатывания хуков и взаимодействовать с хуками определенными в ядре или плагинах, прикрепляя к ним произвольный код. Таким образом влиять на поведение отдельных частей системы без необходимости их прямой модификации. О том как использовать хуки всегда можно прочитать в оф. документациии или каких-нибудь сторонних источниках.
Что касается второго паттерна, то он регулирует принцип хранения информации в БД, обеспечивая неизменность структуры таблиц при изменении структуры данных. Все записи WP хранит в таблице wp_posts, а дополнительные поля в таблице wp_postmeta в виде: meta_id, post_id, meta_key, meta_value. И если требуется добавить новый параметр к какому-либо типу поста, например размер к товару в интернет-магазине. То нет необходимости создавать новые колонки в таблице, достаточно добавить строку в wp_postmeta с соответствующими данными. Такой подход дает возможность быстрого расширения разнообразия данных, но работает несколько медленнее по сравнению с подходом с “широкими таблицами”, где каждый параметр находится в своей собственной колонке. Однако скоростными характеристиками можно пренебречь при небольшом количестве хранимых строк или применить дополнительную оптимизацию если информации становится много. Из опыта могу сказать, что интернет-магазин на WooCommerce, в котором находится 20000-30000 товаров работает вполне себе быстро на среднем по характеристикам VPS без каких-либо танцев с бубном. Если же нужно хранить несколько миллионов записей, то такие данные лучше вынести в отдельную таблицу или даже БД в зависимости от требований к системе. Это является недостатком подхода, однако часто ли возникают такие задачи?
Общая структура проекта
В своей предыдущей статье я рассказал о том, как подружить WordPress и Composer для того чтобы иметь возможность беспрепятственно хранить свои исходники в системе контроля версий, а ядро CMS и плагины сторонних разработчиков подключать как зависимости. В комментариях тогда посоветовали использовать Bedrock для этих целей, как готовое решение. Bedrock прост в использовании и дает готовую структуру проекта на WP, а также инструменты для внедрения зависимостей и деплоймента + возможность конфигурации под конкретное окружение.
Чтобы создать проект новый проект на Bedrock, достаточно выполнить команду:
composer create-project roots/bedrock
дождаться установки и указать настройки окружения в файле .env. Подробные инструкции о том как пользоваться Bedrock можно найти в официальной документации.
Миграции не нужны
В соответствии с концепцией EAV структура таблиц в БД WordPress не должна меняться (за исключением случаев, когда требуется создать свои собственные таблицы, но это уже другая история). Таким образом миграции таблиц в БД тут невозможны по определению. Однако, как же тогда управлять структурой данных? Что делать если нужно добавить новый тип постов или таксономий и прикрутить к ним свои собственные разные параметры? С регистрацией постов и таксономий все просто, для этого существует две функции register_post_type и register_taxonomy. Например, нужно создать тип “Книги” и дать возможность группировать книги по авторам, в общем случае это будет выглядеть так:
//register post type "books"
add_action( 'init', function() {
$labels = [
'name' => _x( 'Books', 'post type general name', 'your-plugin-textdomain' ),
'singular_name' => _x( 'Book', 'post type singular name', 'your-plugin-textdomain' ),
'menu_name' => _x( 'Books', 'admin menu', 'your-plugin-textdomain' ),
'name_admin_bar' => __( 'Book', 'your-plugin-textdomain' ),
'add_new' => __( 'Add New Book', 'your-plugin-textdomain' ),
'add_new_item' => __( 'Add New Book', 'your-plugin-textdomain' ),
'new_item' => __( 'New Book', 'your-plugin-textdomain' ),
'edit_item' => __( 'Edit Book', 'your-plugin-textdomain' ),
'view_item' => __( 'View Book', 'your-plugin-textdomain' ),
'all_items' => __( 'All Books', 'your-plugin-textdomain' ),
'search_items' => __( 'Search Books', 'your-plugin-textdomain' ),
'parent_item_colon' => __( 'Parent Books:', 'your-plugin-textdomain' ),
'not_found' => __( 'No books found.', 'your-plugin-textdomain' ),
'not_found_in_trash' => __( 'No books found in Trash.', 'your-plugin-textdomain' )
];
$supports = ['title', 'author', 'revisions'];
$args = [
'labels' => $labels,
'public' => true,
'supports' => $supports,
'hierarchical' => true,
'menu_position' => 20,
'show_in_nav_menus' => true,
'rewrite' => ['with_front' => false],
'map_meta_cap' => true
];
register_post_type('book', $args);
});
//register taxonomy "authors"
add_action( 'init', function() {
$labels = [
'name' => _x( 'Authors', 'taxonomy general name', 'your-plugin-textdomain' ),
'singular_name' => _x( 'Author', 'taxonomy singular name', 'your-plugin-textdomain' ),
'search_items' => __( 'Search Authors', 'your-plugin-textdomain' ),
'all_items' => __( 'All Authors', 'your-plugin-textdomain' ),
'parent_item' => __( 'Parent Author', 'your-plugin-textdomain' ),
'parent_item_colon' => __( 'Parent Author:', 'your-plugin-textdomain' ),
'edit_item' => __( 'Edit Author', 'your-plugin-textdomain' ),
'update_item' => __( 'Update Author', 'your-plugin-textdomain' ),
'add_new_item' => __( 'Add New Author', 'your-plugin-textdomain' ),
'new_item_name' => __( 'New Author Name', 'your-plugin-textdomain' ),
'menu_name' => __( 'Authors', 'your-plugin-textdomain' ),
];
$args = [
'hierarchical' => true,
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => false,
];
register_taxonomy( 'authors', [
'book',
], $args );
});
Теперь хорошо бы иметь возможность указать у каждой книги количество страниц. Для этого существует функция add_post_meta, с помощью которой можно добавить пару ключ-значение к созданной записи. А в последствии управлять значением поля из админки в блоке “Custom fields” на странице редактирования книги, также можно сразу создать его через админку, в обход вызова add_post_meta в своем коде. Но что делать если таких полей нужны десятки для каждой книги, если нужно указать, кроме количества страниц, тип переплета, год издания, название издателя и другие параметры? Нативным способом это делать немного накладно.
В этот момент приходит на помощь плагин Advanced Custom Fields (ACF). Он предоставляет удобный интерфейс для управления мета-полями записей. При том вся его мощь раскрыта в PRO версии, которая стоит не так дорого и приобрести ее может каждый.
После установки, вы можете пользоваться визуальным конструктором для добавления новых полей в панели управления плагином, но этот путь нам не подойдет поскольку тогда информация о полях будет хранится в БД и при каждом изменении нам нужно будет проделывать действия по созданию полей и на сервере разработчика и в продакшене (благо для этого в плагине присутствует механизм экспорта/импорта). Избежать таких сложных телодвижений можно путем чтения документации к плагину и нахождения там функции acf_add_local_field_group, которая принимает в качестве параметра массив с декларативно объявленной группой полей. Для того чтобы к типу данных “Книга”, добавить поля и информацией об издателе, твердом переплете и описание книги, код будет выглядеть так:
add_action('acf/init', function () {
acf_add_local_field_group([
'key' => 'books_fields',
'title' => 'Books Fields',
'fields' => [
[
'key' => 'books_fields_publisher',
'label' => 'Publisher',
'name' => 'publisher',
'type' => 'text',
'required' => 1,
],
[
'key' => 'books_fields_hard_cover',
'label' => 'Hard Cover',
'name' => 'hard_cover',
'type' => 'true_false',
'message' => 'yes',
],
[
'key' => 'books_fields_description',
'label' => 'Description',
'name' => 'description',
'type' => 'textarea',
'required' => 1,
],
],
'location' => [
[
[
'param' => 'post_type',
'operator' => '==',
'value' => 'book',
],
],
]
]
];
});
И вот мы подобрались к самому главному в этом параграфе. Имея знания о том, как создавать новые типы постов, таксономии и прикреплять к ним мета-поля через ACF, вы можете определить структуру хранения этой информации и оформить ее в качестве плагина, тогда вы всегда будете знать где, как и что у вас создается, и какие данные поддерживает (по сути это аналог объявления модели в MVC).
Структура у плагина может быть следующая:
PostTypes/
Books/
Type.php
Fields.php
Taxonomies/
Authors/
Type.php
Fields.php
AllDataTypes.php
Файл AllDataTypes.php является входной точкой в плагин, там запускается процесс регистрации всего того что вы создали. Будут ли это просто подключаемые файлы или развитая структура классов с методами и интерфейсами решать уже вам в зависимости от вашей задачи.
И в завершении параграфа коротко расскажу о полях типа “Clone” в ACF. Такие поля предназначены для того, чтобы обеспечить возможность повторного использования полей объявленных в других местах. С помощью “Clone” можно делать сквозное использование полей и даже целых групп в разных типах данных не нарушая принципа DRY.
Самый настоящий шаблонизатор
После того, как с данными более-менее разобрались, неплохо бы подумать о выводе информации. Изначально в WP нет даже самого простого шаблонизатора, а вывод предлагается делать так:
<?php echo $bar; ?>
Это не очень красиво и никак не ограничивает встраивание логики прямо в шаблон, чем умело пользуются товарищи, которые пишут “великолепные” статьи (из первого параграфа) и начинающие разработчики.
Исправить ситуацию помогает библиотека Timber, которая добавляет поддержку модного Twig и целую пачку интерфейсов для интеграции с WP_Query. Библиотека существует как пакет для composer (предпочтительно использовать этот вариант установки) и как обычный плагин. Все что потребуется после установки это указать путь до папки в которой будут лежать шаблоны.
Структура темы в моем случае обычно выглядит так:
themes/
my-theme/
templates/
functions.php
index.php
views/
index.twig
В functions.php написано следующее:
Timber::$locations[] = realpath(__DIR__ . '/../views');
В index.php:
$context = Timber::get_context();
$context['posts'] = Timber::get_posts();
//тут может быть какая-то логика, например взаимодействие с кэшем
Timber::render('index.twig', $context);
Для остальных шаблонов все выглядит аналогично. Получается, что в папке templates, если сравнивать с MVC, у нас лежит некоторое подобие контроллеров, в которых мы готовим данные и выводим их в шаблонах из директории views.
В Timber::get_posts() можно передать два параметра: данные для запроса WP_Query и имя класса поста который будет доступен из шаблона по-умолчанию используется класс Timber\Post, но можно указать свой, который является его наследником.
Давайте так и сделаем для наших книг и добавим к этому классу метод get_length, который будет возвращать “long” (длинная), если количество страниц в книге больше 200 и “short” (короткая), если меньше, код класса:
use Timber\Post;
class BooksPost extends Post{
public function __construct($pid = null)
{
parent::__construct($pid);
}
public function get_length()
{
if($this->pages_count > 200){
return 'long';
}
return 'short'
}
}
Помните наш плагин с данными? Давайте поместим объявление подобных классов туда же, поскольку они имеют непосредственное отношение к данным и если бы у нас был MVC-фреймворк, это бы относилось к методам модели.
PostTypes/
Books/
Type.php
Fields.php
Timber.php
Taxonomies/
Authors/
Type.php
Fields.php
AllDataTypes.php
В шаблоне обращаться к таким методам можно как нельзя проще:
{{ book.get_length }}
В результате проведенных действий удалось отделить представление от логики и еще немного улучшить структуру нашего приложения.
В папке с темой должна быть только тема
Сочту нужным вытащить это в отдельный параграф, поскольку проблема того, что в тему, в файл functions.php в частности, обычно пихают все что надо и не надо, от функций, которые отвечают за вывод хлебных крошек, до целых классов, которые реализуют обширную логику манипуляции с данными. Особенно на этом спекулируют производители “премиум” тем, пытаясь вместе с темой внедрить еще и максимум функционала (так тема лучше покупается). Делать это может быть и выгодно с коммерческой точки зрения, но совершенно неправильно с точки зрения разработчика. В папке с темой вся логика должна быть сведена к минимуму, в идеале все, что сложнее строк для указания путей и выражений if...else должно находится в плагинах.
Чего в WP не хватает
Напоследок хотелось бы сказать чего в WordPress явно не хватает. Первое — это встроенный роутинг. Он, мягко говоря, деревянный. Сделать с его помощью путь можно любой, но придется повозится с регулярными выражениями и сопутствующими функциями, что неудобно. Надеюсь в будущем появится какое-нибудь удобное решение для этого. И второй момент — это валидация, встроенные методы для валидации также имеются но их мало и они не очень, опять же, удобные. Можно, конечно дернуть класс валидации от symfony или еще какой, но хотелось бы что-то нативное.
На этом у меня все, ваши вопросы, предложения и пожелания буду рад увидеть в комментариях.