Представьте типичную ситуацию. Интернет-магазин на WooCommerce получает 50–100 заказов в день. Менеджер вручную копирует данные из админки в 1С, потом сверяет остатки на складе, потом обновляет статусы в CRM. На всё это уходит три-четыре часа ежедневно, а ошибки всё равно случаются. Знакомо?

Решение лежит на поверхности — автоматизировать обмен данными через API. WooCommerce из коробки даёт REST API с набором стандартных эндпоинтов. Но вот незадача: встроенные точки доступа возвращают данные в формате, который редко устраивает внешние системы напрямую. Всё равно приходится писать прослойку — маппинг полей, фильтрацию, обогащение данных. Именно здесь на сцену выходят кастомные точки доступа WP REST API, и сегодня разберём, как их грамотно спроектировать и реализовать.

Зачем нужны кастомные эндпоинты, если есть встроенный API WooCommerce

Стандартный REST API WooCommerce (маршрут /wp-json/wc/v3/orders) умеет многое: отдавать заказы, создавать, обновлять статусы. Но у него есть ряд ограничений, которые становятся проблемой при интеграции с реальными внешними системами.

Что не так с дефолтным API заказов

  • Лишние данные. Типичный ответ на запрос списка заказов весит 15–40 КБ на каждый объект. Внешней системе для синхронизации может понадобиться только номер, сумма, статус и список товаров — но она получает полный JSON с метаданными, данными купонов, адресами и прочим.
  • Неудобная структура. ERP-системы ждут плоскую таблицу с полями вроде order_id, customer_name, total, sku, quantity. API WooCommerce отдаёт вложенную структуру, где товары — массив объектов внутри объекта заказа.
  • Нет фильтрации по бизнес-логике. «Отдай все заказы за последние сутки, которые ещё не выгружались в 1С» — такого фильтра в стандартном API нет.
  • Производительность. Выгрузка 500 заказов через стандартный endpoint с пагинацией по 10 штук — это 50 HTTP-запросов. Кастомный endpoint может отдать всё за один раз, в нужном формате, с оптимизированным SQL.
  • Безопасность. Полный доступ к API WooCommerce — это широкие права. Для выгрузки заказов лучше дать внешней системе доступ только к read-only эндпоинту с ограниченными данными.

По сути, кастомные точки доступа — это специализированные каналы связи между вашим сайтом и внешним миром. Один endpoint — одна задача. Чисто, понятно, контролируемо.

Архитектура интеграции: как это работает

Прежде чем писать код, стоит на секунду остановиться и подумать о картине в целом. Кастомный REST API endpoint — это не просто функция, которая возвращает JSON. Это часть архитектуры, и от того, как вы её спроектируете, зависит надёжность всей системы.

Типичная схема выглядит так:

  1. Внешняя система (CRM, ERP, складской учёт, BI-панель) отправляет HTTP-запрос на ваш WordPress-сайт.
  2. WordPress маршрутизирует запрос в зарегистрированный callback через хук rest_api_init.
  3. Callback выполняет нужную бизнес-логику: формирует SQL-запрос, обогащает данные, применяет фильтры.
  4. Ответ уходит в формате JSON (или XML — некоторые ERP-системы до сих пор предпочитают его).

Важный момент: кто инициирует обмен данными. В схеме «вытягивания» (pull) внешняя система сама запрашивает данные. В схеме «проталкивания» (push) WordPress отправляет данные при смене статуса заказа через хуки WooCommerce. Оба подхода можно реализовать через кастомные endpoints — но для выгрузки чаще используют pull.

Регистрация кастомного REST API endpointа

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

Код размещается в файле functions.php дочерней темы или в отдельном плагине. Второй вариант надёжнее — при смене темы логика не потеряется.

add_action('rest_api_init', function () {
    register_rest_route('custom/v1', '/orders-for-export', [
        'methods'  => 'GET',
        'callback' => 'handle_orders_export',
        'permission_callback' => 'verify_api_access',
        'args' => [
            'after' => [
                'required'          => false,
                'type'              => 'string',
                'description'       => 'Дата в формате Y-m-d, заказы после этой даты',
                'sanitize_callback' => 'sanitize_text_field',
            ],
            'status' => [
                'required'          => false,
                'type'              => 'string',
                'default'           => 'processing',
                'sanitize_callback' => 'sanitize_text_field',
            ],
            'per_page' => [
                'required'          => false,
                'type'              => 'integer',
                'default'           => 100,
                'sanitize_callback' => 'absint',
            ],
        ],
    ]);
});

Что здесь происходит. Мы регистрируем маршрут /wp-json/custom/v1/orders-for-export. Три параметра: фильтр по дате, статусу и количество записей на страницу. Параметр permission_callback отвечает за авторизацию — об этом поговорим отдельно.

Callback-функция: формируем ответ

Сама функция-обработчик собирает данные из базы и возвращает структурированный JSON.

function handle_orders_export($request) {
    $after    = $request->get_param('after');
    $status   = $request->get_param('status');
    $per_page = $request->get_param('per_page');

    $args = [
        'limit'  => $per_page,
        'type'   => 'shop_order',
        'orderby'=> 'date',
        'order'  => 'ASC',
    ];

    if ($status) {
        $args['status'] = $status;
    }

    if ($after) {
        $args['date_created'] = '>' . $after . ' 00:00:00';
    }

    $orders = wc_get_orders($args);

    $result = [];

    foreach ($orders as $order) {
        $items = [];
        foreach ($order->get_items() as $item) {
            $product = $item->get_product();
            $items[] = [
                'sku'      => $product ? $product->get_sku() : '',
                'name'     => $item->get_name(),
                'quantity' => $item->get_quantity(),
                'price'    => $item->get_total(),
            ];
        }

        $result[] = [
            'order_id'      => $order->get_id(),
            'date_created'  => $order->get_date_created()->format('Y-m-d H:i:s'),
            'status'        => $order->get_status(),
            'total'         => $order->get_total(),
            'currency'      => $order->get_currency(),
            'payment_method'=> $order->get_payment_method_title(),
            'customer_name' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
            'customer_email'=> $order->get_billing_email(),
            'customer_phone'=> $order->get_billing_phone(),
            'shipping_city' => $order->get_shipping_city(),
            'items'         => $items,
            'meta' => [
                'exported' => $order->get_meta('_exported_to_erp'),
            ],
        ];
    }

    return new WP_REST_Response($result, 200);
}

Обратите внимание на поле meta.exported. Это хитрый приём: с помощью произвольного метаполя _exported_to_erp можно отслеживать, какие заказы уже были переданы. После успешной выгрузки внешняя система (или скрипт на стороне WordPress) проставляет это поле, и повторная выгрузка не дублирует данные.

Авторизация: как защитить кастомный API

Открытый endpoint, который отдаёт данные о заказах — это подарок для злоумышленника. Безопасность нельзя оставлять на потом, она проектируется с самого начала.

Есть несколько подходов к авторизации кастомных точек доступа REST API.

API-ключи через заголовок

Самый простой вариант. Внешняя система передаёт секретный ключ в заголовке запроса. Сервер сверяет его с хранимым значением.

function verify_api_access($request) {
    $token = $request->get_header('X-API-Key');

    if (!$token) {
        return new WP_Error(
            'rest_forbidden',
            'API ключ не передан',
            ['status' => 401]
        );
    }

    $valid_token = get_option('custom_api_export_key');

    if (!$valid_token || !hash_equals($valid_token, $token)) {
        return new WP_Error(
            'rest_forbidden',
            'Неверный API ключ',
            ['status' => 403]
        );
    }

    return true;
}

Ключ генерируется один раз и передаётся администратору внешней системы. Хранится в wp_options, при желании можно зашифровать.

OAuth или JWT

Для более сложных сценариев — например, когда внешних систем несколько и каждой нужен свой уровень доступа — подойдут JWT-токены (плагин JWT Authentication for WP REST API) или полная реализация OAuth 2.0. Это выходит за рамки базовой настройки, но для enterprise-интеграций часто необходимо.

Дополнительные меры

  • Ограничьте доступ по IP — если внешняя система работает с фиксированного адреса, добавьте проверку в permission_callback.
  • Используйте HTTPS. Всегда. Без вариантов. Передача данных о заказах по HTTP — это не просто плохая практика, это нарушение базовых требований информационной безопасности.
  • Логируйте обращения к API. Даже простая запись в отдельную таблицу или лог-файл с IP, timestamp и параметрами запроса потом сэкономит часы при расследовании инцидентов.

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

Когда заказов становится много — тысячи, десятки тысяч — стандартные подходы начинают буксовать. wc_get_orders() загружает объекты в память, инициализирует каждый как полноценный объект WC_Order. Для 100 заказов это незаметно, для 5000 — может привести к исчерпанию памяти или длительному времени ответа.

Прямой SQL-запрос

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

function handle_orders_export_raw($request) {
    global $wpdb;

    $after    = $request->get_param('after');
    $per_page = $request->get_param('per_page');

    $sql = $wpdb->prepare("
        SELECT
            p.ID as order_id,
            p.post_date as date_created,
            pm_status.meta_value as status,
            pm_total.meta_value as total,
            pm_currency.meta_value as currency,
            pm_fname.meta_value as first_name,
            pm_lname.meta_value as last_name,
            pm_email.meta_value as email,
            pm_phone.meta_value as phone
        FROM {$wpdb->posts} p
        LEFT JOIN {$wpdb->postmeta} pm_status
            ON p.ID = pm_status.post_id AND pm_status.meta_key = '_wc_order_status'
        LEFT JOIN {$wpdb->postmeta} pm_total
            ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
        LEFT JOIN {$wpdb->postmeta} pm_currency
            ON p.ID = pm_currency.post_id AND pm_currency.meta_key = '_order_currency'
        LEFT JOIN {$wpdb->postmeta} pm_fname
            ON p.ID = pm_fname.post_id AND pm_fname.meta_key = '_billing_first_name'
        LEFT JOIN {$wpdb->postmeta} pm_lname
            ON p.ID = pm_lname.post_id AND pm_lname.meta_key = '_billing_last_name'
        LEFT JOIN {$wpdb->postmeta} pm_email
            ON p.ID = pm_email.post_id AND pm_email.meta_key = '_billing_email'
        LEFT JOIN {$wpdb->postmeta} pm_phone
            ON p.ID = pm_phone.post_id AND pm_phone.meta_key = '_billing_phone'
        WHERE p.post_type = 'shop_order'
        AND p.post_date > %s
        ORDER BY p.post_date ASC
        LIMIT %d
    ", $after, $per_page);

    $results = $wpdb->get_results($sql, ARRAY_A);

    return new WP_REST_Response($results, 200);
}

Этот запрос выполняется в разы быстрее, чем цикл через wc_get_orders(), особенно на больших объёмах. Правда, он жёстко привязан к структуре метаданных WooCommerce — если в будущих версиях что-то изменится, придётся адаптировать SQL. Но для внутренних интеграций это приемлемый компромисс.

Кэширование и батчинг

Если внешняя система запрашивает данные часто, имеет смысл кэшировать результат. Можно сохранять сформированный JSON-файл на диске и обновлять его по cron-заданию, а endpoint просто отдаёт готовый файл. Для высоконагруженных магазинов это снимает нагрузку с базы данных.

Б

Добавить комментарий

Разработка сайтов на Wordpress