亲爱的哈伯拉维兹,我欢迎您! 自从2013年以来我一直在Magento电子商务平台上进行开发以来,我积淀了勇气,并相信至少在这个领域我可以称自己为一个自信的开发人员,因此我决定在该中心上撰写有关该系统的第一篇文章。 我将从Magento 2中REST API的实现开始。这里,开箱即用的是用于处理请求的功能,我将尝试使用一个简单模块的示例进行演示。 本文更适合已与Magenta合作的人员。 因此,有兴趣的人请注意。
领带
我的想象力很差,所以我想出了以下示例:假设我们需要实现一个博客,只有管理面板中的用户才能编写文章。 有时,某些CRM会联系我们,并将这些文章上传到自己(为什么尚不清楚,但这就是我们使用REST API的理由)。 为了简化该模块,我特别省略了在前端和管理面板中显示文章的实现(您可以自己实现,我推荐
一篇关于网格的好
文章 )。 这里仅查询处理功能会受到影响。
行动发展
首先,创建模块的
结构 ,我们将其
称为AlexPoletaev_Blog (缺乏想象力并没有消失)。 我们将模块放在
app / code目录中。
AlexPoletaev /博客/ 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 /博客/ registration.php <?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'AlexPoletaev_Blog', __DIR__ );
这两个文件是模块所需的最低要求。
如果一切在风水中完成,那么我们需要创建服务合同(洋红色中的内容以及它的工作方式,您可以
在此处和
此处阅读),我们将这样做:
AlexPoletaev /博客/ Api /数据/ 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 /博客/ 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); }
让我们更详细地分析这两个接口。
PostInterface界面显示一个表格,其中包含来自我们博客的文章。 在下面创建一个表。 数据库中的每一列在此接口中都必须具有自己的getter和setter,我们稍后将找出为什么这很重要。
PostRepositoryInterface接口提供了一组标准方法,用于与数据库进行交互并将已加载的实体存储在缓存中。 API使用相同的方法。 另一个重要说明是,这些接口中
必须存在正确的PHPDocs,因为Magenta在处理REST请求时会使用反射来确定方法中的输入参数和返回值。
使用
安装脚本,创建一个表,该表将存储来自博客的帖子:
AlexPoletaev /博客/设置/ 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(); } }
该表将包含以下几列(别忘了,我们将所有内容都尽可能简化):
- id-自动递增
- author_id-管理员用户标识(admin_user表中user_id字段上的外键)
- 标题-标题
- 内容-文章文字
- created_at-创建日期
- Updated_at-编辑日期
现在,您需要创建一组标准的洋红色
模型 ,
ResourceModel和
Collection类。 为什么我不画这些类,所以这个主题很广泛,超出了本文的讨论范围,有兴趣的人可以自己搜索。 简而言之,需要这些类来操作数据库中的实体(文章)。 我建议您阅读有关域模型,存储库和服务层模式的信息。
AlexPoletaev /博客/模型/ 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 /博客/模型/ 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 /博客/模型/ 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); } }
细心的读者会注意到,我们的模型实现了先前创建的接口及其所有的getter和setter。
同时,我们实现存储库及其方法:
AlexPoletaev /博客/模型/ 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)); } }
方法
\AlexPoletaev\Blog\Model\PostRepository::getList()
应该返回某种格式的数据,因此我们还将需要此接口:
AlexPoletaev /博客/ Api /数据/ 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); }
为了使测试模块更容易,我们将创建两个控制台脚本,这些脚本可在表中添加和删除测试数据:
AlexPoletaev /博客/控制台/命令/ 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 /博客/控制台/命令/ 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>'); } } }
Magento 2的主要特征是其
依赖注入实现的广泛使用。 为了使Magenta知道实现对应的接口,我们需要在di.xml文件中指定这些依赖项。 同时,我们将在此文件中注册新创建的控制台脚本:
AlexPoletaev /博客/ 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 /博客/ 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>
在这里,我们告诉Magente在请求特定的URL和特定的http方法(POST,GET等)时使用哪个接口和该接口中的哪个方法。 另外,为简化起见,使用了
anonymous
资源,这绝对允许任何人敲我们的API,否则您需要配置访问权限(ACL)。
高潮
所有其他步骤均假定您启用了开发人员模式。 这样可以避免在部署内容静态和DI编译时进行不必要的操作。
注册我们的新模块,运行命令:
php bin/magento setup:upgrade
。
检查是否已
创建新的
alex_poletaev_blog_post表。
接下来,使用我们的自定义脚本加载测试数据:
php bin/magento -v alex_poletaev:blog:deploy_sample_data admin
此脚本中的
admin参数是
admin_user表中的
用户名 (可能与您不同),也就是admin面板中的用户,该用户名将写在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()
结局
对于那些对请求过程中发生的事情以及Magenta如何处理请求感兴趣的人,下面我将在注释中提供一些方法链接。 如果某些方法不起作用,则必须首先从这些方法中扣除。
负责处理请求的控制器
\ Magento \ Webapi \控制器\ Rest ::派遣()下一个叫
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest()在
processApiRequest
内部调用了许多其他方法,但第二个最重要的方法
\ Magento \ Webapi \控制器\休息\ InputParamsResolver :: resolve()\ Magento \ Webapi \控制器\ Rest \路由器:: match() -确定特定的路由(在内部,通过
\Magento\Webapi\Model\Rest\Config::getRestRoutes()
方法,所有合适的路由均从请求中被拉出)。 路由对象包含处理请求的所有必要数据-类,方法,访问权限等。
\ Magento \ Framework \ Webapi \ ServiceInputProcessor ::进程()-使用
\Magento\Framework\Reflection\MethodsMap::getMethodParams()
,其中方法参数通过反射获取
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: convertValue() -用于将数组转换为DataObject或从DataObject转换为数组的几个选项
\ Magento \ Framework \ Webapi \ ServiceInputProcessor :: _ createFromArray() -直接转换,通过反射检查getter和setter的存在(记住,我在上面说过,我们将返回到它们吗?)并且它们具有公共作用域。 接下来,通过设置器将数据填充到对象中。
在方法的最后
\ Magento \ Webapi \ Controller \ Rest :: processApiRequest() ,通过
call_user_func_array
调用存储库对象
call_user_func_array
方法。
结语
Github模块存储库有两种安装方法:
1)通过作曲家。 为此,请将以下对象添加到composer.json文件中的
repositories
数组中
{ "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
我希望本文对某人有用。 如果您有任何意见,建议或问题,请发表评论。 谢谢您的关注。