Joomla 4 Rest API: создаем свои JSON-эндпоинты с нуля

от автора

По умолчанию Joomla отправляет ответы в формате JSON API, если запрос содержит Accept: application/json или специальный заголовок JSON API. Хотя ядро Joomla не поддерживает другие типы контента, система позволяет разработчикам добавлять дополнительные форматы для ответов.

Цели материала:

  • Получить JSON ответ от API Joomla;

  • Создать необходимый плагин группы webservices и API-часть компонента;

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

Что не является целью?​

  • Обучение созданию расширений. Данное руководство предполагает, что вы уже умеете создавать расширения для Joomla 4. Для работы API потребуются плагин и компонент, но компонент может быть минимальным — без модели (Model), с простой административной частью.

Basic Component Dashboard View

Представление панели управления компонента

Административная панель компонента

Базовая админка компонента необходима, так как 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/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *