ZoomX Быстрый старт - разбираем на практике. Часть первая.

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

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

Таких задач я для себя выделил две.
  1. Построение полноценного RESTfull API
  2. Отказ от парсеров MODX и построение шаблонов сайта на основе правильного взрослого шаблонизатора (это ведет, в том числе к ускорению загрузки сайта)
Ко второй задаче мы вернемся когда-нибудь потом, а пока я планирую рассказать и показать на конкретном живом примере принципы построение REST API.


Постановка задачи


Так как статья посвящена реализации RESTFull приложения — понятное дело рассказ будет о том, как выстроить REST запросы, которые будут что то запускать внутри MODX.
Я подготовил пример реализации классического CRUD (Create.Read.Update.Delete) для страницы-ресурса через API запросы. То есть Создание, Получение, Изменение, Удаление ресурса.

Все запросы должны улетают на виртуальный (несуществующий физически) URI с вот такой, совершенно произвольной, маской адреса /api/pages/*

Каждый запрос отправляется по отдельному HTTP протоколу (GET, POST, PUT, DELETE)

  • Запрос на получение информации о ресурсе GET /api/pages/{id}
  • Запрос на создание нового ресурса POST /api/pages/
  • Запрос на изменение ресурса PUT /api/pages/{id}
  • Запрос на удаление ресурса DELETE /api/pages/{id}

Вводные пояснения и допущения


Документация
В первую очередь надо заметить, что ZoomX имеет неплохую документацию.

Мне в свое время ее не хватило и я несколько дней очень плотно мешал жить Автору @Сергей Шлоков. Попутно нашел несколько багов, и даже стал инициатором некоторых нововведений. Одна из целей моих публикаций дополнить неочевидные моменты документации.

Адреса и протоколы запросов
Предложенные мною URI адреса для запросов, никак не регламентируются внутри ZoomX, они могут быть абсолютно произвольными.
HTTP Протоколы (GET, POST, PUT, DELETE), по которым отправляются запросы также всего лишь рекомендованы. Не будет преступлением отправить все через POST как-нибудь вот так

  • Запрос на получение информации о ресурсе POST /api/get-page/{id}
  • Запрос на создание нового ресурса POST /api/create-page
  • Запрос на изменение ресурса POST /api/update-page/{id}
  • Запрос на удаление ресурса POST /api/remove-page/{id}
Вольные допущения
Так как цель статьи — быстрый старт, и не хотелось бы ее растягивать на целый цикл — то придется пойти на ряд условностей.

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

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

Рабочая среда.


Я развернул абсолютно пустой сайт на MODX расположенный по адресу zoomx-course1.megawebs.kz

Из компонентов установил

  • ZoomX (неожиданно правда?)
  • pdoTools
pdoTools никакого отношения к теме статьи не имеет и для ZoomX не нужен. Я использую его лишь для того, чтобы работать с разметкой шаблона через IDE.
Надеюсь все знают как пользоваться шаблонами и чанками в файлах через инструментарий pdoTools. Если есть затруднения — то в этом месте будет правильно прерваться и изучить такой вопрос как основу. Напишите в комментариях если есть потребность в подобном материале.

Сразу даю ссылку на исходный код проекта

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



Пишем код


Сначала JS

В проекте два совсем небольших JS файла
  • api.js — ловит клики по кнопкам и просит второй файл отправить данные по нужному адресу
  • request.js — Это готовая простенькая библиотека для HTTP запросов
Отдельный файл request.js реализует все, используемые здесь, виды HTTP запросов на языке JS. Не axios конечно, но пользоваться можно.
Библиотека достаточно универсальная и подходит как для статичного сайта, так и для JS фреймворков.
У меня этот файл, практически без изменений, кочует из проекта в проект. В том числе, использую его и внутри VUE разработок.

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

Работает все вот так:

//GET запрос
const response = await request.get('pages/' + id);

//POST запрос
const formData = new FormData($form)
const response = await request.post('pages/', formData);

//DELETE запрос
const response = await request.delete('pages/' + id);

//PUT запрос
const data = {}
const response = await request.put('pages/' + id, data);

Но ZoomX — он не только для JS. Компонент вообще мало что знает о том, каким образом к нему послан запрос. Никто не мешает отправить классический запрос через форму. Причем в документации даже есть примеры как отправить через форму PUT и DELETE запрос, которые HTML не поддерживает.

Примеры построения запроса

По задумке каждый запрос к бэкенду начинается с нажатия кнопки.
Все кнопки работают по единому принципу. Нажатие вызывает JS функцию

Реализация кнопки выглядит вот так
<button class="btn btn-info" onclick="api.getPage({$page.id})">Посмотреть детали</button>
<button class="btn btn-success" type="button" onclick="api.addPage()">Добавить новую страницу</button>
<button class="btn btn-primary" type="button" onclick="api.changePage()">Изменить страницу</button>
<button class="btn btn-danger" onclick="api.removePage({$page.id})">Удалить</button>

Каждая функция это максимально простой вызов библиотеки request.js.
const api = {
    async getPage(id) {
        const response = await request.get('pages/' + id);
    },

    async removePage(id) {
        const response = await request.delete('pages/' + id);
    },

    async addPage() {
        const $newPageForm = document.querySelector('#newPageForm');
        const formData = new FormData($newPageForm)
        const response = await request.post('pages/', formData);
    },

    async changePage() {
        const $changePageForm = document.querySelector('#changePageForm');
        const id = $changePageForm.id.value
        const pagetitle = $changePageForm.pagetitle.value
        const template = $changePageForm.template.value
        const response = await request.put('pages/' + id, {pagetitle, template});
    },
};

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

В моем примере базовый путь запроса такой
https://zoomx-course1.megawebs.kz/api/
Значит запрос вида
const response = await request.get('pages/5');
Дополнится базовой строкой адреса улетит на следующий URL
https://zoomx-course1.megawebs.kz/api/pages/5

Работа с ZoomX начинается с роута.

Каждый запрос передает идентификатор страницы. Мы конечно понимаем, что id запрашиваемой страницы это переменная, она может меняться. Значит на сайте внутри ZoomX будем ловить вот такой запрос

api/pages/{id}
Хост сайта отбрасывается, а переменная обозначаться фигурными скобками

Чтобы ZoomX поймал и обработал запрос, ему нужно рассказать о том какие конкретно запросы ловить.
За это отвечает файл
/core/config/routes.php

Запрос ловит такая строка
$router->get('api/pages/{id}', Zoomx\Controllers\Api\Pages\GetController::class);
Строка состоит из трех частей. Метод get говорит о том, что мы ловим GET запрос. Внутри метода два параметра. Собственно сам запрос, и файл контроллера, который будет вызван, в случае если обнаружен запрос.

Всего в базовом примере получилось 4 строки. По одной на каждое действие
$router->get('api/pages/{id}', Zoomx\Controllers\Api\Pages\GetController::class);
$router->post('api/pages/', Zoomx\Controllers\Api\Pages\CreateController::class);
$router->put('api/pages/{id}', Zoomx\Controllers\Api\Pages\UpdateController::class);
$router->delete('api/pages/{id}', Zoomx\Controllers\Api\Pages\DeleteController::class);

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

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

Рассмотрим контроллер
$router->get('api/pages/{id}', Zoomx\Controllers\Api\Pages\GetController::class);
В нем мы имеем GET запрос, который был отправлен браузером через JS метод, пойман роутером ZoomX и передан внутрь Контроллера

Код контроллера достаточно простой и короткий

<?php

namespace Zoomx\Controllers\Api\Pages;

class GetController extends BaseController
{
    public function index($id)
    {
        $page = $this->modx->getObject(\modResource::class, ['id' => $id, 'deleted' => 0]);
        if ($page) {
            return jsonx([
                'id' => $page->id,
                'pagetitle' => $page->pagetitle,
                'template' => $page->template,
                'createdon' => $page->get('createdon')
            ]);
        }

        return jsonx([], [], 404);
    }
}
Что здесь важно знать. Если в роуте не указан метод контроллера, запрос всегда обращается к главному методу index()

Переменная из роута автоматически передается в метод-обработчик контроллера. Кстати ее именование не обязательно должно равняться тому, как написано в роуте. Вы можете принять в роуте переменную {id} а в контроллера работать с ней как с $page_id. Никакой связи нет.

Внутри index можно писать любой привычный для MODX PHP код, формировать удобный для выдачи массив данных. И вернуть этот массив можно при помощи метода-хелпера jsonx(). Такой метод возвращает ответ в виде JSON и некоторый массив meta данных, которыми в последствии можно легко манипулировать внутри браузера.

{
  "success": true,
  "data": {
    "id": 1,
    "pagetitle": "Главная!",
    "template": 2,
    "createdon": "2022-02-28 15:38:07"
  },
  "meta": {
    "total_time": "0.2962 s",
    "query_time": "0.0282 s",
    "php_time": "0.2680 s",
    "queries": 10,
    "source": "cache",
    "memory": "2 048 KB"
  }
}

Метод jsonx всегда возвращает объект с тремя блоками.
success обычно он всегда true. Но им можно манипулировать, если дополнительно передать методу jsonx HTTP код ответа. Например код 404 даст success = false
return jsonx([], [], 404);

data — массив передаваемых в jsonx данных. В случае если jsonx возвращает success false — то вместо data будет errors

meta — мета данные. Их суть видно по примеру

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

Для более детального разбора в вашем распоряжении
Впереди вторая часть разбора, в которой я дам больше теории по неочевидным моментам и тем местам, с которыми сам столкнулся при разборе компонента, которых нет в докуменатации.
Николай Савин
05 марта 2022, 22:00
modx.pro
4
2 259
+19
Поблагодарить автора Отправить деньги

Комментарии: 5

Сергей Шлоков
06 марта 2022, 08:56
+4
Сделаю несколько пояснений.

pdoTools никакого отношения к теме статьи не имеет и для ZoomX не нужен. Я использую его лишь для того, чтобы работать с разметкой шаблона через IDE.
Это только для тех, кто хочет работать с шаблонизатором Fenom. Если есть желание работать в режиме фреймворка, то можно работать с шаблонизатором Smarty, который идёт с MODX из коробки. Я много раз уже говорил, что Fenom в pdoTools — это костыль, который вроде как добавляет возможности нормального шаблонизатора, но работает по правилам MODX шаблонизатора — многократный парсинг и компиляция контента, который ему подсовывает MODX шаблонизатор. В правильном режиме php шаблонизатор парсит страницу один раз, компилирует её в php файл и использует его при следующих запросах, не тратя время на повторный парсинг и компиляцию.

Если в роуте не указан метод контроллера, запрос всегда обращается к главному методу index()
Сначала проверяется наличие магического метода __invoke. Если его нет, то будет вызван метод index.

Метод jsonx всегда возвращает объект с тремя блоками.
Блок meta можно отключить в системной настройке zoomx_include_request_info
    Николай Савин
    06 марта 2022, 10:58
    0
    Блок meta можно отключить в системной настройке zoomx_include_request_info
    Тогда data напрямую в корень будет выводиться или просто минус один элемент массива на выходе?
      Сергей Шлоков
      06 марта 2022, 11:33
      0
      Просто не будет элемента meta. В следующей версии сделаю возможность выводить свой формат данных.
    deleted
    11 марта 2022, 10:39
    0
    Контроллеры, кстати, можно в своей папке размещать. Из документации не совсем очевидно.
    В core/config/elements.php прописываем
    zoomx()->getLoader()->addPsr4('My\\Controllers\\', MODX_CORE_PATH . 'elements/controllers/');
    В routes.php
    $router->post('request/{key}', ['\My\Controllers\MyController', 'action']);
    В контроллере:
    namespace My\Controllers;
    use Zoomx\Controllers;
    class MyController extends \Zoomx\Controllers\Controller
      Анатолий
      15 сентября 2022, 22:14
      +1
      Если есть затруднения — то в этом месте будет правильно прерваться и изучить такой вопрос как основу. Напишите в комментариях если есть потребность в подобном материале.
      Вот это интересно, можно подробнее? Спасибо.
        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
        5