По умолчанию Joomla отправляет ответы в формате JSON API, если запрос содержит Accept: application/json
или специальный заголовок JSON API. Хотя ядро Joomla не поддерживает другие типы контента, система позволяет разработчикам добавлять дополнительные форматы для ответов.
Цели материала:
-
Получить JSON ответ от API Joomla;
-
Создать необходимый плагин группы
webservices
и API-часть компонента; -
Использовать параметры модуля для моделирования данных, которые мы отправим в ответе API.
Что не является целью?
-
Обучение созданию расширений. Данное руководство предполагает, что вы уже умеете создавать расширения для Joomla 4. Для работы API потребуются плагин и компонент, но компонент может быть минимальным — без модели (Model), с простой административной частью.

Административная панель компонента
Базовая админка компонента необходима, так как XML-манифест, config.xml и access.xml располагаются только в backend-директории компонента. Даже если функционал не требуется, Joomla автоматически создаёт пункт меню компонента в панели управления. Достаточно добавить минимальное представление (view) с сообщением о том, что компонент не требует настроек.
Ключевые моменты:
-
Аутентификация в Joomla API:
-
По токену (рекомендуется).
-
По паролю (не рекомендуется).
-
-
Для публичного контента (например, блога) можно использовать флаг
public
при создании эндпоинта в плагинеwebservices
.
Плагин группы webservices
Плагин отвечает за регистрацию маршрутов (эндпоинтов) API и указание контроллера компонента для обработки запросов. Давайте создадим его.
В рабочей области создайте папку с именем: plg_webservices_vapi. Внутри папки создайте php-файл с именем vapi.php со следующим содержимым:
defined('_JEXEC') || die; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Router\ApiRouter; use Joomla\Router\Route; class PlgWebservicesVapi extends CMSPlugin { /** * Registers com_vapi API's routes in the application * * @param ApiRouter &$router The API Routing object * * @return void * */ public function onBeforeApiRoute(&$router) { $route = new Route( ['GET'], 'v1/vapi/modules/:id', 'module.displayModule', ['id' => '(\d+)'], [ 'component' => 'com_vapi', 'public' => true, 'format' => [ 'application/json' ] ] ), ]; $router->addRoute($route); } }
Давайте выделим некоторые вещи, которые вам следует знать об этом плагине:
-
onBeforeApiRoute:
Этот метод требуется в каждом плагине веб-сервисов. Здесь вы определите свои эндпоинты. -
Создание эндпоинта. Рассмотрим подробнее параметры передаваемые в конструктор класса
Joomla\Router\Route
:-
['GET']
— HTTP методы которые поддерживает этот эндпоинт, значения должны быть написаны заглавными буквами. Допустимые значения:'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'TRACE', 'PATCH'
; -
'v1/vapi/modules/:id'
— шаблон эндпоинта:-
v1
означает версию вашего апи; -
vapi
— cледующей частью рекомендуется указать имя вашего компонента без префиксаcom_
. (Это просто соглашение для определения эндпоинта, не обязательное для исполнения.) -
modules/:id
. Это имя контроллера нашего компонента плюс параметр:id
. В нашем случае этот шаблон сообщает Joomla, что этот эндпоинт будет соответствовать запросу только в том случае, если послеv1/vapi/modules/
следует значение, которое может использоваться какid
. Если этого значения нет, эндпоинт не соответствует, и Joomla не будет его использовать.
-
-
'module.displayModule'
: это имя контроллера API компонента и задача для выполнения, разделенные точкой. -
['id' => '(\d+)']
: Если ваш шаблон эндпоинта имеет параметры, то требуются регулярные выражения, которым должны соответствовать их значения. В нашем случае параметр id (который был определен как :id в шаблоне) должен быть целым числом, состоящим из одной или нескольких цифр (включая значение 0). -
[
И последний, но не менее важный параметр. В нём мы определили:
'component' => 'com_vapi',
'public' => true,
'format' => [
'application/json'
]
]-
'component' => 'com_vapi'
— ассоциированный компонент; -
'public' => true
Флаг публичности эндпоинта; -
'format' => ['application/json']
Здесь определяется, что наше приложение будет обрабатывать ответ в формате json. Без этого Joomla будет использовать JSON-API по умолчанию.
-
-
Не забудьте создать xml-файл плагина.
API часть компонента
Для начала убедитесь, что ваш компонент имеет базовую функциональность.
Joomla 4 позволяет создавать в компонентах API-секцию с отдельными Controller-View-Model
для JSON-вывода, аналогично site/administrator разделам.
Добавьте секцию в XML-манифест, чтобы подключить API-часть компонента.
<api> <files folder="api/"> <folder>src</folder> </files> </api>
В корневой папке установщика вашего компонента создайте папку api
, внутри которой создайте подпапку src
— здесь будут размещаться все файлы и папки API. Начнём с контроллера: создайте папку Controller
и файл ModuleController.php со следующим содержимым:
Controller — описание класса
namespace Carlitorweb\Component\Vapi\Api\Controller; defined('_JEXEC') || die; use Joomla\CMS\MVC\Factory\ApiMVCFactory; use Joomla\CMS\Application\ApiApplication; use Joomla\Input\Input; use Joomla\CMS\Language\Text; use Joomla\Component\Content\Administrator\Extension\ContentComponent; use Joomla\CMS\Component\ComponentHelper; class ModuleController extends \Joomla\CMS\MVC\Controller\BaseController { /** * @var string $default_view Will be used as default for $viewName */ protected $default_view = 'modules'; /** * @var \Joomla\Registry\Registry $moduleParams The module params to set filters in the model */ protected $moduleParams; /** * Constructor. * * @param array $config An optional associative array of configuration settings * * @param ApiMVCFactory $factory The factory. * @param ApiApplication $app The Application for the dispatcher * @param Input $input Input * * @throws \Exception */ public function __construct($config = array(), ApiMVCFactory $factory = null, ?ApiApplication $app = null, ?Input $input = null) { if (\array_key_exists('moduleParams', $config)) { $this->moduleParams = new \Joomla\Registry\Registry($config['moduleParams']); } parent::__construct($config, $factory, $app, $input); } # your methods code from here... }
Обратите внимание на пространство имен контроллера. Вы можете изменить Vapi
(используется в этом примере) и префикс Carlitorweb
на свои. Остальную часть (по соглашению) лучше оставить как есть, но можно полностью адаптировать под свои нужды.
Обратите внимание что стандартный JSON-API Joomla использует ApiController
, а в нашем случае (для простого JSON-ответа) достаточно наследовать BaseController
.
Мы объявили два свойства: для указания файла представления и для хранения параметров модуля, формирующих данные API-ответа.
Теперь давайте посмотрим на методы нашего класса:
Controller — Методы класса
/** * Set the models and execute the view * * @throws \Exception */ public function displayModule(): void { # your code here... } /** * Boot the model and set the states * * @param \Joomla\Registry\Registry $params The module params * */ protected function getMainModelForView($params): \Joomla\Component\Content\Site\Model\ArticlesModel { # your code here... } /** * Set the module params * * @param \Carlitorweb\Component\Vapi\Api\Model\ModuleModel $moduleModel * */ protected function setModuleParams($moduleModel): \Joomla\Registry\Registry { # your code here... }
Метод displayModule()
определен как задача в плагине webservices
и будет первым вызываемым методом контроллера — для тестирования добавьте в его начало код:
var_dump(__METHOD__); die;
Затем, используя API-клиент (например Postman) сделайте GET запрос к URL [yourLocalRootSiteURL]/api/index.php/v1/vapi/modules/[idOfYourModule]
. Вы получите следующий ответ:
string(73)"Carlitorweb\Component\Vapi\Api\Controller\ModuleController::displayModule"
getMainModelForView()
— метод предназначен для загрузки и подготовки главной модели которую будет использовать представление. Я написал «главной» потому что Joomla позволяет представлению взаимодействовать с более чем одной моделью.
setModuleParams()
— здесь мы будем получать параметры модуля для использования в getMainModelForView()
. Как вы заметили, метод использует параметр \Carlitorweb\Component\Vapi\Api\Model\ModuleModel
. Это экземпляр пользовательской модели, который будет у API и нам необхоимо его создать. Вот где мы получим модуль, основанный на id, переданном в качестве параметра в запрошенном URL. На этом этапе вам нужно знать, что этот id должен совпадать с уже созданным модулем.
Поскольку параметры так необходимы, давайте создадим нашу модель.
Model — описание класса
В той же папке src
в которой мы создавали папку Controller
создадим папку Model
c файлом ModuleFolder.php
и следующим кодом:
<?php defined('_JEXEC') || die; use Joomla\CMS\Factory; use Joomla\CMS\Cache\CacheControllerFactoryInterface; use Joomla\Database\ParameterType; use Joomla\CMS\Language\Text; class ModuleModel extends \Joomla\CMS\MVC\Model\BaseDatabaseModel { /** * Get the module * * @return \stdClass|null The Module object * * @throws \InvalidArgumentException If was not set the module ID * @throws \RuntimeException If the module could not be found * */ public function getModule(): ?object { /** @var \Joomla\CMS\Application\CMSApplicationInterface $app */ $app = Factory::getApplication(); $mid = $this->state->get('moduleID', 0); if ($mid === 0) { throw new \InvalidArgumentException( sprintf( 'A module ID is necessary in %s', __METHOD__ ) ); } /** @var \Joomla\Database\DatabaseInterface $db */ $db = $this->getDatabase(); $query = $this->getModuleQuery($db, $mid); // Set the query $db->setQuery($query); // Build a cache ID for the resulting data object $cacheId = 'com_vapi.moduleId' . $mid; try { /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) ->createCacheController('callback', ['defaultgroup' => 'com_modules']); $module = $cache->get(array($db, 'loadObject'), array(), md5($cacheId), false); } catch (\RuntimeException $e) { $app->getLogger()->warning( Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), array('category' => 'jerror') ); return new \stdClass(); } return $module; } /** * Get the module query * * @param int $mid The ID of the module * @param \Joomla\Database\DatabaseInterface $db * */ protected function getModuleQuery($db, $mid): \Joomla\Database\QueryInterface { $query = $db->getQuery(true); $query->select('*') ->from($db->quoteName('#__modules')) ->where( $db->quoteName('id') . ' = :moduleId' ) ->bind(':moduleId', $mid, ParameterType::INTEGER); return $query; } }
Довольно простая модель. Мы получаем id модуля для поиска в строке $this->state->get('moduleID', 0)
. Мы должны установить этот id в нашем контроллере (мы вскоре это сделаем). Затем с помощью метода $this->getModuleQuery()
строится запрос к базе данных, который мы выполним после этого. Наконец, мы используем блок try/catch
, чтобы получить объект модуля и кэшировать его.
Когда модель готова, вернемся к нашему контроллеру, и оттуда мы сможем проверить, работает ли наша модель так, как должна.
Controller — методы класса
Теперь, когда мы можем получить определенный модуль, давайте завершим метод setModuleParams()
.
/** * Set the module params * * @param \Carlitorweb\Component\Vapi\Api\Model\ModuleModel $moduleModel * */ protected function setModuleParams($moduleModel): \Joomla\Registry\Registry { // Get the module params $module = $moduleModel->getModule(); if (is_null($module)) { throw new \UnexpectedValueException( sprintf( '$module need be of type object, %s was returned in %s()', gettype($module), __FUNCTION__ ) ); } return $this->moduleParams = new \Joomla\Registry\Registry($module->params); }
Давайте посмотрим, получает ли свойство контроллера $moduleParams
ожидаемый результат. Для этого отредактируем основной метод displayModule()
:
/** * Set the models and execute the view * * @throws \Exception */ public function displayModule(): void { $moduleID = $this->input->get('id', 0, 'int'); $moduleState = new \Joomla\Registry\Registry(['moduleID' => $moduleID]); /** @var \Carlitorweb\Component\Vapi\Api\Model\ModuleModel $moduleModel */ $moduleModel = $this->factory->createModel('Module', 'Api', ['ignore_request' => true, 'state' => $moduleState]); // Set the params who will be used by the model if (empty($this->moduleParams)) { $this->setModuleParams($moduleModel); } var_dump($this->moduleParams); die; }
-
$this->input->get('id', 0, 'int')
: Значение параметра:id
в URL (который мы определили в webservoces-плагине). -
'state' => $moduleState
: Обратите внимание, что мы передали ID при загрузке модели. Мы уже видели, где использовали этот ID внутри модели.
Снова используем ваш любимый API client, делаем запрос к [yourLocalRootSiteURL]/api/index.php/v1/vapi/modules/[idOfYourModule]
. Вы увидите что-то вроде:
object(Joomla\Registry\Registry)#938 (3) { ["data":protected]=> object(stdClass)#1090 (44) { ["mode"]=> string(6) "normal" ["show_on_article_page"]=> int(1) ["count"]=> int(0) ["show_front"]=> string(4) "only" ["category_filtering_type"]=> int(1) ["catid"]=> array(5) { [0]=> int(2) [1]=> int(8) [2]=> int(9) [3]=> int(10) .....
Теперь, работая с параметрами, доработаем getMainModelForView()
.
/** * Boot the model and set the states * * @param \Joomla\Registry\Registry $params The module Articles - Category params * */ protected function getMainModelForView($params): \Joomla\Component\Content\Site\Model\ArticlesModel { $mvcContentFactory = $this->app->bootComponent('com_content')->getMVCFactory(); // Get an instance of the generic articles model /** @var \Joomla\Component\Content\Site\Model\ArticlesModel $articlesModel */ $articlesModel = $mvcContentFactory->createModel('Articles', 'Site', ['ignore_request' => true]); if (!$articlesModel) { throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); } $appParams = ComponentHelper::getComponent('com_content')->getParams(); $articlesModel->setState('params', $appParams); $articlesModel->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); /* * Set the filters based on the module params */ $articlesModel->setState('list.start', 0); $articlesModel->setState('list.limit', (int) $params->get('count', 0)); $catids = $params->get('catid'); $articlesModel->setState('filter.category_id', $catids); // Ordering $ordering = $params->get('article_ordering', 'a.ordering'); $articlesModel->setState('list.ordering', $ordering); $articlesModel->setState('list.direction', $params->get('article_ordering_direction', 'ASC')); $articlesModel->setState('filter.featured', $params->get('show_front', 'show')); $excluded_articles = $params->get('excluded_articles', ''); if ($excluded_articles) { $excluded_articles = explode("\r\n", $excluded_articles); $articlesModel->setState('filter.article_id', $excluded_articles); // Exclude $articlesModel->setState('filter.article_id.include', false); } return $articlesModel; }
Параметры, которые мы используем для моделирования наших данных, берутся из модуля Articles — Category. Это идентификатор, который мы запрашиваем с параметром :id. (Заметьте, что не все параметры были использованы)
В этом методе не так много пояснений, вы должны быть знакомы с этим кодом. Загружается ArticlesModel и определяется набор состояний модели для получения данных на основе параметров модуля.
Давайте снова отредактируем основной метод displayModule()
для проверки того, что мы получаем объект ArticlesModel
:
/** * Set the models and execute the view * * @throws \Exception */ public function displayModule(): void { $moduleID = $this->input->get('id', 0, 'int'); $moduleState = new \Joomla\Registry\Registry(['moduleID' => $moduleID]); /** @var \Carlitorweb\Component\Vapi\Api\Model\ModuleModel $moduleModel */ $moduleModel = $this->factory->createModel('Module', 'Api', ['ignore_request' => true, 'state' => $moduleState]); // Set the params who will be used by the model if(empty($this->moduleParams)) { $this->setModuleParams($moduleModel); } $mainModel = $this->getMainModelForView($this->moduleParams); var_dump($mainModel::class);die; }
Снова, используя API клиент, сделаем запрос к эндпоинту [yourLocalRootSiteURL]/api/index.php/v1/vapi/modules/[idOfYourModule]
. Вы получите:
string(49) "Joomla\Component\Content\Site\Model\ArticlesModel"
Теперь, когда всё работает, настроим представление и получим JSON-ответ. Внесём последние правки в метод displayModule()
:
/** * Set the models and execute the view * * @throws \Exception */ public function displayModule(): void { $moduleID = $this->input->get('id', 0, 'int'); $moduleState = new \Joomla\Registry\Registry(['moduleID' => $moduleID]); /** @var \Carlitorweb\Component\Vapi\Api\Model\ModuleModel $moduleModel */ $moduleModel = $this->factory->createModel('Module', 'Api', ['ignore_request' => true, 'state' => $moduleState]); // Set the params who will be used by the model if(empty($this->moduleParams)) { $this->setModuleParams($moduleModel); } $mainModel = $this->getMainModelForView($this->moduleParams); /** @var \Joomla\CMS\Document\JsonDocument $document */ $document = $this->app->getDocument(); $viewType = $document->getType(); $viewName = $this->input->get('view', $this->default_view); $viewLayout = $this->input->get('layout', 'default', 'string'); try { /** @var \Carlitorweb\Component\Vapi\Api\View\Modules\JsonView $view */ $view = $this->getView( $viewName, $viewType, '', ['moduleParams' => $this->moduleParams, 'base_path' => $this->basePath, 'layout' => $viewLayout] ); } catch (\Exception $e) { throw new \RuntimeException($e->getMessage()); } // Push the model into the view (as default) $view->setModel($mainModel, true); // Push as secondary model the Module model $view->setModel($moduleModel); $view->document = $this->app->getDocument(); $view->display(); }
-
$this->getView()
: Joomla проверит папкуView/Modules
(из$viewName
) в API-части компонента (рядом с Controller и Model); -
'moduleParams' => $this->moduleParams
: Уведомление было отправлено для просмотра параметров модуля; -
$view->setModel()
: Здесь мы устанавливаем в объект представления две модели, которые мы используем в API. Модель по-умолчанию$mainModel
содержит данные которые мы хотим вывести, а модель $moduleModel — наша пользовательская модель (опциональная). -
$view->display()
: Выполняем и отображаем вывод.
Давайте создадим наш последний файл — представление.
View — Описание и методы класса
namespace Carlitorweb\Component\Vapi\Api\View\Modules; defined('_JEXEC') || die; use \Joomla\CMS\MVC\View\JsonView as BaseJsonView; use \Joomla\CMS\MVC\View\GenericDataException; use Joomla\CMS\HTML\HTMLHelper; use \Carlitorweb\Component\Vapi\Api\Model\ModuleModel; class JsonView extends BaseJsonView { /** * @var array $fieldsToRenderList Array of allowed fields to render */ protected $fieldsToRenderList = [ 'id', 'title', 'alias', 'displayDate', 'metadesc', 'metakey', 'params', 'displayHits', 'displayCategoryTitle', 'displayAuthorName', ]; /** * @var array $display Extra params to prepare the articles */ protected $display = array(); /** * Constructor. * * @param array $config A named configuration array for object construction. * */ public function __construct($config = []) { if (\array_key_exists('moduleParams', $config)) { $params = $config['moduleParams']; // Display options $this->display['show_date'] = $params->get('show_date', 0); $this->display['show_date_field'] = $params->get('show_date_field', 'created'); $this->display['show_date_format'] = $params->get('show_date_format', 'Y-m-d H:i:s'); $this->display['show_category'] = $params->get('show_category', 0); $this->display['show_author'] = $params->get('show_author', 0); $this->display['show_hits'] = $params->get('show_hits', 0); } parent::__construct($config); } /** * Set the data who will be load */ protected function setOutput(array $items = null): void { /** @var \Joomla\CMS\MVC\Model\ListModel $mainModel */ $mainModel = $this->getModel(); /** @var \Carlitorweb\Component\Vapi\Api\Model\ModuleModel $moduleModel */ $moduleModel = $this->getModel('module'); if ($items === null) { $items = []; foreach ($mainModel->getItems() as $item) { $_item = $this->prepareItem($item, $moduleModel); $items[] = $this->getAllowedPropertiesToRender($_item); } } // Check for errors. if (\count($errors = $this->get('Errors'))) { throw new GenericDataException(implode("\n", $errors), 500); } $this->_output = $items; } /** * @param \stdClass $item The article to prepare */ protected function getAllowedPropertiesToRender($item): \stdClass { $allowedFields = new \stdClass; foreach($item as $key => $value) { if (in_array($key, $this->fieldsToRenderList, true)) { $allowedFields->$key = $value; } } return $allowedFields; } /** * Prepare item before render. * * @param object $item The model item * @param ModuleModel $moduleModel * * @return object * */ protected function prepareItem($item, $moduleModel) { $item->slug = $item->alias . ':' . $item->id; if ($this->display['show_date']) { $show_date_field = $this->display['show_date_field']; $item->displayDate = HTMLHelper::_('date', $item->$show_date_field, $this->display['show_date_format']); } $item->displayCategoryTitle = $this->display['show_category'] ? $item->category_title : ''; $item->displayHits = $this->display['show_hits'] ? $item->hits : ''; $item->displayAuthorName = $this->display['show_author'] ? $item->author : ''; return $item; } /** * Execute and display a template script. * * @param string $tpl The name of the template file to parse; automatically searches through the template paths. * * @return void * */ public function display($tpl = null) { // remove any string that could create an invalid JSON // such as PHP Notice, Warning, logs... ob_clean(); // this will clean up any previously added headers, to start clean header_remove(); $this->setOutput(); parent::display($tpl); echo $this->document->render(); } }
-
JsonView
: Это важно. Класс должен называться именно JsonView, так как Joomla ищет его по этому имени. Хотя ядро использует Joomla\CMS\MVC\View\JsonApiView, для нашего JSON-ответа достаточно наследовать Joomla\CMS\MVC\View\JsonView. -
setOutput()
: Используя метод$this->getModel()
, мы получаем доступ к моделям, которые мы установили в контроллере, одна из которых установлена по умолчанию, а другая требует имени в качестве ключа элемента в массивеJoomla\CMS\MVC\View\AbstractView::_models
. -
getAllowedPropertiesToRender()
: Если у вас есть публичный эндпоинт, вы ДОЛЖНЫ подумать над тем какие поля вы будете включать в объект JsonView. Не все поля являются подходящими для публичного отобажения; это может привести к уязвимости безопасности, известной как раскрытие информации.Например, ваш компонент форума может сохранять IP-адрес вместе с идентификатором пользователя и датой и временем создания сообщения на форуме. НЕ делайте IP-адрес и идентификатор пользователя общедоступными. Эта комбинация считается персонально идентифицируемой информацией и может привести к штрафам! Только идентификатор пользователя может быть привилегированной информацией в зависимости от контекста сайта (помните, что имена пользователей не являются привилегированной информацией, а внутренние идентификаторы пользователей являются).
-
$this->document->render():
Отправляет вывод.
Ещё раз отправьте запрос через ваш API-клиент по URL [yourLocalRootSiteURL]/api/index.php/v1/vapi/modules/[idOfYourModule]
. Вы получите:
[ { "id": 11, "title": "Typography", "alias": "typography", "metakey": "", "metadesc": "", "params": {...}, "displayDate": "2022-11-20 20:49:17", "displayCategoryTitle": "Typography", "displayHits": 0, "displayAuthorName": "Carlos Rodriguez" } ]
Готово! Базовый JSON-ответ успешно работает
ссылка на оригинал статьи https://habr.com/ru/articles/896570/
Добавить комментарий