¡Os doy la bienvenida, queridos habravitas! Como he estado desarrollando en la plataforma de comercio electrónico de Magento desde 2013, habiendo reunido valor y creyendo que en esta área me puedo llamar, al menos, un desarrollador confiable, decidí escribir mi primer artículo en el centro sobre este sistema. Y comenzaré con la implementación de la API REST en Magento 2. Aquí, de forma inmediata, hay una funcionalidad para procesar solicitudes y trataré de demostrarlo usando un ejemplo de un módulo simple. Este artículo está más dirigido a aquellos que ya han trabajado con Magenta. Entonces, ¿a quién le interesa, por favor, debajo del gato?
Empate
Mi imaginación es muy mala, así que se me ocurrió el siguiente ejemplo: imagina que necesitamos implementar un blog, solo los usuarios del panel de administración pueden escribir artículos. De vez en cuando, algún CRM nos llama y carga estos artículos para sí mismo (por qué no está claro, pero así es como justificaremos el uso de la API REST). Para simplificar el módulo, omití específicamente la implementación de mostrar artículos en la interfaz y en el panel de administración (puede implementarlo usted mismo, le recomiendo un buen
artículo sobre cuadrículas). Solo la funcionalidad de procesamiento de consultas se verá afectada aquí.
Desarrollo de la acción
Primero, cree la
estructura del módulo, llamémoslo
AlexPoletaev_Blog (la falta de imaginación no ha desaparecido). Colocamos el módulo en el directorio de la
aplicación / código .
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__ );
Estos dos archivos son los mínimos necesarios para el módulo.
Si todo se hace en Feng Shui, entonces necesitamos crear contratos de servicio (qué es dentro del Magenta y cómo funciona, puede leer
aquí y
aquí ), lo que haremos:
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); }
Analicemos estas dos interfaces con más detalle. La interfaz
PostInterface muestra una tabla con artículos de nuestro blog. Crea una tabla a continuación. Cada columna de la base de datos debe tener su propio getter y setter en esta interfaz, descubriremos por qué esto es importante más adelante. La interfaz
PostRepositoryInterface proporciona un conjunto estándar de métodos para interactuar con la base de datos y almacenar entidades cargadas en la memoria caché. Se utilizan los mismos métodos para la API. Otra nota importante, se
requiere la presencia de PHPDocs correctos en estas interfaces, ya que Magenta, al procesar una solicitud REST, utiliza la reflexión para determinar los parámetros de entrada y los valores de retorno en los métodos.
Usando el
script de instalación, cree una tabla donde se almacenarán las publicaciones del blog:
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(); } }
La tabla tendrá las siguientes columnas (no lo olvide, tenemos todo lo más simple posible):
- id: incremento automático
- author_id: identificador de usuario administrador (clave externa en el campo user_id de la tabla admin_user)
- título - título
- contenido - texto del artículo
- created_at - fecha de creación
- updated_at - editar fecha
Ahora necesita crear un conjunto estándar de clases de
modelo magenta,
modelo de recursos y
colección . Por qué no pintaré estas clases, este tema es extenso y va más allá del alcance de este artículo, quienes están interesados pueden buscar en Google por su cuenta. En pocas palabras, estas clases son necesarias para manipular entidades (artículos) de la base de datos. Le aconsejo que lea sobre los patrones del Modelo de Dominio, el Repositorio y la Capa de Servicio.
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); } }
Un lector atento notará que nuestro modelo implementa la interfaz creada previamente y todos sus captadores y establecedores.
Al mismo tiempo, implementamos el repositorio y sus métodos:
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)); } }
El método
\AlexPoletaev\Blog\Model\PostRepository::getList()
debería devolver datos de cierto formato, por lo que también necesitaremos esta interfaz:
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); }
Para facilitar la prueba de nuestro módulo, crearemos dos scripts de consola que agregarán y eliminarán datos de prueba de la tabla:
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>'); } } }
La característica principal de Magento 2 es el uso generalizado de su propia implementación de
Dependency Injection . Para que Magenta sepa a qué interfaz corresponde la implementación, debemos especificar estas dependencias en el archivo di.xml. Al mismo tiempo, registraremos los scripts de consola recién creados en este archivo:
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>
Ahora registre las rutas para la API REST, esto se hace en el archivo 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>
Aquí le decimos a Magente qué interfaz y qué método de esta interfaz usar al solicitar una URL específica y con un método http específico (POST, GET, etc.). Además, para simplificar, se utiliza un recurso
anonymous
, que permite que cualquiera pueda acceder a nuestra API; de lo contrario, debe configurar los derechos de acceso (ACL).
Climax
Todos los pasos posteriores suponen que tiene habilitado el modo desarrollador. Esto evita manipulaciones innecesarias con el despliegue de estadísticas de contenido y compilación DI.
Registre nuestro nuevo módulo, ejecute el comando:
php bin/magento setup:upgrade
.
Compruebe que se haya
creado una nueva tabla
alex_poletaev_blog_post .
A continuación, cargue los datos de prueba con nuestro script personalizado:
php bin/magento -v alex_poletaev:blog:deploy_sample_data admin
El parámetro
admin en este script es el
nombre de
usuario de la tabla
admin_user (puede ser diferente para usted), en una palabra, el usuario del panel de administración, que se escribirá en la columna author_id.
Ahora puedes comenzar a probar. Para las pruebas, utilicé Magento 2.2.4, dominio
http://m224ce.local/
.
Una forma de probar la API REST es abrir
http://m224ce.local/swagger
y usar la funcionalidad swagger, pero recuerde que el método
getList
no funciona correctamente allí. También probé todos los métodos con curl, ejemplos:
Obtenga un artículo con
id = 2 curl -X GET -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/2"
La respuesta es:
{"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"}
Obtenga una lista de artículos con
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"
La respuesta es:
{"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}
Eliminar artículo con
id = 3 curl -X DELETE -H "Accept: application/json" "http://m224ce.local/rest/all/V1/blog/posts/3"
La respuesta es:
true
Guarda el nuevo artículo
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"
La respuesta es:
{"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"}
Tenga en cuenta que para una solicitud con el método http POST, debe pasar la clave de
publicación , que en realidad corresponde al parámetro de entrada ($ post) para el método
\AlexPoletaev\Blog\Api\PostRepositoryInterface::save()
Denouement
Para aquellos que estén interesados en lo que sucede durante la solicitud y cómo lo procesa Magenta, a continuación les daré algunos enlaces a métodos con mis comentarios. Si algo no funciona, entonces estos métodos deben cargarse primero.
El controlador responsable de procesar la solicitud.
\ Magento \ Webapi \ Controller \ Rest :: dispatch ()Siguiente llamado
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest ()Muchos otros métodos se denominan dentro de
processApiRequest
, pero el siguiente más importante
\ Magento \ Webapi \ Controller \ Rest \ InputParamsResolver :: resolve ()\ Magento \ Webapi \ Controller \ Rest \ Router :: match () : se determina una ruta específica (dentro, a través del
\Magento\Webapi\Model\Rest\Config::getRestRoutes()
, todas las rutas adecuadas se extraen de la solicitud de la solicitud). El objeto de ruta contiene todos los datos necesarios para procesar la solicitud: clase, método, derechos de acceso, etc.
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: process ()- usa
\Magento\Framework\Reflection\MethodsMap::getMethodParams()
, donde los parámetros del método se extraen mediante la reflexión
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: convertValue () - varias opciones para convertir una matriz en un DataObject o en una matriz desde un DataObject
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: _ createFromArray () - conversión directa, donde a través de la reflexión se verifica la presencia de captadores y establecedores (recuerde, dije anteriormente que volveremos a ellos?) Y que tienen un alcance público. A continuación, el objeto se llena de datos a través de los colocadores.
Al final, en el método
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest () , a través de
call_user_func_array
método del objeto del repositorio.
Epílogo
Repositorio de módulos de GithubHay dos formas de instalar:
1) Vía compositor. Para hacer esto, agregue el siguiente objeto a la matriz de
repositories
en el archivo composer.json
{ "type": "git", "url": "https://github.com/alexpoletaev/magento2-blog-demo" }
Luego escriba el siguiente comando en la terminal:
composer require alexpoletaev/magento2-blog-demo:dev-master
2) Descargue los archivos del módulo y cópielos manualmente en el directorio
app/code/AlexPoletaev/Blog
Independientemente del método que elija, al final debe ejecutar la actualización:
php bin/magento setup:upgrade
Espero que este artículo sea útil para alguien. Si tiene algún comentario, sugerencia o pregunta, bienvenido a comentar. Gracias por su atencion