Приветствую вас, уважаемые хабравчане! Поскольку я занимаюсь разработкой на e-commerce платформе Magento с 2013 года, то набравшись храбрости и посчитав, что в этой области я могу себя назвать, как минимум, уверенным разработчиком, решил написать свою первую статью на хабре именно об этой системе. И начну я с реализации REST API в Magento 2. Здесь из коробки есть функционал для обработки запросов и я постараюсь продемонстрировать его на примере простого модуля. Данная статья больше рассчитана на тех, кто уже работал с Маджентой. И так, кто заинтересовался, прошу под кат.
С фантазией у меня совсем плохо, поэтому пример я придумал следующий: представим, что нам надо реализовать блог, писать статьи могут только юзеры из админки. Периодически к нам стучится какая-нибудь CRM и выгружает к себе эти статьи (зачем непонятно, но так мы оправдаем использование REST API). Для упрощения модуля я специально опустил реализацию отображения статей на фронтенде и в админке (можете реализовать самостоятельно, рекомендую хорошую статью по гридам). Здесь будет затронут только функционал обработки запросов.
Для начала создаем структуру модуля, назовем его AlexPoletaev_Blog (отсутствие фантазии еще никуда не делось). Модуль размещаем в директории app/code.
Эти два файла — необходимый минимум для модуля.
Если все делать по фэншую, то нам необходимо создать service контракты (что это такое в пределах мадженты и как работает почитать можно тут и тут), чем мы и займемся:
Разберем немного подробнее эти два интерфейса. Интерфейс PostInterface отображает таблицу со статьями из нашего блога. Таблицу создадим ниже. Каждая колонка из базы данных должна иметь свой геттер и сеттер в этом интерфейсе, почему это важно — выясним позже. Интерфейс PostRepositoryInterface предоставляет стандартный набор методов для взаимодействия с базой данных и хранения в кэше загруженных сущностей. Эти же методы используются и для API. Еще одно важное замечание, наличие корректных PHPDocs в этих интерфейсах обязательно, так как Маджента, обрабатывая REST запрос, использует рефлексию для определения входящих параметров и возвращаемых значений в методах.
С помощью install скрипта создаем таблицу, где будут храниться посты из блога:
В таблице будут следующие колонки (не забываем, у нас все максимально упрощено):
Теперь необходимо создать стандартный для Мадженты набор Model, ResourceModel и Collection классов. Для чего эти классы я расписывать не буду, эта тема обширна и выходит за рамки этой статьи, кому интересно, может погуглить самостоятельно. Если в двух словах, то эти классы нужны для манипуляций с сущностями (статьями) из базы данных. Советую еще почитать про паттерны Domain Model, Repository и Service Layer.
Внимательный читатель заметит, что наша модель как раз реализует созданный ранее интерфейс и все его геттеры и сеттеры.
Заодно реализуем репозиторий и его методы:
Метод
Чтобы проще было тестировать наш модуль, создадим два консольных скрипта, которые добавляют и удаляют тестовые данные из таблицы:
Основной «фишкой» Magento 2 является повсеместное использование собственной реализации Dependency Injection. Чтобы Маджента знала какому интерфейсу какая соответствует реализация, нам необходимо указать эти зависимости в файле di.xml. Заодно и зарегистрируем в этом файле только что созданные консольные скрипты:
Теперь регистрируем роуты для REST API, делается это в файле webapi.xml:
Здесь мы указываем Мадженте какой интерфейс и какой метод из этого интерфейса использовать при запросе на определенный URL и с определенным http методом (POST, GET и тд.). Также, в целях упрощения, используется
Все дальнейшие действия подразумевают, что у вас включен developer mode. Это позволяет избежать лишних манипуляций с деплоем статик контента и DI компиляцией.
Регистрируем наш новый модуль, запускаем команду:
Проверяем, что создалась новая таблица alex_poletaev_blog_post.
Далее загружаем тестовые данные, используя наш кастомный скрипт:
Параметр admin в данном скрипте это username из таблицы admin_user (у вас он может отличаться), одним словом, юзер из админки, который пропишется в колонку author_id.
Теперь можно приступать к тестированию. Для тестов я использовал Magento 2.2.4, домен
Один из способов потестить REST API — это открыть
Получить статью с id = 2
Ответ:
Получить список статей, у которых author_id = 2
Ответ:
Удалить статью с id = 3
Ответ:
Сохраняем новую статью
Ответ:
Обратите внимание, что для запроса с http методом POST обязательно надо передать ключ post, что на самом деле соответствует входному параметру ($post) для метода
Для тех, кому интересно, что происходит во время запроса и как Маджента его обрабатывает, ниже я приведу несколько ссылок на методы с моими комментариями. Если что-то не работает, то эти методы надо дебажить в первую очередь.
Контроллер, отвечающий за обработку запроса
\Magento\Webapi\Controller\Rest::dispatch()
Далее вызывается
\Magento\Webapi\Controller\Rest::processApiRequest()
Внутри
\Magento\Webapi\Controller\Rest\InputParamsResolver::resolve()
\Magento\Webapi\Controller\Rest\Router::match() — определяется конкретный роут (внутри через
\Magento\Framework\Webapi\ServiceInputProcessor::process()
— используется
\Magento\Framework\Webapi\ServiceInputProcessor::convertValue() — несколько вариантов конвертирования массива в DataObject или в массив из DataObject
\Magento\Framework\Webapi\ServiceInputProcessor::_createFromArray() — непосредственно конвертация, где через рефлексию проверяется наличие геттеров и сеттеров (помните, я говорил выше, что мы к ним вернемся?) и то, что они имеют публичную область видимости. Далее объект заполняется данными через сеттеры.
В самом конце, в методе
\Magento\Webapi\Controller\Rest::processApiRequest(), через
Репозиторий модуля на гитхабе
Установить можно двумя способами:
1) Через composer. Для этого добавьте следующий объект к массиву
Далее набрать в терминале следующую команду:
2) Скачать файлы модуля и вручную скопировать их в директорию
Независимо от того, какой способ вы выбрали, в конце надо запустить апгрейд:
Надеюсь, эта статья окажется кому-нибудь полезной. Если есть какие-либо замечания, пожелания или вопросы, то добро пожаловать в комментарии. Спасибо за внимание.
Завязка
С фантазией у меня совсем плохо, поэтому пример я придумал следующий: представим, что нам надо реализовать блог, писать статьи могут только юзеры из админки. Периодически к нам стучится какая-нибудь CRM и выгружает к себе эти статьи (зачем непонятно, но так мы оправдаем использование REST API). Для упрощения модуля я специально опустил реализацию отображения статей на фронтенде и в админке (можете реализовать самостоятельно, рекомендую хорошую статью по гридам). Здесь будет затронут только функционал обработки запросов.
Развитие действия
Для начала создаем структуру модуля, назовем его AlexPoletaev_Blog (отсутствие фантазии еще никуда не делось). Модуль размещаем в директории app/code.
AlexPoletaev/Blog/etc/module.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="AlexPoletaev_Blog" setup_version="1.0.0"/> </config>
AlexPoletaev/Blog/registration.php
<?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'AlexPoletaev_Blog', __DIR__ );
Эти два файла — необходимый минимум для модуля.
Если все делать по фэншую, то нам необходимо создать service контракты (что это такое в пределах мадженты и как работает почитать можно тут и тут), чем мы и займемся:
AlexPoletaev/Blog/Api/Data/PostInterface.php
<?php namespace AlexPoletaev\Blog\Api\Data; /** * Interface PostInterface * @package AlexPoletaev\Api\Data * @api */ interface PostInterface { /**#@+ * Constants * @var string */ const ID = 'id'; const AUTHOR_ID = 'author_id'; const TITLE = 'title'; const CONTENT = 'content'; const CREATED_AT = 'created_at'; const UPDATED_AT = 'updated_at'; /**#@-*/ /** * @return int */ public function getId(); /** * @param int $id * @return $this */ public function setId($id); /** * @return int */ public function getAuthorId(); /** * @param int $authorId * @return $this */ public function setAuthorId($authorId); /** * @return string */ public function getTitle(); /** * @param string $title * @return $this */ public function setTitle(string $title); /** * @return string */ public function getContent(); /** * @param string $content * @return $this */ public function setContent(string $content); /** * @return string */ public function getCreatedAt(); /** * @param string $createdAt * @return $this */ public function setCreatedAt(string $createdAt); /** * @return string */ public function getUpdatedAt(); /** * @param string $updatedAt * @return $this */ public function setUpdatedAt(string $updatedAt); }
AlexPoletaev/Blog/Api/PostRepositoryInterface.php
<?php namespace AlexPoletaev\Blog\Api; use AlexPoletaev\Blog\Api\Data\PostInterface; use Magento\Framework\Api\SearchCriteriaInterface; /** * Interface PostRepositoryInterface * @package AlexPoletaev\Api * @api */ interface PostRepositoryInterface { /** * @param int $id * @return \AlexPoletaev\Blog\Api\Data\PostInterface */ public function get(int $id); /** * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \AlexPoletaev\Blog\Api\Data\PostSearchResultInterface */ public function getList(SearchCriteriaInterface $searchCriteria); /** * @param \AlexPoletaev\Blog\Api\Data\PostInterface $post * @return \AlexPoletaev\Blog\Api\Data\PostInterface */ public function save(PostInterface $post); /** * @param \AlexPoletaev\Blog\Api\Data\PostInterface $post * @return bool */ public function delete(PostInterface $post); /** * @param int $id * @return bool */ public function deleteById(int $id); }
Разберем немного подробнее эти два интерфейса. Интерфейс PostInterface отображает таблицу со статьями из нашего блога. Таблицу создадим ниже. Каждая колонка из базы данных должна иметь свой геттер и сеттер в этом интерфейсе, почему это важно — выясним позже. Интерфейс PostRepositoryInterface предоставляет стандартный набор методов для взаимодействия с базой данных и хранения в кэше загруженных сущностей. Эти же методы используются и для API. Еще одно важное замечание, наличие корректных PHPDocs в этих интерфейсах обязательно, так как Маджента, обрабатывая REST запрос, использует рефлексию для определения входящих параметров и возвращаемых значений в методах.
С помощью install скрипта создаем таблицу, где будут храниться посты из блога:
AlexPoletaev/Blog/Setup/InstallSchema.php
<?php namespace AlexPoletaev\Blog\Setup; use AlexPoletaev\Blog\Api\Data\PostInterface; use AlexPoletaev\Blog\Model\ResourceModel\Post as PostResource; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Ddl\Table; use Magento\Framework\Setup\InstallSchemaInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; use Magento\Security\Setup\InstallSchema as SecurityInstallSchema; /** * Class InstallSchema * @package AlexPoletaev\Blog\Setup */ class InstallSchema implements InstallSchemaInterface { /** * Installs DB schema for a module * * @param SchemaSetupInterface $setup * @param ModuleContextInterface $context * @return void */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); $table = $setup->getConnection() ->newTable( $setup->getTable(PostResource::TABLE_NAME) ) ->addColumn( PostInterface::ID, Table::TYPE_INTEGER, null, ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], 'Post ID' ) ->addColumn( PostInterface::AUTHOR_ID, Table::TYPE_INTEGER, null, ['unsigned' => true, 'nullable' => true,], 'Author ID' ) ->addColumn( PostInterface::TITLE, Table::TYPE_TEXT, 255, [], 'Title' ) ->addColumn( PostInterface::CONTENT, Table::TYPE_TEXT, null, [], 'Content' ) ->addColumn( 'created_at', Table::TYPE_TIMESTAMP, null, ['nullable' => false, 'default' => Table::TIMESTAMP_INIT], 'Creation Time' ) ->addColumn( 'updated_at', Table::TYPE_TIMESTAMP, null, ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE], 'Update Time' ) ->addForeignKey( $setup->getFkName( PostResource::TABLE_NAME, PostInterface::AUTHOR_ID, SecurityInstallSchema::ADMIN_USER_DB_TABLE_NAME, 'user_id' ), PostInterface::AUTHOR_ID, $setup->getTable(SecurityInstallSchema::ADMIN_USER_DB_TABLE_NAME), 'user_id', Table::ACTION_SET_NULL ) ->addIndex( $setup->getIdxName( PostResource::TABLE_NAME, [PostInterface::AUTHOR_ID], AdapterInterface::INDEX_TYPE_INDEX ), [PostInterface::AUTHOR_ID], ['type' => AdapterInterface::INDEX_TYPE_INDEX] ) ->setComment('Posts') ; $setup->getConnection()->createTable($table); $setup->endSetup(); } }
В таблице будут следующие колонки (не забываем, у нас все максимально упрощено):
- id — автоинкремент
- author_id — идентификатор юзера админки (внешний ключ на поле user_id из таблицы admin_user)
- title — заголовок
- content — текст статьи
- created_at — дата создания
- updated_at — дата редактирования
Теперь необходимо создать стандартный для Мадженты набор Model, ResourceModel и Collection классов. Для чего эти классы я расписывать не буду, эта тема обширна и выходит за рамки этой статьи, кому интересно, может погуглить самостоятельно. Если в двух словах, то эти классы нужны для манипуляций с сущностями (статьями) из базы данных. Советую еще почитать про паттерны Domain Model, Repository и Service Layer.
AlexPoletaev/Blog/Model/Post.php
<?php namespace AlexPoletaev\Blog\Model; use AlexPoletaev\Blog\Api\Data\PostInterface; use AlexPoletaev\Blog\Model\ResourceModel\Post as PostResource; use Magento\Framework\Model\AbstractModel; /** * Class Post * @package AlexPoletaev\Blog\Model */ class Post extends AbstractModel implements PostInterface { /** * @var string */ protected $_idFieldName = PostInterface::ID; //@codingStandardsIgnoreLine /** * @inheritdoc */ protected function _construct() //@codingStandardsIgnoreLine { $this->_init(PostResource::class); } /** * @return int */ public function getAuthorId() { return $this->getData(PostInterface::AUTHOR_ID); } /** * @param int $authorId * @return $this */ public function setAuthorId($authorId) { $this->setData(PostInterface::AUTHOR_ID, $authorId); return $this; } /** * @return string */ public function getTitle() { return $this->getData(PostInterface::TITLE); } /** * @param string $title * @return $this */ public function setTitle(string $title) { $this->setData(PostInterface::TITLE, $title); return $this; } /** * @return string */ public function getContent() { return $this->getData(PostInterface::CONTENT); } /** * @param string $content * @return $this */ public function setContent(string $content) { $this->setData(PostInterface::CONTENT, $content); return $this; } /** * @return string */ public function getCreatedAt() { return $this->getData(PostInterface::CREATED_AT); } /** * @param string $createdAt * @return $this */ public function setCreatedAt(string $createdAt) { $this->setData(PostInterface::CREATED_AT, $createdAt); return $this; } /** * @return string */ public function getUpdatedAt() { return $this->getData(PostInterface::UPDATED_AT); } /** * @param string $updatedAt * @return $this */ public function setUpdatedAt(string $updatedAt) { $this->setData(PostInterface::UPDATED_AT, $updatedAt); return $this; } }
AlexPoletaev/Blog/Model/ResourceModel/Post.php
<?php namespace AlexPoletaev\Blog\Model\ResourceModel; use AlexPoletaev\Blog\Api\Data\PostInterface; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; /** * Class Post * @package AlexPoletaev\Blog\Model\ResourceModel */ class Post extends AbstractDb { /** * @var string */ const TABLE_NAME = 'alex_poletaev_blog_post'; /** * Resource initialization * * @return void */ protected function _construct() //@codingStandardsIgnoreLine { $this->_init(self::TABLE_NAME, PostInterface::ID); } }
AlexPoletaev/Blog/Model/ResourceModel/Post/Collection.php
<?php namespace AlexPoletaev\Blog\Model\ResourceModel\Post; use AlexPoletaev\Blog\Model\Post; use AlexPoletaev\Blog\Model\ResourceModel\Post as PostResource; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; /** * Class Collection * @package AlexPoletaev\Blog\Model\ResourceModel\Post */ class Collection extends AbstractCollection { /** * @inheritdoc */ protected function _construct() //@codingStandardsIgnoreLine { $this->_init(Post::class, PostResource::class); } }
Внимательный читатель заметит, что наша модель как раз реализует созданный ранее интерфейс и все его геттеры и сеттеры.
Заодно реализуем репозиторий и его методы:
AlexPoletaev/Blog/Model/PostRepository.php
<?php namespace AlexPoletaev\Blog\Model; use AlexPoletaev\Blog\Api\Data\PostInterface; use AlexPoletaev\Blog\Api\Data\PostSearchResultInterface; use AlexPoletaev\Blog\Api\Data\PostSearchResultInterfaceFactory; use AlexPoletaev\Blog\Api\PostRepositoryInterface; use AlexPoletaev\Blog\Model\ResourceModel\Post as PostResource; use AlexPoletaev\Blog\Model\ResourceModel\Post\Collection as PostCollection; use AlexPoletaev\Blog\Model\ResourceModel\Post\CollectionFactory as PostCollectionFactory; use AlexPoletaev\Blog\Model\PostFactory; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; /** * Class PostRepository * @package AlexPoletaev\Blog\Model */ class PostRepository implements PostRepositoryInterface { /** * @var array */ private $registry = []; /** * @var PostResource */ private $postResource; /** * @var PostFactory */ private $postFactory; /** * @var PostCollectionFactory */ private $postCollectionFactory; /** * @var PostSearchResultInterfaceFactory */ private $postSearchResultFactory; /** * @param PostResource $postResource * @param PostFactory $postFactory * @param PostCollectionFactory $postCollectionFactory * @param PostSearchResultInterfaceFactory $postSearchResultFactory */ public function __construct( PostResource $postResource, PostFactory $postFactory, PostCollectionFactory $postCollectionFactory, PostSearchResultInterfaceFactory $postSearchResultFactory ) { $this->postResource = $postResource; $this->postFactory = $postFactory; $this->postCollectionFactory = $postCollectionFactory; $this->postSearchResultFactory = $postSearchResultFactory; } /** * @param int $id * @return PostInterface * @throws NoSuchEntityException */ public function get(int $id) { if (!array_key_exists($id, $this->registry)) { $post = $this->postFactory->create(); $this->postResource->load($post, $id); if (!$post->getId()) { throw new NoSuchEntityException(__('Requested post does not exist')); } $this->registry[$id] = $post; } return $this->registry[$id]; } /** * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \AlexPoletaev\Blog\Api\Data\PostSearchResultInterface */ public function getList(SearchCriteriaInterface $searchCriteria) { /** @var PostCollection $collection */ $collection = $this->postCollectionFactory->create(); foreach ($searchCriteria->getFilterGroups() as $filterGroup) { foreach ($filterGroup->getFilters() as $filter) { $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; $collection->addFieldToFilter($filter->getField(), [$condition => $filter->getValue()]); } } /** @var PostSearchResultInterface $searchResult */ $searchResult = $this->postSearchResultFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); $searchResult->setTotalCount($collection->getSize()); return $searchResult; } /** * @param \AlexPoletaev\Blog\Api\Data\PostInterface $post * @return PostInterface * @throws StateException */ public function save(PostInterface $post) { try { /** @var Post $post */ $this->postResource->save($post); $this->registry[$post->getId()] = $this->get($post->getId()); } catch (\Exception $exception) { throw new StateException(__('Unable to save post #%1', $post->getId())); } return $this->registry[$post->getId()]; } /** * @param \AlexPoletaev\Blog\Api\Data\PostInterface $post * @return bool * @throws StateException */ public function delete(PostInterface $post) { try { /** @var Post $post */ $this->postResource->delete($post); unset($this->registry[$post->getId()]); } catch (\Exception $e) { throw new StateException(__('Unable to remove post #%1', $post->getId())); } return true; } /** * @param int $id * @return bool */ public function deleteById(int $id) { return $this->delete($this->get($id)); } }
Метод
\AlexPoletaev\Blog\Model\PostRepository::getList() должен возвращать данные определенного формата, поэтому нам понадобится еще и такой интерфейс:AlexPoletaev/Blog/Api/Data/PostSearchResultInterface.php
<?php namespace AlexPoletaev\Blog\Api\Data; use Magento\Framework\Api\SearchResultsInterface; /** * Interface PostSearchResultInterface * @package AlexPoletaev\Blog\Api\Data */ interface PostSearchResultInterface extends SearchResultsInterface { /** * @return \AlexPoletaev\Blog\Api\Data\PostInterface[] */ public function getItems(); /** * @param \AlexPoletaev\Blog\Api\Data\PostInterface[] $items * @return $this */ public function setItems(array $items); }
Чтобы проще было тестировать наш модуль, создадим два консольных скрипта, которые добавляют и удаляют тестовые данные из таблицы:
AlexPoletaev/Blog/Console/Command/DeploySampleDataCommand.php
<?php namespace AlexPoletaev\Blog\Console\Command; use AlexPoletaev\Blog\Api\PostRepositoryInterface; use AlexPoletaev\Blog\Model\Post; use AlexPoletaev\Blog\Model\PostFactory; use Magento\User\Api\Data\UserInterface; use Magento\User\Model\User; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Class DeploySampleDataCommand * @package AlexPoletaev\Blog\Console\Command */ class DeploySampleDataCommand extends Command { /**#@+ * @var string */ const ARGUMENT_USERNAME = 'username'; const ARGUMENT_NUMBER_OF_RECORDS = 'number_of_records'; /**#@-*/ /** * @var PostFactory */ private $postFactory; /** * @var PostRepositoryInterface */ private $postRepository; /** * @var UserInterface */ private $user; /** * @param PostFactory $postFactory * @param PostRepositoryInterface $postRepository * @param UserInterface $user */ public function __construct( PostFactory $postFactory, PostRepositoryInterface $postRepository, UserInterface $user ) { parent::__construct(); $this->postFactory = $postFactory; $this->postRepository = $postRepository; $this->user = $user; } /** * @inheritdoc */ protected function configure() { $this->setName('alex_poletaev:blog:deploy_sample_data') ->setDescription('Blog: deploy sample data') ->setDefinition([ new InputArgument( self::ARGUMENT_USERNAME, InputArgument::REQUIRED, 'Username' ), new InputArgument( self::ARGUMENT_NUMBER_OF_RECORDS, InputArgument::OPTIONAL, 'Number of test records' ), ]) ; parent::configure(); } /** * @inheritdoc */ protected function execute(InputInterface $input, OutputInterface $output) { $username = $input->getArgument(self::ARGUMENT_USERNAME); /** @var User $user */ $user = $this->user->loadByUsername($username); if (!$user->getId() && $output->getVerbosity() > 1) { $output->writeln('<error>User is not found</error>'); return null; } $records = $input->getArgument(self::ARGUMENT_NUMBER_OF_RECORDS) ?: 3; for ($i = 1; $i <= (int)$records; $i++) { /** @var Post $post */ $post = $this->postFactory->create(); $post->setAuthorId($user->getId()); $post->setTitle('test title ' . $i); $post->setContent('test content ' . $i); $this->postRepository->save($post); if ($output->getVerbosity() > 1) { $output->writeln('<info>Post with the ID #' . $post->getId() . ' has been created.</info>'); } } } }
AlexPoletaev/Blog/Console/Command/RemoveSampleDataCommand.php
<?php namespace AlexPoletaev\Blog\Console\Command; use AlexPoletaev\Blog\Model\ResourceModel\Post as PostResource; use Magento\Framework\App\ResourceConnection; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Class RemoveSampleDataCommand * @package AlexPoletaev\Blog\Console\Command */ class RemoveSampleDataCommand extends Command { /** * @var ResourceConnection */ private $resourceConnection; /** * @param ResourceConnection $resourceConnection */ public function __construct( ResourceConnection $resourceConnection ) { parent::__construct(); $this->resourceConnection = $resourceConnection; } /** * @inheritdoc */ protected function configure() { $this->setName('alex_poletaev:blog:remove_sample_data') ->setDescription('Blog: remove sample data') ; parent::configure(); } /** * @inheritdoc */ protected function execute(InputInterface $input, OutputInterface $output) { $connection = $this->resourceConnection->getConnection(); $connection->truncateTable($connection->getTableName(PostResource::TABLE_NAME)); if ($output->getVerbosity() > 1) { $output->writeln('<info>Sample data has been successfully removed.</info>'); } } }
Основной «фишкой» Magento 2 является повсеместное использование собственной реализации Dependency Injection. Чтобы Маджента знала какому интерфейсу какая соответствует реализация, нам необходимо указать эти зависимости в файле di.xml. Заодно и зарегистрируем в этом файле только что созданные консольные скрипты:
AlexPoletaev/Blog/etc/di.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="AlexPoletaev\Blog\Api\Data\PostInterface" type="AlexPoletaev\Blog\Model\Post"/> <preference for="AlexPoletaev\Blog\Api\PostRepositoryInterface" type="AlexPoletaev\Blog\Model\PostRepository"/> <preference for="AlexPoletaev\Blog\Api\Data\PostSearchResultInterface" type="Magento\Framework\Api\SearchResults" /> <type name="Magento\Framework\Console\CommandList"> <arguments> <argument name="commands" xsi:type="array"> <item name="deploy_sample_data" xsi:type="object">AlexPoletaev\Blog\Console\Command\DeploySampleDataCommand</item> <item name="remove_sample_data" xsi:type="object">AlexPoletaev\Blog\Console\Command\RemoveSampleDataCommand</item> </argument> </arguments> </type> </config>
Теперь регистрируем роуты для REST API, делается это в файле webapi.xml:
AlexPoletaev/Blog/etc/webapi.xml
<?xml version="1.0"?> <routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> <route url="/V1/blog/posts" method="POST"> <service class="AlexPoletaev\Blog\Api\PostRepositoryInterface" method="save"/> <resources> <resource ref="anonymous"/> </resources> </route> <route url="/V1/blog/posts/:id" method="DELETE"> <service class="AlexPoletaev\Blog\Api\PostRepositoryInterface" method="deleteById"/> <resources> <resource ref="anonymous"/> </resources> </route> <route url="/V1/blog/posts/:id" method="GET"> <service class="AlexPoletaev\Blog\Api\PostRepositoryInterface" method="get"/> <resources> <resource ref="anonymous"/> </resources> </route> <route url="/V1/blog/posts" method="GET"> <service class="AlexPoletaev\Blog\Api\PostRepositoryInterface" method="getList"/> <resources> <resource ref="anonymous"/> </resources> </route> </routes>
Здесь мы указываем Мадженте какой интерфейс и какой метод из этого интерфейса использовать при запросе на определенный URL и с определенным http методом (POST, GET и тд.). Также, в целях упрощения, используется
anonymous resource, что позволяет абсолютно любому человеку постучаться в наш API, в противном случае надо настраивать права доступа (ACL).Кульминация
Все дальнейшие действия подразумевают, что у вас включен developer mode. Это позволяет избежать лишних манипуляций с деплоем статик контента и DI компиляцией.
Регистрируем наш новый модуль, запускаем команду:
php bin/magento setup:upgrade.Проверяем, что создалась новая таблица alex_poletaev_blog_post.
Далее загружаем тестовые данные, используя наш кастомный скрипт:
php bin/magento -v alex_poletaev:blog:deploy_sample_data admin
Параметр admin в данном скрипте это username из таблицы admin_user (у вас он может отличаться), одним словом, юзер из админки, который пропишется в колонку author_id.
Теперь можно приступать к тестированию. Для тестов я использовал Magento 2.2.4, домен
http://m224ce.local/.Один из способов потестить REST API — это открыть
http://m224ce.local/swagger и воспользоваться функционалом swagger-а, но следует помнить, метод getList там работает некорректно. Я также проверил все методы с помощью curl, примеры:Получить статью с id = 2
curl -X GET -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/2"
Ответ:
{"id":2,"author_id":1,"title":"test title 2","content":"test content 2","created_at":"2018-06-06 21:35:54","updated_at":"2018-06-06 21:35:54"}
Получить список статей, у которых author_id = 2
curl -g -X GET -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts?searchCriteria[filterGroups][0][filters][0][field]=author_id&searchCriteria[filterGroups][0][filters][0][value]=1&searchCriteria[filterGroups][0][filters][0][conditionType]=eq"
Ответ:
{"items":[{"id":1,"author_id":1,"title":"test title 1","content":"test content 1","created_at":"2018-06-06 21:35:54","updated_at":"2018-06-06 21:35:54"},{"id":2,"author_id":1,"title":"test title 2","content":"test content 2","created_at":"2018-06-06 21:35:54","updated_at":"2018-06-06 21:35:54"},{"id":3,"author_id":1,"title":"test title 3","content":"test content 3","created_at":"2018-06-06 21:35:54","updated_at":"2018-06-06 21:35:54"}],"search_criteria":{"filter_groups":[{"filters":[{"field":"author_id","value":"1","condition_type":"eq"}]}]},"total_count":3}
Удалить статью с id = 3
curl -X DELETE -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/3"
Ответ:
true
Сохраняем новую статью
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{"post": {"author_id": 1, "title": "test title 4", "content": "test content 4"}}' "http://m224ce.local/rest/all/V1/blog/posts"
Ответ:
{"id":4,"author_id":1,"title":"test title 4","content":"test content 4","created_at":"2018-06-06 21:44:24","updated_at":"2018-06-06 21:44:24"}
Обратите внимание, что для запроса с http методом POST обязательно надо передать ключ post, что на самом деле соответствует входному параметру ($post) для метода
\AlexPoletaev\Blog\Api\PostRepositoryInterface::save()
Развязка
Для тех, кому интересно, что происходит во время запроса и как Маджента его обрабатывает, ниже я приведу несколько ссылок на методы с моими комментариями. Если что-то не работает, то эти методы надо дебажить в первую очередь.
Контроллер, отвечающий за обработку запроса
\Magento\Webapi\Controller\Rest::dispatch()
Далее вызывается
\Magento\Webapi\Controller\Rest::processApiRequest()
Внутри
processApiRequest вызывается много других методов, но следующий самый важный\Magento\Webapi\Controller\Rest\InputParamsResolver::resolve()
\Magento\Webapi\Controller\Rest\Router::match() — определяется конкретный роут (внутри через
\Magento\Webapi\Model\Rest\Config::getRestRoutes() метод по данным из реквеста вытаскиваются все подходящие роуты). Объект роута содержит все необходимые данные, чтобы обработать запрос — класс, метод, права доступа и т.д.\Magento\Framework\Webapi\ServiceInputProcessor::process()
— используется
\Magento\Framework\Reflection\MethodsMap::getMethodParams(), где через рефлексию вытаскиваются параметры метода\Magento\Framework\Webapi\ServiceInputProcessor::convertValue() — несколько вариантов конвертирования массива в DataObject или в массив из DataObject
\Magento\Framework\Webapi\ServiceInputProcessor::_createFromArray() — непосредственно конвертация, где через рефлексию проверяется наличие геттеров и сеттеров (помните, я говорил выше, что мы к ним вернемся?) и то, что они имеют публичную область видимости. Далее объект заполняется данными через сеттеры.
В самом конце, в методе
\Magento\Webapi\Controller\Rest::processApiRequest(), через
call_user_func_array вызывается метод объекта репозитория.Эпилог
Репозиторий модуля на гитхабе
Установить можно двумя способами:
1) Через composer. Для этого добавьте следующий объект к массиву
repositories в файле composer.json{ "type": "git", "url": "https://github.com/alexpoletaev/magento2-blog-demo" }
Далее набрать в терминале следующую команду:
composer require alexpoletaev/magento2-blog-demo:dev-master
2) Скачать файлы модуля и вручную скопировать их в директорию
app/code/AlexPoletaev/BlogНезависимо от того, какой способ вы выбрали, в конце надо запустить апгрейд:
php bin/magento setup:upgrade
Надеюсь, эта статья окажется кому-нибудь полезной. Если есть какие-либо замечания, пожелания или вопросы, то добро пожаловать в комментарии. Спасибо за внимание.
