Sejam bem-vindos, queridos Habravites! Desde que desenvolvo a plataforma de comércio eletrônico Magento desde 2013, reunindo coragem e acreditando que nesta área posso me chamar, pelo menos, de um desenvolvedor confiante, decidi escrever meu primeiro artigo no hub sobre esse sistema. E começarei com a implementação da API REST no Magento 2. Aqui, fora da caixa, há funcionalidade para processar solicitações e tentarei demonstrá-la usando um exemplo de módulo simples. Este artigo é mais voltado para aqueles que já trabalharam com Magenta. E então, quem estiver interessado, por favor, sob o gato.
Gravata
Minha imaginação é muito ruim, então criei o seguinte exemplo: imagine que precisamos implementar um blog, apenas usuários do painel de administração podem escrever artigos. De tempos em tempos, alguns CRM nos batem e carregam esses artigos para si (por que não está claro, mas é assim que justificaremos o uso da API REST). Para simplificar o módulo, omiti especificamente a implementação da exibição de artigos no frontend e no painel de administração (você pode implementá-lo você mesmo, recomendo um bom
artigo sobre grades). Somente a funcionalidade de processamento da consulta será afetada aqui.
Desenvolvimento de ação
Primeiro, crie a
estrutura do módulo, vamos chamá-lo de
AlexPoletaev_Blog (a falta de imaginação não desapareceu). Colocamos o módulo no diretório
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__ );
Esses dois arquivos são o mínimo necessário para o módulo.
Se tudo for feito no Feng Shui, precisamos criar contratos de serviço (o que é dentro do Magenta e como funciona, você pode ler
aqui e
aqui ), o que faremos:
AlexPoletaev / Blog / API / Dados / 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); }
Vamos analisar essas duas interfaces com mais detalhes. A interface
PostInterface exibe uma tabela com artigos de nosso blog. Crie uma tabela abaixo. Cada coluna do banco de dados deve ter seu próprio getter e setter nessa interface; descobriremos por que isso é importante posteriormente. A interface
PostRepositoryInterface fornece um conjunto padrão de métodos para interagir com o banco de dados e armazenar entidades carregadas no cache. Os mesmos métodos são usados para a API. Outra observação importante é a presença de PHPDocs corretos nessas interfaces, pois o Magenta, ao processar uma solicitação REST, usa reflexão para determinar os parâmetros de entrada e retornar valores nos métodos.
Usando o
script de instalação, crie uma tabela na qual as postagens do blog serão armazenadas:
AlexPoletaev / Blog / Instalação / 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(); } }
A tabela terá as seguintes colunas (não se esqueça, temos tudo da forma mais simples possível):
- id - incremento automático
- author_id - identificador de usuário admin (chave estrangeira no campo user_id da tabela admin_user)
- title - title
- conteúdo - texto do artigo
- created_at - data de criação
- updated_at - data de edição
Agora você precisa criar um conjunto padrão de classes Magenta
Model ,
ResourceModel e
Collection . Por que não pintarei essas classes, este tópico é extenso e vai além do escopo deste artigo, que está interessado, pode pesquisar no Google por conta própria. Em poucas palavras, essas classes são necessárias para manipular entidades (artigos) do banco de dados. Aconselho que você leia sobre os padrões de Modelo de Domínio, Repositório e Camada de Serviço.
AlexPoletaev / Blog / Modelo / 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 / Modelo / 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 / Modelo / 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); } }
Um leitor atento perceberá que nosso modelo implementa a interface criada anteriormente e todos os seus getters e setters.
Ao mesmo tempo, implementamos o repositório e seus métodos:
AlexPoletaev / Blog / Modelo / 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)); } }
O método
\AlexPoletaev\Blog\Model\PostRepository::getList()
deve retornar dados de um determinado formato, portanto, também precisaremos dessa interface:
AlexPoletaev / Blog / API / Dados / 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); }
Para facilitar o teste de nosso módulo, criaremos dois scripts de console que adicionam e removem dados de teste da tabela:
AlexPoletaev / Blog / Console / Comando / 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 / Comando / 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>'); } } }
A principal característica do Magento 2 é o uso generalizado de sua própria implementação da
Injeção de
Dependência . Para que o Magenta saiba a qual interface a implementação corresponde, precisamos especificar essas dependências no arquivo di.xml. Ao mesmo tempo, registraremos os scripts de console recém-criados neste arquivo:
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>
Agora registre as rotas para a API REST, isso é feito no arquivo 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>
Aqui, informamos ao Magente qual interface e qual método dessa interface deve ser usado ao solicitar uma URL específica e com um método http específico (POST, GET, etc.). Além disso, para simplificar, um recurso
anonymous
é usado, o que permite que qualquer pessoa quebre a nossa API, caso contrário, você precisa configurar os direitos de acesso (ACLs).
Climax
Todas as etapas adicionais assumem que você tem o modo de desenvolvedor ativado. Isso evita manipulações desnecessárias com a implantação de estática de conteúdo e compilação de DI.
Registre nosso novo módulo, execute o comando:
php bin/magento setup:upgrade
.
Verifique se uma nova tabela
alex_poletaev_blog_post foi
criada .
Em seguida, carregue os dados de teste usando nosso script personalizado:
php bin/magento -v alex_poletaev:blog:deploy_sample_data admin
O parâmetro
admin neste script é o
nome de
usuário da tabela
admin_user (pode ser diferente para você), em uma palavra, o usuário do painel de administração, que será gravado na coluna author_id.
Agora você pode começar a testar. Para testes, usei o Magento 2.2.4, domínio
http://m224ce.local/
.
Uma maneira de testar a API REST é abrir
http://m224ce.local/swagger
e usar a funcionalidade swagger, mas lembre-se de que o método
getList
não funciona lá corretamente. Também testei todos os métodos com curl, exemplos:
Obter um artigo com
id = 2 curl -X GET -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/2"
A resposta é:
{"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"}
Obtenha uma lista de artigos com
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"
A resposta é:
{"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}
Excluir artigo com
id = 3 curl -X DELETE -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/3"
A resposta é:
true
Salve o novo artigo
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"
A resposta é:
{"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"}
Observe que, para uma solicitação com o método http POST, você deve passar a chave
post , que realmente corresponde ao parâmetro de entrada ($ post) para o método
\AlexPoletaev\Blog\Api\PostRepositoryInterface::save()
Negociação
Para aqueles que estão interessados no que acontece durante a solicitação e como a Magenta a processa, abaixo darei alguns links para métodos com meus comentários. Se algo não funcionar, esses métodos deverão ser debitados primeiro.
O controlador responsável pelo processamento da solicitação
\ Magento \ Webapi \ Controller \ Rest :: dispatch ()Próximo chamado
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest ()Muitos outros métodos são chamados dentro de
processApiRequest
, mas o próximo mais importante
\ Magento \ Webapi \ Controller \ Rest \ InputParamsResolver :: resolve ()\ Magento \ Webapi \ Controller \ Rest \ Router :: match () - uma rota específica é determinada (dentro, através do
\Magento\Webapi\Model\Rest\Config::getRestRoutes()
, todas as rotas adequadas são extraídas da solicitação da solicitação). O objeto de rota contém todos os dados necessários para processar a solicitação - classe, método, direitos de acesso, etc.
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: process ()- usa
\Magento\Framework\Reflection\MethodsMap::getMethodParams()
, onde os parâmetros do método são atraídos pela reflexão
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: convertValue () - várias opções para converter uma matriz em um DataObject ou em uma matriz de um DataObject
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: _ createFromArray () - conversão direta, onde através da reflexão a presença de getters e setters é verificada (lembre-se, eu disse acima que retornaremos a eles?) E que eles têm um escopo público. Em seguida, o objeto é preenchido com dados através dos configuradores.
No final, no método
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest () , através do
call_user_func_array
método do objeto de repositório é chamado.
Epílogo
Repositório do módulo GithubExistem duas maneiras de instalar:
1) Via compositor. Para fazer isso, inclua o seguinte objeto na matriz de
repositories
no arquivo composer.json
{ "type": "git", "url": "https://github.com/alexpoletaev/magento2-blog-demo" }
Em seguida, digite o seguinte comando no terminal:
composer require alexpoletaev/magento2-blog-demo:dev-master
2) Faça o download dos arquivos do módulo e copie-os manualmente para o diretório
app/code/AlexPoletaev/Blog
Independentemente de qual método você escolher, no final, você precisará executar a atualização:
php bin/magento setup:upgrade
Espero que este artigo seja útil para alguém. Se você tiver quaisquer comentários, sugestões ou perguntas, bem-vindo ao comentar. Obrigado pela atenção.