וואלהאירוע ירי בטמרה - חשוד נעצרCNN TürkİSTANBULKART 1 TL ÖĞRENCİ ABONMAN BAŞVURUSU 2026| İBB aylık 1 TL öğrenci abonmanı başvurusu nasıl yapılır, kimler başvurabilir?PunchFlood alert: Lagos orders vulnerable residents to relocateESPN DeportesMéxico apunta a Monterrey para jugar en noviembreInquirerVP trial Day 28: Defense to cross-examine SEC execBollywood HungamaThe Vvaan makers acquire sync rights after Sargam Vaish’s copyright claim over ‘Aigiri Nandini’ESPNDart's injury hinders Giants as Stafford, Rams pick up MNF winInquirer EntertainmentAnne Curtis responds to ‘retokada’ claims, comments about daughter DahliaSözcüÖdülü duyan sokak sokak aramaya çıktı: 3 milyon lira verilecekBusiness AMDe oorlogen in Oekraïne en Iran bewijzen hoe hard moderne oorlogsvoering is veranderd20 MinutenRettungshund Tsunami erhält zum Ruhestand eine StatueCumhuriyet2026 KYK burs ve kredi başvuruları ne zaman başlayacak? GSB başvuru tarihleri, şartları ve e-devlet adımları
The Daily Newsstand · Free, Always
Tuesday, September 22, 2026

Организация бизнес-логики сложного сайта на WordPress

Translate

WordPress за более чем 20 лет своей истории прошел огромный путь от маленького блогового-движка до ультимативного решения в сфере разработки контентных сайтов.   Кодовая база WordPress огромна. Можно встретить спагетти-код написанный на PHP в 2003 году рядом с современным реактивным JavaScript, и это только в ядре. А если посмотреть код бесчисленных плагинов к этой CMS, то там найдется вообще все что угодно. WordPress одновременно про свободу и про обратную совместимость. Звучит круто, но задумка «делай как хочешь, обновления ничего не сломают», на деле превращается в «придумай жесткие рамки, чтобы обновления ничего не сломали».

WordPress никак не диктует разработчику, что ему делать, не дает конкретного ответа на «бытовые» вопросы вроде «где нужно регистрировать пользовательские типы данных» или «как организовать страницу настроек». Более того, в силу возраста движка, который всю дорогу следовал всем новым веяниям «разработческой» моды, для многих вещей есть по несколько вариантов реализаций обратно совместимых между собой. Разработчик может использовать их все, удобным ему путем. Для простых сайтов, которые делаются один раз и больше не развиваются, это не проблема. Но если в проект часто дорабатывается, обрастает новым функционалом и содержимым, то приходится что-то придумывать, чтобы сайт не превращался в чудовище Франкенштейна со временем.

Описанный в этом тексте подход, я использую каждый день во многих своих проектах. И пришел я к нему исключительно путем личного опыта, проб и ошибок. Я много лет занимаюсь разработкой больших новостных и корпоративных сайтов на WordPress, набив множество шишек собственным лбом.

Что делать не стоит

Начнем с того чего делать нельзя, если вы хотите, чтобы ваш проект прожил как можно дольше до полной переделки:

Хранить весь функционал вместе с темой оформления

Многие разработчики используют файл functions.php, находящийся в корне темы оформления, для добавления собственного функционала. Этот файл подключается на ранних этапах загрузки движка, и в нем можно вызвать любой хук WordPress. На интернет-форумах в ответах на вопрос вида «как сделать такой-то функционал» отвечают «вставь этот код в functions.php». Это делается в основном для того, чтобы не объяснять новичку как делать плагины, а дать простой ответ, работоспособность которого можно сразу проверить. Важно помнить: так делать не хорошо. То что исполняется и подключается внутри файла functions.php темы оформления обязано касаться только этой конкретной темы оформления. Если при переключении темы оформления в админке сайта что-либо меняется или вообще перестает работать, то вы пошли по ложному пути. Код, от которого зависит функционал сайта, следует хранить в плагинах. Вы всегда должны иметь возможность переключаться между темами оформления без каких-либо проблем в один клик. Это очень пригодится вам при редизайне, который на большом проекте неминуемо случится и может происходить параллельно с внедрением нового функционала.

Хранить регистрацию пользовательских типов, таксономий и мета-полей в базе данных

Используя плагины ACF, SCF, Pods, Carbon Fields и прочие подобные, разработчик получает возможность управлять пользовательскими типами и мета-полями прямо из админки. Это удобно пока вы работаете один. Если на проекте появляется другой разработчик, то изменения сделанные в админке придется как-то синхронизировать. Можно конечно просить собрата по несчастью скачивать к себе на компьютер свежую базу после того, как вы внесли туда изменения, но это трудозатратно и может порождать конфликты, как в коде, так и между людьми. А если учесть что разработчиков может быть больше двух, как и сред, где запущен сайт (например тестовый и боевой сервер), то подход становится вообще нежизнеспособным. К счастью, многие их этих плагинов позволят работать с ними через код, регистрируя пользовательские поля и типы с помощью вызова функций. Код легко может быть сохранен в систему контроля версий и доставлен всем участникам разработки в неизменном виде. Всегда можно понять что менялось и откатиться к предыдущей версии. О том как организовать проект на WordPress, чтобы было удобно работать с системой контроля версий git, я писал в своей предыдущей статье.

Хранить шаблоны страниц в базе данных

Все аналогично предыдущему пункту за исключением одного нюанса: если для управления пользовательскими полями вам нужен специальный плагин, то возможность хранить части шаблонов в базе данных предоставляет сам движок WordPress через FSE темы. FSE идеально подходит для no-code разработки, когда WordPress используется как SaaS. Но для управления большим сайтом FSE очень спорный инструмент: теряется возможность внятного версионирования шаблонов, пользователям админке проще все сломать. В своей практике я часто ограничиваю доступ к полному функционалу редактирования темы пользователем. Пользователь может оперировать блоками в заданных рамках, не меняя сам шаблон. Получается симбиоз классической темы и FSE: у пользователя есть набор блоков, которыми он может оперировать как угодно, но выйти за рамки дозволенного система ему не даст. Все шаблоны страниц я храню внутри файлов темы оформления в git.

Держаться за WordPress до последнего

Важно понимать границы возможностей используемого инструмента. WordPress не всегда подходит для решения всех задач, и было бы странно, если бы подходил. Движок создан для работы с контентом, но для функционала другого рода он может не подойти.

Например, журналисты новостного сайта попросили сделать для них в админке монитор новостей, публикуемых другими СМИ, чтобы было проще искать новые темы не покидая окно редактора текста. Стояла задача получать новости с нескольких сотен новостников-конкурентов. Можно было бы сделать отдельный плагин для текущего сайта и раз в минуту по расписанию отправлять сотни запросов к другим ресурсам. Но это создало бы дополнительную нагрузку на сервер. Плюс хотелось сохранять результаты куда-то в базу в отдельные таблицы, разбивая ссылки по темам, а запросы отправлять параллельно несколькими процессами из очереди. Работа с БД и очередями не является сильной стороной WordPress. Поэтому я сделал отдельный небольшой сервис на Laravel и настроил вывод результатов работы этого сервиса в админку WordPress через API. Для конечного пользователя это все выглядит как одна система, но на деле каждый сервис отвечает за свою часть. Не бойтесь выносить нестандартный функционала из WordPress в отдельные специализированные сервисы, это сэкономит вам и время и нервы.

Что делать стоит

Прежде всего стоит обозначить еще раз (хотя это и так очевидно), что всю логику стоит хранить в плагинах.

Я предпочитаю использовать один большой «must use» плагин для регистрации и управления пользовательским типами данных, таксономиями, мета-полями, блоками контента. Все это вместе я обозвал сущностями (Entities), чтобы просто объединить под одним именем. Не стоит искать параллелей с DDD, здесь их нет.

«Must use» плагин невозможно отключить через админку, и он загружается раньше обычных плагинов, что дает дополнительную гарантию того, что пользователь не станет причиной поломки.

Содержимое этого плагина уникально под каждый сайт, но структура плагина всегда одинакова.

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

Далее будут примеры кода, весь код я залил на GitHub: https://github.com/IvanZhuck/izwp-custom-entities. Это не полноценный код плагина, а только пример для иллюстрации статьи, если его скопировать к себе и запустить «как есть» чуда не получится. Репозиторий нужен только, чтобы ознакомиться с общей концепцией.

Я использую ACF в качестве движка пользовательских полей, поэтому весь плагин ориентирован на работу с ним. Но решение легко переделать под любую другую реализацию мета-полей, подход будет одинаковый.

Структура файлов плагина:

Структура файлов плагина izwp-custom-entities

Структура файлов плагина izwp-custom-entities

Используем composer

Первое, на что хочу обратить внимание, что в корне проекта лежит файл composer.json, а значит плагин может быть подключен через менеджер зависимостей. Я всегда делаю новые сайты на базе собственной заготовки WordPress Sandbox, о которой была предыдущая статья. WordPress Sandbox позволяет подключать плагины как composer-зависимости. В плагин это добавляет автозагрузку файлов по стандарту PSR-4. Что избавляет от необходимости подключать файлы плагина друг к другу вручную. Кроме того можно внедрить любую PHP-библиотеку, доступную через composer, и использовать ее в проекте.

Инициализация плагина

Начало работы плагина начинается в файле izwp-custom-entities.php, где создается объект класса IzwpCustomEntities\Core\Main:

<?php

declare(strict_types=1);

namespace IzwpCustomEntities\Core;

use IzwpCustomEntities\Core\BaseEntities\CustomFieldTypes;
use IzwpCustomEntities\Core\Tools\ClassFinder;

/**
 * Инициализация плагина
 */
final class Main
{
    public function __construct()
    {
        Settings::instance();
        CustomFieldTypes::instance();

        $this->load('Entities\\PostTypes');
        $this->load('Entities\\Taxonomies');
        $this->load('Entities\\PageTemplates');
        $this->load('Entities\\PostTemplates');
        $this->load('Entities\\CustomFields');
        $this->load('Entities\\OptionPages');
        $this->load('Entities\\ContentBlocks');
        $this->load('Tweaks'); // Различные "допилы" функционала WordPress
    }

    /**
     * Загружаем данные о типах постов, полях и прочих сущностях из папки src/Data
     */
    private function load($namespace): void
    {
        $classes = ClassFinder::getClassesInNamespace($namespace);

        foreach ($classes as $class) {
            $class::instance();
        }
    }
}

В конструкторе этого класса подключаются настройки плагина, и регистрируются все основные сущности. Из достойного внимания:

  1. Файлы каждой сущности представляют собой класс, который лежит в соответствующей директории внутри каталога Entities.

  2. Каждый класс сущности реализует singleton. Это значит, что создав объект этого класса один раз, он будет доступен постоянно в процессе жизни приложения. Таким образом исключена ситуация, что одна и та же сущность зарегистрируется повторно.

  3. Файлы классов подключаются автоматически. Чтобы например добавить новую таксономию, достаточно создать ее файл в каталоге Entities/Taxonomies, не задумываясь о подключении этого файла и инициализации класса таксономии.

Какие поддерживаются сущности

Структура каталога Entities

Структура каталога Entities

Плагин поддерживает блоки контента (блоки Gutenberg), пользовательские поля (мета-поля), страницы настроек (Options Page из ACF), пользовательские типы записей и таксономии.

Например, чтобы добавить пользовательский тип «Клиенты», нужно создать класс в файле Entities/PostTypes/Customer.php, где в методе addPostType() указать параметры нового типа, а метод addCustomFields() должен вернуть массив настроек полей ACF. Для разных сущностей будет отличаться только название метода регистрации, например для таксономий вместо addPostType() используется addTaxonomy(). Все очень просто.

Содержимое файла Customer.php:

<?php

declare(strict_types=1);

namespace IzwpCustomEntities\Entities\PostTypes;

use IzwpCustomEntities\Core\BaseEntities\PostType;
use IzwpCustomEntities\Core\Traits\SingleTon;

/**
 * Тип записей "Клиент"
 */
class Customer extends PostType
{
    use SingleTon;

    protected string $typeName = 'customer';

    protected function initInstance(): self
    {
        $this->init();

        return $this;
    }

    protected function addPostType(): array
    {
        return [
            'label' => __('Клиент', 'izwp'),
            'labels' => [
                'name' => __('Клиенты', 'izwp'),
                'singular_name' => __('Клиент', 'izwp'),
                'add_new' => __('Добавить клиента', 'izwp'),
                'add_new_item' => __('Добавить нового клиента', 'izwp'),
                'edit_item' => __('Редактировать клиента', 'izwp'),
                'new_item' => __('Новый клиент', 'izwp'),
                'view_item' => __('Смотреть клиента', 'izwp'),
                'search_items' => __('Искать клиента', 'izwp'),
                'not_found' => __('Не найдено', 'izwp'),
                'not_found_in_trash' => __('Не найдено в корзине', 'izwp'),
                'parent_item_colon' => '',
                'menu_name' => __('Клиенты', 'izwp'),
            ],
            'description' => '',
            'public' => true,
            'publicly_queryable' => false,
            'exclude_from_search' => true,
            'show_ui' => true,
            'show_in_nav_menus' => false,
            'show_in_admin_bar' => true,
            'show_in_menu' => true,
            'show_in_rest' => true,
            'rest_base' => $this->getPostTypeName(),
            'menu_position' => 25,
            'menu_icon' => 'dashicons-businessman',
            'hierarchical' => false,
            'supports' => ['title', 'revisions'],
            'taxonomies' => [],
            'has_archive' => false,
            'rewrite' => false,
            'query_var' => false,
        ];
    }

    protected function addCustomFields(): array
    {
        return [
            'key' => 'custom_fields_post_type_customer',
            'title' => __('Клиент', 'izwp'),
            'fields' => [
                [
                    'key' => 'custom_fields_post_type_customer_name',
                    'label' => __('Название', 'izwp'),
                    'name' => 'name',
                    'type' => 'text',
                ],
                [
                    'key' => 'custom_fields_post_type_customer_description',
                    'label' => __('Описание', 'izwp'),
                    'name' => 'description',
                    'type' => 'textarea',
                ],
                [
                    'key' => 'custom_fields_post_type_customer_featured',
                    'label' => __('Ключевой', 'izwp'),
                    'name' => 'featured',
                    'type' => 'true_false',
                    'default_value' => false,
                    'ui_on_text' => __('Вкл', 'izwp'),
                    'ui_off_text' => __('Выкл', 'izwp'),
                    'ui' => true,
                ],
            ],
            'location' => [
                [
                    [
                        'param' => 'post_type',
                        'operator' => '==',
                        'value' => $this->getPostTypeName(),
                    ],
                ],
            ],
            'menu_order' => 0,
            'position' => 'normal',
            'style' => 'default',
            'label_placement' => 'top',
            'instruction_placement' => 'label',
            'hide_on_screen' => '',
            'active' => true,
            'description' => '',
        ];
    }
}

Бизнес-логика

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

Регистрируем таксономию ContentAuthor (опустим содержимое методов addTaxonomy() и addCustomFields()):

<?php

declare(strict_types=1);

namespace IzwpCustomEntities\Entities\Taxonomies;

use IzwpCustomEntities\Core\BaseEntities\Taxonomy;
use IzwpCustomEntities\Core\Traits\SingleTon;
use WP_Query;

class ContentAuthor extends Taxonomy
{
    use SingleTon;

    protected string $taxonomyName = 'content_author';
    protected array $taxonomyPostTypes = ['post'];

    protected function initInstance(): self
    {
        $this->init();

        add_filter('manage_edit-content_author_columns', [$this, 'removeDescriptionColumnFromAdmin']);
        add_action('content_author_edit_form', [$this, 'hideDescriptionField']);
        add_action('content_author_add_form', [$this, 'hideDescriptionField']);
        add_action('pre_get_posts', [$this, 'customArchiveQuery']);

        /**
         * Заменяем стандартные архивы по автору (/author/...), которые строятся на базе
         * пользовательских аккаунтах WP, архивы таксономии "Авторы"
         */
        add_filter('rewrite_rules_array', [$this, 'replaceDefaultRewriteRules']);
        add_filter('user_row_actions', [$this, 'disableAdminUserViewLinks']);

        return $this;
    }

    protected function addTaxonomy(): array
    {
       ...
    }

    protected function addCustomFields(): array
    {
       ...
    }

    public function removeDescriptionColumnFromAdmin(array $columns): array
    {
        if (isset($columns['description'])) {
            unset($columns['description']);
        }

        return $columns;
    }

    public function hideDescriptionField(): void
    {
        echo '<style> .term-description-wrap { display:none; } </style>';
    }

    public function replaceDefaultRewriteRules(array $rules): array
    {
        foreach ($rules as $rule => &$rewrite) {
            if (preg_match('/author\//', $rule)) {
                $rewrite = str_replace('author_name=', $this->getTaxonomyName() . '=', $rewrite);
            }
        }

        return $rules;
    }

    public function disableAdminUserViewLinks(array $actions): array
    {
        if (isset($actions['view'])) {
            unset($actions['view']);
        }

        return $actions;
    }

    public function customArchiveQuery(WP_Query $query): void
    {
        if (!is_admin() && $query->is_main_query() && is_tax($this->getTaxonomyName())) {
            $relatedUsers = get_field('related_users', 'term_' . $query->queried_object_id, false);

            if (is_array($relatedUsers) && !empty($relatedUsers)) {
                add_filter('posts_where', function (string $where, WP_Query $query) use ($relatedUsers) {
                    global $wpdb;

                    $where = str_replace(
                        "{$wpdb->term_relationships}.term_taxonomy_id",
                        "{$wpdb->posts}.post_author IN (" . implode(',', $relatedUsers) . ") OR {$wpdb->term_relationships}.term_taxonomy_id",
                        $where
                    );

                    return $where;
                }, 10, 2);
            }
        }
    }
}

Внутри метода initInstance() мы можем дергать любые хуки WordPress, а следовательно подключать любой функционал. Если логики немного, ее стоит писать прямо в этом классе. В данном случае мы отключили вывод поля стандартного описания таксономии и переопределили логику архива публикаций авторов, отвязав их от пользователей. Далее останется только добавить поле выбора автора в редактор публикации. Если нужно сделать что-то более сложное, и класс начнет разрастаться, то в initInstance() можно вызвать код из другого плагина или PHP-библиотеки. Усложнять себе жизнь и внедрять DDD тут определенно нет необходимости, поскольку если приходится городить что-то действительно большое, то стоит рассмотреть возможность выноса этого в отдельный сервис за пределы WordPress.

Самое важное, что теперь известно, что весь код связанный с авторами публикаций будет инициализироваться в этом файле. Разработчику больше не нужно вспоминать в каких местах он вызывал хуки, связанные с этим функционалом. Если проект забросят на несколько лет, а потом потребуется что-то доделать, то только взглянув на этот файл легко понять, что он делает.

Твики

Во время работы с WordPress, постоянно возникают задачи изменить какое-либо поведение ядра или стороннего плагина через хук. Очень важно аккумулировать такие изменения в одном месте, чтобы не бегать по всем углам проекта выясняя, где какой хук вызывается. Чаще всего это происходит в файле functions.php темы, поскольку делать под каждый допил плагин - накладно. А как написано выше, логику не касающуюся конкретной темы, в теме хранить не рекомендуется. Я выделил такие доработки в отдельную директорию и назвал ее Tweaks. Каждый твик - это маленький класс, выполняющий что-то одно. Например, я использую плагин для генерации Markdown-версий страниц сайта, чтобы нейроботы и агенты тратили на мой сайт меньше токенов и получали более структурированное содержимое. На Markdown-страницах требуется специфический вывод тегов изображений. Я создаю твик MarkdownFormat и кладу туда все необходимое:

<?php

declare(strict_types=1);

namespace IzwpCustomEntities\Tweaks;

use IzwpCustomEntities\Core\Traits\SingleTon;
use DOMElement;
use IzwpCustomEntities\EntityViews\PostView;
use WP_Http;
use WP_Post;

/**
 * Настройки md-страниц
 */
final class MarkdownFormat
{
    use SingleTon;

    protected function initInstance(): self
    {
        add_filter('iz_md_pages_convert_tag', [$this, 'renderHtmlImgTag'], 3, 20);
        add_filter('iz_md_render_custom_placeholder_content_authors', [$this, 'renderContentAuthors'], 2, 20);
        add_filter('iz_md_render_block_acf/advertisement', '__return_empty_string', 20);

        return $this;
    }

    /**
     * Переопределяет вывод HTML-тега img
     */
    public function renderHtmlImgTag(?string $override, string $tagName, DOMElement $element): ?string
    {
        if ($tagName != 'img') {
            return $override;
        }

        $override = '';

        $src = $element->getAttribute('data-src');

        if (empty($src)) {
            $src = $element->getAttribute('src');
        }

        $alt = $element->getAttribute('alt');

        if (!empty($src)) {
            $src = WP_Http::make_absolute_url(esc_url($src), home_url());
            $override = '![' . $alt . '](' . $src . ')';
        }

        return $override;
    }

    /**
     * Добавляет плейсхолдер вывода авторов
     */
    public function renderContentAuthors(?string $replacement, array $args, WP_Post $post): ?string
    {
        $authorMarkdown = [];
        $postView = new PostView($post);
        $authors = $postView->authors();

        foreach ($authors as $author) {
            $authorMarkdown[] = "[{$author->fullName()}]({$author->link()})";
        }

        return implode(', ', $authorMarkdown);
    }
}

В разных проектах у меня накапливается до 50 таких классов, некоторые примеры можно посмотреть на GitHub: https://github.com/IvanZhuck/izwp-custom-entities/tree/master/src/Tweaks

Часть твиков перемещается из проекта в проект «как есть», например ограничение JSON API для публичной части проекта (DisableJsonApi) или твик DisableXmlRpc, отключающий XML-RPC для безопасности.

Когда я нахожу какую-нибудь новую штуку, которая должна быть на каждом сайте (например фикс безопасности для одного из сторонних плагинов) в 99% случаев я оформляю ее как твик и раскатываю на все свои проекты. Потом при необходимости также убираю. Стандартизация снимает много головной боли.

Отображения сущностей

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

<div class="name"><?php echo get_field('name'); ?></div>

Затем я приобрел опыт и стал сначала запрашивать данные, а затем отображать, если они есть:

<?php 
	$name = (string) get_field('name');
?>

...

<?php if (!empty($name)): ?>
    <div class="name"><?php echo $name; ?></div>
<?php endif; ?>

Это хорошее решение, но оно становится неоптимальным, когда нужно вывести поле в разных файлах темы. При изменении например имени поля, придется менять код в нескольких местах. Поэтому между данными и шаблоном, я добавил еще один слой, который назвал EntityViews.

Для каждой сущности, создается класс отображения, например для типа записей Сustomer, создается такой класс:

<?php

declare(strict_types=1);

namespace IzwpCustomEntities\EntityViews;

use IzwpCustomEntities\Core\BaseViews\AbstractPostView;
use IzwpCustomEntities\EntityViews\Traits\WithCustomerInfo;
use IzwpCustomEntities\EntityViews\Traits\WithTitle;

class CustomerView extends AbstractPostView
{
    use WithTitle;
    use WithCustomerInfo;
}

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

 <?php

declare(strict_types=1);

namespace IzwpCustomEntities\EntityViews\Traits;

trait WithTitle
{
    public function title(): string
    {
        // Убираем &nbsp; из заголовков, чтобы они не ехали в верстке
        return str_replace("\xc2\xa0", ' ', get_the_title($this->post()));
    }
}

В примере выше, трейт WithTitle реализует метод title(), который выводит заголовок записи, отфильтровав из него неразрывные пробелы. Внимательный разработчик на WordPress на этом моменте может подавиться чаем от этого безобразия и возразить, что можно было просто изменить результат функции get_the_title() через фильтр the_title и не городить никаких классов и трейтов. Он будет прав до тех пор, пока не столкнется с по-настоящему большим проектом, где такого поведения много.

В случае расширения функционала. Например, мы хотим не только неразрывные пробелы убирать, но и кавычки «лапки» заменять на кавычки «елочки» во всех заголовках. Придется либо искать место вызова хука в коде, либо делать второй вызов хука. А при следующем изменении искать их оба. К тому же никто не дает гарантии, что ваш заголовок всегда будет выводиться через get_the_title() возможно для некоторых записей, отображаемый заголовок может браться из мета-поля или вообще генерироваться при выводе. В случае с классическим WordPress-подходом будет очень легко запутаться и наделать ошибок.

Когда же мы добавляем такие трейты мы решаем сразу две задачи: избегаем дублирования кода, делая его переиспользуемым, и даем себе гарантию того, что все попадающее в вывод отправляется туда из одного строго определенного источника. А если вы пишете модульные тесты (что в мире WordPress редкость), то тестировать такие классы значительно проще, чем разрозненные функции обратного вызова для хуков.

Вернемся к примеру с мета-полем name, в котором содержится название компании клиента и сделаем для него метод в трейте, который содержит все поля с информацией о клиенте:

<?php

declare(strict_types=1);

namespace IzwpCustomEntities\EntityViews\Traits;

trait WithCustomerInfo
{
    public function name(): string
    {
        return (string) get_field('name', $this->post()->ID);
    }

    public function description(): string
    {
        return (string) get_field('description', $this->post()->ID);
    }

    public function featured(): bool
    {
        return (bool) get_field('featured', $this->post()->ID);
    }
}

Теперь, чтобы вывести это в файле темы, я могу сделать следующее:

<?php

$customer = new CustomerView($post);

?>

<?php if (!empty($customer->name())): ?>
    <div class="name">
        <?php echo $customer->name(); ?>
    </div>
<?php endif; ?>

<?php if (!empty($customer->name())): ?>
    <div class="description">
        <?php echo $customer->description(); ?>
    </div>
<?php endif; ?>

Отпала необходимость вызвать ACF функцию get_field(), в случае перехода на другой плагин, не придется менять шаблоны.  

Код шаблона выглядит громоздко, обычно я использую шаблонизатор twig, чтобы все выглядело аккуратнее, то же самое тогда превращается в:

<?php

izwp_view_render('customer.twig', [
     'customer' => new CustomerView($post);
]);

?>

izwp_view_render() - это моя собственная функция для рендера twig шаблонов, у вас она будет другой, тут пишу просто для примера.

В файле customer.twig будет такое содержимое:

{% if customer.name is not empty %}
    <div class="name">
        {{ customer.name }}
    </div>
{% endif %}

{% if customer.description is not empty %}
    <div class="description">
        {{ customer.description }}
    </div>
{% endif %}

Повторный вызов одних и тех же методов View-класса может быть операцией снижающей производительность, поэтому часто к этим методам я прикручиваю кеширование, чтобы значение вычислялось только один раз. В текущем примере это опущено.

Поскольку от стандартного вывода WordPress тут вообще ничего не осталось, то эти же файлы шаблонов могут быть использованы при вынесении этого функционала в отдельный сервис, который сделан уже не на WordPress без необходимости все переделывать.

Заключение

При разумном подходе на WordPress можно делать действительно большие проекты, не нарушая его философии, но и не скатываясь в стандарты разработки прошлого десятилетия. Метод работы, изложенный в этой статье, не претендует на единственный верный, но он сэкономил мне кучу времени и сил при работе с сайтами для медиа, где может произойти все что угодно, от запроса в короткие сроки реализовать рекламный проект с кастомной логикой отображения материала, до необходимости запилить микро-CRM для сбора заявок от читателей на участие в конкурсе. Многое из этого функционала одноразовое, многое будет использоваться многократно, при чем не всегда можно сказать, какой функционал в какую группу отнести на этапе реализации, поэтому любое решение приходится взвешивать и подходить к его разработке аккуратно.

Читайте предыдущую публикацию о разработке на WordPress: Структура современного WordPress-сайта. Composer, Docker, Bedrock

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

0%Оптимизация производительности WordPress для большого новостного сайта0

0%Автоматизация деплоя с zero downtime для WordPress0

0%Разработка WordPress-тем для своих проектов. Жонглируем php, react и twig, чтобы всем угодить0

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.