Magento2 开发者指南(三)

原文:zh.annas-archive.org/md5/22e3fb2c2e59e824ce1774e2331a1019

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章. Web API

在前面的章节中,我们学习了如何使用一些后端组件,以便店主可以管理和操作数据,如客户、产品、类别、订单等。有时这还不够,比如当我们从第三方系统中拉取或推送数据时。在这些情况下,Magento Web API 框架使得通过 REST 或 SOAP 调用 Magento 服务变得容易。

在本章中,我们将涵盖以下主题:

  • 用户类型

  • 认证方法

  • REST 与 SOAP

  • 基于令牌的认证实践

  • 基于 OAuth 的认证实践

  • 基于 OAuth 的 Web API 调用

  • 基于会话的认证实践

  • 创建自定义 Web API

  • 列表过滤的搜索条件接口

在我们开始进行 Web API 调用之前,我们必须验证我们的身份并拥有访问 API 资源的必要权限(授权)。认证允许 Magento 识别调用者的用户类型。根据用户的(管理员、集成、客户或访客)访问权限,确定 API 调用资源的可访问性。

用户类型

我们可以访问的资源列表取决于我们的用户类型,并在我们的模块配置文件webapi.xml中定义。

API 已知有三种用户类型,如下所示:

  • 管理员或集成者:管理员或集成者被授权的资源。例如,如果管理员被授权访问Magento_Cms::page resource,他们可以发起POST /V1/cmsPage调用。

  • 客户:客户被授权的资源。这些资源具有匿名或自我权限。

  • 访客用户:访客被授权的资源。这些资源具有匿名权限。

两个文件在定义 API 方面起着至关重要的作用:我们的模块acl.xmlwebapi.xml文件。

acl.xml是我们定义我们的模块访问控制列表ACL)的地方。它定义了访问资源的可用权限集。所有 Magento 模块的acl.xml文件被合并以构建一个 ACL 树,该树用于选择允许的 admin 角色资源或第三方集成的访问(系统 | 扩展 | 集成 | 添加新集成 | 可用 API)。

webapi.xml是我们定义 Web API 资源和它们权限的地方。当我们创建webapi.xml时,acl.xml中定义的权限被引用以创建每个 API 资源的访问权限。

让我们看看以下(截断的)来自核心Magento_Cms模块的webapi.xml

<routes  xsi:noNamespaceSchemaLocation= "urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    ...
    <route url="/V1/cmsPage" method="POST">
        <service class="Magento\Cms\Api\PageRepositoryInterface" method="save" />
        <resources>
            <resource ref="Magento_Cms::page" />
        </resources>
    </route>
    ...
    <route url="/V1/cmsBlock/search" method="GET">
        <service class="Magento\Cms\Api\BlockRepositoryInterface" method="getList" />
        <resources>
            <resource ref="Magento_Cms::block" />
        </resources>
    </route>
    ...
</routes>

在 CMS 页面 API 的前述 webapi.xml 文件中,只有拥有 Magento_Cms::page 授权的用户可以访问 POST /V1/cmsPageGET /V1/cmsBlock/search。我们将在示例中稍后更详细地解释路由;目前,我们的重点是 resource。我们可以在资源下分配多个子 resource 元素。在这些情况下,用户只要有任何一个这些 ACL 被分配,就能进行 API 调用。

实际的授权是授予给管理员或集成商,他们在 Magento 管理员中定义,可以选择完整的组或 ACL 树中选定的特定资源,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00061.jpeg

由于 webapi.xmlacl.xml 相互关联,让我们看看核心模块 Magento_Cms 的(截断的)acl.xml 文件:

<resources>
    <resource id="Magento_Backend::admin">
        <resource id="Magento_Backend::content">
            <resource id="Magento_Backend::content_elements">
                <resource id="Magento_Cms::page" ...>
                    ...
                </resource>
            </resource>
        </resource>
    </resource>
</resources>

注意到 Magento_Cms::page 资源的位置嵌套在 Magento_Backend::content_elements 之下,而 Magento_Backend::content_elements 又嵌套在 Magento_Backend::content 之下,Magento_Backend::content 又嵌套在 Magento_Backend::admin 之下。这告诉 Magento 在显示 角色资源 树时,在 Magento 管理员下渲染 ACL 的位置,如前一个截图所示。这并不意味着如果用户被授权访问所有这些父 Magento_Backend 资源,他仍然无法访问 Magento_Cms::page 资源所对应的 API。

对资源进行授权是一种相对简单的事情。在授权时没有树形检查。因此,当在 acl.xml 下定义时,每个资源都需要在 resource 元素上有一个唯一的 id 属性值。

正如之前所列出的,这些定义的资源是管理员或集成商被授权访问的资源。

相反,客户被分配了一个名为 anonymousself 的资源。如果我们对所有 Magento 核心模块进行完整的 <resource ref="anonymous" /> 字符串搜索,会出现几个匹配项。

让我们来看看核心模块 vendor/magento/module-catalog/etc/webapi.xml 文件(以下为截断内容):

<route url="/V1/products" method="GET">
    <service class= "Magento\Catalog\Api\ProductRepositoryInterface" method="getList"/>
    <resources>
        <resource ref="anonymous" />
    </resources>
</route>

上述 XML 定义了一个 API 端点路径,其值为 /V1/products,可以通过 HTTP GET 方法访问。它进一步定义了一个名为 anonymous 的资源,这意味着当前登录的客户或访客用户可以调用此 API 端点。

anonymous 是一种特殊权限,在 acl.xml 中不需要定义。因此,它不会在 Magento 管理员下的权限树中显示。这仅仅意味着当前在 webapi.xml 中的资源可以无需认证即可访问。

最后,我们来看看 self 资源,其示例可以在(截断的)vendor/magento/module-customer/etc/webapi.xml 文件中找到如下:

<route url="/V1/customers/me" method="PUT">
    <service class= "Magento\Customer\Api\CustomerRepositoryInterface" method="save"/>
    <resources>
        <resource ref="self"/>
    </resources>
    <data>
        <parameter name="customer.id" force="true">%customer_id%</parameter>
    </data>
</route>

self是一种特殊的访问方式,允许用户访问他们拥有的资源,前提是我们已经与系统建立了认证会话。例如,GET /V1/customers/me获取已登录客户的详细信息。这对于基于 JavaScript 的组件/小部件通常很有用。

认证方法

根据 Magento 的视角,移动应用、第三方应用和 JavaScript 组件/小部件(店面或管理员)是三种主要的客户端类型。尽管客户端基本上是与我们的 API 进行通信的一切,但每种类型的客户端都有一个首选的认证方法。

Magento 支持三种类型的认证方法,如下列出:

  • 基于令牌的认证

  • 基于 OAuth 的认证

  • 基于会话的认证

基于令牌的认证最适合移动应用,其中令牌就像一个电子钥匙,提供对 Web API 的访问。基于令牌的认证系统背后的概念相对简单。用户在初始认证期间提供用户名和密码,以便从系统中获取一个时间有限的令牌。如果成功获取令牌,所有后续的 API 调用都使用该令牌进行。

基于 OAuth 的认证适用于与 Magento 集成的第三方应用。一旦应用通过OAuth 1.0a 握手过程获得授权,它就能访问 Magento Web API。在此我们必须了解三个关键术语:用户(资源所有者)、客户端(消费者)和服务器(服务提供商)。用户或资源所有者是请求允许访问其受保护资源的人。想象一下,一个客户作为用户(资源所有者)允许第三方应用访问其订单。在这种情况下,这个第三方应用就是客户端(消费者),而 Magento 及其 Web API 则是服务器(服务提供商)。

基于会话的认证可能是最容易理解的一种。作为客户,您使用客户凭证登录到 Magento 店面。作为管理员,您使用管理员凭证登录到 Magento 管理员界面。Magento Web API 框架使用您的登录会话信息来验证您的身份并授权访问请求的资源。

REST 与 SOAP 的比较

Magento 支持 Web API 的两种通信类型:SOAP(即简单对象访问协议)和REST(即表征状态转移)。认证方法本身并不绑定于任何一种。我们可以使用相同的认证方法和 Web API 方法调用,无论是 SOAP 还是 REST。

我们可能如下概述一些 REST 的特定内容:

  • 我们通过 cURL 命令或 REST 客户端运行 REST Web API 调用。

  • 请求支持HTTP动词:GETPOSTPUTDELETE

  • 一个 HTTP 标头需要一个授权参数,指定使用 Bearer HTTP 授权方案 的认证令牌,Authorization: Bearer <TOKEN><TOKEN> 是 Magento 令牌服务返回的认证令牌。

  • 我们可以使用 HTTP 标头 Accept: application/<FORMAT>,其中 <FORMAT> 是 JSON 或 XML 之一。

我们可能如下概述一些 SOAP 特性:

  • 我们通过 cURL 命令或 SOAP 客户端运行 SOAP Web API 调用。

  • 只有为我们请求的服务才会生成 Web 服务定义语言WSDL)文件。没有为所有服务合并的一个大 WSDL 文件。

  • Magento Web API 使用 WSDL 1.2,符合 WS-I 2.0 基本配置文件。

  • 每个作为服务合同一部分的 Magento 服务接口在 WSDL 中都表示为一个独立的服务。

  • 消费多个服务意味着在 WSDL 端点 URL 中以逗号分隔的方式指定它们,例如 http://<magento.host>/soap/<optional_store_code>?wsdl&services=<service_name_1>,<service_name_2>

  • 我们可以通过在浏览器中访问类似 http://<SHOP-URL>/soap/default?wsdl_list 的 URL 来获取所有可用服务的列表。

以下 REST 和 SOAP 示例将广泛使用 cURL,它本质上是一个允许您从命令行或不同语言实现(如 PHP)中发出 HTTP 请求的程序。我们可以进一步将 cURL 描述为控制台浏览器,或我们的网络 view source 工具。我们可以用各种花哨的 REST 和 SOAP 库做的任何事情,我们也可以用 cURL 做到;它只是被认为是一个更底层的做法。

使用 cURL 或其他没有内部实现 WSDL/XML 解析的任何东西进行 SOAP 请求是繁琐的。因此,使用 PHP SoapClient 或更健壮的工具是必须的。SoapClient 是 PHP 的一个集成、积极维护的部分,因此通常是可用的。

即使有负分,我们仍将使用控制台 cURL、PHP cURL 和 PHP SoapClient 示例来展示所有我们的 API 调用。鉴于库抽象了如此多的功能,开发人员对 cURL 有一个坚实的理解是绝对必要的,即使是为了进行 SOAP 调用。

基于令牌的认证实践

基于令牌的认证的核心如下:

  • 客户端使用用户名和密码请求访问

  • 应用程序验证凭证

  • 应用程序向客户端提供一个签名令牌

以下代码示例演示了针对客户用户的控制台 cURL REST 请求:

curl -X POST "http://magento2.ce/rest/V1/integration/customer/token"\
 -H "Content-Type:application/json"\
 -d '{"username":"john@change.me", "password":"abc123"}'

以下代码示例演示了针对客户用户的 PHP cURL REST 请求:

$data = array('username' => 'john@change.me', 'password' => 'abc123');
$data_string = json_encode($data);

$ch = curl_init('http://magento2.ce/rest/V1/integration /customer/token');
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($data_string))
);

$result = curl_exec($ch);

以下代码示例演示了针对客户用户的控制台 cURL SOAP 请求:

curl -X POST -H 'Content-Type: application/soap+xml;
charset=utf-8; action= "integrationCustomerTokenServiceV1CreateCustomerAccessToken"'
-d @request.xml http://magento2.ce/index.php/soap/default?services= integrationCustomerTokenServiceV1

注意到 -d @request.xml 部分。在这里,我们告诉 curl 命令取 request.xml 文件的内容,并将其作为 POST 主体数据传递,其中 request.xml 文件的内容由前面的 curl 命令定义如下:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:integrationCustomerTokenServiceV1CreateCustomer AccessTokenRequest>
            <username>john@change.me</username>
            <password>abc123</password>
        </ns1:integrationCustomerTokenServiceV1CreateCustomer AccessTokenRequest>
    </env:Body>
</env:Envelope>

以下代码示例演示了针对客户用户的 PHP cURL SOAP-like 请求:

$data_string = file_get_contents('request.xml');

$ch = curl_init('http://magento2.ce/index.php/soap/default?services= integrationCustomerTokenServiceV1');
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/soap+xml; charset=utf-8; action="integrationCustomerTokenServiceV1 CreateCustomerAccessToken"',
    'Content-Length: ' . strlen($data_string))
);

$result = curl_exec($ch);

以下代码示例演示了使用 PHP SoapClient 进行 Web API 调用的用法:

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/default?wsdl&services= integrationCustomerTokenServiceV1',
    array('soap_version' => SOAP_1_2, 'trace' => 1)
);

$token = $request->integrationCustomerTokenServiceV1Create CustomerAccessToken(array('username' => 'john@change.me', 'password' => 'abc123'));

管理员用户认证的 API 调用几乎相同,并且取决于我们采取的三种方法中的哪一种。区别仅在于在 REST 的情况下,使用https://magento2.ce/rest/V1/integration/admin/token作为端点 URL,以及在使用http://magento2.ce/index.php/soap/default?services=integrationCustomerTokenServiceV1的情况下。此外,对于 SOAP 调用,我们在$request对象上调用integrationAdminTokenServiceV1CreateAdminAccessToken

在认证成功的情况下,无论是客户还是管理员 API 调用,响应都会是一个看起来随机的 32 个字符长字符串,我们称之为令牌。此令牌随后被保存在数据库中的oauth_token表中的令牌列下。

这可能对oauth_token表与令牌认证有什么关系有些困惑。

注意

如果我们仔细思考,基于令牌的认证可以看作是 OAuth 的简化版本,其中用户会使用用户名和密码进行认证,然后将获得的时效性令牌提供给第三方应用程序使用。

在认证失败的情况下,服务器会返回HTTP 401 未授权,其体包含一个 JSON 消息:

{"message":"Invalid login or password."}

注意我们如何能够调用 API 方法,尽管我们尚未进行认证?这意味着我们必须调用由匿名资源类型定义的 API。快速查看 API 端点可以给我们一些关于其定义位置的提示。在vendor/magento/module-integration/etc/webapi.xml文件下查看,我们可以看到以下(截断的)XML:

<route url="/V1/integration/admin/token" method="POST">
    <service class="Magento\Integration\Api\AdminTokenServiceInterface" method="createAdminAccessToken"/>
    <resources>
        <resource ref="anonymous"/>
    </resources>
</route>
<route url="/V1/integration/customer/token" method="POST">
    <service class="Magento\Integration\Api\ CustomerTokenServiceInterface" method="createCustomerAccessToken"/>
    <resources>
        <resource ref="anonymous"/>
    </resources>
</route>

我们可以清楚地看到,即使是基于令牌的认证本身也被定义为 API,使用匿名资源以便每个人都可以访问它。简而言之,基于令牌的认证是Magento\Integration模块的一个特性。

现在我们已经有了认证令牌,我们可以开始进行其他 API 调用。记住,令牌仅仅意味着我们已经对给定的用户名和密码进行了认证。它并不意味着我们能够访问所有 Web API 方法。这进一步取决于我们的客户或用户是否有适当的访问角色。

基于 OAuth 的认证实践

基于 OAuth 的认证是 Magento 支持的最复杂但最灵活的一种。在我们使用它之前,商家必须将我们的外部应用程序注册为与 Magento 实例的集成。以商家的身份,我们在系统 | 扩展 | 集成下的 Magento 管理区域进行操作。点击添加新集成按钮会打开如下截图所示的界面:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00062.jpeg

External Book App的值是我们外部应用的自由命名。如果我们将其与 Twitter 连接,我们就可以轻松地将它的名字放在这里。在名称旁边,我们有电子邮件回调 URL身份链接 URL字段。电子邮件的值并不是特别重要。回调 URL 和身份链接 URL 定义了接收 OAuth 凭证的外部应用端点。这些链接的值指向作为 OAuth 客户端的外部应用。我们稍后会回到这一点。

可用 API面板下的API选项卡中,我们将资源访问设置为全部自定义。如果设置为自定义,我们可以在资源选项中进一步微调我们希望允许访问此集成的资源,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00063.jpeg

我们应该始终给外部应用我们正在使用的最小必要资源。这样,我们最小化了可能的安全风险。前面的截图显示我们只定义了SalesProductsCustomerMarketing资源到集成中。这意味着 API 用户将无法使用内容资源,例如保存或删除页面。

如果我们现在点击保存按钮,我们应该被重定向回系统 | 扩展 | 集成屏幕,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00064.jpeg

在这里,我们需要关注三件事情。首先,我们看到一个集成不安全的消息。这是因为当我们定义回调 URL 和身份链接 URL 时,我们使用了 HTTP 协议而不是 HTTPS 协议。在现实世界的连接中,出于安全考虑,我们需要确保使用 HTTPS。此外,我们注意到状态列仍然显示为不活跃

位于状态列右侧的激活链接是启动双因素 OAuth 握手之前的步骤。只有能够访问后端集成列表的管理员才能启动此步骤。

在这一点上,我们需要从以下位置拉取External Book App OAuth 客户端背后的全部 PHP 代码,github.com/ajzele/B05032-BookAppOauthClient,并将其放置在我们 Magento 安装根目录下的pub/external-book-app/文件夹中,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00065.jpeg

这些文件的功能是模拟我们自己的迷你 OAuth 客户端。我们不会深入探讨这些文件的内容,更重要的是将其视为一个外部 OAuth 客户端应用。当 Magento 触发回调和配置在上一页输出图像下的身份链接 URL 时,callback-url.phpidentity-link-url.php文件将会执行。

一旦 OAuth 客户端文件就绪,我们回到我们的集成列表。在这里,我们点击 激活 链接。这打开了一个模态框,要求我们批准访问 API 资源,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00066.jpeg

注意这里列出的 API 资源与我们在创建集成时在 API 选项卡下设置的那些相匹配。我们在这里实际上只能做两件事:要么点击 取消,要么点击 允许 来开始双因素 OAuth 握手。点击 允许 按钮并行执行两件事。

首先,它立即将凭据发送到创建 外部图书应用 集成时指定的端点(回调 URL)。从 Magento 到回调 URL 的 HTTP POST 包含与以下类似的参数值:

Array
(
    [oauth_consumer_key] => cn5anfyvkg7sgm2lrv8cxvq0dxcrj7xm
    [oauth_consumer_secret] => wvmgy0dmlkos2vok04k3h94r40jvi5ye
    [store_base_url] => http://magento2-merchant.loc/index.php/
    [oauth_verifier] => hlnsftola6c7b6wjbtb6wwfx4tow2x6x
)

基本上,一个 HTTP POST 请求正在击中 callback-url.php 文件,其内容(部分)如下:

session_id('BookAppOAuth');
session_start();

$_SESSION['oauth_consumer_key'] = $_POST['oauth_consumer_key'];
$_SESSION['oauth_consumer_secret'] = $_POST['oauth_consumer_secret'];
$_SESSION['store_base_url'] = $_POST['store_base_url'];
$_SESSION['oauth_verifier'] = $_POST['oauth_verifier'];

session_write_close();

header('HTTP/1.0 200 OK');

echo 'Response';

我们可以看到,通过 Magento 传递的参数被存储在一个名为 BookAppOAuth 的外部应用会话中。稍后,在 check-login.php 文件中,这些参数将被用来实例化 BookAppOauthClient,这将进一步被用来获取一个请求令牌,这是一个预先授权的令牌。

回调 URL HTTP POST 并行,我们打开了一个弹出窗口,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00067.jpeg

我们在弹出窗口中看到的登录表单只是我们放在 identity-link-url.php 文件下的某些虚拟内容。Magento 通过 HTTP GET 向此文件传递两个值。这些是 consumer_idsuccess_call_backconsumer_id 的值是我们创建在管理区域中的集成 ID。OAuth 客户端应用决定是否要使用此值。success_call_back URL 指向我们的 Magento admin integration/loginSuccessCallback 路径。如果我们查看 identity-link-url.php 文件的代码,我们可以看到表单被设置为在 URL 上执行 POST 动作,例如 check-login.php?consumer_id={$consumerId}&callback_url={$callbackUrl}

如果我们现在点击 登录 按钮,表单将 POST 数据到 check-login.php 文件,并在 URL 中作为 GET 参数传递 consumer_idcallback_url

check-login.php 的内容(部分)定义如下:

require '../../vendor/autoload.php';

$consumer = $_REQUEST['consumer_id'];
$callback = $_REQUEST['callback_url'];

session_id('BookAppOAuth');
session_start();

$consumerKey = $_SESSION['oauth_consumer_key'];
$consumerSecret = $_SESSION['oauth_consumer_secret'];
$magentoBaseUrl = rtrim($_SESSION['store_base_url'], '/');
$oauthVerifier = $_SESSION['oauth_verifier'];

define('MAGENTO_BASE_URL', $magentoBaseUrl);

$credentials = new \OAuth\Common\Consumer\Credentials($consumerKey, $consumerSecret, $magentoBaseUrl);
$oAuthClient = new BookAppOauthClient($credentials);
$requestToken = $oAuthClient->requestRequestToken();

$accessToken = $oAuthClient->requestAccessToken(
    $requestToken->getRequestToken(),
    $oauthVerifier,
    $requestToken->getRequestTokenSecret()
);

header('Location: '. $callback);

为了简化,我们在这里没有进行真正的用户登录检查。我们可能在 OAuth 相关调用之上添加了一个,然后在允许使用 OAuth 之前对用户进行用户名和密码的验证。然而,出于简化原因,我们从我们的示例 OAuth 客户端应用中省略了这部分。

check-login.php 文件中,我们可以看到,基于之前存储的会话参数,我们执行以下操作:

  • 通过传递存储在会话中的 oauth_consumer_keyoauth_consumer_secretstore_base_url 来实例化 \OAuth\Common\Consumer\Credentials 对象

  • 实例化 BookAppOauthClient 对象,将其构造函数传递整个凭据对象

  • 使用 OauthClient 对象获取请求令牌

  • 使用请求令牌获取长期访问令牌

如果一切执行成功,弹出窗口将关闭,我们将被重定向回集成列表。现在的不同之处在于,查看网格时,我们有一个活动状态,旁边有一个重新授权链接,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00068.jpeg

我们真正想要在这个阶段的是访问令牌访问令牌密钥。如果我们编辑 External Book App 集成,我们可以看到这些值。这些值现在应该出现在以下截图所示的集成详细信息选项卡上:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00069.jpeg

访问令牌是我们所有后续 API 调键的密钥,并且我们成功地完成了基于 OAuth 的身份验证部分。

基于 OAuth 的 Web API 调用

一旦我们获得了 OAuth 访问令牌,从前面的步骤中,我们可以开始对其他方法进行 Web API 调用。尽管 Web API 覆盖范围对 REST 和 SOAP 都相同,但在进行方法调用时存在显著差异。

为了提供一个更健壮的示例,我们将针对客户组 save 方法,部分定义在 vendor/magento/module-customer/etc/webapi.xml 文件中,如下所示:

<route url="/V1/customerGroups" method="POST">
    <service class="Magento\Customer\Api\GroupRepositoryInterface" method="save"/>
    <resources>
        <resource ref="Magento_Customer::group"/>
    </resources>
</route>

要使用访问令牌进行 Web API 调用,例如 POST /V1/customerGroups,我们需要在调用中包含这些请求参数在授权请求头中:

  • oauth_consumer_key,从 Magento 管理区域获取,在集成编辑屏幕下。

  • oauth_nonce,随机值,应用程序为每个请求唯一生成。

  • oauth_signature_method,用于签名请求的签名方法名称。有效值有:HMAC-SHA1RSA-SHA1PLAINTEXT

  • 尽管 OAuth 协议支持PLAINTEXT,但 Magento 不支持。我们将使用HMAC-SHA1

  • oauth_timestamp,整数值,Unix-like 时间戳。

  • oauth_token,从 Magento 管理区域获取,在集成编辑屏幕下。

  • oauth_version,Magento 支持 Oauth 1.0a,因此我们使用 1.0

  • oauth_signature,生成的签名值,在签名生成过程中省略。

要为 HTTP 请求生成 OAuth 1.0a HMAC-SHA1 签名需要集中精力,如果手动完成。

我们需要确定请求的 HTTP 方法和 URL,它等于 POST http://magento2-merchant.loc/rest/V1/customerGroups。在这里使用正确的协议非常重要,所以请确保 URL 的 https://http:// 部分与实际发送到 API 的请求相匹配。

我们然后收集请求中包含的所有参数。这些附加参数有两个位置:URL(作为查询字符串的一部分)和请求体。

在 HTTP 请求中,参数被 URL 编码,但我们需要收集原始值。除了请求参数外,每个 oauth_* 参数都需要包含在签名中,除了 oauth_signature 本身之外。

参数按照以下方式归一化为单个字符串:

  • 参数按名称排序,使用字典序字节值排序。如果两个或多个参数具有相同的名称,则按其值排序。

  • 参数按排序顺序连接成一个字符串。对于每个参数,名称与相应的值由一个 = 字符(ASCII 码 61)分隔,即使值是空的。每个名称-值对由一个 & 字符(ASCII 码 38)分隔。

此外,我们将签名密钥定义为 {消费者密钥}+{&}+{访问令牌密钥} 的值。

一旦我们将字符串归一化规则应用于参数并确定签名密钥,我们就调用 hash_hmac('sha1', $data, {签名密钥}, true) 来获取最终的 oauth_signature 值。

这应该会得到一个随机的 28 个字符长的字符串作为 oauth_signature,类似于这个 – Pi/mGfA0SOlIxO9W30sEch6bjGE=

理解如何生成签名字符串很重要,但每次都正确地获取它既繁琐又耗时。我们可以通过实例化内置的 \OAuth\Common\Consumer\Credentials\OAuth\OAuth1\Signature\Signature 类的对象来帮助自己,如下(部分)所示:

$credentials = new \OAuth\Common\Consumer\Credentials($consumerKey, $consumerSecret, $magentoBaseUrl);
$signature = new \OAuth\OAuth1\Signature\Signature($credentials);
$signature->setTokenSecret($accessTokenSecret);
$signature->setHashingAlgorithm('HMAC-SHA1');

echo $signature->getSignature($uri, array(
    'oauth_consumer_key' => $consumerKey,
    'oauth_nonce' => 'per-request-unique-token',
    'oauth_signature_method' => 'HMAC-SHA1',
    'oauth_timestamp' => '1437319569',
    'oauth_token' => $accessToken,
    'oauth_version' => '1.0',
), 'POST');

现在我们有了 oauth_signature 值,我们就准备好在我们的控制台 curl REST 示例中操作了。这归结为在控制台上运行以下命令:

curl -X POST http://magento2.ce/rest/V1/customerGroups
-H 'Content-Type: application/json'
-H 'Authorization: OAuth
oauth_consumer_key="vw2xi6kaq0o3f7ay60owdpg2f8nt66g6",
oauth_nonce="per-request-token-by-app-1",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1437319569",
oauth_token="cney3fmk9p5282bm1khb83q846l7dner",
oauth_version="1.0",
oauth_signature="Pi/mGfA0SOlIxO9W30sEch6bjGE="'
-d '{"group": {"code": "The Book Writer", "tax_class_id": "3"}}'

注意,前面的命令只是从视觉上换行。它应该在控制台上是一行。一旦执行,API 调用将创建一个新的客户组,名为 The Book Writer。观察 curl 命令时,可能会有人问,为什么我们没有对通过 -d 标志开关传递的 JSON POST 数据进行归一化。这是因为如果内容类型为 application/x-www-form-urlencoded,则只有 HTTP POST 请求体中的参数才被考虑用于签名生成。

控制台 cURL SOAP 请求不需要使用 OAuth 签名。我们可以通过将 Authorization: Bearer {访问令牌值} 传递到请求头中执行 SOAP 请求,如下所示:

curl -X POST http://magento2.ce/index.php/soap/default?services= customerGroupRepositoryV1 -H 'Content-Type: application/soap+xml; charset=utf-8; action="customerGroupRepositoryV1Save"' -H 'Authorization: Bearer cney3fmk9p5282bm1khb83q846l7dner' -d @request.xml

其中 request.xml 包含以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:customerGroupRepositoryV1SaveRequest>
            <group>
                <code>The Book Writer</code>
                <taxClassId>3</taxClassId>
            </group>
        </ns1:customerGroupRepositoryV1SaveRequest>
    </env:Body>
</env:Envelope>

以下代码示例演示了针对客户组 save 方法调用的 PHP cURL SOAP 类似请求:

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/?wsdl&services= customerGroupRepositoryV1',
    array(
        'soap_version' => SOAP_1_2,
        'stream_context' => stream_context_create(array(
            'http' => array(
                'header' => 'Authorization: Bearer cney3fmk9p5282bm1khb83q846l7dner')
            )
        )
    )
);

$response = $request->customerGroupRepositoryV1Save(array(
    'group' => array(
        'code' => 'The Book Writer',
        'taxClassId' => 3
    )
));

注意方法名 customerGroupRepositoryV1Save 实际上由服务名 customerGroupRepositoryV1 和服务内部实际方法的 Save 名称组成。

我们可以通过在浏览器中打开类似 http://magento2.ce/soap/default?wsdl_list 的 URL 来获取所有定义的服务列表(取决于我们的 Magento 安装)。

基于会话认证的实践操作

基于会话的认证是 Magento 中第三种也是最简单的一种认证方式。在这里我们没有 token 传递的复杂性。作为客户,我们使用客户凭证登录到 Magento 店面。作为管理员,我们使用管理员凭证登录到 Magento 后台。Magento 使用名为PHPSESSID的 cookie 来跟踪存储我们的登录状态的会话。Web API 框架使用我们登录的会话信息来验证我们的身份并授权访问请求的资源。

客户可以访问在webapi.xml配置文件中配置了匿名或自授权的资源,如GET /rest/V1/customers/me

如果我们在浏览器中尝试打开http://magento2.ce/rest/V1/customers/me URL,但没有以客户身份登录,我们会得到以下响应:

<response>
    <message>Consumer is not authorized to access %resources</message>
    <parameters>
        <resources>self</resources>
    </parameters>
</response>

如果我们以客户身份登录然后尝试打开相同的 URL,我们会得到以下响应:

<response>
    <id>2</id>
    <group_id>1</group_id>
    <created_at>2015-11-22 14:15:33</created_at>
    <created_in>Default Store View</created_in>
    <email>john@change.me</email>
    <firstname>John</firstname>
    <lastname>Doe</lastname>
    <store_id>1</store_id>
    <website_id>1</website_id>
    <addresses/>
    <disable_auto_group_change>0</disable_auto_group_change>
</response>

管理员用户可以访问分配给他们的 Magento 管理员配置文件的资源。

创建自定义 Web API

Magento 附带了一系列我们可以调用的 API 方法。然而,有时这还不够,因为我们的业务需求需要额外的逻辑,我们需要能够向 Web API 添加我们自己的方法。

创建我们自己的 API 的最佳部分是,我们不必担心它们是 REST 还是 SOAP。Magento 抽象化这一点,使得我们的 API 方法自动对 REST 和 SOAP 调用可用。

在概念上,添加新的 API 涉及到两个方面:通过各种类定义业务逻辑,并通过webapi.xml文件公开它。然而,正如我们很快就会看到的,这有很多样板代码

让我们创建一个名为Foggyline_Slider的微型模块,我们将演示创建(POST)更新(PUT)删除(DELETE)列表(GET)方法调用。

创建一个模块注册文件,app/code/Foggyline/Slider/registration.php,内容(部分)如下:

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Foggyline_Slider',
    __DIR__
);

创建一个模块配置文件,app/code/Foggyline/Slider/etc/module.xml,内容如下:

<config  xsi:noNamespaceSchemaLocation="urn:magento:framework:Module /etc/module.xsd">
    <module name="Foggyline_Slider" setup_version="1.0.0"/>
</config>

创建一个安装脚本,我们的未来模型将持久化模块数据。我们通过创建app/code/Foggyline/Slider/Setup/InstallSchema.php文件来实现,内容(部分)如下:

namespace Foggyline\Slider\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface
{
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();

        /**
         * Create table 'foggyline_slider_slide'
         */
        $table = $installer->getConnection()
            ->newTable($installer- >getTable('foggyline_slider_slide'))
            ->addColumn(
                'slide_id',
                \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
                null,
                ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
                'Slide Id'
            )
            ->addColumn(
                'title',
                \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
                200,
                [],
                'Title'
            )
            ->setComment('Foggyline Slider Slide');
        $installer->getConnection()->createTable($table);
        ...
        $installer->endSetup();

    }
}

现在我们指定我们资源的 ACL。我们的资源是我们对我们模块实体执行的 CRUD 操作。我们将以slideimage作为独立实体来结构化我们的模块,其中一张幻灯片可以与多个图像实体相关联。因此,我们希望能够分别控制每个实体的保存和删除操作的访问权限。我们通过定义app/code/Foggyline/Slider/etc/acl.xml文件如下来实现:

<config  xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/ acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::content">
                    <resource id= "Magento_Backend::content_elements">
                        <resource id="Foggyline_Slider::slider" title="Slider" sortOrder="10">
                            <resource id="Foggyline_Slider::slide" title="Slider Slide" sortOrder="10">
                                <resource id= "Foggyline_Slider::slide_save" title="Save Slide" sortOrder="10" />
                                <resource id="Foggyline_Slider:: slide_delete" title="Delete Slide" sortOrder="20" />
                            </resource>
                            <resource id="Foggyline_Slider::image" title="Slider Image" sortOrder="10">
                                <resource id= "Foggyline_Slider::image_save" title="Save Image" sortOrder="10" />
                                <resource id= "Foggyline_Slider::image_delete" title="Delete Image" sortOrder="20" />
                            </resource>
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

现在 ACL 已经设置好了,我们在app/code/Foggyline/Slider/etc/webapi.xml文件(部分)中定义我们的 Web API 资源,如下所示:

<routes  xsi:noNamespaceSchemaLocation= "urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    <route url="/V1/foggylineSliderSlide/:slideId" method="GET">
        <service class="Foggyline\Slider\Api\ SlideRepositoryInterface" method="getById" />
        <resources>
            <resource ref="Foggyline_Slider::slide" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderSlide/search" method="GET">
        <service class="Foggyline\Slider\Api\ SlideRepositoryInterface" method="getList" />
        <resources>
            <resource ref="anonymous" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderSlide" method="POST">
        <service class="Foggyline\Slider\Api\ SlideRepositoryInterface" method="save" />
        <resources>
            <resource ref="Foggyline_Slider::slide_save" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderSlide/:id" method="PUT">
        <service class="Foggyline\Slider\Api\ SlideRepositoryInterface" method="save" />
        <resources>
            <resource ref="Foggyline_Slider::slide_save" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderSlide/:slideId" method="DELETE">
        <service class="Foggyline\Slider\Api\ SlideRepositoryInterface" method="deleteById" />
        <resources>
            <resource ref="Foggyline_Slider::slide_delete" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderImage/:imageId" method="GET">
        <service class="Foggyline\Slider\Api\ ImageRepositoryInterface" method="getById" />
        <resources>
            <resource ref="Foggyline_Slider::image" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderImage/search" method="GET">
        <service class="Foggyline\Slider\Api\ ImageRepositoryInterface" method="getList" />
        <resources>
            <resource ref="Foggyline_Slider::image" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderImage" method="POST">
        <service class="Foggyline\Slider\Api\ ImageRepositoryInterface" method="save" />
        <resources>
            <resource ref="Foggyline_Slider::image_save" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderImage/:id" method="PUT">
        <service class="Foggyline\Slider\Api\ ImageRepositoryInterface" method="save" />
        <resources>
            <resource ref="Foggyline_Slider::image_save" />
        </resources>
    </route>
    <route url="/V1/foggylineSliderImage/:imageId" method="DELETE">
        <service class="Foggyline\Slider\Api\ ImageRepositoryInterface" method="deleteById" />
        <resources>
            <resource ref="Foggyline_Slider::image_delete" />
        </resources>
    </route>
</routes>

注意到每个服务类属性都指向接口,而不是类。这是我们构建可公开服务的方式,总是有一个接口定义在它们后面。正如我们很快就会看到的,使用di.xml,这并不意味着 Magento 会直接从这些接口创建对象。

现在我们创建app/code/Foggyline/Slider/etc/di.xml文件,内容(部分)如下:

<config  xsi:noNamespaceSchemaLocation= "urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="Foggyline\Slider\Api\Data\SlideInterface" type="Foggyline\Slider\Model\Slide"/>

    <preference for="Foggyline\Slider\Api\ SlideRepositoryInterface" type= "Foggyline\Slider\Model\SlideRepository"/>
    ...
</config>

这里发生的事情是我们告诉 Magento 类似于,“嘿,每当你需要传递一个符合Foggyline\Slider\Api\Data\SlideInterface接口的实例时,最好使用Foggyline\Slider\Model\Slide类。”

到目前为止,我们还没有实际创建任何这些接口或模型类。在创建 API 时,我们应该首先从定义接口开始,然后我们的模型应该从这些接口扩展。

接口Foggyline\Slider\Api\Data\SlideInterface定义在app/code/Foggyline/Slider/Api/Data/SlideInterface.php文件中(部分)如下:

namespace Foggyline\Slider\Api\Data;

/**
* @api
*/
interface SlideInterface
{
    const PROPERTY_ID = 'slide_id';
    const PROPERTY_SLIDE_ID = 'slide_id';
    const PROPERTY_TITLE = 'title';

    /**
    * Get Slide entity 'slide_id' property value
    * @return int|null
    */
    public function getId();

    /**
    * Set Slide entity 'slide_id' property value
    * @param int $id
    * @return $this
    */
    public function setId($id);

    /**
    * Get Slide entity 'slide_id' property value
    * @return int|null
    */
    public function getSlideId();

    /**
    * Set Slide entity 'slide_id' property value
    * @param int $slideId
    * @return $this
    */
    public function setSlideId($slideId);

    /**
    * Get Slide entity 'title' property value
    * @return string|null
    */
    public function getTitle();

    /**
    * Set Slide entity 'title' property value
    * @param string $title
    * @return $this
    */
    public function setTitle($title);
}

我们正在追求极致的简化。我们的Slide实体实际上只有 ID 和标题值。idslide_id指向数据库中的同一字段,它们的 getter 和 setter 的实现应该产生相同的结果。

虽然API/Data/*.php接口成为我们的数据模型的设计蓝图要求,但我们也有Api/*RepositoryInterface.php文件。这里的想法是将创建、更新、删除、搜索和类似的数据处理逻辑从数据模型类提取出来,放入它自己的类中。这样,我们的模型类就变成了更纯粹的数据和业务逻辑类,而其余的持久化和搜索相关逻辑则移动到这些存储库类中。

我们的幻灯片存储接口定义在app/code/Foggyline/Slider/Api/SlideRepositoryInterface.php文件中,如下所示:

namespace Foggyline\Slider\Api;

/**
* @api
*/
interface SlideRepositoryInterface
{
    /**
    * Retrieve slide entity.
    * @param int $slideId
    * @return \Foggyline\Slider\Api\Data\SlideInterface
    * @throws \Magento\Framework\Exception\NoSuchEntityException If slide with the specified ID does not exist.
    * @throws \Magento\Framework\Exception\LocalizedException
    */
    public function getById($slideId);

    /**
    * Save slide.
    * @param \Foggyline\Slider\Api\Data\SlideInterface $slide
    * @return \Foggyline\Slider\Api\Data\SlideInterface
    * @throws \Magento\Framework\Exception\LocalizedException
    */
    public function save(\Foggyline\Slider\Api\Data\SlideInterface $slide);

    /**
    * Retrieve slides matching the specified criteria.
    * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
    * @return \Magento\Framework\Api\SearchResultsInterface
    * @throws \Magento\Framework\Exception\LocalizedException
    */
    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria);

    /**
    * Delete slide by ID.
    * @param int $slideId
    * @return bool true on success
    * @throws \Magento\Framework\Exception\NoSuchEntityException
    * @throws \Magento\Framework\Exception\LocalizedException
    */
    public function deleteById($slideId);
}

在接口就位后,我们可以继续到模型类。为了在数据库中持久化和获取数据,我们的Slide实体在Model目录下确实需要三个文件。这些被称为数据模型资源类集合类

数据模型类定义在app/code/Foggyline/Slider/Model/Slide.php文件中(部分)如下:

namespace Foggyline\Slider\Model;

class Slide extends \Magento\Framework\Model\AbstractModel
    implements \Foggyline\Slider\Api\Data\SlideInterface{/**
    * Initialize Foggyline Slide Model
    *
    * @return void
    */
    protected function _construct()
    {
        /* _init($resourceModel) */
        $this->_init ('Foggyline\Slider\Model\ResourceModel\Slide');
    }

    /**
    * Get Slide entity 'slide_id' property value
    *
    * @api
    * @return int|null
    */
    public function getId()
    {
        return $this->getData(self::PROPERTY_ID);
    }

    /**
    * Set Slide entity 'slide_id' property value
    *
    * @api
    * @param int $id
    * @return $this
    */
    public function setId($id)
    {
        $this->setData(self::PROPERTY_ID, $id);
        return $this;
    }

    /**
    * Get Slide entity 'slide_id' property value
    *
    * @api
    * @return int|null
    */
    public function getSlideId()
    {
        return $this->getData(self::PROPERTY_SLIDE_ID);
    }

    /**
    * Set Slide entity 'slide_id' property value
    *
    * @api
    * @param int $slideId
    * @return $this
    */
    public function setSlideId($slideId)
    {
        $this->setData(self::PROPERTY_SLIDE_ID, $slideId);
        return $this;
    }

    /**
    * Get Slide entity 'title' property value
    *
    * @api
    * @return string|null
    */
    public function getTitle()
    {
        return $this->getData(self::PROPERTY_TITLE);
    }

    /**
    * Set Slide entity 'title' property value
    *
    * @api
    * @param string $title
    * @return $this
    */
    public function setTitle($title)
    {
        $this->setData(self::PROPERTY_TITLE, $title);
    }
}

接着是模型资源类,定义在app/code/Foggyline/Slider/Model/ResourceModel/Slide.php文件中(部分)如下:

namespace Foggyline\Slider\Model\ResourceModel;

/**
* Foggyline Slide resource
*/
class Slide extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
    /**
    * Define main table
    *
    * @return void
    */
    protected function _construct()
    {
        /* _init($mainTable, $idFieldName) */
        $this->_init('foggyline_slider_slide', 'slide_id');
    }
}

最后,第三部分是模型集合类,定义在app/code/Foggyline/Slider/Model/ResourceModel/Slide/Collection.php文件中,如下所示:

namespace Foggyline\Slider\Model\ResourceModel\Slide;

/**
* Foggyline slides collection
*/
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\ AbstractCollection
{
    /**
    * Define resource model and model
    *
    * @return void
    */
    protected function _construct()
    {
        /* _init($model, $resourceModel) */
        $this->_init('Foggyline\Slider\Model\Slide', 'Foggyline\Slider\Model\ResourceModel\Slide');
    }
}

如果我们现在手动实例化模型数据类,我们就能在数据库中持久化数据。为了完成di.xml的要求,我们仍然缺少一个最后的成分——Model/SlideRepository类文件。

让我们创建app/code/Foggyline/Slider/Model/SlideRepository.php文件,内容(部分)如下:

namespace Foggyline\Slider\Model;

use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Reflection\DataObjectProcessor;

class SlideRepository implements \Foggyline\Slider\Api\SlideRepositoryInterface
{
    /**
    * @var \Foggyline\Slider\Model\ResourceModel\Slide
    */
    protected $resource;

    /**
    * @var \Foggyline\Slider\Model\SlideFactory
    */
    protected $slideFactory;

    /**
    * @var \Foggyline\Slider\Model\ResourceModel\Slide\ CollectionFactory
    */
    protected $slideCollectionFactory;

    /**
    * @var \Magento\Framework\Api\SearchResultsInterface
    */
    protected $searchResultsFactory;

    /**
    * @var \Magento\Framework\Api\DataObjectHelper
    */
    protected $dataObjectHelper;

    /**
    * @var \Magento\Framework\Reflection\DataObjectProcessor
    */
    protected $dataObjectProcessor;

    /**
    * @var \Foggyline\Slider\Api\Data\SlideInterfaceFactory
    */
    protected $dataSlideFactory;

    /**
    * @param ResourceModel\Slide $resource
    * @param SlideFactory $slideFactory
    * @param ResourceModel\Slide\CollectionFactory $slideCollectionFactory
    * @param \Magento\Framework\Api\SearchResultsInterface $searchResultsFactory
    * @param DataObjectHelper $dataObjectHelper
    * @param DataObjectProcessor $dataObjectProcessor
    * @param \Foggyline\Slider\Api\Data\SlideInterfaceFactory $dataSlideFactory
    */
    public function __construct(
        \Foggyline\Slider\Model\ResourceModel\Slide $resource,
        \Foggyline\Slider\Model\SlideFactory $slideFactory,
        \Foggyline\Slider\Model\ResourceModel\Slide\ CollectionFactory $slideCollectionFactory,
        \Magento\Framework\Api\SearchResultsInterface $searchResultsFactory,
        \Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
        \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor,
        \Foggyline\Slider\Api\Data\SlideInterfaceFactory $dataSlideFactory

    )
    {
        $this->resource = $resource;
        $this->slideFactory = $slideFactory;
        $this->slideCollectionFactory = $slideCollectionFactory;
        $this->searchResultsFactory = $searchResultsFactory;
        $this->dataObjectHelper = $dataObjectHelper;
        $this->dataObjectProcessor = $dataObjectProcessor;
        $this->dataSlideFactory = $dataSlideFactory;
    }
    ...
}

可能看起来这里有很多事情要做,但实际上我们只是在构造函数中传递一些类和接口名称,以便实例化我们将用于 webapi.xml 文件中定义的各个服务方法的对象。

我们列表中的第一个服务方法是 getById,在 SlideRepository.php 中定义如下:

/**
* Retrieve slide entity.
*
* @api
* @param int $slideId
* @return \Foggyline\Slider\Api\Data\SlideInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException If slide with the specified ID does not exist.
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function getById($slideId)
{
    $slide = $this->slideFactory->create();
    $this->resource->load($slide, $slideId);
    if (!$slide->getId()) {
        throw new NoSuchEntityException(__('Slide with id %1 does not exist.', $slideId));
    }
    return $slide;
}

然后我们有 save 方法,在 SlideRepository.php 中定义如下:

/**
* Save slide.
*
* @param \Foggyline\Slider\Api\Data\SlideInterface $slide
* @return \Foggyline\Slider\Api\Data\SlideInterface
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function save(\Foggyline\Slider\Api\Data\SlideInterface $slide)
{
    try {
        $this->resource->save($slide);
    } catch (\Exception $exception) {
        throw new CouldNotSaveException(__($exception- >getMessage()));
    }
    return $slide;
}

save 方法处理了 webapi.xml 中定义的 POSTPUT 请求,因此有效地处理了新幻灯片的创建或现有幻灯片的更新。

进一步来说,我们有 getList 方法,在 SlideRepository.php 中定义如下:

/**
* Retrieve slides matching the specified criteria.
*
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \Magento\Framework\Api\SearchResultsInterface
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
{
    $this->searchResultsFactory->setSearchCriteria ($searchCriteria);

    $collection = $this->slideCollectionFactory->create();

    foreach ($searchCriteria->getFilterGroups() as $filterGroup) {foreach ($filterGroup->getFilters() as $filter) {
            $condition = $filter->getConditionType() ?: 'eq';
            $collection->addFieldToFilter($filter->getField(), [$condition => $filter->getValue()]);
        }
    }
    $this->searchResultsFactory->setTotalCount($collection-> getSize());
    $sortOrders = $searchCriteria->getSortOrders();
    if ($sortOrders) {
        foreach ($sortOrders as $sortOrder) {
            $collection->addOrder(
                $sortOrder->getField(),
                (strtoupper($sortOrder->getDirection()) === 'ASC') ? 'ASC' : 'DESC'
            );
        }
    }
    $collection->setCurPage($searchCriteria->getCurrentPage());
    $collection->setPageSize($searchCriteria->getPageSize());
    $slides = [];
    /** @var \Foggyline\Slider\Model\Slide $slideModel */
    foreach ($collection as $slideModel) {
        $slideData = $this->dataSlideFactory->create();
        $this->dataObjectHelper->populateWithArray(
            $slideData,
            $slideModel->getData(),
            '\Foggyline\Slider\Api\Data\SlideInterface'
        );
        $slides[] = $this->dataObjectProcessor-> buildOutputDataArray(
            $slideData,
            '\Foggyline\Slider\Api\Data\SlideInterface'
        );
    }
    $this->searchResultsFactory->setItems($slides);
    return $this->searchResultsFactory;
}

最后,我们有 deleteById 方法,在 SlideRepository.php 中定义如下:

/**
* Delete Slide
*
* @param \Foggyline\Slider\Api\Data\SlideInterface $slide
* @return bool
* @throws CouldNotDeleteException
*/
public function delete(\Foggyline\Slider\Api\Data\SlideInterface $slide)
{
    try {
        $this->resource->delete($slide);
    } catch (\Exception $exception) {
        throw new CouldNotDeleteException(__($exception-> getMessage()));
    }
    return true;
}

/**
* Delete slide by ID.
*
* @param int $slideId
* @return bool true on success
* @throws \Magento\Framework\Exception\NoSuchEntityException
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function deleteById($slideId)
{
    return $this->delete($this->getById($slideId));
}

请记住,我们只在前面的部分代码示例中涵盖了 Slide 实体,这对于进一步进行 API 调用示例已经足够。

API 调用示例

由于我们定义的所有 API 都受资源保护,我们首先需要以管理员用户身份进行身份验证,假设管理员用户可以访问我们定义的所有自定义资源,包括我们定义的资源。为了简化,我们将使用基于令牌的认证方法,例如,本章前面给出的示例。一旦认证成功,我们应该有一个 32 个随机字符长的令牌,例如 pk8h93nq9cevaw55bohkjbp0o7kpl4d3

一旦获取了令牌密钥,我们将使用控制台 cURL、PHP cURL、PHP SoapClient 以及控制台 SOAP 风格 cURL 示例来测试以下 API 调用:

  • GET /V1/foggylineSliderSlide/:slideId, 调用 getById 服务方法,需要 Foggyline_Slider::slide 资源

  • GET /V1/foggylineSliderSlide/search, 调用 getList 服务方法,需要 Foggyline_Slider::slide 资源

  • POST /V1/foggylineSliderSlide, 调用 save 服务方法,需要 Foggyline_Slider::slide_save 资源

  • PUT /V1/foggylineSliderSlide/:id, 调用 save 服务方法,需要 Foggyline_Slider::slide_save 资源

  • DELETE /V1/foggylineSliderSlide/:slideId, 调用 deleteById 服务方法,需要 Foggyline_Slider::slide_delete 资源

getById 服务方法调用示例

执行 GET /V1/foggylineSliderSlide/:slideId 的控制台 cURL 风格如下:

curl -X GET -H 'Content-type: application/json' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
http://magento2.ce/rest/V1/foggylineSliderSlide/1

执行 GET /V1/foggylineSliderSlide/:slideId 的 PHP cURL 风格如下:

$ch = curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/1');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3'
));

$result = curl_exec($ch);

控制台和 PHP cURL 风格的响应应该是一个类似于以下 JSON 字符串:

{"slide_id":1,"title":"Awesome stuff #1"}

执行 GET /V1/foggylineSliderSlide/:slideId 的 PHP SoapClient 风格如下:

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/? wsdl&services=foggylineSliderSlideRepositoryV1',
    array(
        'soap_version' => SOAP_1_2,
        'stream_context' => stream_context_create(array(
                'http' => array(
                    'header' => 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
            )
        )
    )
);
$response = $request-> foggylineSliderSlideRepositoryV1GetById(array('slideId'=>1));

PHP SoapClient 风格的响应应该是如下 stdClass PHP 对象:

object(stdClass)#2 (1) {
    ["result"]=>
    object(stdClass)#3 (2) {
    ["slideId"]=>
    int(1)
    ["title"]=>
    string(16) "Awesome stuff #1"
    }
}

执行 GET /V1/foggylineSliderSlide/:slideId 的控制台 SOAP 风格 cURL 如下:

curl -X POST \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="foggylineSliderSlideRepositoryV1GetById"' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d @request.xml \
http://magento2.ce/index.php/soap/default?services=foggyline SliderSlideRepositoryV1

其中 request.xml 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1GetByIdRequest>
            <slideId>1</slideId>
        </ns1:foggylineSliderSlideRepositoryV1GetByIdRequest>
    </env:Body>
</env:Envelope>

注意我们实际上并没有做 GET 操作,而是做了 POST 类型的请求。此外,我们指向的 POST 请求的 URL 并非与之前的请求相同。这是因为 Magento SOAP 请求始终是 POST(或 PUT)类型,因为数据是以 XML 格式提交的。返回的 XML 格式指定了服务,而请求头中的操作指定了要在服务上调用的方法。

控制台 SOAP 风格 cURL 的响应应该是一个如下所示的 XML:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1GetByIdResponse>
            <result>
                <slideId>1</slideId>
                <title>Awesome stuff #1</title>
            </result>
        </ns1:foggylineSliderSlideRepositoryV1GetByIdResponse>
    </env:Body>
</env:Envelope>

获取列表服务方法调用示例

执行 GET /V1/foggylineSliderSlide/search 的控制台 cURL 风格如下:

curl -X GET -H 'Content-type: application/json' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
"http://magento2.ce/rest/V1/foggylineSliderSlide/search?search_criteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bfield%5D=title&search_criteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bvalue%5D=%25some%25&search_criteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bcondition_type%5D=like&search_criteria%5Bcurrent_page%5D=1&search_criteria%5Bpage_size%5D=10&search_criteria%5Bsort_orders%5D%5B0%5D%5Bfield%5D=slide_id&search_criteria%5Bsort_orders%5D%5B0%5D%5Bdirection%5D=ASC"

执行 GET /V1/foggylineSliderSlide/search 的 PHP cURL 风格如下:

$searchCriteriaJSON = '{
  "search_criteria": {
    "filter_groups": [
      {
        "filters": [
          {
            "field": "title",
            "value": "%some%",
            "condition_type": "like"
          }
        ]
      }
    ],
    "current_page": 1,
    "page_size": 10,
    "sort_orders": [
      {
        "field": "slide_id",
        "direction": "ASC"
      }
    ]
  }
}';

$searchCriteriaQueryString = http_build_query(json_decode($searchCriteriaJSON));

$ch = curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/ search?' . $searchCriteriaQueryString);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
      'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3'
  ));

$result = curl_exec($ch);

控制台和 PHP cURL 风格的响应应该是一个类似于以下 JSON 字符串:

{"items":[{"slide_id":2,"title":"Just some other slider"},{"slide_id":1,"title":"Awesome stuff #1"}], "search_criteria":{"filter_groups":[{"filters": [{"field":"title","value":"%some%","condition_type":"like"}]}], "sort_orders":[{"field":"slide_id","direction":"- 1"}],"page_size":10,"current_page":1},"total_count":2}

执行 GET /V1/foggylineSliderSlide/search 的 PHP SoapClient 风格如下:

$searchCriteria = [
    'searchCriteria' =>
        [
            'filterGroups' =>
                [
                    [
                        'filters' =>
                            [
                                [
                                    'field' => 'title',
                                    'value' => '%some%',
                                    'condition_type' => 'like',
                                ],
                            ],
                    ],
                ],
            'currentPage' => 1,
            'pageSize' => 10,
            'sort_orders' =>
                [
                    [
                        'field' => 'slide_id',
                        'direction' =>'ASC',
                    ],
                ],
        ],
];

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/?wsdl&services= foggylineSliderSlideRepositoryV1',
    array(
        'soap_version' => SOAP_1_2,
        'trace'=>1,
        'stream_context' => stream_context_create(array(
                'http' => array(
                    'header' => 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
            )
        )
    )
);

$response = $request-> foggylineSliderSlideRepositoryV1GetList($searchCriteria);

PHP SoapClient 风格的响应应该是如下所示的 stdClass PHP 对象:

object(stdClass)#2 (1) {
  ["result"]=>
  object(stdClass)#3 (3) {
    ["items"]=>
    object(stdClass)#4 (0) {
    }
    ["searchCriteria"]=>
    object(stdClass)#5 (3) {
      ["filterGroups"]=>
      object(stdClass)#6 (1) {
        ["item"]=>
        object(stdClass)#7 (1) {
          ["filters"]=>
          object(stdClass)#8 (1) {
            ["item"]=>
            object(stdClass)#9 (2) {
              ["field"]=>
              string(5) "title"
              ["value"]=>
              string(6) "%some%"
            }
          }
        }
      }
      ["pageSize"]=>
      int(10)
      ["currentPage"]=>
      int(1)
    }
    ["totalCount"]=>
    int(0)
  }
}

执行 GET /V1/foggylineSliderSlide/search 的控制台 SOAP 风格 cURL 如下:

curl -X POST \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="foggylineSliderSlideRepositoryV1GetList"' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d @request.xml \
http://magento2.ce/index.php/soap/default?services=foggyline SliderSlideRepositoryV1

其中 request.xml 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1GetListRequest>
            <searchCriteria>
                <filterGroups>
                    <item>
                        <filters>
                            <item>
                                <field>title</field>
                                <value>%some%</value>
                            </item>
                        </filters>
                    </item>
                </filterGroups>
                <pageSize>10</pageSize>
                <currentPage>1</currentPage>
            </searchCriteria>
        </ns1:foggylineSliderSlideRepositoryV1GetListRequest>
    </env:Body>
</env:Envelope>

注意我们实际上并没有做 GET 操作,而是做了 POST。此外,我们指向的 POST 请求的 URL 并非与之前的请求相同。这是因为 Magento SOAP 请求始终是 POST 类型,因为数据是以 XML 格式提交的。返回的 XML 格式指定了服务,而请求头中的操作指定了要在服务上调用的方法。

控制台 SOAP 风格 cURL 的响应应该是一个如下所示的 XML:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1GetListResponse>
            <result>
                <items/>
                <searchCriteria>
                    <filterGroups>
                        <item>
                            <filters>
                                <item>
                                    <field>title</field>
                                    <value>%some%</value>
                                </item>
                            </filters>
                        </item>
                    </filterGroups>
                    <pageSize>10</pageSize>
                    <currentPage>1</currentPage>
                </searchCriteria>
                <totalCount>0</totalCount>
            </result>
        </ns1:foggylineSliderSlideRepositoryV1GetListResponse>
    </env:Body>
</env:Envelope>

保存(作为新文件)服务方法调用示例

执行 POST /V1/foggylineSliderSlide 的控制台 cURL 风格如下:

curl -X POST -H 'Content-type: application/json' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d '{"slide": {"title": "API test"}}' \
http://magento2.ce/rest/V1/foggylineSliderSlide/

执行 POST /V1/foggylineSliderSlide 的 PHP cURL 风格如下:

$slide = json_encode(['slide'=>['title'=> 'API test']]);

$ch = curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide');
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_POSTFIELDS, $slide);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
      'Content-Length: ' . strlen($slide),
      'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3'
  ));

$result = curl_exec($ch);

控制台和 PHP cURL 风格的响应应该是一个类似于以下 JSON 字符串:

{"slide_id":4,"title":"API test"}

执行 POST /V1/foggylineSliderSlide 的 PHP SoapClient 风格如下:

$slide = ['slide'=>['title'=> 'API test']];

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/?wsdl&services= foggylineSliderSlideRepositoryV1',
    array(
        'soap_version' => SOAP_1_2,
        'trace'=>1,
        'stream_context' => stream_context_create(array(
                'http' => array(
                    'header' => 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
            )
        )
    )
);

$response = $request-> foggylineSliderSlideRepositoryV1Save($slide);

PHP SoapClient 风格的响应应该是如下所示的 stdClass PHP 对象:

object(stdClass)#2 (1) {
  ["result"]=>
  object(stdClass)#3 (2) {
    ["slideId"]=>
    int(6)
    ["title"]=>
    string(8) "API test"
  }
}

执行 POST /V1/foggylineSliderSlide 的控制台 SOAP 风格 cURL 如下:

curl -X POST \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="foggylineSliderSlideRepositoryV1Save"' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d @request.xml \
http://magento2.ce/index.php/soap/default?services=foggyline SliderSlideRepositoryV1

其中 request.xml 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1SaveRequest>
            <slide>
                <title>API test</title>
            </slide>
        </ns1:foggylineSliderSlideRepositoryV1SaveRequest>
    </env:Body>
</env:Envelope>

控制台 SOAP 风格 cURL 的响应应该是一个如下所示的 XML:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1SaveResponse>
            <result>
                <slideId>8</slideId>
                <title>API test</title>
            </result>
        </ns1:foggylineSliderSlideRepositoryV1SaveResponse>
    </env:Body>
</env:Envelope>

保存(作为更新)服务方法调用示例

执行 PUT /V1/foggylineSliderSlide/:id 的控制台 cURL 风格如下:

curl -X PUT -H 'Content-type: application/json' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d '{"slide": {"slide_id": 2, "title": "API update test"}}' \
http://magento2.ce/rest/V1/foggylineSliderSlide/2

执行 PUT /V1/foggylineSliderSlide/:id 的 PHP cURL 风格如下:

$slideId = 2;
$slide = json_encode(['slide'=>['slide_id'=> $slideId, 'title'=> 'API update test']]);

$ch = curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/' . $slideId);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
  curl_setopt($ch, CURLOPT_POSTFIELDS, $slide);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
      'Content-Length: ' . strlen($slide),
      'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3'
  ));

$result = curl_exec($ch);

控制台和 PHP cURL 风格的响应应该是一个类似于以下 JSON 字符串:

{"id":2,"slide_id":2,"title":"API update test"}

执行 PUT /V1/foggylineSliderSlide/:id 的 PHP SoapClient 风格如下:

$slideId = 2;
$slide = ['slide'=>['slideId'=> $slideId, 'title'=> 'API update test']];

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/?wsdl&services= foggylineSliderSlideRepositoryV1',
    array(
        'soap_version' => SOAP_1_2,
        'trace'=>1,
        'stream_context' => stream_context_create(array(
                'http' => array(
                    'header' => 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
            )
        )
    )
);

$response = $request-> foggylineSliderSlideRepositoryV1Save($slide);

PHP SoapClient 风格的响应应该是如下所示的 stdClass PHP 对象:

object(stdClass)#2 (1) {
  ["result"]=>
  object(stdClass)#3 (2) {
    ["slideId"]=>
    int(2)
    ["title"]=>
    string(15) "API update test"
  }
}

执行 PUT /V1/foggylineSliderSlide/:id 的控制台 SOAP 风格 cURL 如下:

curl -X PUT \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="foggylineSliderSlideRepositoryV1Save"' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d @request.xml \
http://magento2.ce/index.php/soap/default?services= foggylineSliderSlideRepositoryV1

其中 request.xml 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1SaveRequest>
            <slide>
                <slideId>2</slideId>
                <title>API update test</title>
            </slide>
        </ns1:foggylineSliderSlideRepositoryV1SaveRequest>
    </env:Body>
</env:Envelope>

控制台 SOAP 风格 cURL 的响应应该是一个如下所示的 XML:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1SaveResponse>
            <result>
                <slideId>2</slideId>
                <title>API update test</title>
            </result>
        </ns1:foggylineSliderSlideRepositoryV1SaveResponse>
    </env:Body>
</env:Envelope>

deleteById 服务方法调用示例

执行 DELETE /V1/foggylineSliderSlide/:slideId 的控制台 cURL 风格如下:

curl -X DELETE -H 'Content-type: application/json' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
http://magento2.ce/rest/V1/foggylineSliderSlide/3

执行 DELETE /V1/foggylineSliderSlide/:slideId 的 PHP cURL 风格如下:

$slideId = 4;

$ch = curl_init('http://magento2.ce/rest/V1/foggylineSliderSlide/' . $slideId);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
      'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3'
  ));

$result = curl_exec($ch);

控制台和 PHP cURL 风格的响应应该是一个类似于以下 JSON 字符串的字符串:

true

执行 DELETE /V1/foggylineSliderSlide/:slideId 的 PHP SoapClient 风格如下:

$slideId = 2;

$request = new SoapClient(
    'http://magento2.ce/index.php/soap/?wsdl&services= foggylineSliderSlideRepositoryV1',
    array(
        'soap_version' => SOAP_1_2,
        'trace'=>1,
        'stream_context' => stream_context_create(array(
                'http' => array(
                    'header' => 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3')
            )
        )
    )
);

$response = $request-> foggylineSliderSlideRepositoryV1DeleteById(array('slideId'=> $slideId));

PHP SoapClient 风格的响应应该是类似于以下 stdClass PHP 对象:

object(stdClass)#2 (1) {
  ["result"]=>
  bool(true)
}

执行 DELETE /V1/foggylineSliderSlide/:slideId 的控制台 SOAP 风格 cURL 如下:

curl -X POST \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="foggylineSliderSlideRepositoryV1DeleteById"' \
-H 'Authorization: Bearer pk8h93nq9cevaw55bohkjbp0o7kpl4d3' \
-d @request.xml \
http://magento2.ce/index.php/soap/default?services= foggylineSliderSlideRepositoryV1

其中 request.xml 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1DeleteByIdRequest>
            <slideId>5</slideId>
        </ns1:foggylineSliderSlideRepositoryV1DeleteByIdRequest>
    </env:Body>
</env:Envelope>

控制台 SOAP 风格 cURL 的响应应该是一个类似于以下 XML 的字符串:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope  >
    <env:Body>
        <ns1:foggylineSliderSlideRepositoryV1DeleteByIdResponse>
            <result>true</result>
        </ns1:foggylineSliderSlideRepositoryV1DeleteByIdResponse>
    </env:Body>
</env:Envelope>

上述 API 调用示例涵盖了我们对 Slide 实体自定义定义的所有 API。

回顾一下 $searchCriteria 变量,我们使用了 GET 类型的 HTTP 方法,将整个变量作为查询字符串传递。如果我们这样考虑,我们可以在 Web API 资源定义期间指定 POST,并将 $searchCriteria 变量的内容打包到请求体中。尽管 GET 方法的方法可能看起来有点脏,但想象一下如果我们为资源分配了匿名或自角色:我们就可以简单地打开一个长 URL 并获取搜索结果。考虑一个可能的小部件用途,其中小部件会简单地向 URL 发送 AJAX 请求并获取访客或客户的搜索结果。

完整的模块源代码可以在以下位置找到:github.com/ajzele/B05032-Foggyline_Slider。除了 Slide 实体外,完整的模块代码还包括 Image 实体。由于每个幻灯片可以包含多个图片,我们可以进一步测试类似于前面调用的 Image API 调用。

列表过滤的搜索条件接口

了解如何进行适当的列表过滤以获取匹配特定查找条件的实体对于有效使用核心 Magento 的 getList 服务以及可能的自定义编码 API 是至关重要的。例如,获取过去 24 小时内注册的客户列表以获取最新添加的产品。

让我们回顾一下 app/code/Foggyline/Slider/etc/webapi.xml 文件,其中我们定义了服务 method="getList"。服务类定义为 Foggyline\Slider\Api\SlideRepositoryInterface,它被定义为 Foggyline\Slider\Model\SlideRepository 类的偏好。最后,在 SlideRepository 类中,我们有实际的 getList 方法。getList 方法定义如下:

getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria);

我们可以看到,getList 方法只接受一个参数,即符合 SearchCriteriaInterface 的对象实例,称为 $searchCriteria

这意味着我们已经有以下类型的(不完整)JSON 对象来传递给 getList 方法:

{
  "search_criteria": {
  }
}

为了进一步理解search_criteria的内部工作原理,我们需要了解SearchCriteriaInterface,它部分定义如下:

interface SearchCriteriaInterface
{
    /* @param \Magento\Framework\Api\Search\FilterGroup[] $filterGroups */
    public function setFilterGroups(array $filterGroups = null);

    /* @param \Magento\Framework\Api\SortOrder[] $sortOrders */
    public function setSortOrders(array $sortOrders = null);

    /* @param int $pageSize */
    public function setPageSize($pageSize);

    /* @param int $currentPage */
    public function setCurrentPage($currentPage);
}

每个接口的获取器和设置器方法都期望在传递的 API 参数中找到值。这意味着getPageSize()setPageSize()方法会期望search_criteria有一个整型page_size属性。同样,getFilterGroups()setFilterGroups()方法会期望传递给它们的search_criteria有一个\Magento\Framework\Api\Search\FilterGroup数组。这些见解使我们达到了传递给getList方法的以下(不完整)JSON 对象类型:

{
  "search_criteria": {
    "filter_groups": [
    ],
    "current_page": 1,
    "page_size": 10,
    "sort_orders": [
    ]
  }
}

现在我们已经到了需要确定filter_groupssort_orders包含什么内容的时候了,因为这些不是简单类型,而是复合值。

进一步查看\Magento\Framework\Api\Search\FilterGroup,我们看到getFilters()setFilters()方法的定义,这些方法与\Magento\Framework\Api\Filter对象数组一起工作。这意味着filter_groups有一个属性过滤器,它是一个由\Magento\Framework\Api\Filter定义的单独过滤器对象的数组。考虑到这一点,我们现在来看search_criteria JSON 对象的以下形式:

{
  "search_criteria": {
    "filter_groups": [
      {
        "filters": [
        ]
      }
    ],
    "current_page": 1,
    "page_size": 10,
    "sort_orders": [
    ]
  }
}

进一步查看单个\Magento\Framework\Api\Filter,通过其获取器和设置器定义,我们可以得出fieldvaluecondition_type等属性。这使我们进一步接近最终确定我们的search_criteria JSON 对象,现在它的结构如下:

{
  "search_criteria": {
    "filter_groups": [
      {
        "filters": [
          {
            "field": "title",
            "value": "%some%",
            "condition_type": "like"
          }
        ]
      }
    ],
    "current_page": 1,
    "page_size": 10,
    "sort_orders": [
    ]
  }
}

让我们来看看最后的sort_orderssort_orders\Magento\Framework\Api\SortOrder类型,它为字段和方向属性提供了获取器和设置器。了解这一点后,我们能够完全构建我们的search_criteria JSON 对象(或数组),这是我们传递给getList()服务方法调用的,如下所示:

{
  "search_criteria": {
    "filter_groups": [
      {
        "filters": [
          {
            "field": "title",
            "value": "%some%",
            "condition_type": "like"
          }
        ]
      }
    ],
    "current_page": 1,
    "page_size": 10,
    "sort_orders": [
      {
        "field": "slide_id",
        "direction": -1
      }
    ]
  }
}

当我们在filter_groupsfilterssort_orders下定义多个条目时会发生什么?逻辑预期是,当它们到达数据库时,这些会分解成ANDOR操作符。令人惊讶的是,这并不总是情况,至少在我们的先前列表中不是这样。由于getList方法的实际实现留给我们处理,我们可以决定我们想要如何处理过滤器组和过滤器。

回顾我们的getList方法,如以下部分所示,我们没有做任何暗示使用OR操作符的事情,所以所有内容最终都在数据库上以AND条件结束:

foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
    foreach ($filterGroup->getFilters() as $filter) {
        $condition = $filter->getConditionType() ?: 'eq';
        $collection->addFieldToFilter($filter->getField(), [$condition => $filter->getValue()]);
  }
}

之前的代码只是简单地遍历所有过滤器组,将组内的所有过滤器拉入,并对所有内容调用相同的 addFieldToFilter 方法。类似的行为在核心 Magento 模块中得到了实现。尽管过滤本身遵循 \Magento\Framework\Api\SearchCriteriaInterface 接口,但在 Magento 范围内没有统一的处理方法来强制执行过滤中的 ANDOR 操作符。

然而,像 GET 产品这样的 Magento 核心 API 确实实现了 ANDOR 条件。在这些情况下,过滤器组导致 OR 条件,而组内的过滤器导致 AND 条件。

小贴士

遵循最佳实践,我们应该确保我们的模块在实现搜索条件时,尊重 filter_groups/filtersOR/AND 的关系。

摘要

在本章中,我们涵盖了与 Magento API 相关的许多内容。还有很多话要说,但这里概述的步骤应该足以让我们开始使用更高级的 API。我们以了解用户类型和支持的认证方法开始本章。我们特别强调了进行多种类型的 API 调用,如控制台 cURL、PHP cURL、PHP SoapClient 和控制台 cURL SOAP。这是为了鼓励开发者更深入地理解 API 调用的内部工作原理,而不仅仅是使用高级库。

在下一章中,我们将探讨 Magento 的一些主要部分。

第十章。主要功能区域

Magento 平台包含各种模块,提供各种功能。开发者通常更熟悉某一组功能而不是其他功能。一些最常用的功能示例包括与 CMS 块和页面、分类、产品、客户、导入、自定义产品类型、自定义支付和运输模块相关的功能。这并不是说其他功能就不重要。在本章中,我们将快速查看 Magento 管理区域、PHP 代码和 API 调用中的功能。本章分为以下部分:

  • CMS 管理

  • 目录管理

  • 客户管理

  • 产品和客户导入

  • 自定义产品类型

  • 自定义离线运输方法

  • 自定义离线支付方法

目的不是深入了解每个功能区域,而是展示管理界面以及相应的程序性和 API 方法,以实现基本管理。

CMS 管理

内容是帮助区分一个商店与另一个商店的因素。优质内容可以提高商店在搜索引擎中的可见性,为购买产品的客户提供信息洞察,并提供信誉和信任。Magento 提供了一个强大的内容管理系统,可用于为商店创建丰富内容。我们还可以用它来管理块和页面。

手动管理块

一个 CMS 块是内容的一个小型模块化单元,可以在页面的几乎任何位置定位。它们甚至可以被调用到另一个块中。块支持 HTML 和 JavaScript 作为其内容。因此,它们能够显示静态信息,如文本、图像和嵌入的视频,以及动态信息。

块可以通过管理界面、API 或代码创建。

以下步骤概述了从管理界面内创建块的过程:

  1. 登录到 Magento 管理区域。

  2. 内容 | 元素 | 菜单中,点击添加新块。这将打开一个类似于以下截图的屏幕:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00070.jpeg

  3. 填写所需字段的值(块标题标识符商店视图状态内容)并点击保存块按钮。

保存块后,您将在浏览器中看到您已保存块的成功消息。CMS 块存储在数据库中的cms_blockcms_block_store表中。

标识符值可能是这里最有趣的部分。我们可以在 CMS 页面、另一个 CMS 块或某些代码中使用它来获取我们刚刚创建的块。

假设我们已经创建了一个具有标识符值为foggyline_hello的块,我们可以通过以下表达式在 CMS 页面或另一个块中调用它:

{{widget type="Magento\\Cms\\Block\\Widget\\Block" template="widget/static_block/default.phtml" block_id="foggyline_hello"}}

我们也可以将块的实际整数 ID 值传递给前面的表达式,如下所示:

{{widget type="Magento\\Cms\\Block\\Widget\\Block" template="widget/static_block/default.phtml" block_id="2"}}

然而,这种方法要求我们知道块的实际整数 ID。

前面的表达式表明,块通过小部件(也称为前端应用)包含在页面或另一个块中。Magento\Cms\Block\Widget\Block类类型的小部件正在使用widget/static_block/default.phtml模板文件来渲染实际的 CMS 块。

通过代码管理块

除了通过管理界面手动创建块之外,我们还可以使用代码创建 CMS 块,如下面的代码片段所示:

$model = $this->_objectManager->create('Magento\Cms\Model\Block');
$model->setTitle('Test block');
$model->setIdentifier('test_block');
$model->setContent('Test block!');
$model->setIsActive(true);
$model->save();

在这里,我们使用了实例管理器来创建Magento\Cms\Model\Block类的新模型实例。然后,我们通过定义的方法设置了一些属性,最后调用了save方法。

我们可以使用类似于以下代码的代码片段加载和更新现有块:

$model = $this->_objectManager->create('Magento\Cms\Model\Block');
//$model->load(3);
$model->load('test_block');
$model->setTitle('Updated Test block');
$model->setStores([0]);
$model->save();

块的load方法接受一个整数块 ID 或一个字符串块标识符。

最后,我们可以通过可用的 API 方法来管理块创建和更新的操作。以下代码片段显示了如何通过控制台 cURL REST API 调用创建 CMS 块:

curl -X POST "http://magento2.ce/index.php/rest/V1/cmsBlock" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8" \
 -d '{"block": {"identifier": "test_api_block", "title": "Test API Block", "content": "API Block Content"}}'

携带者字符串只是一个登录令牌,我们通过首先运行前面章节中描述的认证 API 调用来获取。一旦我们有了认证令牌,我们就可以发送一个V1/cmsBlock POST请求,传递一个 JSON 对象作为数据。

通过 API 管理块

我们可以通过执行类似于以下代码的代码片段通过 API 获取新创建的 CMS 块:

curl -X GET "http://magento2.ce/index.php/rest/V1/cmsBlock/4" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8"

我们可以通过使用 API 并执行类似于以下代码的代码片段来更新现有的 CMS 块:

curl -X PUT "http://magento2.ce/index.php/rest/V1/cmsBlock/4" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8" \
 -d '{"block": {"title": "Updated Test API Block"}}'

在这里,我们使用了 HTTP PUT 方法,并将整数4作为V1/cmsBlock/4 URL 的一部分传递。数字 4 代表数据库中块的 ID 值。

手动管理页面

CMS 页面是健壮的内容单元,与简单地嵌入到某些页面中的 CMS 块不同。CMS 页面可以有自己的 URL。CMS 页面的例子包括404 未找到主页启用 Cookies隐私和 Cookies 政策。在处理 CMS 页面时,我们的想法是我们可以控制页面内容区域,而不会影响网站范围的元素,如页眉、页脚或侧边栏。除了前面列出的之外,Magento 并没有提供很多开箱即用的 CMS 页面。

与块一样,页面也可以通过管理界面、API 或代码创建。

以下步骤概述了从管理界面内部创建页面的过程:

  1. 登录到 Magento 管理区域。

  2. 内容 | 元素 | 页面菜单中,点击添加新页面。这将打开一个与以下截图类似的屏幕:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00071.jpeg

  3. 填写所需字段的值(页面标题商店视图状态内容)并点击保存块按钮。

页面保存后,你将在浏览器中看到你已保存此页面的成功消息。CMS 页面存储在数据库中的 cms_pagecms_page_store 表中。

假设我们已经创建了一个页面标题值为信息的页面,我们可以通过类似 http://magento2.ce/info 的 URL 在浏览器中访问此页面。尽管我们可能需要在新页面编辑屏幕中指定URL 键值,但 Magento 会自动分配与页面标题匹配的URL 键

通过代码管理页面

除了通过管理界面手动创建之外,我们还可以通过以下代码片段创建 CMS 页面:

$model = $this->_objectManager->create('Magento\Cms\Model\Page');
$model->setTitle('Test page');
$model->setIdentifier('test-page');
$model->setPageLayout('1column');
$model->setContent('Test page!');
$model->setIsActive(true);
$model->setStores([0]);
$model->save();

在这里,我们使用了实例管理器来创建 Magento\Cms\Model\Page 类的新模型实例。然后,我们通过定义的方法设置了一些属性,并最终调用了 save 方法。通过管理界面设置的URL 键实际上是通过 setIdentifier 方法调用设置的标识符。

通过 API 管理页面

我们可以使用类似于以下代码段的代码片段来加载和更新现有的页面:

$model = $this->_objectManager->create('Magento\Cms\Model\Page');
//$model->load(6);
$model->load('test-page');
$model->setContent('Updated Test page!');
$model->save();

页面模型 load 方法接受页面标识符(URL 键)的整数 ID 值。

最后,我们可以通过可用的 API 方法来管理页面的创建和更新。以下代码片段显示了如何通过控制台 cURL REST API 调用来创建 CMS 页面:

curl -X POST "http://magento2.ce/index.php/rest/V1/cmsPage" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8" \
 -d '{"page": {"identifier": "test-api-page", "title": "Test API Page", "content": "API Block Content"}}'

 similar to the following one:
curl -X GET "http://magento2.ce/index.php/rest/V1/cmsPage/7" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8"

我们可以通过执行类似于以下代码段的代码来通过 API 更新现有的 CMS 页面:

curl -X PUT "http://magento2.ce/index.php/rest/V1/cmsPage/7" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8" \
 -d '{"page": {"content": "Updated Test API Page", "identifier":"updated-page"}}'

在这里,我们使用了 HTTP PUT 方法,将整数 7 作为 V1/cmsPage/7 URL 的一部分传递。数字 7 代表数据库中页面的 ID 值。

目录管理

Magento_Catalog 模块是整个 Magento 平台的骨架之一。它为各种产品类型的库存管理提供强大的支持。该模块负责管理产品、类别及其属性、前端显示以及许多其他事情。

手动管理类别

我们可以通过导航到产品 | 库存 | 目录产品 | 库存 | 类别来访问 Magento 管理区域内的目录功能。

如果我们从空白的 Magento 安装开始,我们可能会首先创建类别作为要创建的第一个实体之一。我们可以通过以下步骤手动创建类别:

  1. 登录到 Magento 管理区域。

  2. 前往产品 | 库存 | 类别菜单。这将打开一个类似于以下截图的屏幕:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00072.jpeg

  3. 在屏幕的左侧点击默认类别。然后,当页面重新加载时,点击添加子类别按钮。

  4. 虽然看起来好像没有发生任何变化,因为屏幕内容没有改变,但现在我们应该在一般信息选项卡中填写所需的选项,将名称设置为某个字符串值,并将是否激活设置为是。

  5. 最后,点击保存类别按钮。

新类别现在应该已创建。在左侧屏幕区域,如果您点击新创建的类别的名称,您将在一般信息选项卡上方看到其 ID 值,如图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00073.jpeg

注意

知道类别 ID 后,您可以直接在浏览器中打开一个类似于http://magento2.ce/index.php/catalog/category/view/id/3的 URL 来测试它,其中数字3是类别的 ID。您将看到一个加载的类别页面,可能显示找不到与选择匹配的产品的消息,这是好的,因为我们还没有将产品分配给类别。

虽然我们不会深入探讨其细节,但值得注意的是,我们在这里只是触及了表面,因为类别使我们能够通过显示设置自定义设计选项卡提供许多额外的选项。

由于类别是 EAV 实体,它们的数据存储在数据库的多个表中,如下所示:

  • catalog_category_entity

  • catalog_category_entity_datetime

  • catalog_category_entity_decimal

  • catalog_category_entity_int

  • catalog_category_entity_text

  • catalog_category_entity_varchar

有几个额外的表将类别链接到产品:

  • catalog_category_product

  • catalog_category_product_index

  • catalog_category_product_index_tmp

  • catalog_url_rewrite_product_category

通过代码管理类别

除了通过管理界面手动创建之外,我们还可以通过以下代码片段所示的方式通过代码创建类别:

$parentId = \Magento\Catalog\Model\Category::TREE_ROOT_ID;

$parentCategory = $this->_objectManager
                       ->create('Magento\Catalog\Model\Category')
                       ->load($parentId);

$category = $this->_objectManager
                ->create('Magento\Catalog\Model\Category');

$category->setPath($parentCategory->getPath());
$category->setParentId($parentId);
$category->setName('Test');
$category->setIsActive(true);

$category->save();

这里特别的是,在创建新类别时,我们首先创建了一个$parentCategory实例,它代表根类别对象。我们使用Category模型的TREE_ROOT_ID常量作为父类别 ID 的 ID 值。然后,我们创建了一个类别实例,设置了其pathparent_idnameis_active值。

通过 API 管理类别

我们可以通过可用的 API 方法进一步管理类别创建。以下代码片段显示了通过控制台 cURL REST API 调用创建类别:

curl -X POST "http://magento2.ce/index.php/rest/V1/categories" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8" \
 -d '{"category": {"parent_id": "1", "name": "Test API Category", "is_active": true}}'

承载字符串只是一个登录令牌,我们通过首先运行上一章中描述的认证 API 调用来获取它。一旦我们有了认证令牌,我们就可以发出一个/V1/categories POST请求,传递一个 JSON 对象作为数据。

我们可以通过执行以下类似代码片段的代码片段,通过 API 获取新创建的类别作为一个 JSON 对象:

curl -X GET "http://magento2.ce/index.php/rest/V1/categories/9" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8"

手动管理产品

现在,让我们看看如何创建一个新的产品。我们可以通过以下步骤手动创建产品:

  1. 登录到 Magento 管理区域。

  2. 产品 | 库存 | 目录菜单中,点击添加产品按钮。这将打开一个类似于以下截图的屏幕:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00074.jpeg

  3. 现在,在产品详情选项卡中填写所需的选项。

  4. 最后,点击保存按钮。

如果保存成功,页面将重新加载并显示您已保存产品的消息。

与类别一样,我们在这里只是触及了产品的表面。查看其他可用的选项卡,有许多其他选项可以分配给产品。只需分配所需的选项就足以让我们在商店的前端 URL(如http://magento2.ce/index.php/catalog/product/view/id/4)上看到产品,其中数字4是产品的 ID 值。

产品也是 EAV 实体,其数据存储在数据库的多个表中,如下所示:

  • catalog_product_entity

  • catalog_product_entity_datetime

  • catalog_product_entity_decimal

  • catalog_product_entity_gallery

  • catalog_product_entity_group_price

  • catalog_product_entity_int

  • catalog_product_entity_media_gallery

  • catalog_product_entity_media_gallery_value

  • catalog_product_entity_text

  • catalog_product_entity_tier_price

  • catalog_product_entity_varchar

还有大量其他引用产品的表,例如catalog_product_bundle_selection,但这些主要用于链接功能片段。

通过代码管理产品

除了通过管理界面手动创建外,我们还可以通过代码创建产品,如下面的代码片段所示:

$catalogConfig = $this->_objectManager
    ->create('Magento\Catalog\Model\Config');

$attributeSetId = $catalogConfig->getAttributeSetId(4, 'Default');

$product = $this->_objectManager
    ->create('Magento\Catalog\Model\Product');

$product
    ->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
    ->setAttributeSetId($attributeSetId)
    ->setWebsiteIds([$this->storeManager->getWebsite()->getId()])
    ->setStatus(\Magento\Catalog\Model\Product\Attribute \Source\Status::STATUS_ENABLED)
    ->setStockData(['is_in_stock' => 1, 'manage_stock' => 0])
    ->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID)
    ->setVisibility(\Magento\Catalog\Model\Product \Visibility::VISIBILITY_BOTH);

$product
    ->setName('Test API')
    ->setSku('tets-api')
    ->setPrice(19.99);

$product->save();

通过 API 管理产品

以下示例使用 REST API 创建一个新的简单产品:

curl -X POST "http://magento2.ce/index.php/rest/V1/products" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8" \
 -d '{"product":{"sku":"test_api_1","name":"Test API #1","attribute_set_id":4,"price":19.99,"status":1, "visibility":4,"type_id":"simple","weight":1}}'

应该通过使用身份验证请求预先获取Bearer令牌。响应应该是一个包含所有公开产品数据的 JSON 对象。

我们可以通过执行以下代码片段的 API 获取现有产品信息:

curl -X GET "http://magento2.ce/index.php/rest/V1/products /product_dynamic_125" \
 -H "Content-Type:application/json"

在前面的 URL 中,product_dynamic_125部分代表这个特定的产品 SKU 值。响应是一个包含所有公开产品数据的 JSON 对象。

可用的整个目录 API 列表可以在vendor/magento/module-catalog/etc/webapi.xml文件中查看。

客户管理

管理客户是 Magento 平台的重要方面之一。大多数情况下,客户创建是由新客户自己完成的。访问商店的新客户启动注册过程,并最终创建客户账户。一旦注册,客户就可以在我的账户页面上进一步编辑他们的账户详情,该页面通常在类似http://magento2.ce/index.php/customer/account/index/的链接上可用。

作为本节的一部分,我们感兴趣的是通过使用管理区域、代码和 API 来管理客户账户的可能性。

手动管理客户

以下步骤概述了从管理界面内部创建客户账户的过程:

  1. 登录到 Magento 管理区域。

  2. 客户 | 所有客户菜单中,点击添加新客户按钮。这将打开一个类似于以下截图的屏幕:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00075.jpeg

  3. 填写所需字段的值(关联到网站电子邮件)并点击保存客户按钮。

一旦客户被保存,你将在浏览器中看到您已保存客户的成功消息。

对于此类情况,关联到网站值可能是最重要的值,在这种情况下,客户账户是由非客户用户间接创建的。

注意

由于 Magento 支持设置多个网站,客户账户可以根据商店 | 设置 | 配置 | 客户 | 客户配置 | 账户共享选项 | 共享客户账户选项设置为全局按网站。因此,如果共享客户账户选项已设置为按网站,将关联到网站值指向正确的网站至关重要。否则,将创建客户账户,但客户将无法在店面登录。

Magento_Customer模块使用 EAV 结构来存储客户数据。因此,没有单个表存储客户信息。相反,根据客户属性及其数据类型存在多个表。

以下列表包含存储客户实体的表:

  • customer_entity

  • customer_entity_datetime

  • customer_entity_decimal

  • customer_entity_int

  • customer_entity_text

  • customer_entity_varchar

没有客户地址的客户账户将不会真正完整。地址可以通过在管理区域客户编辑屏幕下的地址选项卡添加,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00076.jpeg

注意,Magento 允许我们设置其中一个地址为默认发货地址默认账单地址

与客户实体类似,客户地址实体也使用 EAV 结构来存储其数据。

以下列表包含存储客户地址实体的表:

  • customer_address_entity

  • customer_address_entity_datetime

  • customer_address_entity_decimal

  • customer_address_entity_int

  • customer_address_entity_text

  • customer_address_entity_varchar

通过代码管理客户

除了通过管理界面手动创建之外,我们还可以通过以下代码片段创建客户:

$model = $this->_objectManager-> create('Magento\Customer\Model\Customer');
$model->setWebsiteId(1);
$model->setGroupId(1);
$model->setFirstname('John');
$model->setLastname('Doe');
$model->setEmail('john.doe@mail.com');
$model->save();

在这里,我们使用实例管理器来创建Magento\Customer\Model\Customer类的新模型实例。然后我们可以通过定义的方法设置一些属性,并最终调用save方法。

我们可以使用类似于以下代码片段的代码片段来加载和更新现有客户:

$model = $this->_objectManager-> create('Magento\Customer\Model\Customer');
$model->setWebsiteId(1);
//$model->loadByEmail('john.doe@mail.com');
$model->load(1);
$model->setFirstname('Updated John');
$model->save();

我们可以使用loadloadByEmail方法调用。load方法接受现有客户实体的整数 ID 值,而loadByEmail接受一个字符串电子邮件地址。值得注意的是,必须在任何加载方法之前调用setWebsiteId。否则,我们将收到一个错误消息,指出在使用网站范围时必须指定客户网站 ID

通过 API 管理客户

最后,我们可以使用可用的 API 方法来管理客户信息的创建和更新。以下代码片段显示了如何通过控制台 cURL REST API 调用创建客户:

curl -X POST "http://magento2.ce/index.php/rest/V1/customers" \
 -H "Content-Type:application/json" \
     -H "Authorization: Bearer r9ok12c3wsusrxqomyxiwo0v7etujw9h" \
 -d '{"customer": {"website_id": 1, "group_id": 1, "firstname": "John", "lastname": "Doe", "email": "john.doe@mail.com"}, "password":"abc123"}'

一旦我们有了认证令牌,我们就可以发送一个V1/customers POST请求,传递一个 JSON 对象作为数据。

我们可以通过执行一个类似于以下代码片段的代码,通过 API 获取新创建的客户:

curl -X GET "http://magento2.ce/index.php/rest/V1/customers/24" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8"

我们可以通过执行类似于以下代码片段的代码片段通过 API 更新现有客户:

curl -X PUT "http://magento2.ce/index.php/rest/V1/customers/24" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer r9ok12c3wsusrxqomyxiwo0v7etujw9h" \
 -d '{"customer": {"id":24, "website_id": 1, "firstname": "John Updated", "lastname": "Doe", "email": "john2@mail.com"}, "password_hash":"cda57c7995e5f03fe07ad52d99686ba130e0d3e fe0d84dd5ee9fe7f6ea632650:cEf8i1f1ZXT1L2NwawTRNEqDWGyru6h3:1"}'

在这里,我们使用了 HTTP PUT 方法,将整数24作为V1/customers/24的一部分以及作为 URL 的主体部分。数字 24 代表数据库中客户的 ID 值。此外,请注意password_hash值;如果没有它,更新将失败。

通过代码管理客户地址

与客户类似,我们可以使用代码创建客户地址,如下面的代码片段所示:

$model = $this->_objectManager-> create('Magento\Customer\Model\Address');
//$model->setCustomer($customer);
$model->setCustomerId(24);
$model->setFirstname('John');
$model->setLastname('Doe');
$model->setCompany('Foggyline');
$model->setStreet('Test street');
$model->setCity('London');
$model->setCountryId('GB');
$model->setPostcode('GU22 7PY');
$model->setTelephone('112233445566');
$model->setIsDefaultBilling(true);
$model->setIsDefaultShipping(true);
$model->save();

在这里,我们使用了实例管理器创建Magento\Customer\Model\Address类的新模型实例。然后,我们通过定义的方法设置一些属性,并最终调用save方法。

我们可以通过类似于以下代码片段的代码片段加载和更新现有的客户地址:

$model = $this->_objectManager-> create('Magento\Customer\Model\Address');
$model->load(22);
$model->setCity('Update London');
$model->save();

在这里,我们使用了load方法通过其 ID 值加载现有的地址。然后,我们调用setCity方法并传递更新的字符串。在执行save方法后,地址应该反映这一变化。

通过 API 管理客户地址

令人惊讶的是,客户地址不能通过 API 调用直接创建或更新,因为没有定义POSTPUT REST API。然而,我们仍然可以通过以下方式使用 API 获取现有的客户地址信息:

curl -X GET "http://magento2.ce/index.php/rest/V1/customers /addresses/22" \
 -H "Content-Type:application/json" \
 -H "Authorization: Bearer lcpnsrk4t6al83lymhfs86jabbi9mmt8"

可用的所有客户 API 列表可以在vendor/magento/module-customer/etc/webapi.xml文件中看到。

产品和客户导入

Magento 通过以下模块提供开箱即用的批量导入和导出功能:

  • AdvancedPricingImportExport

  • BundleImportExport

  • CatalogImportExport

  • ConfigurableImportExport

  • CustomerImportExport

  • GroupedImportExport

  • ImportExport

  • TaxImportExport

导入功能的核心实际上在于ImportExport模块,而其他模块通过vendor/magento/module-{partialModuleName}-import-export/etc/import.xmlvendor/magento/module-{partialModuleName}-import-export/etc/export.xml文件提供单独的导入和导出实体。

这些功能可以从 Magento 管理区域中的系统 | 数据传输菜单访问。它们使我们能够导出和导入多个实体类型,例如高级定价产品主客户文件客户地址

以下截图显示了导入设置屏幕的实体类型选项:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00077.jpeg

导入设置旁边,当我们为导入选择实体类型时,会出现导入行为部分,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00078.jpeg

大多数实体类型都有类似的导入行为选项。大多数时候,我们将对添加/更新行为感兴趣。

由于导入过程比导出过程复杂一些,我们将重点关注导入和 CSV 文件格式。更具体地说,我们的重点是产品主客户文件客户地址的导入。

当与干净的 Magento 安装一起工作时,以下列在产品导入期间是必需的,以便在之后使产品在店面可见:

  • sku(例如,“test-sku”):这可以具有几乎任何值,只要它在 Magento 中是唯一的。

  • attribute_set_code(例如,“默认”):这可以具有在执行SELECT DISTINCT attribute_set_name FROM eav_attribute_set;查询时在数据库中找到的任何值。

  • product_type(例如,“简单”):这可以具有simpleconfigurablegroupedvirtualbundledownloadable的值。此外,如果我们创建或安装了一个添加新产品类型的第三方模块,我们也可以使用该模块。

  • categories(例如,“根/鞋子”):使用“根类别名称/子类别名称/子子类别名称”语法创建完整的类别路径。如果有多个类别,则使用竖线(“|”)分隔它们。例如,“根类别名称/子类别名称/子子类别名称| 根类别名称/子 _2 类别名称”。

  • product_websites(例如,“基础”):这可以具有在执行SELECT DISTINCT code FROM store_website;查询时在数据库中找到的值。

  • name(例如,“测试”):这可以具有几乎任何值。

  • product_online(例如,1):这可以是1表示“可见”或0表示“不可见”。

  • visibility(例如,“目录,搜索”):这可以具有“单独不可见”、“目录”、“搜索”或“目录,搜索”的值。

  • price(例如,“9.99”):这可以是一个整数或小数值。

  • qty(例如,“100”):这可以是一个整数或小数值。

虽然产品将仅通过包含一组列的前列列表导入,但我们通常希望为它们分配额外的信息,例如描述和图片。我们可以通过以下列来实现:

  • description(例如,“描述”):这可以包含任何字符串值。支持 HTML 和 JavaScript。

  • short_description(例如,“简短描述”):这可以包含任何字符串值。支持 HTML 和 JavaScript。

  • base_image(例如,butterfly.jpg):这是最终的导入图像名称。

  • small_image(例如,galaxy.jpg

  • thumbnail_image(例如,serenity.jpg

关于图像的导入,只要在导入期间设置了图像文件目录路径,我们只需要提供最终的图像名称。我们可以使用相对路径,例如 Magento 安装的var/exportvar/importvar/export/some/dir

导入完成后,建议通过控制台运行php bin/magento indexer:reindex命令。否则,直到运行索引器之前,产品在店面上将不可见。

重新索引完成后,我们可以尝试打开店面 URL,它看起来像http://magento2.ce/index.php/catalog/product/view/id/1。在这种情况下,数字1是新导入的产品 ID。

当与干净的 Magento 安装一起工作时,在客户主要文件导入期间需要以下列出的列,以便我们的客户之后能够成功登录到店面:

  • email(例如,<john.doe@fake.mail>):作为字符串值的电子邮件地址

  • _website(例如,基础):这可以包含在执行SELECT DISTINCT code FROM store_website;查询时在数据库中找到的任何值

  • firstname(例如,约翰):一个字符串值

  • lastname(例如,多):一个字符串值

  • group_id(例如,1):这可以包含在执行SELECT customer_group_id code FROM customer_group WHERE customer_group_id != 0;查询时在数据库中找到的任何值

尽管客户只需使用之前列出的列集就能登录到店面,但我们通常希望分配其他相关的信息。我们可以通过以下列来实现:

  • gender(例如,男性):这可以是男性或女性

  • taxvat(例如,HR33311122299):任何有效的增值税号,尽管导入将接受无效的增值税号

  • dob(例如,1983-01-16):出生日期

  • prefix(例如,先生):任何字符串值

  • middlename(例如,开发人员):任何字符串值

  • suffix(例如,工程师):任何字符串值

  • password(例如,123abc):任何长度至少为 6 个字符的字符串值,如通过\Magento\CustomerImportExport\Model\Import\Customer::MIN_PASSWORD_LENGTH定义。

我们需要特别注意password列。这是一个明文密码。因此,我们需要小心不要以非安全的方式分发 CSV 文件。理想情况下,我们可以提供password_hash列而不是password。然而,password_hash列下的条目需要通过Magento\Customer\Model\Customer类中的hashPassword方法调用的相同算法进行哈希处理。这进一步在Magento\Framework\Encryption\Encryptor类的一个实例上调用getHash方法,最终解析为md5sha256算法。

当与干净的 Magento 安装一起工作时,在客户地址导入期间需要以下列,以便我们的客户之后能够成功地在店面使用地址:

  • _website (例如,base):这可以具有在执行SELECT DISTINCT code FROM store_website;查询时在数据库中找到的任何值

  • _email (例如,<john@change.me>):作为字符串值的电子邮件地址

  • _entity_id

  • firstname (例如,约翰):任何字符串值

  • lastname (例如,多伊):任何字符串值

  • street (例如,阿什顿巷):任何字符串值

  • city (例如,奥斯汀):任何字符串值

  • telephone (例如,00 385 91 111 000):任何字符串值

  • country_id (例如,GB):ISO-2 格式的国家代码

  • postcode (例如,TX 78753):任何字符串值

尽管客户只需列出的一组列就可以在店面使用地址,但我们通常希望分配其他相关的信息。我们可以通过以下列来实现:

  • region (例如,加利福尼亚):这可以是空白、自由形式的字符串,或者与在执行SELECT DISTINCT default_name FROM directory_country_region;查询时在数据库中找到的任何值匹配的特定字符串。当运行SELECT DISTINCT country_id FROM directory_country_region;时,显示 13 个不同的国家代码,这些代码在directory_country_region表中都有条目——ATBRCACHDEEEESFIFRLTLVROUS。这意味着具有该代码的国家需要分配一个适当的地区名称。

  • company (例如,雾线):这可以是任何字符串值。

  • fax (例如,00 385 91 111 000):这可以是任何字符串值。

  • middlename (例如,开发者):这可以是任何字符串值。

  • prefix (例如,先生):这可以是任何字符串值。

  • suffix (例如,工程师):这可以是任何字符串值。

  • vat_id (例如,HR33311122299):这可以是任何有效的增值税号,尽管导入将接受甚至非有效的那些。

  • _address_default_billing_ (例如,“1”):这可以是"1"作为是或"0"作为否,以标记地址为默认账单地址。

  • _address_default_shipping_(例如,“1”):这可以是“1”表示是,或者“0”表示否,以标记该地址为默认的配送地址。

虽然 CSV 导入是一种非常好且相对快速的大规模导入产品、客户及其地址的方法,但它也有一些局限性。CSV 只是平面数据。我们无法对其应用任何逻辑。根据数据有多干净和有效,CSV 导入可能做得很好。否则,我们可能需要选择 API。我们需要记住,CSV 导入比产品和服务器的 API 创建要快得多,因为 CSV 导入直接在数据库上批量插入,而 API 则实例化完整的模型,尊重事件观察者等。

定制产品类型

Magento 提供以下六种开箱即用的产品类型:

  • 简单产品

  • 可配置产品

  • 组合产品

  • 虚拟产品

  • 组合产品

  • 可下载的产品

每个产品都有其特定属性。例如,虚拟和可下载的产品没有 weight 属性。因此,它们被排除在标准配送计算之外。通过围绕内置产品类型进行自定义编码,使用观察器和插件,我们可以实现几乎任何功能。然而,有时这还不够,或者没有解决方案来满足需求。在这种情况下,我们可能需要创建自己的产品类型,以便以更简洁的方式满足项目需求。

让我们创建一个名为 Foggyline_DailyDeal 的小型模块,它将为 Magento 添加一个新的产品类型。

首先,创建一个名为 app/code/Foggyline/DailyDeal/registration.php 的模块注册文件,其中包含以下部分内容:

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Foggyline_DailyDeal',
    __DIR__
);

然后,创建一个包含以下内容的 app/code/Foggyline/DailyDeal/etc/module.xml 文件:

<config  xsi:noNamespaceSchemaLocation="urn:magento:framework:Module /etc/module.xsd">
    <module name="Foggyline_DailyDeal" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>

现在,创建一个包含以下内容的 app/code/Foggyline/DailyDeal/etc/product_types.xml 文件:

<config  xsi:noNamespaceSchemaLocation="urn:magento:module: Magento_Catalog:etc/product_types.xsd">
    <type name="foggylinedailydeal"
          label="Daily Deal"
          modelInstance="Foggyline\DailyDeal\Model\Product\Type \DailyDeal"
          composite="false"
          isQty="true"
          canUseQtyDecimals="false">
        <priceModel instance="Foggyline\DailyDeal\Model \Product\Price"/>
        <indexerModel instance="Foggyline\DailyDeal\Model \ResourceModel\Indexer\Price"/>
        <stockIndexerModel instance="Foggyline\DailyDeal\Model \ResourceModel\Indexer\Stock"/>
        <!-- customAttributes parsed by Magento\Catalog\Model\ProductTypes\Config -->
        <customAttributes>
            <attribute name="is_real_product" value="true"/>
            <attribute name="refundable" value="false"/>
            <attribute name="taxable" value="true"/>
        </customAttributes>
    </type>
</config>

customAttributes 元素由 vendor/magento/module-catalog/Model/ProductTypes/Config.php 解析。

创建一个包含部分内容的 app/code/Foggyline/DailyDeal/Model/Product/Type/DailyDeal.php 文件,如下所示:

namespace Foggyline\DailyDeal\Model\Product\Type;

class DailyDeal extends \Magento\Catalog\Model\Product\Type\AbstractType
{
    const TYPE_DAILY_DEAL = 'foggylinedailydeal';

    public function deleteTypeSpecificData (\Magento\Catalog\Model\Product $product)
    {
        // TODO: Implement deleteTypeSpecificData() method.
    }
}

现在,创建一个包含部分内容的 app/code/Foggyline/DailyDeal/Model/Product/Price.php 文件,如下所示:

namespace Foggyline\DailyDeal\Model\Product;

class Price extends \Magento\Catalog\Model\Product\Type\Price
{

}

完成此操作后,创建一个包含部分内容的 app/code/Foggyline/DailyDeal/Model/ResourceModel/Indexer/Price.php 文件,如下所示:

namespace Foggyline\DailyDeal\Model\ResourceModel\Indexer;

class Price extends \Magento\Catalog\Model\ResourceModel\Product \Indexer\Price\DefaultPrice
{
}

然后,创建一个包含部分内容的 app/code/Foggyline/DailyDeal/Model/ResourceModel/Indexer/Stock.php 文件,如下所示:

namespace Foggyline\DailyDeal\Model\ResourceModel\Indexer;

class Stock extends \Magento\CatalogInventory\Model\ResourceModel \Indexer\Stock\DefaultStock
{

}

最后,创建一个包含部分内容的 app/code/Foggyline/DailyDeal/Setup/InstallData.php 文件,如下所示:

namespace Foggyline\DailyDeal\Setup;

class InstallData implements \Magento\Framework\Setup\InstallDataInterface
{
    private $eavSetupFactory;

    public function __construct(\Magento\Eav\Setup\EavSetupFactory $eavSetupFactory)
    {
        $this->eavSetupFactory = $eavSetupFactory;
    }

    public function install(
        \Magento\Framework\Setup\ModuleDataSetupInterface $setup,
        \Magento\Framework\Setup\ModuleContextInterface $context
    )
    {
        // the "foggylinedailydeal" type specifics
    }
}

通过在 InstallData 类中添加以下 foggylinedailydeal 类型特定内容来扩展 install 方法:

$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$type = \Foggyline\DailyDeal\Model\Product\Type\ DailyDeal::TYPE_DAILY_DEAL;

$fieldList = [
    'price',
    'special_price',
    'special_from_date',
    'special_to_date',
    'minimal_price',
    'cost',
    'tier_price',
    'weight',
];

// make these attributes applicable to foggylinedailydeal products
foreach ($fieldList as $field) {
    $applyTo = explode(
        ',',
        $eavSetup->getAttribute (\Magento\Catalog\Model\Product::ENTITY, $field, 'apply_to')
    );

    if (!in_array($type, $applyTo)) {
        $applyTo[] = $type;
        $eavSetup->updateAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            $field,
            'apply_to',
            implode(',', $applyTo)
        );
    }
}

现在,从控制台运行 php bin/magento setup:upgrade

如果你现在在管理区域打开产品 | 库存 | 目录菜单,并单击添加产品按钮旁边的下拉图标,你将在列表中看到Daily Deal产品类型,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00079.jpeg

在下拉列表中单击Daily Deal产品类型应该会打开产品编辑页面,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00080.jpeg

自定义产品类型编辑屏幕与内置产品类型之一之间没有明显的区别。

假设我们已经将产品命名为Daily Deal Test Product并保存,我们应该能够在店面中看到它,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00081.jpeg

如果我们将产品添加到购物车并执行结账,应该会创建一个订单,就像任何其他产品类型一样。在管理区域中,在订单查看页面上,在已订购项目下,我们应该能够在列表中看到该产品,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00082.jpeg

再次,自定义产品类型与在已订购项目部分下渲染的内置产品类型之间没有明显的区别。

最后,我们应该在控制台上运行php bin/magento indexer:reindex命令。即使我们实际上没有在索引器中实现任何代码,这也只是为了确保现有的索引器没有损坏。

整个模块代码可以从github.com/ajzele/B05032-Foggyline_DailyDeal下载。

自定义离线配送方法

Magento 提供了几种开箱即用的离线配送方式,例如FlatrateFreeshippingPickupTablerate。我们可以在vendor/magento/module-offline-shipping/Model/Carrier目录中看到这些。

然而,项目需求通常是这样的,我们需要一个自定义编码的配送方法,其中应用了特殊业务逻辑。因此,我们可以控制配送价格的计算。在这种情况下,了解如何编写我们自己的离线配送方法可能会很有用。

让我们继续创建一个名为Foggyline_Shipbox的小模块,为 Magento 提供额外的离线配送方法。

首先,创建一个名为app/code/Foggyline/Shipbox/registration.php的模块注册文件,内容如下所示:

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Foggyline_Shipbox',
    __DIR__
);

然后,创建一个包含以下内容的app/code/Foggyline/Shipbox/etc/module.xml文件:

<config  xsi:noNamespaceSchemaLocation="urn:magento:framework:Module /etc/module.xsd">
    <module name="Foggyline_Shipbox" setup_version="1.0.0">
        <sequence>
            <module name="Magento_OfflineShipping"/>
        </sequence>
    </module>
</config>

现在,创建一个包含以下内容的app/code/Foggyline/Shipbox/etc/config.xml文件:

<config  xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store :etc/config.xsd">
    <default>
        <carriers>
            <shipbox>
                <active>0</active>
                <sallowspecific>0</sallowspecific>
                <model> Foggyline\Shipbox\Model\Carrier\Shipbox</model>
                <name>Shipbox</name>
                <price>4.99</price>
                <title>Foggyline Shipbox</title>
                <specificerrmsg>This shipping method is not available. To use this shipping method, please contact us.</specificerrmsg>
            </shipbox>
        </carriers>
    </default>
</config>

完成此操作后,创建一个包含以下内容的app/code/Foggyline/Shipbox/etc/adminhtml/system.xml文件,如下所示:

<config  xsi:noNamespaceSchemaLocation="urn:magento:module: Magento_Config:etc/system_file.xsd">
    <system>
        <section id="carriers">
            <group id="shipbox" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Foggyline Shipbox</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model> Magento\Config\Model\Config\Source\Yesno </source_model>
                </field>
                <field id="name" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Method Name</label>
                </field>
                <field id="price" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Price</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="sallowspecific" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Ship to Applicable Countries</label>
                    <frontend_class>shipping-applicable-country </frontend_class>
                    <source_model> Magento\Shipping\Model\Config\Source \Allspecificcountries </source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="91" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Ship to Specific Countries</label>
                    <source_model> Magento\Directory\Model \Config\Source\Country </source_model>
                    <can_be_empty>1</can_be_empty>
                </field>
            </group>
        </section>
    </system>
</config>

现在,创建一个包含以下内容的app/code/Foggyline/Shipbox/Model/Carrier/Shipbox.php文件:

namespace Foggyline\Shipbox\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;

class Shipbox extends \Magento\Shipping\Model\Carrier\AbstractCarrier
    implements \Magento\Shipping\Model\Carrier\CarrierInterface
{
    protected $_code = 'shipbox';
    protected $_isFixed = true;
    protected $_rateResultFactory;
    protected $_rateMethodFactory;

    public function __construct(
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
        \Magento\Quote\Model\Quote\Address\RateResult \MethodFactory $rateMethodFactory,
        array $data = []
    )
    {
        $this->_rateResultFactory = $rateResultFactory;
        $this->_rateMethodFactory = $rateMethodFactory;
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
    }

    public function collectRates(RateRequest $request)
    {
        //implement business logic
    }

    public function getAllowedMethods()
    {
        return ['shipbox' => $this->getConfigData('name')];
    }
}

Carrier\Shipbox类中扩展collectRates方法,如下所示:

public function collectRates(RateRequest $request)
{
    if (!$this->getConfigFlag('active')) {
        return false;
    }

    //Do some filtering of items in cart
    if ($request->getAllItems()) {
        foreach ($request->getAllItems() as $item) {
            //$item->getQty();
            //$item->getFreeShipping()
            //$item->isShipSeparately()
            //$item->getHasChildren()
            //$item->getProduct()->isVirtual()
            //...
        }
    }

    //After filtering, start forming final price
    //Final price does not have to be fixed like below
    $shippingPrice = $this->getConfigData('price');
    $result = $this->_rateResultFactory->create();

    $method = $this->_rateMethodFactory->create();

    $method->setCarrier('shipbox');
    $method->setCarrierTitle($this->getConfigData('title'));

    $method->setMethod('shipbox');
    $method->setMethodTitle($this->getConfigData('name'));

    $method->setPrice($shippingPrice);
    $method->setCost($shippingPrice);

    $result->append($method);

    return $result;
}

在 Magento 管理区域,如果你现在查看商店 | 设置 | 配置 | 销售 | 运费方法,你将在列表中看到Foggyline Shipbox,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00083.jpeg

启用选项设置为,然后点击保存配置按钮。

如果你现在在 MySQL 服务器上运行SELECT * FROM core_config_data WHERE path LIKE "%shipbox%";查询,你将看到如下截图所示的类似结果:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00084.jpeg

注意,在前面的截图中的代码片段中没有直接与适用于的国家特定国家选项相关的代码,因为这些选项的处理已经内置在父AbstractCarrier类中。因此,只需在config.xmlsystem.xml中添加sallowspecific选项,我们就可以启用一个功能,即运费方法可以显示或隐藏在某些国家。

实现的核心在于collectRates方法。这是我们实现自己的业务逻辑的地方,该逻辑应该根据购物车中的商品计算运费。我们可以在collectRates方法中使用$request->getAllItems()来获取所有购物车商品的集合,遍历它们,根据各种条件形成最终的运费,等等。

现在,让我们继续跳转到店面以测试结账流程。我们应该能在结账页面上看到我们的方法,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00085.jpeg

如果我们完成一个订单,我们还应该在订单本身上看到运费方法详情。在管理区域中,在销售 | 操作 | 订单下,如果我们查看支付与运费方法部分的查看订单,我们应该能看到运费方法,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00086.jpeg

类似地,在订单总额部分,我们应该看到运费及处理费中的运费金额,如下截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00087.jpeg

通过这种方式,我们完成了自定义离线运费方法模块。完整的模块可以在github.com/ajzele/B05032-Foggyline_Shipbox找到。

自定义离线支付方法

Magento 提供了一些现成的离线支付方法,例如BanktransferCashondeliveryCheckmoPurchaseorder。你可以在vendor/magento/module-offline-payments/Model目录中看到它们。

当涉及到支付方式时,更常见的是使用在线支付服务提供商(网关),例如 PayPal 或 Braintree。有时,项目需求可能要求我们可能需要一个自定义编码的支付方式。你需要考虑程序化产品导入和订单创建脚本,这些脚本可能专门针对某些特别标记的支付方式。因此,支付过程将由我们控制。

在这种情况下,了解如何编写我们自己的离线支付方法可能很有用。值得注意的是,虽然我们可以创建一个离线支付,它会抓取用户的信用卡信息,但除非我们的基础设施符合 PCI 标准,否则这样做并不真正建议。

让我们继续创建一个名为 Foggyline_Paybox 的小模块,为 Magento 提供额外的离线支付方式。

首先,创建一个名为 app/code/Foggyline/Paybox/registration.php 的模块注册文件,部分内容如下:

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Foggyline_Paybox',
    __DIR__
);

然后,创建一个名为 app/code/Foggyline/Paybox/etc/module.xml 的文件,内容如下:

<config  xsi:noNamespaceSchemaLocation="urn:magento:framework:Module /etc/module.xsd">
    <module name="Foggyline_Paybox" setup_version="1.0.0">
        <sequence>
            <module name="Magento_OfflinePayments"/>
        </sequence>
    </module>
</config>

完成此操作后,创建一个名为 app/code/Foggyline/Paybox/etc/config.xml 的文件,内容如下:

<config  xsi:noNamespaceSchemaLocation="urn:magento:module: Magento_Store:etc/config.xsd">
    <default>
        <payment>
            <paybox>
                <active>0</active>
                <model>Foggyline\Paybox\Model\Paybox</model>
                <order_status>pending</order_status>
                <title>Foggyline Paybox</title>
                <allowspecific>0</allowspecific>
                <group>offline</group>
            </paybox>
        </payment>
    </default>
</config>

然后,创建一个名为 app/code/Foggyline/Paybox/etc/payment.xml 的文件,内容如下:

<payment  xsi:noNamespaceSchemaLocation="urn:magento:module: Magento_Payment:etc/payment.xsd">
    <methods>
        <method name="paybox">
            <allow_multiple_address>1</allow_multiple_address>
        </method>
    </methods>
</payment>

现在,创建一个名为 app/code/Foggyline/Paybox/etc/adminhtml/system.xml 的文件,内容如下:

<config  xsi:noNamespaceSchemaLocation="urn:magento:module: Magento_Config:etc/system_file.xsd">
    <system>
        <section id="payment">
            <group id="paybox" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Paybox</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model> Magento\Config\Model\Config\Source\Yesno </source_model>
                </field>
                <field id="order_status" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>New Order Status</label>
                    <source_model> Magento\Sales\Model\Config \Source\Order\Status\NewStatus </source_model>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                    <frontend_class> validate-number</frontend_class>
                </field>
                <field id="title" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="allowspecific" translate="label" type="allowspecific" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Payment from Applicable Countries </label>
                    <source_model> Magento\Payment\Model\ Config\Source\Allspecificcountries </source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="51" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Payment from Specific Countries</label>
                    <source_model> Magento\Directory\Model \Config\Source\Country </source_model>
                    <can_be_empty>1</can_be_empty>
                </field>
                <field id="payable_to" translate="label" sortOrder="61" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Make Check Payable to</label>
                </field>
                <field id="mailing_address" translate="label" type="textarea" sortOrder="62" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Send Check to</label>
                </field>
                <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Minimum Order Total</label>
                </field>
                <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Maximum Order Total</label>
                </field>
                <field id="model"></field>
            </group>
        </section>
    </system>
</config>

创建一个名为 app/code/Foggyline/Paybox/etc/frontend/di.xml 的文件,内容如下:

<config  xsi:noNamespaceSchemaLocation="urn:magento:framework: ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Model\CompositeConfigProvider">
        <arguments>
            <argument name="configProviders" xsi:type="array">
                <item name= "offline_payment_paybox_config_provider" xsi:type="object">
                    Foggyline\Paybox\Model\PayboxConfigProvider
                </item>
            </argument>
        </arguments>
    </type>
</config>

完成此操作后,创建一个名为 app/code/Foggyline/Paybox/Model/Paybox.php 的文件,内容如下:

namespace Foggyline\Paybox\Model;

class Paybox extends \Magento\Payment\Model\Method\AbstractMethod
{
    const PAYMENT_METHOD_PAYBOX_CODE = 'paybox';
    protected $_code = self::PAYMENT_METHOD_PAYBOX_CODE;

    protected $_isOffline = true;

    public function getPayableTo()
    {
        return $this->getConfigData('payable_to');
    }

    public function getMailingAddress()
    {
        return $this->getConfigData('mailing_address');
    }
}

现在,创建一个名为 app/code/Foggyline/Paybox/Model/PayboxConfigProvider.php 的文件,内容如下:

namespace Foggyline\Paybox\Model;

class PayboxConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface
{
    protected $methodCode = \Foggyline\Paybox\Model\Paybox::PAYMENT_METHOD_PAYBOX_CODE;
    protected $method;
    protected $escaper;

    public function __construct(
        \Magento\Payment\Helper\Data $paymentHelper
    )
    {
        $this->method = $paymentHelper->getMethodInstance($this-> methodCode);
    }

    public function getConfig()
    {
        return $this->method->isAvailable() ? [
            'payment' => [
                'paybox' => [
                    'mailingAddress' => $this-> getMailingAddress(),
                    'payableTo' => $this->getPayableTo(),
                ],
            ],
        ] : [];
    }

    protected function getMailingAddress()
    {
        $this->method->getMailingAddress();
    }

    protected function getPayableTo()
    {
        return $this->method->getPayableTo();
    }
}

将整个 vendor/magento/module-offline-payments/view/frontend/layout/checkout_index_index.xml Magento 核心文件复制到 app/code/Foggyline/Paybox/view/frontend/layout/checkout_index_index.xml 模块中。然后,通过替换整个 <item name="offline-payments" xsi:type="array"> 元素及其子元素,编辑模块的 checkout_index_index.xml 文件,如下所示:

<item name="foggline-offline-payments" xsi:type="array">
    <item name="component" xsi:type="string"> Foggyline_Paybox/js/view/payment/foggline-offline-payments </item>
    <item name="methods" xsi:type="array">
        <item name="paybox" xsi:type="array">
            <item name="isBillingAddressRequired" xsi:type="boolean">true</item>
        </item>
    </item>
</item>

然后,创建一个名为 app/code/Foggyline/Paybox/view/frontend/web/js/view/payment/offline-payments.js 的文件,内容如下:

/*browser:true*/
/*global define*/
define(
    [
        'uiComponent',
        'Magento_Checkout/js/model/payment/renderer-list'
    ],
    function (
        Component,
        rendererList
    ) {
        'use strict';
        rendererList.push(
            {
                type: 'paybox',
                component: 'Foggyline_Paybox/js/view/payment/method- renderer/paybox'
            }
        );
        return Component.extend({});
    }
);

完成此操作后,创建一个名为 app/code/Foggyline/Paybox/view/frontend/web/js/view/payment/method-renderer/paybox.js 的文件,内容如下:

/*browser:true*/
/*global define*/
define(
    [
        'Magento_Checkout/js/view/payment/default'
    ],
    function (Component) {
        'use strict';

        return Component.extend({
            defaults: {
                template: 'Foggyline_Paybox/payment/paybox'
            },

            getMailingAddress: function () {
                return window.checkoutConfig.payment. paybox.mailingAddress;
            },

            getPayableTo: function () {
                return window.checkoutConfig.payment. paybox.payableTo;
            }
        });
    }
);

现在,创建一个名为 app/code/Foggyline/Paybox/view/frontend/web/template/payment/paybox.html 的文件,内容如下:

<div class="payment-method" data-bind="css: {'_active': (getCode()  == isChecked())}">
    <div class="payment-method-title field choice">
        <input type="radio"
               name="payment[method]"
               class="radio"
               data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
        <label data-bind="attr: {'for': getCode()}" class="label"><span data-bind="text: getTitle()"></span></label>
    </div>
    <div class="payment-method-content">
        <div class="payment-method-billing-address">
            <!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) -->
            <!-- ko template: getTemplate() --><!-- /ko -->
            <!--/ko-->
        </div>
        <!-- ko if: getMailingAddress() || getPayableTo() -->
        <dl class="items check payable">
            <!-- ko if: getPayableTo() -->
            <dt class="title"><!-- ko i18n: 'Make Check payable toooooo:' --><!-- /ko --></dt>
            <dd class="content"><!-- ko i18n: getPayableTo() --> <!-- /ko --></dd>
            <!-- /ko -->
            <!-- ko if: getMailingAddress() -->
            <dt class="title"><!-- ko i18n: 'Send Check toxyz:' -- ><!-- /ko --></dt>
            <dd class="content">
                <address class="paybox mailing address" data-bind ="html: $t(getMailingAddress())"></address>
            </dd>
            <!-- /ko -->
        </dl>
        <!-- /ko -->
        <div class="checkout-agreements-block">
            <!-- ko foreach: $parent.getRegion('before-place- order') -->
            <!-- ko template: getTemplate() --><!-- /ko -->
            <!--/ko-->
        </div>
        <div class="actions-toolbar">
            <div class="primary">
                <button class="action primary checkout"
                        type="submit"
                        data-bind="
                        click: placeOrder,
                        attr: {title: $t('Place Order')},
                        css: {disabled: !isPlaceOrderActionAllowed()},
                        enable: (getCode() == isChecked())
                        "
                        disabled>
                    <span data-bind="i18n: 'Place Order'"></span>
                </button>
            </div>
        </div>
    </div>
</div>

通过这种方式,我们完成了自定义离线支付方法模块。整个模块可以在 github.com/ajzele/B05032-Foggyline_Paybox 找到。

摘要

在本章中,我们讨论了一些开发者最常接触到的功能点。我们学习了在管理区域中查找信息的位置,以及如何编程管理这些功能背后的实体。因此,我们能够有效地手动和编程创建和获取 CMS 页面、块、分类和产品。我们还学习了如何创建产品和客户导入脚本。最后,我们研究了如何创建我们自己的自定义产品类型、简单支付和运输模块。

以下章节将引导我们了解 Magento 内置测试的使用方法,以及如何利用它们有效地进行应用程序的质量保证,以保持其健康状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值