Технический стандарт

Стандарт разработки сайтов на 1С-Битрикс и WordPress

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

Я разрабатываю сайты и отдельные модули по собственному техническому стандарту, основанному на официальной документации 1С-Битрикс и WordPress, отраслевых практиках PHP-разработки и опыте поддержки реальных проектов. Конкретная реализация зависит от архитектуры, версии CMS, состава установленных модулей и задач проекта, но основные принципы остаются неизменными.

  • 0 правок в ядре CMS
  • 2 платформы: Битрикс и WordPress
  • 7 этапов от анализа до документации
Посмотреть услуги

Основные принципы разработки

Восемь правил, которые действуют одинаково на 1С-Битрикс и на WordPress. Всё, что описано ниже в разделах по конкретным платформам, — это способы соблюсти именно их. В каждом подразделе примеры даны отдельно для каждой CMS.

  • 01

    Ядро не трогаем

    Доработки живут отдельно от платформы и переживают обновления.

  • 02

    Документированные API

    ORM, события, публичные функции — вместо запросов в служебные таблицы.

  • 03

    Разделение слоёв

    Данные, бизнес-правила, интеграции и представление — в разных файлах.

  • 04

    История в Git

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

  • 05

    Копия перед боем

    Опасные изменения проверяются на тестовой среде, а не на живом сайте.

  • 06

    Безопасность в проекте

    Проверка прав и данных закладывается при проектировании, а не после.

  • 07

    Измеримая скорость

    Запросы, кеш и вес ресурсов контролируются по замерам, а не «на глаз».

  • 08

    Описанные решения

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

Не изменять ядро системы

Системные файлы 1С-Битрикс, WordPress и сторонних расширений не предназначены для размещения проектных доработок. Прямое редактирование таких файлов может привести к потере изменений при обновлении, конфликтам версий и сложностям при дальнейшем сопровождении сайта.

Новый функционал размещается отдельно от ядра:

  • в каталоге /local/, собственных компонентах и модулях на 1С-Битрикс;
  • в теме, дочерней теме или отдельном плагине на WordPress;
  • в самостоятельных классах и сервисах, если задача требует отдельного слоя бизнес-логики.

1С-Битрикс

/bitrix/modules/iblock/classes/general/result.phpКак не надоPHP
// Правка прямо в файле модуля: «быстро добавить наценку».
// Обновление модуля перезапишет файл — доработка исчезнет
// без следов, а поиск причины займёт часы.
public function GetNext($textHtmlAuto = true, $useTilda = true)
{
    $result = parent::GetNext($textHtmlAuto, $useTilda);

    if ($result && isset($result['PROPERTY_PRICE_VALUE'])) {
        $result['PROPERTY_PRICE_VALUE'] *= 1.15;
    }

    return $result;
}
/local/php_interface/init.phpКак надоPHP
use Bitrix\Main\EventManager;

// Та же задача через штатную точку расширения: файл лежит
// вне ядра, обновление модуля его не касается.
EventManager::getInstance()->addEventHandler(
    'iblock',
    'OnAfterIBlockElementUpdate',
    ['\Project\Catalog\PriceHandler', 'onElementUpdate']
);

Обработчик лежит в /local/, регистрируется в init.php и переживает любое обновление модуля «Информационные блоки».

WordPress

wp-content/plugins/woocommerce/includes/wc-product-functions.phpКак не надоPHP
// Правка в файле стороннего плагина. Автообновление
// WooCommerce вернёт исходный файл, наценка тихо пропадёт,
// и «цены сломались» придётся искать с нуля.
function wc_get_price_to_display($product, $args = array()) {
    $price = $args['price'] ?? $product->get_price();

    $price = (float) $price * 1.15;

    return $price;
}
wp-content/plugins/project-core/src/Shop/Price.phpКак надоPHP
// Та же задача через документированный фильтр WooCommerce.
// Код лежит в своём плагине, обновление магазина его не трогает.
add_filter('woocommerce_product_get_price', 'project_apply_markup', 10, 2);

/**
 * @param string     $price   Цена товара.
 * @param WC_Product $product Товар.
 * @return string
 */
function project_apply_markup($price, $product) {
    if ($price === '' || !$product->is_type('simple')) {
        return $price;
    }

    return (string) round((float) $price * 1.15, 2);
}

Фильтр можно снять, заменить и протестировать отдельно; при удалении плагина сайт возвращается к исходному поведению.

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

Использовать документированные API

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

Это позволяет сохранить совместимость с новыми версиями CMS и уменьшить зависимость проекта от внутреннего устройства конкретной версии платформы.

1С-Битрикс

/local/php_interface/include/catalog.phpКак не надоPHP
global $DB;

// Прямой запрос в служебные таблицы инфоблоков: не учитывает
// права, сайт, активность по датам и кеш. Структура таблиц —
// внутреннее дело платформы и меняется между версиями.
$rs = $DB->Query(
    "SELECT ID, NAME FROM b_iblock_element
     WHERE IBLOCK_ID = 12 AND ACTIVE = 'Y'
     ORDER BY SORT ASC LIMIT 20"
);
/local/classes/Project/Catalog/ElementRepository.phpКак надоPHP
use Bitrix\Iblock\ElementTable;
use Bitrix\Main\Loader;

Loader::includeModule('iblock');

// Штатный ORM: учитывает права, активность, кеш и переживает
// смену внутренней структуры таблиц.
$elements = ElementTable::getList([
    'select' => ['ID', 'NAME'],
    'filter' => ['=IBLOCK_ID' => 12, '=ACTIVE' => 'Y'],
    'order'  => ['SORT' => 'ASC'],
    'limit'  => 20,
    'cache'  => ['ttl' => 3600],
])->fetchAll();

Ключ cache включает штатное кеширование выборки — оптимизация не требует отказа от API.

WordPress

wp-content/themes/project-theme/functions.phpКак не надоPHP
global $wpdb;

// Прямой запрос в служебные таблицы: не учитывает статусы,
// языки, права доступа и объектный кеш. Любое изменение схемы
// или префикса ломает выборку.
$rows = $wpdb->get_results(
    "SELECT ID, post_title FROM wp_posts
     WHERE post_type = 'product' ORDER BY post_date DESC LIMIT 10"
);
wp-content/plugins/project-core/src/Catalog/Repository.phpКак надоPHP
// Штатный API: учитывает статусы, права, фильтры и объектный кеш.
$query = new WP_Query([
    'post_type'              => 'product',
    'post_status'            => 'publish',
    'posts_per_page'         => 10,
    'orderby'                => 'date',
    'order'                  => 'DESC',
    'no_found_rows'          => true,  // пагинация не нужна
    'update_post_term_cache' => false, // термины не читаем
    'fields'                 => 'ids',
]);

$ids = $query->posts;

Флаги no_found_rows и update_post_term_cache убирают лишние запросы — API не мешает оптимизации, если знать его параметры.

Разделять ответственность компонентов

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

  1. получение и сохранение данных;
  2. проверка и преобразование информации;
  3. бизнес-правила;
  4. интеграции и обмены;
  5. подготовка данных для отображения;
  6. HTML-шаблоны и клиентская логика.

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

1С-Битрикс

/local/classes/Project/Order/DiscountCalculator.phpPHP
namespace Project\Order;

/**
 * Правила скидок. Класс ничего не знает ни о CMS, ни о HTML:
 * получает данные на вход, возвращает результат. Поэтому его
 * можно покрыть тестами и переиспользовать в обмене с 1С.
 */
final class DiscountCalculator
{
    private const MAX_PERCENT = 30;

    /** @var array<string,int> Скидка по коду группы покупателя. */
    private $rules;

    public function __construct(array $rules)
    {
        $this->rules = $rules;
    }

    public function apply(float $sum, string $group): float
    {
        $percent = $this->rules[$group] ?? 0;
        $percent = min($percent, self::MAX_PERCENT);

        return round($sum * (1 - $percent / 100), 2);
    }
}

Компонент вызывает класс в class.php, кладёт результат в arResult и передаёт в шаблон — бизнес-правило описано в одном месте.

WordPress

wp-content/plugins/project-core/src/Order/DiscountCalculator.phpPHP
namespace Project\Core\Order;

/**
 * Тот же слой бизнес-правил в плагине WordPress: ни хуков,
 * ни вывода, ни обращений к базе — только расчёт.
 */
final class DiscountCalculator
{
    private const MAX_PERCENT = 30;

    /** @var array<string,int> */
    private array $rules;

    public function __construct(array $rules)
    {
        $this->rules = $rules;
    }

    public function apply(float $sum, string $group): float
    {
        $percent = min($this->rules[$group] ?? 0, self::MAX_PERCENT);

        return round($sum * (1 - $percent / 100), 2);
    }
}
wp-content/plugins/project-core/src/Shop/CartHooks.phpPHP
use Project\Core\Order\DiscountCalculator;

/**
 * Слой интеграции с CMS: хук только собирает данные, вызывает
 * расчёт и отдаёт результат. Правил скидок здесь нет.
 */
add_filter('woocommerce_calculated_total', 'project_apply_discount', 10, 2);

function project_apply_discount(float $total, WC_Cart $cart): float {
    $group = (string) get_user_meta(get_current_user_id(), 'project_group', true);

    return (new DiscountCalculator(project_discount_rules()))->apply($total, $group);
}

Шаблон темы такой код не содержит вовсе: его дело — вывести уже посчитанные значения.

Хранить историю изменений

Проектный код ведётся в системе контроля версий Git, если это допускают условия проекта и доступная инфраструктура. История изменений помогает определить, когда и почему была внесена доработка, сравнить версии файлов и при необходимости вернуть рабочее состояние.

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

1С-Битрикс

.gitignoregitignore
# Ядро и модули: ставятся и обновляются штатным обновлением платформы
/bitrix/

# Конфигурация с доступами к базе и ключами
/bitrix/.settings.php
/bitrix/php_interface/dbconn.php

# Пользовательские загрузки, кеш, логи, бэкапы
/upload/
/bitrix/cache/
/bitrix/managed_cache/
/bitrix/stack_cache/
/bitrix/backup/
/bitrix/tmp/
*.log

# В репозитории — только проектный код
!/local/

Каталог /bitrix/ исключается целиком: там нет ничего нашего. Всё проектное лежит в /local/ — см. принцип «не изменять ядро».

WordPress

.gitignoregitignore
# Ядро: обновляется из админки или через WP-CLI
/wp-admin/
/wp-includes/
/wp-*.php
!/wp-config-sample.php

# Конфигурация с доступами к базе, солями и ключами
/wp-config.php

# Медиафайлы, кеш, логи, бэкапы
/wp-content/uploads/
/wp-content/cache/
/wp-content/upgrade/
/wp-content/backup*/
/wp-content/debug.log
*.sql
*.tar.gz

# Сторонние темы и плагины ставятся из репозитория WordPress
/wp-content/themes/*
/wp-content/plugins/*

# В репозитории — только собственные тема и плагины
!/wp-content/themes/project-child/
!/wp-content/plugins/project-core/

Сторонние плагины исключаются по маске, а свои — возвращаются точечно. Так репозиторий не разрастается чужим кодом, который и без того версионируется автором.

Разрабатывать и проверять изменения на копии сайта

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

Перед изменением структуры данных, обновлением системы или развёртыванием крупной доработки создаётся резервная копия файлов и базы данных либо проверяется наличие актуальной копии со стороны хостинга.

1С-Битрикс

deploy/backup-bitrix.shShell
# Доступы читаем из конфигурации платформы, а не храним в скрипте.
STAMP=$(date +%Y-%m-%d_%H-%M)

# --single-transaction не блокирует таблицы InnoDB на живом сайте
mysqldump --single-transaction --quick --default-character-set=utf8mb4 \
    -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" | gzip > "backup/db-$STAMP.sql.gz"

# Проектный код и пользовательские файлы. Кеш не архивируем.
tar -czf "backup/code-$STAMP.tar.gz" \
    --exclude='bitrix/cache' --exclude='bitrix/managed_cache' \
    local/ upload/ bitrix/.settings.php

# Копия считается сделанной только после проверки
gzip -t "backup/db-$STAMP.sql.gz" && echo "backup ok"

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

WordPress

deploy/backup-wp.shShell
# WP-CLI сам берёт доступы из wp-config.php — пароль в скрипте не нужен.
STAMP=$(date +%Y-%m-%d_%H-%M)

wp db export - --single-transaction --quick | gzip > "backup/db-$STAMP.sql.gz"

# Своя тема, свои плагины и медиафайлы. Кеш и upgrade пропускаем.
tar -czf "backup/content-$STAMP.tar.gz" \
    --exclude='wp-content/cache' --exclude='wp-content/upgrade' \
    wp-content/uploads wp-content/themes/project-child \
    wp-content/plugins/project-core wp-config.php

gzip -t "backup/db-$STAMP.sql.gz" && echo "backup ok"

# Развёртывание копии: адреса в базе меняются штатной командой,
# а не поиском-заменой по дампу — сериализованные данные не поедут.
wp search-replace 'https://example.com' 'https://staging.example.com' --precise

Резервная копия считается сделанной только после проверки — файл нулевого размера обнаруживается в самый неподходящий момент.

Учитывать безопасность на этапе разработки

Безопасность не добавляется после завершения проекта отдельной настройкой. Она учитывается при проектировании форм, обработчиков, API-методов, административных разделов и интеграций. В рамках разработки выполняются:

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

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

1С-Битрикс

/local/classes/Project/Order/Controller.phpPHP
namespace Project\Order;

use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\Response\AjaxJson;

final class StatusController extends Controller
{
    /** Защита запроса: авторизация и CSRF — на уровне контроллера. */
    protected function getDefaultPreFilters(): array
    {
        return [
            new ActionFilter\Authentication(),
            new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
            new ActionFilter\Csrf(),
        ];
    }

    public function updateAction(int $orderId, string $status): ?AjaxJson
    {
        // Право на операцию проверяется отдельно от CSRF-токена.
        if (!\CModule::IncludeModule('sale')
            || $GLOBALS['APPLICATION']->GetGroupRight('sale') < 'W') {
            $this->addError(new \Bitrix\Main\Error('Недостаточно прав'));

            return null;
        }

        // Входящие значения — по белому списку, а не «что пришло».
        if (!in_array($status, ['N', 'P', 'F'], true)) {
            $this->addError(new \Bitrix\Main\Error('Недопустимый статус'));

            return null;
        }

        return AjaxJson::createSuccess(['status' => $status]);
    }
}

Типы аргументов объявлены в сигнатуре — ядро приводит и проверяет их до входа в метод.

WordPress

wp-content/plugins/project-core/src/Admin/AjaxController.phpPHP
add_action('wp_ajax_project_update_status', 'project_update_status');

function project_update_status(): void
{
    // 1. Защита запроса: nonce отсекает сторонний вызов…
    check_ajax_referer('project_order_action', 'nonce');

    // 2. …но право на действие проверяется отдельно.
    if (!current_user_can('edit_shop_orders')) {
        wp_send_json_error(['message' => 'Недостаточно прав'], 403);
    }

    // 3. Входящие данные приводим к ожидаемому типу и диапазону.
    $order_id = isset($_POST['order_id']) ? absint($_POST['order_id']) : 0;
    $status   = isset($_POST['status']) ? sanitize_key($_POST['status']) : '';

    if ($order_id === 0 || !in_array($status, ['new', 'paid', 'done'], true)) {
        wp_send_json_error(['message' => 'Некорректные данные'], 400);
    }

    // 4. Проверяем, что объект вообще доступен этому пользователю.
    if (get_post_type($order_id) !== 'shop_order') {
        wp_send_json_error(['message' => 'Заказ не найден'], 404);
    }

    update_post_meta($order_id, '_project_status', $status);

    wp_send_json_success(['status' => $status]);
}

Четыре проверки: подлинность запроса, полномочия, формат данных и доступность объекта. Отсутствие любой из них — уязвимость, а не мелочь.

Контролировать производительность

При разработке учитывается не только корректность функции, но и её влияние на скорость сайта и нагрузку на сервер. Особое внимание уделяется:

  • количеству запросов к базе данных;
  • объёму выбираемых данных;
  • повторным вычислениям;
  • кешированию;
  • размеру подключаемых CSS- и JavaScript-файлов;
  • обработке изображений;
  • работе фоновых задач;
  • обращениям к внешним API;
  • поведению сайта при росте каталога или количества пользователей.

1С-Битрикс

Запрос в циклеКак не надоPHP
// N+1: на 500 товарах — 501 запрос к базе.
// На витрине это видно сразу, в админке — только под нагрузкой.
foreach ($productIds as $id) {
    $price = PriceTable::getList([
        'filter' => ['=PRODUCT_ID' => $id],
        'select' => ['PRICE'],
    ])->fetch();

    $result[$id] = $price['PRICE'] ?? null;
}
Одна выборкаКак надоPHP
// Один запрос на всю пачку, выбираем только нужные поля.
$rows = PriceTable::getList([
    'filter' => ['@PRODUCT_ID' => $productIds],
    'select' => ['PRODUCT_ID', 'PRICE'],
])->fetchAll();

$result = array_column($rows, 'PRICE', 'PRODUCT_ID');

WordPress

Мета в циклеКак не надоPHP
// Кеш метаданных не прогрет: каждый get_post_meta()
// уходит в базу отдельным запросом.
$ids = get_posts(['post_type' => 'product', 'fields' => 'ids',
                  'numberposts' => 500]);

foreach ($ids as $id) {
    $result[$id] = get_post_meta($id, '_price', true);
}
Прогрев кеша метаданныхКак надоPHP
$ids = get_posts(['post_type' => 'product', 'fields' => 'ids',
                  'numberposts' => 500]);

// Один запрос поднимает мету всех записей в объектный кеш —
// дальше get_post_meta() читает из памяти.
update_meta_cache('post', $ids);

foreach ($ids as $id) {
    $result[$id] = get_post_meta($id, '_price', true);
}

Оптимизация проводится на основании измерений, а не только субъективной оценки скорости загрузки страницы.

Документировать нестандартные решения

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

Документация должна позволять разобраться в устройстве функционала без длительного анализа всего проекта.

1С-Битрикс

/local/modules/project.exchange/include.phpPHP
/**
 * Обмен заказами с 1С:УТ 11.
 *
 * Назначение : выгрузка новых заказов и загрузка статусов оплаты.
 * Файлы      : /local/modules/project.exchange/lib/
 * Настройки  : Настройки модуля → project.exchange (URL, логин, ключ)
 * Зависимости: модули sale, catalog; PHP 8.1+; curl
 * События    : OnSaleOrderSaved (постановка в очередь)
 * Агент      : \Project\Exchange\Agent::run() — каждые 5 минут
 * Формат     : JSON, спецификация — docs/exchange-1c.md
 * Запуск     : php -f /local/modules/project.exchange/cli/run.php --once
 * Отключение : снять агент и обработчик, данные очереди сохраняются
 * Журнал     : /local/logs/exchange-YYYY-MM-DD.log, хранится 30 дней
 */

WordPress

wp-content/plugins/project-exchange/project-exchange.phpPHP
/**
 * Plugin Name: Project Exchange
 * Description: Обмен заказами и остатками с 1С:УТ 11.
 * Version:     2.1.0
 * Requires at least: 6.4
 * Requires PHP: 8.1
 * Requires Plugins: woocommerce
 *
 * Назначение : выгрузка заказов WooCommerce, загрузка остатков и цен.
 * Файлы      : src/ (Api, Queue, Cli), шаблоны писем — templates/
 * Настройки  : WooCommerce → Настройки → Обмен с 1С (URL, ключ)
 * Хуки       : woocommerce_new_order → постановка в очередь;
 *              project_exchange_before_push — фильтр полезной нагрузки
 * Расписание : событие project_exchange_run, интервал five_minutes,
 *              запускается системным cron (DISABLE_WP_CRON = true)
 * Формат     : JSON, спецификация — docs/exchange-1c.md
 * Запуск     : wp project exchange run --limit=200
 * Отключение : деактивация плагина снимает расписание, очередь
 *              остаётся в таблице {prefix}_project_exchange_queue
 * Журнал     : WooCommerce → Статус → Журналы, источник project-exchange
 */

Шапка отвечает на вопросы, которые возникают у следующего разработчика в первые пять минут. Заголовок Requires Plugins заодно не даёт активировать плагин без WooCommerce.

Платформа

Стандарт разработки на 1С-Битрикс

Bitrix Framework даёт готовые точки расширения почти для всего: события, собственные компоненты, модули, ORM, управляемый кеш. Стандарт сводится к тому, чтобы ими пользоваться, а не обходить их.

Размещение доработок в каталоге /local/

Собственные компоненты, шаблоны, классы, обработчики событий, модули и другие проектные файлы размещаются преимущественно в каталоге /local/. Каталог /bitrix/ считается частью платформы и не используется для хранения собственных доработок, кроме случаев, когда это обусловлено устройством старого проекта и немедленный перенос может нарушить его работу.

Структура проектного кодаДерево
/local/
├── components/          собственные компоненты
│   └── project/
│       └── order.form/
│           ├── class.php         логика компонента
│           ├── .parameters.php   параметры
│           ├── lang/ru/          языковые фразы
│           └── templates/.default/
├── templates/           шаблоны сайта и компонентов
├── modules/             собственные модули
│   └── project.exchange/
├── php_interface/       проектные обработчики и настройки
│   ├── init.php
│   └── include/events/
├── classes/             прикладные классы (PSR-4)
│   └── Project/
└── migrations/          миграции структуры данных

Структура может отличаться в зависимости от масштаба проекта, но проектный код должен быть отделён от ядра.

Использование D7 и актуального API

Для нового функционала преимущественно используются возможности современного ядра D7:

  • пространства имён;
  • автозагрузка классов;
  • ORM;
  • события;
  • конфигурация;
  • локализация;
  • штатные классы модулей;
  • объектный подход к работе с сущностями.

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

/local/php_interface/init.phpPHP
use Bitrix\Main\Loader;

// Автозагрузка проектных классов: пространство имён Project\
// отображается на /local/classes/Project/ по правилам PSR-4.
Loader::registerAutoLoadClasses(null, []);

spl_autoload_register(static function (string $class): void {
    $prefix = 'Project\\';

    if (strpos($class, $prefix) !== 0) {
        return;
    }

    $relative = substr($class, strlen($prefix));
    $path     = $_SERVER['DOCUMENT_ROOT'] . '/local/classes/Project/'
        . str_replace('\\', '/', $relative) . '.php';

    if (is_readable($path)) {
        require_once $path;
    }
});

// Обработчики подключаются отдельными файлами по назначению,
// а не сваливаются в init.php одним списком.
require_once __DIR__ . '/include/events/catalog.php';
require_once __DIR__ . '/include/events/sale.php';

Работа с данными через ORM и API модулей

Для чтения и изменения данных используются ORM и официальные методы соответствующего модуля. Прямые SQL-запросы допускаются только в обоснованных случаях: обработка больших объёмов данных, сложная аналитическая выборка или отсутствие подходящего API.

При работе с базой данных:

  • выбираются только необходимые поля;
  • ограничивается количество записей;
  • учитываются индексы и сортировка;
  • исключаются запросы внутри циклов;
  • контролируется количество обращений к базе;
  • операции изменения выполняются с проверкой результата;
  • при необходимости используются транзакции.
/local/classes/Project/Catalog/ElementRepository.phpPHP
namespace Project\Catalog;

use Bitrix\Iblock\Elements\ElementCatalogTable;
use Bitrix\Main\Loader;
use Bitrix\Main\LoaderException;

final class ElementRepository
{
    /**
     * Активные элементы раздела: одна выборка, только нужные поля,
     * жёсткий лимит и заранее подготовленная сортировка по индексу.
     *
     * @return array<int,array<string,mixed>>
     * @throws LoaderException
     */
    public function findBySection(int $sectionId, int $limit = 50): array
    {
        Loader::includeModule('iblock');

        return ElementCatalogTable::getList([
            'select' => ['ID', 'NAME', 'CODE', 'PREVIEW_PICTURE'],
            'filter' => [
                '=IBLOCK_SECTION_ID' => $sectionId,
                '=ACTIVE'            => 'Y',
            ],
            'order'  => ['SORT' => 'ASC', 'ID' => 'DESC'],
            'limit'  => $limit,
            'cache'  => ['ttl' => 3600],
        ])->fetchAll();
    }
}

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

Изменение данных с проверкой результатаКак надоPHP
use Bitrix\Main\Application;

$connection = Application::getConnection();
$connection->startTransaction();

try {
    foreach ($items as $item) {
        $result = OrderItemTable::update($item['ID'], ['QUANTITY' => $item['QUANTITY']]);

        // Результат операции проверяется всегда: «молча не сохранилось» —
        // самый дорогой в поддержке класс ошибок.
        if (!$result->isSuccess()) {
            throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
        }
    }

    $connection->commitTransaction();
} catch (\Throwable $e) {
    $connection->rollbackTransaction();
    \Bitrix\Main\Diag\Debug::writeToFile($e->getMessage(), '', '/local/logs/order.log');

    throw $e;
}

Компоненты и шаблоны

Компонент отвечает за получение и подготовку данных, а шаблон — за их отображение. Сложная бизнес-логика не размещается непосредственно в HTML-шаблоне. При разработке компонентов:

  • параметры имеют понятные названия и значения по умолчанию;
  • входящие параметры проверяются;
  • результат содержит только необходимые шаблону данные;
  • кеш зависит от параметров, влияющих на результат;
  • динамические области отделяются от кешируемой части;
  • учитывается работа в составе комплексных компонентов;
  • AJAX-запросы защищаются и проверяют права пользователя;
  • языковые фразы выносятся в файлы локализации.
/local/components/project/catalog.list/class.phpPHP
use Bitrix\Main\Engine\Contract\Controllerable;
use Bitrix\Main\Localization\Loc;

class ProjectCatalogListComponent extends CBitrixComponent implements Controllerable
{
    /**
     * Входящие параметры проверяются и приводятся к типу здесь,
     * а не в шаблоне и не в местах использования.
     */
    public function onPrepareComponentParams($params): array
    {
        $params['SECTION_ID']  = (int) ($params['SECTION_ID'] ?? 0);
        $params['COUNT']       = max(1, min(100, (int) ($params['COUNT'] ?? 20)));
        $params['CACHE_TIME']  = isset($params['CACHE_TIME']) ? (int) $params['CACHE_TIME'] : 3600;

        return $params;
    }

    public function executeComponent(): void
    {
        // Кеш зависит от всех параметров, влияющих на результат,
        // иначе разные разделы получат одинаковый вывод.
        $cacheKeys = [$this->arParams['SECTION_ID'], $this->arParams['COUNT']];

        if ($this->startResultCache($this->arParams['CACHE_TIME'], $cacheKeys)) {
            $repository = new \Project\Catalog\ElementRepository();

            $this->arResult['ITEMS'] = $repository->findBySection(
                $this->arParams['SECTION_ID'],
                $this->arParams['COUNT']
            );

            // Тегированный кеш: сбросится сам при изменении инфоблока.
            $this->includeComponentTemplate();
        }

        $this->setResultCacheKeys(['ITEMS']);
    }

    /** Действия AJAX объявляются явно и проверяют права. */
    public function configureActions(): array
    {
        return [];
    }
}

Шаблоны стандартных компонентов копируются в шаблон сайта. Исходные файлы компонента в ядре не редактируются.

Собственные модули

Связанный функционал, который используется в нескольких разделах сайта или имеет собственные настройки, оформляется в виде отдельного модуля. Модуль должен самостоятельно:

  • устанавливаться и удаляться;
  • регистрировать необходимые события;
  • создавать и удалять собственные таблицы;
  • подключать классы;
  • хранить настройки;
  • добавлять административные страницы;
  • выполнять обновление своей структуры;
  • корректно обрабатывать отсутствие зависимостей.
/local/modules/project.exchange/install/index.phpPHP
use Bitrix\Main\EventManager;
use Bitrix\Main\ModuleManager;

class project_exchange extends CModule
{
    public $MODULE_ID = 'project.exchange';

    public function DoInstall(): bool
    {
        // Зависимости проверяются до установки, а не в момент работы.
        if (!ModuleManager::isModuleInstalled('sale')) {
            $GLOBALS['APPLICATION']->ThrowException('Требуется модуль «Интернет-магазин»');

            return false;
        }

        ModuleManager::registerModule($this->MODULE_ID);

        $this->installDatabase();
        $this->installEvents();

        return true;
    }

    public function DoUninstall(): bool
    {
        // Удаление — зеркало установки: снимаем обработчики,
        // убираем таблицы, снимаем регистрацию модуля.
        $this->uninstallEvents();
        $this->uninstallDatabase();

        ModuleManager::unRegisterModule($this->MODULE_ID);

        return true;
    }

    private function installEvents(): void
    {
        EventManager::getInstance()->registerEventHandler(
            'sale',
            'OnSaleOrderSaved',
            $this->MODULE_ID,
            '\Project\Exchange\Queue',
            'onOrderSaved'
        );
    }
}

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

Обработчики событий

Для изменения поведения платформы используются события Bitrix Framework. Обработчики не должны содержать неконтролируемую тяжёлую логику, которая будет выполняться при каждом сохранении элемента, заказа или пользователя. Перед регистрацией обработчика определяется:

  • при каком событии он должен запускаться;
  • может ли событие вызываться повторно;
  • требуется ли защита от рекурсии;
  • как обработчик влияет на скорость операции;
  • что произойдёт при ошибке внешнего сервиса;
  • нужен ли перенос тяжёлой операции в очередь или агент.
/local/classes/Project/Catalog/PriceHandler.phpPHP
namespace Project\Catalog;

final class PriceHandler
{
    /** Защита от рекурсии: обновление элемента внутри обработчика
     *  снова вызовет это же событие. */
    private static bool $inProgress = false;

    public static function onElementUpdate(array &$fields): void
    {
        if (self::$inProgress) {
            return;
        }

        self::$inProgress = true;

        try {
            // В обработчике — только быстрая операция.
            // Тяжёлое (обмен, письма, пересчёт каталога) ставим в очередь.
            Queue::push('recalculate', ['id' => (int) $fields['ID']]);
        } catch (\Throwable $e) {
            // Ошибка фоновой задачи не должна ломать сохранение элемента.
            \Bitrix\Main\Diag\Debug::writeToFile(
                $e->getMessage(),
                'PriceHandler',
                '/local/logs/catalog.log'
            );
        } finally {
            self::$inProgress = false;
        }
    }
}

Обработчики группируются по назначению и не распределяются без системы по одному большому файлу.

Кеширование

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

  • параметры, влияющие на содержимое кеша;
  • срок актуальности данных;
  • права групп пользователей;
  • язык и сайт;
  • управляемый и тегированный кеш;
  • корректный сброс после изменения данных;
  • объём сохраняемой информации;
  • разделение статической и динамической части.
/local/classes/Project/Catalog/MenuCache.phpPHP
use Bitrix\Main\Application;
use Bitrix\Main\Data\Cache;

$cache      = Cache::createInstance();
$cacheTime  = 86400;
$cacheDir   = '/project/catalog-menu';

// Ключ включает всё, что влияет на результат: сайт, язык,
// группы пользователя и раздел. Иначе один посетитель увидит
// содержимое, подготовленное для другого.
$cacheKey = implode('|', [
    SITE_ID,
    LANGUAGE_ID,
    implode(',', \CUser::GetUserGroup($USER->GetID())),
    $sectionId,
]);

if ($cache->initCache($cacheTime, $cacheKey, $cacheDir)) {
    $items = $cache->getVars();
} elseif ($cache->startDataCache()) {
    $taggedCache = Application::getInstance()->getTaggedCache();
    $taggedCache->startTagCache($cacheDir);
    $taggedCache->registerTag('iblock_id_' . IBLOCK_CATALOG);

    $items = (new ElementRepository())->findBySection($sectionId);

    $taggedCache->endTagCache();
    $cache->endDataCache($items);
}

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

Агенты и фоновые задачи

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

/local/classes/Project/Exchange/Agent.phpPHP
namespace Project\Exchange;

use Bitrix\Main\Config\Option;

final class Agent
{
    private const BATCH  = 200;   // порция за один запуск
    private const LOCK   = 'project_exchange_lock';
    private const EXPIRE = 900;   // «протухание» блокировки, сек

    /** Возвращает строку перезапуска — так требует планировщик агентов. */
    public static function run(): string
    {
        $self = '\Project\Exchange\Agent::run();';

        // Один процесс за раз: параллельный запуск обработал бы
        // одни и те же записи дважды.
        $lockedAt = (int) Option::get('project.exchange', self::LOCK, 0);

        if ($lockedAt > 0 && (time() - $lockedAt) < self::EXPIRE) {
            return $self;
        }

        Option::set('project.exchange', self::LOCK, (string) time());

        try {
            // Данные обрабатываются частями: ограничения времени
            // и памяти не зависят от объёма очереди.
            $processed = Queue::process(self::BATCH);
            Logger::info('Обработано записей: ' . $processed);
        } catch (\Throwable $e) {
            Logger::error($e->getMessage());
        } finally {
            Option::set('project.exchange', self::LOCK, '0');
        }

        return $self;
    }
}

Задача ведёт журнал выполнения, корректно обрабатывает повторный запуск и не создаёт несколько параллельных процессов над одними данными.

Безопасность на 1С-Битрикс

При создании обработчиков, форм и административных страниц применяются штатные механизмы платформы. Проверяются:

  • авторизация пользователя;
  • права на модуль и выполняемую операцию;
  • сессия и защита изменяющих запросов;
  • входящие параметры;
  • допустимые типы загружаемых файлов;
  • безопасность HTML-вывода;
  • параметры AJAX- и API-запросов;
  • доступ к административным действиям;
  • ответы внешних сервисов.
/local/modules/project.exchange/admin/project_exchange_run.phpPHP
use Bitrix\Main\Application;
use Bitrix\Main\Loader;

Loader::includeModule('project.exchange');

$request = Application::getInstance()->getContext()->getRequest();

// 1. Право на модуль: 'D' — доступ запрещён.
if ($APPLICATION->GetGroupRight('project.exchange') < 'W') {
    $APPLICATION->AuthForm('Доступ запрещён');
}

if ($request->isPost() && $request->getPost('action') === 'run') {
    // 2. Защита изменяющего запроса.
    if (!check_bitrix_sessid()) {
        ShowError('Сессия истекла, повторите действие');

        return;
    }

    // 3. Входящие параметры: тип, диапазон, белый список.
    $limit = min(1000, max(1, (int) $request->getPost('limit')));
    $mode  = (string) $request->getPost('mode');

    if (!in_array($mode, ['orders', 'stocks'], true)) {
        ShowError('Неизвестный режим обмена');

        return;
    }

    $count = \Project\Exchange\Queue::process($limit, $mode);

    // 4. Безопасный вывод: данные экранируются по контексту.
    echo 'Обработано: ' . htmlspecialcharsbx((string) $count);
}

Конфиденциальные ключи, пароли и токены не размещаются в публичных файлах, JavaScript-коде и открытом репозитории — их место в .settings.php или переменных окружения.

Обмены и интеграции

Интеграции с 1С, CRM, службами доставки, платёжными системами, маркетплейсами и другими сервисами проектируются с учётом нестабильности внешнего соединения. Для интеграции предусматриваются:

  • журнал запросов и ответов без раскрытия секретных данных;
  • контроль тайм-аутов;
  • повторная отправка;
  • защита от дублирования;
  • проверка формата ответа;
  • обработка частичного выполнения;
  • ограничение частоты запросов;
  • уведомление о критических ошибках;
  • возможность безопасно повторить обмен.
/local/classes/Project/Exchange/Client.phpPHP
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;

/**
 * Запрос к внешнему сервису: тайм-аут, повторы с нарастающей паузой,
 * проверка формата ответа и журнал без секретных данных.
 */
public function send(string $method, array $payload): array
{
    $client = new HttpClient([
        'socketTimeout' => 5,   // соединение
        'streamTimeout' => 20,  // чтение ответа
        'waitResponse'  => true,
    ]);
    $client->setHeader('Content-Type', 'application/json');
    $client->setHeader('Authorization', 'Bearer ' . $this->token);

    for ($attempt = 1; $attempt <= self::RETRIES; $attempt++) {
        // Ключ идемпотентности: повтор не создаст второй заказ.
        $client->setHeader('Idempotency-Key', $payload['uuid']);

        $raw    = $client->post($this->url . $method, Json::encode($payload));
        $status = $client->getStatus();

        Logger::info($method, ['status' => $status, 'attempt' => $attempt]);

        if ($status === 200) {
            $data = Json::decode((string) $raw);

            // Ответ проверяется: 200 не гарантирует ожидаемую структуру.
            if (!isset($data['result'])) {
                throw new \RuntimeException('Некорректный формат ответа');
            }

            return $data['result'];
        }

        if ($status < 500) {
            break; // ошибка запроса — повторять бессмысленно
        }

        sleep($attempt * 2);
    }

    throw new \RuntimeException('Сервис недоступен: ' . $method);
}

Остановка внешнего сервиса не должна без необходимости блокировать весь сайт.

Проверка проекта на 1С-Битрикс

Перед запуском или передачей крупной доработки проверяются:

  • работа основных пользовательских сценариев;
  • административные операции;
  • права доступа;
  • кеширование;
  • отправка почты;
  • фоновые задачи;
  • журнал ошибок;
  • совместимость с используемой версией PHP;
  • отсутствие изменений в ядре;
  • показатели производительности;
  • результаты доступных тестов Монитора качества.

Обновление платформы сначала проверяется на копии сайта — особенно если в проекте установлены сторонние решения или присутствует унаследованный код.

Платформа

Стандарт разработки на WordPress

WordPress почти не ограничивает разработчика — и именно поэтому нужен стандарт. Ключевой вопрос каждый раз один: что должно пережить смену темы, а что нет.

Разделение темы и функциональности

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

Остаётся в теме

  • шаблоны страниц и записей;
  • вывод контента и меню;
  • оформление, стили и скрипты вида;
  • небольшие правки представления.

Уходит в плагин

  • интеграции и обмены с внешними системами;
  • формы со сложной обработкой;
  • собственные типы данных и REST API;
  • фоновые задачи и бизнес-правила;
  • административные инструменты;
  • синхронизация товаров и заказов.

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

Собственная или дочерняя тема

Если используется готовая сторонняя тема, изменения выполняются в дочерней теме либо через предусмотренные разработчиком точки расширения. Редактирование файлов родительской темы не используется как основной способ кастомизации: обновление перезапишет внесённые изменения.

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

wp-content/themes/project-child/functions.phpPHP
/**
 * Дочерняя тема: свои стили подключаются зависимостью от родительских,
 * версия берётся из filemtime — правки подхватываются без ручного
 * сброса кеша браузера.
 */
add_action('wp_enqueue_scripts', 'project_child_assets', 20);

function project_child_assets(): void
{
    $path = get_stylesheet_directory() . '/assets/css/project.css';

    if (!is_readable($path)) {
        return;
    }

    wp_enqueue_style(
        'project-child',
        get_stylesheet_directory_uri() . '/assets/css/project.css',
        ['parent-style'],
        filemtime($path)
    );
}

Плагины и структура кода

Собственный плагин получает уникальный префикс или пространство имён, чтобы его функции, классы, настройки и хуки не конфликтовали с другими расширениями. Внутри плагина разделяются: загрузка и инициализация, административная и публичная части, работа с данными, API, интеграции, фоновые процессы, шаблоны, ресурсы, установка и удаление.

wp-content/plugins/project-core/project-core.phpPHP
/**
 * Plugin Name: Project Core
 * Description: Бизнес-логика проекта: интеграции, типы данных, обмены.
 * Version:     1.4.0
 * Requires at least: 6.4
 * Requires PHP: 8.1
 * Text Domain: project-core
 */

declare(strict_types=1);

namespace Project\Core;

// Прямое обращение к файлу плагина — не точка входа.
if (!defined('ABSPATH')) {
    exit;
}

const VERSION = '1.4.0';

define('PROJECT_CORE_FILE', __FILE__);
define('PROJECT_CORE_DIR', plugin_dir_path(__FILE__));

require_once PROJECT_CORE_DIR . 'vendor/autoload.php';

// Инициализация отделена от загрузки файла: так плагин можно
// подключить в тестах, не запуская его побочные эффекты.
add_action('plugins_loaded', [Plugin::class, 'boot']);

register_activation_hook(__FILE__, [Installer::class, 'activate']);
register_deactivation_hook(__FILE__, [Installer::class, 'deactivate']);

Для небольшой задачи не создаётся искусственно сложная архитектура. При росте функциональности код разбивается на классы и сервисы.

Использование WordPress API

Для разработки применяются штатные механизмы WordPress:

  • actions и filters;
  • Options API;
  • Settings API;
  • Metadata API;
  • Transients API;
  • HTTP API;
  • REST API;
  • Cron API;
  • Filesystem API;
  • роли и capabilities;
  • функции локализации;
  • штатные функции работы с пользователями, записями и таксономиями.

Прямое подключение внутренних файлов WordPress и копирование логики ядра избегаются, если задача решается публичным API.

WordPress Coding Standards

PHP, HTML, CSS и JavaScript оформляются с учётом официальных стандартов WordPress: единое форматирование, понятные названия, документирование функций и классов, совместимость с экосистемой, локализация интерфейсов, безопасная обработка данных и корректное использование API.

phpcs.xml.distXML
<?xml version="1.0"?>
<ruleset name="Project Core">
    <file>./src</file>
    <file>./project-core.php</file>

    <arg name="extensions" value="php"/>
    <arg value="ps"/>

    <rule ref="WordPress">
        <!-- Пространство имён вместо префикса функций -->
        <exclude name="WordPress.NamingConventions.PrefixAllGlobals"/>
    </rule>

    <rule ref="WordPress.WP.I18n">
        <properties>
            <property name="text_domain" type="array" value="project-core"/>
        </properties>
    </rule>

    <config name="minimum_wp_version" value="6.4"/>
    <config name="testVersion" value="8.1-"/>
</ruleset>
Проверка кодаShell
composer require --dev wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer

# Отчёт по нарушениям стандарта
vendor/bin/phpcs

# Автоисправление того, что исправляется механически
vendor/bin/phpcbf

В автономных PHP-библиотеках, не зависящих от устройства WordPress, могут применяться PSR и другие подходящие стандарты. Внутри темы или плагина приоритет отдаётся соглашениям WordPress.

Actions и filters

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

Анонимная функцияКак не надоPHP
// Такой обработчик невозможно снять: remove_filter()
// требует ту же ссылку на функцию, а её больше нет.
add_filter('the_content', function ($content) {
    return $content . '<p>Подпись</p>';
}, 20);
Именованный обработчикКак надоPHP
add_filter('the_content', 'project_append_signature', 20);

/**
 * @param string $content Содержимое записи.
 * @return string
 */
function project_append_signature(string $content): string
{
    // Контекст проверяется явно: фильтр вызывается и в лентах,
    // и в выдержках, и в письмах.
    if (!is_singular('post') || !in_the_loop() || !is_main_query()) {
        return $content;
    }

    return $content . '<p class="project-signature">Подпись</p>';
}

// При необходимости обработчик снимается или подменяется:
// remove_filter('the_content', 'project_append_signature', 20);

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

Работа с базой данных

Для стандартных сущностей применяются функции WordPress, WP_Query и соответствующие API. Прямые запросы через $wpdb используются только при необходимости. В прямых запросах пользовательские значения передаются через подготовленные выражения, выбираются только необходимые поля, учитываются индексы, исключаются запросы в циклах, контролируется объём результата и проверяется результат выполнения.

wp-content/plugins/project-core/src/Report/SalesRepository.phpPHP
global $wpdb;

$table = $wpdb->prefix . 'project_orders';

// Прямой запрос оправдан: агрегация по миллиону строк через
// WP_Query потребовала бы выборки всех записей в память.
// Значения подставляются только через prepare().
$rows = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT status, COUNT(*) AS cnt, SUM(total) AS amount
         FROM {$table}
         WHERE created_at BETWEEN %s AND %s
         GROUP BY status
         LIMIT %d",
        $from,
        $to,
        100
    ),
    ARRAY_A
);

if ($wpdb->last_error !== '') {
    throw new \RuntimeException('Ошибка выборки: ' . $wpdb->last_error);
}

Создание собственных таблиц оправдано для структурированных данных, которые плохо соответствуют модели записей, метаданных и таксономий WordPress.

Безопасная обработка данных

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

Функция подбирается под место вывода, а не «одна на все случаи»
Контекст На входе На выводе
Текст в HTML sanitize_text_field() esc_html()
Значение атрибута sanitize_key() esc_attr()
Ссылка esc_url_raw() esc_url()
Разметка от редактора wp_kses_post() wp_kses_post()
Данные в JavaScript absint(), sanitize_key() wp_json_encode(), esc_js()
Запрос к базе приведение типа $wpdb->prepare()

Данные из базы, стороннего API или настроек не считаются автоматически безопасными для вывода. Экранирование выполняется в момент вывода, а не «когда-то раньше при сохранении».

Nonce и проверка прав

Nonce применяется для защиты форм и запросов от нежелательного повторного или стороннего вызова. Одновременно с nonce всегда проверяются полномочия пользователя. Серверный обработчик должен определить:

  1. авторизован ли пользователь;
  2. имеет ли он право выполнить действие;
  3. относится ли действие к доступному ему объекту;
  4. прошёл ли запрос необходимую защитную проверку;
  5. допустимы ли переданные значения.
wp-content/plugins/project-core/src/Admin/MetaBox.phpPHP
/** Форма: nonce кладётся в разметку рядом с полями. */
function project_render_box(WP_Post $post): void
{
    wp_nonce_field('project_save_' . $post->ID, 'project_nonce');

    $value = get_post_meta($post->ID, '_project_code', true);

    printf(
        '<input type="text" name="project_code" value="%s" class="widefat">',
        esc_attr($value)
    );
}

/** Обработчик: пять проверок до единственной операции записи. */
add_action('save_post_product', 'project_save_box', 10, 2);

function project_save_box(int $post_id, WP_Post $post): void
{
    // Автосохранение и ревизии — не пользовательское действие.
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }

    // Подлинность запроса…
    $nonce = isset($_POST['project_nonce']) ? sanitize_key($_POST['project_nonce']) : '';

    if (!wp_verify_nonce($nonce, 'project_save_' . $post_id)) {
        return;
    }

    // …и отдельно — право на этот конкретный объект.
    if (!current_user_can('edit_post', $post_id)) {
        return;
    }

    $code = isset($_POST['project_code']) ? sanitize_text_field(wp_unslash($_POST['project_code'])) : '';

    if ($code !== '' && !preg_match('/^[A-Z]{2}-\d{4,8}$/', $code)) {
        return; // формат не соответствует ожидаемому
    }

    update_post_meta($post_id, '_project_code', $code);
}

Скрытая кнопка или отсутствие ссылки в интерфейсе не являются ограничением доступа. Защита выполняется на стороне сервера.

Подключение CSS и JavaScript

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

wp-content/plugins/project-core/src/Front/Assets.phpPHP
add_action('wp_enqueue_scripts', 'project_assets');

function project_assets(): void
{
    // Условие загрузки: калькулятор нужен только на странице услуги.
    if (!is_singular('service')) {
        return;
    }

    $file = PROJECT_CORE_DIR . 'assets/js/calculator.js';

    wp_enqueue_script(
        'project-calculator',                                  // идентификатор
        plugins_url('assets/js/calculator.js', PROJECT_CORE_FILE),
        [],                                                    // зависимости
        filemtime($file),                                      // версия
        ['in_footer' => true, 'strategy' => 'defer']           // место и стратегия
    );

    // Данные для JS передаются контролируемо: только то, что можно
    // показать посетителю. Ключи и внутренние идентификаторы — нельзя.
    wp_add_inline_script(
        'project-calculator',
        'window.projectCalc = ' . wp_json_encode([
            'ajaxUrl'  => admin_url('admin-ajax.php'),
            'nonce'    => wp_create_nonce('project_calc'),
            'currency' => '₽',
        ]) . ';',
        'before'
    );
}

Производительность WordPress

При разработке контролируются:

  • количество запросов;
  • сложность WP_Query;
  • объём метаданных;
  • автозагружаемые настройки;
  • повторные обращения к внешним API;
  • размеры изображений;
  • подключение ресурсов;
  • фоновые задачи;
  • кеширование;
  • влияние сторонних плагинов.
wp-content/plugins/project-core/src/Api/RatesClient.phpPHP
/**
 * Курсы валют: внешний запрос выполняется не чаще раза в час
 * и никогда — синхронно на каждой странице.
 */
function project_get_rates(): array
{
    $cached = get_transient('project_rates');

    if (is_array($cached)) {
        return $cached;
    }

    $response = wp_remote_get('https://api.example.com/rates', [
        'timeout'     => 5,     // ограничение времени ожидания
        'redirection' => 2,
        'user-agent'  => 'project-core/1.4',
    ]);

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        // Сервис недоступен — короткий кеш, чтобы не долбить его
        // на каждом хите, и работа по последним известным данным.
        set_transient('project_rates', [], 5 * MINUTE_IN_SECONDS);

        return [];
    }

    $rates = json_decode(wp_remote_retrieve_body($response), true);
    $rates = is_array($rates) ? $rates : [];

    set_transient('project_rates', $rates, HOUR_IN_SECONDS);

    return $rates;
}

Кеширование не должно нарушать актуальность цен, остатков, персональных данных и другой динамической информации.

WP-Cron и фоновые процессы

WP-Cron зависит от посещаемости сайта и не всегда обеспечивает запуск строго в назначенное время. Для важных регулярных процессов используется системный cron с запуском планировщика WordPress.

wp-config.php + crontabShell
# 1. Отключаем запуск планировщика на хитах посетителей:
#    define('DISABLE_WP_CRON', true);  — в wp-config.php

# 2. Дёргаем планировщик системным cron каждые 5 минут
*/5 * * * * curl -s "https://example.com/wp-cron.php?doing_wp_cron" > /dev/null

# Альтернатива для тяжёлых задач — WP-CLI без HTTP-слоя
*/5 * * * * cd /var/www/example.com && wp cron event run --due-now --quiet

Фоновые задачи должны:

  • обрабатывать данные частями;
  • исключать параллельный повторный запуск;
  • вести журнал ошибок;
  • иметь возможность продолжить обработку;
  • ограничивать количество запросов к внешнему сервису;
  • не блокировать открытие страниц пользователями.

Для объёмных импортов и синхронизаций применяются очереди или поэтапная обработка.

Совместимость и обновления

В коде учитываются:

  • минимальная версия PHP;
  • версия WordPress;
  • версия WooCommerce, если она используется;
  • зависимости от сторонних плагинов;
  • изменившиеся и устаревшие функции;
  • особенности новой версии редактора;
  • совместимость с активной темой.
wp-content/plugins/project-core/src/Plugin.phpPHP
/**
 * Зависимость проверяется до использования, а не в момент падения.
 * Отсутствие WooCommerce отключает часть плагина, а не весь сайт.
 */
add_action('plugins_loaded', 'project_boot_woocommerce', 20);

function project_boot_woocommerce(): void
{
    if (!class_exists('WooCommerce')) {
        add_action('admin_notices', 'project_notice_missing_wc');

        return;
    }

    if (version_compare(WC_VERSION, '8.0', '<')) {
        add_action('admin_notices', 'project_notice_old_wc');

        return;
    }

    (new \Project\Core\Shop\Sync())->register();
}

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

Тестирование WordPress-проекта

Перед публикацией проверяются:

  • основные страницы и шаблоны;
  • формы;
  • роли пользователей;
  • административные настройки;
  • мобильная версия;
  • различные объёмы контента;
  • изображения разных пропорций;
  • отсутствие PHP- и JavaScript-ошибок;
  • отправка почты;
  • фоновые задачи;
  • интеграции;
  • кеширование;
  • работа после очистки кеша;
  • сохранение данных;
  • обновление разработанного плагина или темы.

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

Требования к вёрстке и интерфейсам

Независимо от CMS, интерфейс должен корректно работать на распространённых размерах экранов и не зависеть от идеального объёма контента.

  • семантическая структура HTML;
  • последовательность заголовков;
  • адаптивность;
  • управление с клавиатуры;
  • видимое состояние фокуса;
  • подписи полей формы;
  • сообщения об ошибках;
  • альтернативные описания изображений;
  • контрастность текста;
  • поведение длинных заголовков;
  • отсутствие неконтролируемых сдвигов страницы.
Поле без подписиКак не надоHTML
<!-- Placeholder не подпись: он исчезает при вводе,
     не читается скринридером как label и не кликабелен. -->
<div class="field">
    <input type="email" name="email" placeholder="E-mail">
    <div class="error">Ошибка</div>
</div>
Поле с подписью и сообщением об ошибкеКак надоHTML
<div class="field">
    <label class="field__label" for="order-email">E-mail</label>

    <input
        class="field__input"
        id="order-email"
        type="email"
        name="email"
        autocomplete="email"
        required
        aria-describedby="order-email-error"
        aria-invalid="true">

    <!-- Роль alert: сообщение будет озвучено сразу после появления -->
    <p class="field__error" id="order-email-error" role="alert">
        Укажите адрес в формате name@example.com
    </p>
</div>

Подпись связана с полем через for/id, ошибка — через aria-describedby. Это работает и для мыши, и для клавиатуры, и для скринридера.

assets/css/project.cssCSS
/* Видимый фокус — обязательное состояние, а не «мешает дизайну».
   :focus-visible показывает кольцо только при клавиатурной навигации. */
.field__input:focus-visible,
.btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Длинный заголовок не должен ломать сетку */
.card__title {
    overflow-wrap: anywhere;
    hyphens: auto;
}

/* Резерв места под изображение — нет сдвига макета при загрузке */
.card__image {
    aspect-ratio: 16 / 9;
    width: 100%;
    height: auto;
    object-fit: cover;
}

Полное соответствие определённому уровню WCAG является отдельной задачей и требует специализированного аудита. В стандартной разработке учитываются основные требования доступности, применимые к конкретному интерфейсу.

Порядок разработки

Последовательность одинакова и для отдельной доработки, и для нового модуля — меняется только глубина каждого этапа.

  1. 01

    Анализ задачи

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

  2. 02

    Анализ существующего проекта

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

  3. 03

    Выбор архитектуры

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

  4. 04

    Реализация

    Код создаётся с учётом правил платформы, безопасности, производительности и последующего сопровождения. Связанные изменения объединяются в логические этапы.

  5. 05

    Тестирование

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

  6. 06

    Развёртывание

    Перед установкой на рабочий сайт проверяется наличие резервной копии. Изменения переносятся контролируемо, после чего выполняется повторная проверка основных сценариев.

  7. 07

    Документирование

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

Что получает заказчик

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

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

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

Официальная основа стандарта

Стандарт сформирован с учётом:

1С-Битрикс

  • официальный курс «Разработчик Bitrix Framework»;
  • документация по безопасной кастомизации;
  • рекомендации по использованию каталога /local/;
  • документация ядра D7 и ORM;
  • требования Монитора качества.

WordPress и общие практики

  • WordPress Coding Standards;
  • WordPress Plugin Handbook;
  • WordPress Theme Handbook;
  • WordPress Common APIs Handbook;
  • официальные рекомендации по безопасности и доступности;
  • применимые рекомендации PHP и OWASP.

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

Частые вопросы

Можно ли обновлять сайт после доработок?

Да, если доработки отделены от ядра и используют поддерживаемые механизмы расширения. Перед крупным обновлением всё равно рекомендуется создать резервную копию и проверить совместимость на тестовой среде.

Почему нельзя просто изменить файл ядра или плагина?

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

Всегда ли нужен отдельный модуль или плагин?

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

Можно ли применять стандарт к уже работающему сайту?

Да. Сначала проводится анализ существующей архитектуры. Новые функции создаются по стандарту, а старый код перерабатывается поэтапно там, где это необходимо для безопасности, обновления или дальнейшего развития проекта.

Гарантирует ли стандарт отсутствие ошибок?

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

Входит ли проверка безопасности в разработку?

Базовые меры безопасности входят в реализацию каждой функции. Полный аудит сайта, тестирование на проникновение и анализ всех установленных компонентов являются отдельными услугами.

Разработка с учётом дальнейшей поддержки

Я разрабатываю и дорабатываю сайты на 1С-Битрикс и WordPress, создаю собственные модули и плагины, интеграции, обмены между системами и нестандартные PHP-решения.

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

Услуги и цены

Что будем искать? Например,Продвижение

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