Greetings to you, dear habravchane! Since I have been developing on the e-commerce platform Magento since 2013, having gained courage and considering that in this area I can call myself, at least, a confident developer, I decided to write my first article on Habré about this system. And I will start with the implementation of the REST API in Magento 2. Out of the box there is a functionality for processing requests and I will try to demonstrate it using the example of a simple module. This article is more intended for those who have already worked with Magenta. And so, who is interested, I ask under the cat.
Outset
I have a very bad imagination, so I came up with the following example: let's imagine that we need to implement a blog, only users from the admin panel can write articles. Periodically, some CRM knocks on us and unloads these articles to itself (why it is not clear, but in this way we will justify using the REST API). To simplify the module, I specifically omitted the implementation of displaying articles on the front end and in the admin panel (you can implement it yourself, I recommend a good
article on grids). Only the query processing functionality will be affected here.
Action development
First we create the
structure of the module, let's call it
AlexPoletaev_Blog (the lack of fantasy has not gone away yet). The module is placed in the
app / code directory.
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__ );
These two files are the minimum required for the module.
If we do everything according to Feng Shui, then we need to create service contracts (what is it within the magenta and how it works, you can read
here and
here ), which we will do:
AlexPoletaev / Blog / Api / Data / PostInterface.php <?php namespace AlexPoletaev\Blog\Api\Data; interface PostInterface { const ID = 'id'; const AUTHOR_ID = 'author_id'; const TITLE = 'title'; const CONTENT = 'content'; const CREATED_AT = 'created_at'; const UPDATED_AT = 'updated_at'; public function getId(); public function setId($id); public function getAuthorId(); public function setAuthorId($authorId); public function getTitle(); public function setTitle(string $title); public function getContent(); public function setContent(string $content); public function getCreatedAt(); public function setCreatedAt(string $createdAt); public function getUpdatedAt(); 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 { public function get(int $id); public function getList(SearchCriteriaInterface $searchCriteria); public function save(PostInterface $post); public function delete(PostInterface $post); public function deleteById(int $id); }
Let us examine these two interfaces in more detail. Interface
PostInterface displays a table with articles from our blog. Create a table below. Each column from the database should have its own getter and setter in this interface, why this is important - find out later. The
PostRepositoryInterface interface provides a standard set of methods for interacting with the database and storing the loaded entities in the cache. The same methods are used for the API. Another important note, the presence of valid PHPDocs in these interfaces is
mandatory , since the Magenta, processing the REST request, uses reflection to determine the input parameters and return values in the methods.
Using the
install script, create a table where posts from the blog will be stored:
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 implements InstallSchemaInterface { 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(); } }
The table will contain the following columns (do not forget, everything is as simple as possible):
- id - auto increment
- author_id - user admin id (foreign key on user_id field from admin_user table)
- title - title
- content - the text of the article
- created_at - creation date
- updated_at - edit date
Now it is necessary to create a standard set of
Model ,
ResourceModel and
Collection classes for Magenta. For what these classes, I will not paint, this topic is extensive and is beyond the scope of this article, who are interested, can google it yourself. In a nutshell, these classes are needed for manipulating entities (articles) from the database. I advise you to read about the Domain Model, Repository and Service Layer patterns.
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 extends AbstractModel implements PostInterface { protected $_idFieldName = PostInterface::ID;
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 extends AbstractDb { const TABLE_NAME = 'alex_poletaev_blog_post'; 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 extends AbstractCollection { protected function _construct() //@codingStandardsIgnoreLine { $this->_init(Post::class, PostResource::class); } }
The attentive reader will notice that our model implements the interface created earlier and all its getters and setters.
At the same time we implement the repository and its methods:
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 implements PostRepositoryInterface { private $registry = []; private $postResource; private $postFactory; private $postCollectionFactory; private $postSearchResultFactory; public function __construct( PostResource $postResource, PostFactory $postFactory, PostCollectionFactory $postCollectionFactory, PostSearchResultInterfaceFactory $postSearchResultFactory ) { $this->postResource = $postResource; $this->postFactory = $postFactory; $this->postCollectionFactory = $postCollectionFactory; $this->postSearchResultFactory = $postSearchResultFactory; } 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]; } public function getList(SearchCriteriaInterface $searchCriteria) { $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()]); } } $searchResult = $this->postSearchResultFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); $searchResult->setTotalCount($collection->getSize()); return $searchResult; } public function save(PostInterface $post) { try { $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()]; } public function delete(PostInterface $post) { try { $this->postResource->delete($post); unset($this->registry[$post->getId()]); } catch (\Exception $e) { throw new StateException(__('Unable to remove post #%1', $post->getId())); } return true; } public function deleteById(int $id) { return $this->delete($this->get($id)); } }
The
\AlexPoletaev\Blog\Model\PostRepository::getList()
should return data of a specific format, so we will need another interface:
AlexPoletaev / Blog / Api / Data / PostSearchResultInterface.php <?php namespace AlexPoletaev\Blog\Api\Data; use Magento\Framework\Api\SearchResultsInterface; interface PostSearchResultInterface extends SearchResultsInterface { public function getItems(); public function setItems(array $items); }
To make it easier to test our module, create two console scripts that add and remove test data from the table:
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 extends Command { const ARGUMENT_USERNAME = 'username'; const ARGUMENT_NUMBER_OF_RECORDS = 'number_of_records'; private $postFactory; private $postRepository; private $user; public function __construct( PostFactory $postFactory, PostRepositoryInterface $postRepository, UserInterface $user ) { parent::__construct(); $this->postFactory = $postFactory; $this->postRepository = $postRepository; $this->user = $user; } 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(); } protected function execute(InputInterface $input, OutputInterface $output) { $username = $input->getArgument(self::ARGUMENT_USERNAME); $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++) { $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 extends Command { private $resourceConnection; public function __construct( ResourceConnection $resourceConnection ) { parent::__construct(); $this->resourceConnection = $resourceConnection; } protected function configure() { $this->setName('alex_poletaev:blog:remove_sample_data') ->setDescription('Blog: remove sample data') ; parent::configure(); } 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>'); } } }
The main feature of Magento 2 is the widespread use of its own implementation of
Dependency Injection . In order for Magenta to know which interface corresponds to which implementation, we need to specify these dependencies in the di.xml file. At the same time, we will register the newly created console scripts in this file:
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>
Now we register the routes for the REST API, this is done in the webapi.xml file:
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>
Here we tell Magenta which interface and which method from this interface to use when requesting a specific URL and with a specific http method (POST, GET, etc.). Also, in order to simplify,
anonymous
resource is used, which allows absolutely anyone to knock on our API, otherwise you need to configure access rights (ACL).
Climax
All further actions imply that you have enabled developer mode. This allows you to avoid unnecessary manipulations with the deployment of static content and DI compilation.
We register our new module, run the command:
php bin/magento setup:upgrade
.
Check that a new table,
alex_poletaev_blog_post , has been created.
Next, load the test data using our custom script:
php bin/magento -v alex_poletaev:blog:deploy_sample_data admin
The
admin parameter in this script is the
username from the
admin_user table (you may have it different), in a word, the user from the admin that is listed in the author_id column.
Now you can start testing. For tests, I used Magento 2.2.4, domain
http://m224ce.local/
.
One way to test the REST API is to open
http://m224ce.local/swagger
and use the swagger functionality, but remember, the
getList
method does not work there correctly. I also checked all the methods with curl, examples:
Get an article with
id = 2 curl -X GET -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/2"
Answer:
{"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"}
Get a list of articles that have
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"
Answer:
{"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}
Delete article with
id = 3 curl -X DELETE -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/3"
Answer:
true
Save the new article
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"
Answer:
{"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"}
Note that for a request with http method POST, you must pass the key
post , which actually corresponds to the input parameter ($ post) for the method
\AlexPoletaev\Blog\Api\PostRepositoryInterface::save()
Decoupling
For those who are interested in what happens during the request and how Magenta handles it, below I will provide a few references to methods with my comments. If something does not work, then these methods should be debugged first.
The controller responsible for processing the request
\ Magento \ Webapi \ Controller \ Rest :: dispatch ()Next is called
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest ()Inside
processApiRequest
, many other methods are called, but the next most important
\ Magento \ Webapi \ Controller \ Rest \ InputParamsResolver :: resolve ()\ Magento \ Webapi \ Controller \ Rest \ Router :: match () - a specific route is determined (inside, through the
\Magento\Webapi\Model\Rest\Config::getRestRoutes()
method, all suitable routes are pulled out from the request). The route object contains all the necessary data to process the request - class, method, access rights, etc.
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: process ()- use
\Magento\Framework\Reflection\MethodsMap::getMethodParams()
, where method parameters are pulled out through reflection
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: convertValue () - several options for converting an array into a DataObject or an array from a DataObject
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: _ createFromArray () is a direct conversion where the presence of getters and setters is checked through reflection (remember, I said above that we will return to them?) And that they have a public scope. Next, the object is filled with data via setters.
At the very end, in the method
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest () , via the
call_user_func_array
method of the repository object is called.
Epilogue
Githaba module repositoryYou can install in two ways:
1) Through the composer. To do this, add the following object to the
repositories
array in the composer.json file
{ "type": "git", "url": "https://github.com/alexpoletaev/magento2-blog-demo" }
Then type the following command in the terminal:
composer require alexpoletaev/magento2-blog-demo:dev-master
2) Download the module files and manually copy them to the
app/code/AlexPoletaev/Blog
directory
Regardless of which method you choose, at the end you need to run an upgrade:
php bin/magento setup:upgrade
I hope this article will be useful to someone. If there are any comments, suggestions or questions, then welcome to the comments. Thanks for attention.