Magento PHP 开发指南(二)

原文:zh.annas-archive.org/md5/f2e271327b273df27fc8bf4ef750d5c2

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:后端开发

在上一章中,我们为礼品注册表添加了所有前端功能。现在客户能够创建注册表并向客户注册表添加产品,并且通常可以完全控制自己的注册表。

在本章中,我们将构建所有商店所有者需要通过 Magento 后端管理和控制注册表的功能。

Magento 后端在许多方面可以被视为与 Magento 前端分开的应用程序;它使用完全不同的主题、样式和不同的基本控制器。

对于我们的礼品注册表,我们希望允许商店所有者查看所有客户注册表,修改信息,并添加和删除项目。在本章中,我们将涵盖以下内容:

  • 使用配置扩展 Adminhtml

  • 使用网格小部件

  • 使用表单小部件

  • 使用访问控制列表限制访问和权限

扩展 Adminhtml

Mage_Adminhtml是一个单一模块,通过使用配置提供 Magento 的所有后端功能。正如我们之前学到的,Magento 使用范围来定义配置。在上一章中,我们使用前端范围来设置我们自定义模块的配置。

要修改后端,我们需要在配置文件中创建一个名为admin的新范围。执行以下步骤来完成:

  1. 打开config.xml文件,可以在app/code/loca/Mdg/Giftregistry/etc/位置找到。

  2. 将以下代码添加到其中:

<admin>
 <routers>
   <giftregistry>
     <use>admin</use>
       <args>
           <module>Mdg_Giftregistry_Adminhmtl</module>
           <frontName>giftregistry</frontName>
       </args>
   </giftregistry>
 </routers>
</admin>

这段代码与我们以前用来指定前端路由的代码非常相似;然而,通过这种方式声明路由,我们正在打破一个未写的 Magento 设计模式。

为了在后端保持一致,所有新模块都应该扩展主管理路由。

与以前的代码定义路由不同,我们正在创建一个全新的管理路由。通常情况下,除非您正在创建一个需要管理员访问但不需要 Magento 后端其余部分的新路由,否则不要在 Magento 后端这样做。管理员操作的回调 URL 就是这种情况的一个很好的例子。

幸运的是,有一种非常简单的方法可以在 Magento 模块之间共享路由名称。

注意

在 Magento 1.3 中引入了共享路由名称,但直到今天,我们仍然看到一些扩展没有正确使用这种模式。

让我们更新我们的代码:

  1. 打开config.xml文件,可以在app/code/loca/Mdg/Giftregistry/etc/位置找到。

  2. 使用以下代码更新路由配置:

<admin>
 <routers>
   <adminhtml>
     <args>
       <modules>
         <mdg_giftregistry before="Mage_Adminhtml">Mdg_Giftregistry_Adminhtml</mdg_giftregistry>
       </modules>
     </args>
   </adminhtml>
 </routers>
</admin>

做出这些改变后,我们可以通过管理命名空间正确访问我们的管理控制器;例如,http://magento.localhost.com/giftregistry/index现在将是http://magento.localhost.com/admin/giftregistry/index

我们的下一步将是创建一个新的控制器,我们可以用来管理客户注册表。我们将把这个控制器称为GiftregistryController.php。执行以下步骤来完成:

  1. 导航到您的模块控制器文件夹。

  2. 创建一个名为Adminhtml的新文件夹。

  3. app/code/loca/Mdg/Giftregistry/controllers/Adminhtml/位置创建名为GiftregistryController.php的文件。

  4. 将以下代码添加到其中:

<?php
class Mdg_Giftregistry_Adminhtml_GiftregistryController extends Mage_Adminhtml_Controller_Action
{
    public function indexAction()
    {
        $this->loadLayout();
        $this->renderLayout();
        return $this;
    }

    public function editAction()
    {
        $this->loadLayout();
        $this->renderLayout();
        return $this;
    }

    public function saveAction()
    {
        $this->loadLayout();
        $this->renderLayout();
        return $this;
    }

    public function newAction()
    {
        $this->loadLayout();
        $this->renderLayout();
        return $this;
    }

    public function massDeleteAction()
    {
        $this->loadLayout();
        $this->renderLayout();
        return $this;
    }
}

请注意一个重要的事情:这个新控制器扩展了Mage_Adminhtml_Controller_Action而不是我们到目前为止一直在使用的Mage_Core_Controller_Front_Action。这样做的原因是Adminhtml控制器具有额外的验证,以防止非管理员用户访问它们的操作。

请注意,我们将我们的控制器放在controllers/目录内的一个新子文件夹中;通过使用这个子目录,我们可以保持前端和后端控制器的组织。这是一个被广泛接受的 Magento 标准实践。

现在,让我们暂时不管这个空控制器,让我们扩展 Magento 后端导航并向客户编辑页面添加一些额外的选项卡。

回到配置

到目前为止,我们已经看到,大多数情况下 Magento 由 XML 配置文件控制,后端布局也不例外。我们需要创建一个新的adminhtml布局文件。执行以下步骤来完成:

  1. 导航到设计文件夹。

  2. 创建一个名为adminhtml的新文件夹,并在其中创建以下文件夹结构:

  • adminhtml/

  • --default/

  • ----default/

  • ------template/

  • ------layout/

  1. layout文件夹内,让我们在位置app/code/design/adminhtml/default/default/layout/创建一个名为giftregistry.xml的新布局文件。

  2. 将以下代码复制到布局文件中:

<?xml version="1.0"?>
<layout version="0.1.0">
    <adminhtml_customer_edit>
        <reference name="left">
            <reference name="customer_edit_tabs">
                <block type="mdg_giftregistry/adminhtml_customer_edit_tab_giftregistry" name="tab_giftregistry_main" template="mdg_giftregistry/giftregistry/customer/main.phtml">
                </block>
                <action method="addTab">
                 <name>mdg_giftregistry</name>
              <block>tab_giftregistry_main</block>
          </action>
            </reference>
        </reference>
    </adminhtml_customer_edit>
</layout>

我们还需要将新的布局文件添加到config.xml模块中。执行以下步骤来完成:

  1. 导航到etc/文件夹。

  2. 打开config.xml文件,可以在位置app/code/loca/Mdg/Giftregistry/etc/找到。

  3. 将以下代码复制到config.xml文件中:

<adminhtml>
        <layout>
            <updates>
                <mdg_giftregistry module="mdg_giftregistry">
                    <file>giftregistry.xml</file>
                </mdg_giftregistry>
            </updates>
        </layout>
    </adminhtml>

在布局内部,我们正在创建一个新的容器块,并声明一个包含此块的新选项卡。

让我们通过登录到 Magento 后端并打开客户管理,进入客户 | 管理客户,快速测试我们迄今为止所做的更改。

我们应该在后端得到以下错误:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_05_01.jpg

这是因为我们正在尝试添加一个尚未声明的块;为了解决这个问题,我们需要创建一个新的块类。执行以下步骤来完成:

  1. 导航到块文件夹,并按照目录结构创建一个名为Giftregistry.php的新块类,位置在app/code/loca/Mdg/Giftregistry/Block/Adminhtml/Customer/Edit/Tab/

  2. 将以下代码添加到其中:

<?php 
class Mdg_Giftregistry_Block_Adminhtml_Customer_Edit_Tab_Giftregistry
    extends Mage_Adminhtml_Block_Template
    implements Mage_Adminhtml_Block_Widget_Tab_Interface {

    public function __construct()
    {
        $this->setTemplate('mdg/giftregistry/customer/main.phtml');
        parent::_construct();
    }

    public function getCustomerId()
    {
        return Mage::registry('current_customer')->getId();
    }

    public function getTabLabel()
    {
        return $this->__('GiftRegistry List');
    }

    public function getTabTitle()
    {
        return $this->__('Click to view the customer Gift Registries');
    }

    public function canShowTab()
    {
        return true;
    }

    public function isHidden()
    {
        return false;
    }
}

这个块类有一些有趣的事情发生。首先,我们正在扩展一个不同的块类Mage_Adminhtml_Block_Template,并实现一个新的接口Mage_Adminhtml_Block_Widget_Tab_Interface。这样做是为了访问 Magento 后端的所有功能和功能。

我们还在类的构造函数中设置了块模板;同样在getCustomerId下,我们使用 Magento 全局变量来获取当前客户。

我们的下一步将是为此块创建相应的模板文件,否则我们将在块初始化时出现错误。

  1. 在位置app/code/design/adminhtml/default/default/template/mdg/giftregistry/customer/创建一个名为main.phtml的模板文件。

  2. 将以下代码复制到其中:

<div class="entry-edit">
    <div class="entry-edit-head">
        <h4 class="icon-head head-customer-view"><?php echo $this->__('Customer Gift Registry List') ?></h4>
    </div>
    <table cellspacing="2" class="box-left">
        <tr>
            <td>
                Nothing here 
            </td>
        </tr>
    </table>
</div>

目前,我们只是向模板添加占位内容,以便我们实际上可以看到我们的选项卡在操作中;现在,如果我们转到后端的客户部分,我们应该看到一个新的选项卡可用,并且单击该选项卡将显示我们的占位内容。

到目前为止,我们已经修改了后端,并通过更改配置和添加一些简单的块和模板文件,向客户部分添加了Customers选项卡。但到目前为止,这还没有特别有用,所以我们需要一种方法来显示所有客户礼品注册在礼品注册选项卡下。

网格小部件

我们可以重用 Magento Adminhtml模块已经提供的块,而不必从头开始编写我们自己的网格块。

我们将要扩展的块称为网格小部件;网格小部件是一种特殊类型的块,旨在以特定的表格网格中呈现 Magento 对象的集合。

网格小部件通常呈现在网格容器内;这两个元素的组合不仅允许以网格形式显示我们的数据,还添加了搜索、过滤、排序和批量操作功能。执行以下步骤:

  1. 导航到块Adminhtml/文件夹,并在位置app/code/loca/Mdg/Giftregistry/Block/Adminhtml/Customer/Edit/Tab/创建一个名为Giftregistry/的文件夹。

  2. 在该文件夹内创建一个名为List.php的类。

  3. 将以下代码复制到Giftregistry/List.php文件中:

<?php
class Mdg_Giftregistry_Block_Adminhtml_Customer_Edit_Tab_Giftregistry_List extends Mage_Adminhtml_Block_Widget_Grid
{
    public function __construct()
    {
        parent::__construct();
        $this->setId('registryList');
        $this->setUseAjax(true);
        $this->setDefaultSort('event_date');
        $this->setFilterVisibility(false);
        $this->setPagerVisibility(false);
    }

    protected function _prepareCollection()
    {
        $collection = Mage::getModel('mdg_giftregistry/entity')
            ->getCollection()
            ->addFieldToFilter('main_table.customer_id', $this->getRequest()->getParam('id'));
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn('entity_id', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Id'),
            'width'    => 50,
            'index'    => 'entity_id',
            'sortable' => false,
        ));

        $this->addColumn('event_location', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Location'),
            'index'    => 'event_location',
            'sortable' => false,
        ));

        $this->addColumn('event_date', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Event Date'),
            'index'    => 'event_date',
            'sortable' => false,
        ));

        $this->addColumn('type_id', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Event Type'),
            'index'    => 'type_id',
            'sortable' => false,
        ));
        return parent::_prepareColumns();
    }
}

看看我们刚刚创建的类,只涉及三个函数:

  • __construct()

  • _prepareCollection()

  • _prepareColumns()

__construct函数中,我们指定了关于我们的网格类的一些重要选项。我们设置了gridId;默认排序为eventDate,并启用了分页和过滤。

__prepareCollection()函数加载了一个由当前customerId过滤的注册表集合。这个函数也可以用来在我们的集合中执行更复杂的操作;例如,连接一个辅助表以获取有关客户或其他相关记录的更多信息。

最后,通过使用__prepareColumns()函数,我们告诉 Magento 应该显示哪些列和数据集的属性,以及如何渲染它们。

现在我们已经创建了一个功能性的网格块,我们需要对我们的布局 XML 文件进行一些更改才能显示它。执行以下步骤:

  1. 打开giftregistry.xml文件,该文件位于app/design/adminhtml/default/default/layout/位置。

  2. 进行以下更改:

<?xml version="1.0"?>
<layout version="0.1.0">
    <adminhtml_customer_edit>
        <reference name="left">
            <reference name="customer_edit_tabs">
                <block type="mdg_giftregistry/adminhtml_customer_edit_tab_giftregistry" name="tab_giftregistry_main" template="mdg/giftregistry/customer/main.phtml">
                    <block type="mdg_giftregistry/adminhtml_customer_edit_tab_giftregistry_list" name="tab_giftregistry_list" as="giftregistry_list" />
                </block>
                <action method="addTab">
                    <name>mdg_giftregistry</name>
                    <block>mdg_giftregistry/adminhtml_customer_edit_tab_giftregistry</block>
                </action>
            </reference>
        </reference>
    </adminhtml_customer_edit>
</layout>

我们所做的是将网格块添加为我们的主块的一部分,但如果我们转到客户编辑页面并点击礼品注册选项卡,我们仍然看到旧的占位文本,并且网格没有显示。

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_05_02.jpg

这是因为我们还没有对main.phtml模板文件进行必要的更改。为了显示子块,我们需要明确告诉模板系统加载任何或特定的子块;现在,我们将只加载我们特定的网格块。执行以下步骤:

  1. 打开main.phtml模板文件,该文件位于app/design/adminhtml/default/default/template/customer/位置。

  2. 用以下内容替换模板代码:

<div class="entry-edit">
    <div class="entry-edit-head">
        <h4 class="icon-head head-customer-view"><?php echo $this->__('Customer Gift Registry List') ?></h4>
    </div>
    <?php echo $this->getChildHtml('tab_giftregistry_list'); ?>
</div>

getChildHtml()函数负责渲染所有子块。

getChildHtml()函数可以使用特定的子块名称或无参数调用;如果没有参数调用,它将加载所有可用的子块。

在我们的扩展情况下,我们只对实例化特定的子块感兴趣,所以我们将传递块名称作为函数参数。现在,如果我们刷新页面,我们应该看到我们的网格块加载了该特定客户的所有可用礼品注册。

管理注册表

现在,如果我们想要管理特定客户的注册表,这很方便,但如果我们想要管理商店中所有可用的注册表,这并不真正帮助我们。为此,我们需要创建一个加载所有可用礼品注册的网格。

由于我们已经为后端创建了礼品注册控制器,我们可以使用索引操作来显示所有可用的注册表。

我们需要做的第一件事是修改 Magento 后端导航,以显示指向我们新控制器索引操作的链接。同样,我们可以通过使用 XML 来实现这一点。在这种特殊情况下,我们将创建一个名为adminhtml.xml的新 XML 文件。执行以下步骤:

  1. 导航到您的模块etc文件夹,该文件夹位于app/code/local/Mdg/Giftregistry/位置。

  2. 创建一个名为adminhtml.xml的新文件。

  3. 将以下代码放入该文件中:

<?xml version="1.0"?>
<config>
    <menu>
        <mdg_giftregistry module="mdg_giftregistry">
            <title>Gift Registry</title>
            <sort_order>71</sort_order>
            <children>
                <items module="mdg_giftregistry">
                    <title>Manage Registries</title>
                    <sort_order>0</sort_order>
                    <action>adminhtml/giftregistry/index</action>
                </items>
            </children>
        </mdg_giftregistry>
    </menu>
</config>

注意

虽然标准是将此配置添加到adminhtml.xml中,但您可能会遇到未遵循此标准的扩展。此配置可以位于config.xml中。

这个配置代码正在创建一个新的主级菜单和一个新的子级选项;我们还指定了菜单应映射到哪个操作,在这种情况下,是我们的礼品注册控制器的索引操作。

如果我们现在刷新后端,我们应该会看到一个新的礼品注册菜单添加到顶级导航中。

权限和 ACL

有时我们需要根据管理员规则限制对模块的某些功能甚至整个模块的访问。Magento 允许我们使用一个称为ACL访问控制列表的强大功能来实现这一点。Magento 后端中的每个角色都可以具有不同的权限和不同的 ACL。

启用我们自定义模块的 ACL 的第一步是定义 ACL 应该受限制的资源;这由配置 XML 文件控制,这并不奇怪。执行以下步骤:

  1. 打开adminhtml.xml配置文件,该文件位于app/code/local/Mdg/Giftregistry/etc/位置。

  2. 在菜单路径之后添加以下代码:

<acl>
    <resources>
        <admin>
            <children>
                <giftregistry translate="title" module="mdg_giftregistry">
                    <title>Gift Registry</title>
                    <sort_order>300</sort_order>
                    <children>
                        <items translate="title" module="mdg_giftregistry">
                            <title>Manage Registries</title>
                            <sort_order>0</sort_order>
                        </items>
                    </children>
                </giftregistry>
            </children>
        </admin>
    </resources>
</acl>

现在,在 Magento 后端,如果我们导航到系统 | 权限 | 角色,选择管理员角色,并尝试在列表底部设置角色资源,我们可以看到我们创建的新 ACL 资源,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_05_06.jpg

通过这样做,我们可以精细控制每个用户可以访问哪些操作。

如果我们点击管理注册表菜单,我们应该会看到一个空白页面;因为我们还没有创建相应的网格块、布局和模板,所以我们应该会看到一个完全空白的页面。

所以让我们开始创建我们新网格所需的块;我们创建礼品注册表网格的方式将与我们为客户选项卡所做的略有不同。

我们需要创建一个网格容器块和一个网格块。网格容器用于保存网格标题、按钮和网格内容,而网格块只负责呈现带有分页、过滤和批量操作的网格。执行以下步骤:

  1. 导航到您的块Adminhtml文件夹。

  2. app/code/local/Mdg/Giftregistry/Block/Adminhtml/位置创建一个名为Registries.php的新块:

  3. 将以下代码添加到其中:

<?php
class Mdg_Giftregistry_Block_Adminhtml_Registries extends Mage_Adminhtml_Block_Widget_Grid_Container
{
public function __construct(){
    $this->_controller = 'adminhtml_registries';
    $this->_blockGroup = 'mdg_giftregistry';
    $this->_headerText = Mage::helper('mdg_giftregistry')->__('Gift Registry Manager');
    parent::__construct();
  }
}

我们在网格容器内的construct函数中设置的一个重要的事情是使用受保护的_controller_blockGroup值,Magento 网格容器通过这些值来识别相应的网格块。

重要的是要澄清,$this->_controller不是实际的控制器名称,而是块类名称,$this->_blockGroup实际上是模块名称。

让我们继续创建网格块,正如我们之前学到的那样,它有三个主要功能:_construct_prepareCollection()_prepareColumns()。但在这种情况下,我们将添加一个名为_prepareMassActions()的新功能,它允许我们修改所选记录集而无需逐个编辑。执行以下步骤:

  1. 导航到您的块Adminhtml文件夹并创建一个名为Registries的新文件夹。

  2. Model文件夹下,在app/code/local/Mdg/Giftregistry/Block/Adminhtml/Registries/位置创建一个名为Grid.php的新块。

  3. 将以下代码添加到Grid.php中:

File Location: Grid.php
<?php
class Mdg_Giftregistry_Block_Adminhtml_Registries_Grid extends Mage_Adminhtml_Block_Widget_Grid
{
    public function __construct(){
        parent::__construct();
        $this->setId('registriesGrid');
        $this->setDefaultSort('event_date');
        $this->setDefaultDir('ASC');
        $this->setSaveParametersInSession(true);
    }

    protected function _prepareCollection(){
        $collection = Mage::getModel('mdg_giftregistry/entity')->getCollection();
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn('entity_id', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Id'),
            'width'    => 50,
            'index'    => 'entity_id',
            'sortable' => false,
        ));

        $this->addColumn('event_location', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Location'),
            'index'    => 'event_location',
            'sortable' => false,
        ));

        $this->addColumn('event_date', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Event Date'),
            'index'    => 'event_date',
            'sortable' => false,
        ));

        $this->addColumn('type_id', array(
            'header'   => Mage::helper('mdg_giftregistry')->__('Event Type'),
            'index'    => 'type_id',
            'sortable' => false,
        ));
        return parent::_prepareColumns();
    }

    protected function _prepareMassaction(){
    }
}

这个网格代码与我们之前为客户选项卡创建的非常相似,唯一的区别是这次我们不是特别按客户记录进行过滤,而且我们还创建了一个网格容器块,而不是实现一个自定义块。

最后,为了在我们的控制器动作中显示网格,我们需要执行以下步骤:

  1. 打开giftregistry.xml文件,该文件位于app/code/design/adminhtml/default/default/layout/位置。

  2. 将以下代码添加到其中:

<adminhtml_giftregistry_index>
         <reference name="content">
             <block type="mdg_giftregistry/adminhtml_registries" name="registries" />
         </reference>
     </adminhtml_giftregistry_index>

由于我们使用了网格容器,我们只需要指定网格容器块,Magento 将负责加载匹配的网格容器。

无需为网格或网格容器指定或创建模板文件,因为这两个块都会自动从adminhtml/base/default主题加载基本模板。

现在,我们可以通过在后端导航到礼品注册 | 管理注册表来检查我们新添加的礼品注册。

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_05_03.jpg

使用大规模操作进行批量更新

在创建我们的基本网格块时,我们定义了一个名为_prepareMassactions()的函数,它提供了一种简单的方式来操作网格中的多条记录。在我们的情况下,现在让我们只实现一个大规模删除动作。执行以下步骤:

  1. 打开注册表格块Grid.php,该文件位于app/code/local/Mdg/Giftregistry/Block/Adminhtml/Registries/位置。

  2. 用以下代码替换_prepareMassaction()函数:

protected function _prepareMassaction(){
    $this->setMassactionIdField('entity_id');
    $this->getMassactionBlock()->setFormFieldName('registries');

    $this->getMassactionBlock()->addItem('delete', array(
        'label'     => Mage::helper('mdg_giftregistry')->__('Delete'),
        'url'       => $this->getUrl('*/*/massDelete'),
        'confirm'   => Mage::helper('mdg_giftregistry')->__('Are you sure?')
    ));
    return $this;
}

大规模操作的工作方式是通过将一系列选定的 ID 传递给我们指定的控制器动作;在这种情况下,massDelete()动作将添加代码来迭代注册表集合并删除每个指定的注册表。执行以下步骤:

  1. 打开GiftregistryController.php文件,该文件位于app/code/local/Mdg/Giftregistry/controllers/Adminhtml/位置。

  2. 用以下代码替换空白的massDelete()动作:

public function massDeleteAction()
{
    $registryIds = $this->getRequest()->getParam('registries');
        if(!is_array($registryIds)) {
             Mage::getSingleton('adminhtml/session')->addError(Mage::helper('mdg_giftregistry')->__('Please select one or more registries.'));
        } else {
            try {
                $registry = Mage::getModel('mdg_giftregistry/entity');
                foreach ($registryIds as $registryId) {
                    $registry->reset()
                        ->load($registryId)
                        ->delete();
                }
                Mage::getSingleton('adminhtml/session')->addSuccess(
                Mage::helper('adminhtml')->__('Total of %d record(s) were deleted.', count($registryIds))
                );
            } catch (Exception $e) {
                Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
            }
        }
        $this->_redirect('*/*/index');
}

注意

挑战:添加两个新的大规模操作,将注册表的状态分别更改为启用或禁用。要查看完整代码和详细分解的答案,请访问www.magedevguide.com/

最后,我们还希望能够编辑我们网格中列出的记录。为此,我们需要向我们的注册表格类添加一个新函数;这个函数被称为getRowUrl(),它用于指定单击网格行时要执行的操作;在我们的特定情况下,我们希望将该函数映射到editAction()。执行以下步骤:

  1. 打开Grid.php文件,该文件位于app/code/local/Mdg/Giftregistry/Block/Adminhtml/Registries/位置。

  2. 向其中添加以下函数:

public function getRowUrl($row)
{
    return $this->getUrl('*/*/edit', array('id' => $row->getEntityId()));
}

表单小部件

到目前为止,我们一直在处理礼品注册表格,但现在我们除了获取所有可用注册表的列表或批量删除注册表之外,无法做更多事情。我们需要一种方法来获取特定注册表的详细信息;我们可以将其映射到编辑控制器动作。

edit动作将显示特定注册表的详细信息,并允许我们修改注册表的详细信息和状态。我们需要为此动作创建一些块和模板。

为了查看和编辑注册表信息,我们需要实现一个表单小部件块。表单小部件的工作方式与网格小部件块类似,需要有一个表单块和一个扩展Mage_Adminhtml_Block_Widget_Form_Container类的表单容器块。为了创建表单容器,让我们执行以下步骤:

  1. 导航到Registries文件夹。

  2. app/code/local/Mdg/Giftregistry/Block/Adminhtml/Registries/位置创建一个名为Edit.php的新类文件。

  3. 向类文件中添加以下代码:

class Mdg_Giftregistry_Block_Adminhtml_Registries_Edit extends Mage_Adminhtml_Block_Widget_Form_Container
{
    public function __construct(){
        parent::__construct();
        $this->_objectId = 'id';
        $this->_blockGroup = 'registries';
        $this->_controller = 'adminhtml_giftregistry';
        $this->_mode = 'edit';

        $this->_updateButton('save', 'label', Mage::helper('mdg_giftregistry')->__('Save Registry'));
        $this->_updateButton('delete', 'label', Mage::helper('mdg_giftregistry')->__('Delete Registry'));
    }

    public function getHeaderText(){
        if(Mage::registry('registries_data') && Mage::registry('registries_data')->getId())
            return Mage::helper('mdg_giftregistry')->__("Edit Registry '%s'", $this->htmlEscape(Mage::registry('registries_data')->getTitle()));
        return Mage::helper('mdg_giftregistry')->__('Add Registry');
    }
}

与网格小部件类似,表单容器小部件将自动识别并加载匹配的表单块。

在表单容器中声明的另一个受保护属性是 mode 属性;这个受保护属性被容器用来指定表单块的位置。

我们可以在Mage_Adminhtml_Block_Widget_Form_Container类中找到负责创建表单块的代码:

$this->getLayout()->createBlock($this->_blockGroup . '/' . $this->_controller . '_' . $this->_mode . '_form')

现在我们已经创建了表单容器块,我们可以继续创建匹配的表单块。执行以下步骤:

  1. 导航到Registries文件夹。

  2. 创建一个名为Edit的新文件夹。

  3. app/code/local/Mdg/Giftregistry/Block/Adminhtml/Registries/Edit/位置创建一个名为Form.php的新文件。

  4. 向其中添加以下代码:

<?php
class Mdg_Giftregistry_Block_Adminhtml_Registries_Edit_Form extends  Mage_Adminhtml_Block_Widget_Form
{
    protected function _prepareForm(){
        $form = new Varien_Data_Form(array(
            'id' => 'edit_form',
            'action' => $this->getUrl('*/*/save', array('id' => $this->getRequest()->getParam('id'))),
            'method' => 'post',
            'enctype' => 'multipart/form-data'
        ));
        $form->setUseContainer(true);
        $this->setForm($form);

        if (Mage::getSingleton('adminhtml/session')->getFormData()){
            $data = Mage::getSingleton('adminhtml/session')->getFormData();
            Mage::getSingleton('adminhtml/session')->setFormData(null);
        }elseif(Mage::registry('registry_data'))
            $data = Mage::registry('registry_data')->getData();

        $fieldset = $form->addFieldset('registry_form', array('legend'=>Mage::helper('mdg_giftregistry')->__('Gift Registry information')));

        $fieldset->addField('type_id', 'text', array(
            'label'     => Mage::helper('mdg_giftregistry')->__('Registry Id'),
            'class'     => 'required-entry',
            'required'  => true,
            'name'      => 'type_id',
        ));

        $fieldset->addField('website_id', 'text', array(
            'label'     => Mage::helper('mdg_giftregistry')->__('Website Id'),
            'class'     => 'required-entry',
            'required'  => true,
            'name'      => 'website_id',
        ));

        $fieldset->addField('event_location', 'text', array(
            'label'     => Mage::helper('mdg_giftregistry')->__('Event Location'),
            'class'     => 'required-entry',
            'required'  => true,
            'name'      => 'event_location',
        ));

        $fieldset->addField('event_date', 'text', array(
            'label'     => Mage::helper('mdg_giftregistry')->__('Event Date'),
            'class'     => 'required-entry',
            'required'  => true,
            'name'      => 'event_date',
        ));

        $fieldset->addField('event_country', 'text', array(
            'label'     => Mage::helper('mdg_giftregistry')->__('Event Country'),
            'class'     => 'required-entry',
            'required'  => true,
            'name'      => 'event_country',
        ));

        $form->setValues($data);
        return parent::_prepareForm();
    }
}

我们还需要修改我们的布局文件,并告诉 Magento 加载我们的表单容器。

将以下代码复制到布局文件giftregistry.xml中,该文件位于app/code/design/adminhtml/default/default/layout/位置:

<?xml version="1.0"?>
<layout version="0.1.0"><adminhtml_giftregistry_edit>
        <reference name="content">
            <block type="mdg_giftregistry/adminhtml_registries_edit" name="new_registry_tabs" />
        </reference>
    </adminhtml_giftregistry_edit>

此时,我们可以进入 Magento 后端,点击我们的示例注册表之一,查看我们的进展。我们应该看到以下表单:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_05_04.jpg

但似乎有一个问题。没有加载任何数据;我们只有一个空表单,因此我们必须修改我们的控制器editAction()以加载数据。

加载数据

让我们从修改GiftregistryController.php文件中的editAction()开始,该文件位于app/code/local/Mdg/Giftregistry/controllers/Adminhtml/位置:

public function editAction()
{
    $id     = $this->getRequest()->getParam('id', null);
    $registry  = Mage::getModel('mdg_giftregistry/entity');

    if ($id) {
        $registry->load((int) $id);
        if ($registry->getId()) {
            $data = Mage::getSingleton('adminhtml/session')->getFormData(true);
            if ($data) {
                $registry->setData($data)->setId($id);
            }
        } else {
            Mage::getSingleton('adminhtml/session')->addError(Mage::helper('awesome')->__('The Gift Registry does not exist'));
            $this->_redirect('*/*/');
        }
    }
    Mage::register('registry_data', $registry);

    $this->loadLayout();
    $this->getLayout()->getBlock('head')->setCanLoadExtJs(true);
    $this->renderLayout();
}

我们在editAction()中所做的是检查是否存在具有相同 ID 的注册表,如果存在,我们将加载该注册表实体并使其可用于我们的表单。之前,在将表单代码添加到文件app/code/local/Mdg/Giftregistry/Block/Adminhtml/Registries/Edit/Form.php时,我们包括了以下内容:

if (Mage::getSingleton('adminhtml/session')->getFormData()){
    $data = Mage::getSingleton('adminhtml/session')->getFormData();
    Mage::getSingleton('adminhtml/session')->setFormData(null);
}elseif(Mage::registry('registry_data'))
    $data = Mage::registry('registry_data')->getData();

现在,我们可以通过重新加载表单来测试我们的更改:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_05_05.jpg

保存数据

现在我们已经为编辑注册表创建了表单,我们需要创建相应的操作来处理并保存表单提交的数据。我们可以使用保存表单操作来处理这个过程。执行以下步骤:

  1. 打开GiftregistryController.php类,该类位于app/code/local/Mdg/Giftregistry/controllers/Adminhtml/位置。

  2. 用以下代码替换空白的saveAction()函数:

public function saveAction()
{
    if ($this->getRequest()->getPost())
    {
        try {
            $data = $this->getRequest()->getPost();
            $id = $this->getRequest()->getParam('id');

            if ($data && $id) {
                $registry = Mage::getModel('mdg_giftregistry/entity')->load($id);
                $registry->setData($data);
                $registry->save();
                  $this->_redirect('*/*/edit', array('id' => $this->getRequest()->getParam('registry_id')));
            }
        } catch (Exception $e) {
            $this->_getSession()->addError(
                Mage::helper('mdg_giftregistry')->__('An error occurred while saving the registry data. Please review the log and try again.')
            );
            Mage::logException($e);
            $this->_redirect('*/*/edit', array('id' => $this->getRequest()->getParam('registry_id')));
            return $this;
        }
    }
}

让我们逐步分解一下这段代码在做什么:

  1. 我们检查请求是否具有有效的提交数据。

  2. 我们检查$data$id变量是否都设置了。

  3. 如果两个变量都设置了,我们加载一个新的注册表实体并设置数据。

  4. 最后,我们尝试保存注册表实体。

我们首先要做的是检查提交的数据不为空,并且我们在参数中获取了注册表 ID;我们还检查注册表 ID 是否是注册表实体的有效实例。

总结

在本章中,我们学会了如何修改和扩展 Magento 后端以满足我们的特定需求。

前端扩展了客户和用户可以使用的功能;扩展后端允许我们控制这个自定义功能以及客户与其交互的方式。

网格和表单是 Magento 后端的重要部分,通过正确使用它们,我们可以添加很多功能,而不必编写大量代码或重新发明轮子。

最后,我们学会了如何使用权限和 Magento ACL 来控制和限制我们的自定义扩展以及 Magento 的权限。

在下一章中,我们将深入研究 Magento API,并学习如何扩展它以使用多种方法(如 SOAP、XML-RPC 和 REST)来操作我们的注册表数据。

第六章:Magento API

在上一章中,我们扩展了 Magento 后端,并学习了如何使用一些后端组件,以便商店所有者可以管理和操作每个客户的礼品注册数据。

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

  • Magento 核心 API

  • 可用的多个 API 协议(REST、SOAP、XML-RPC)

  • 如何使用核心 API

  • 如何扩展 API 以实现新功能

  • 如何将 API 的部分限制为特定的 Web 用户角色

虽然后端提供了日常操作的界面,但有时我们需要访问和/或传输来自第三方系统的数据。Magento 已经为大多数核心功能提供了 API 功能,但对于我们的自定义礼品注册扩展,我们需要扩展Mage_Api功能。

核心 API

在谈论 API 时,我经常听到开发人员谈论 Magento SOAP API 或 Magento XML-RPC API 或 RESTful API。但重要的事实是,这些并不是针对每个协议的单独 API;相反,Magento 有一个单一的核心 API。

正如您可能注意到的,Magento 主要建立在抽象和配置(主要是 XML)周围,Magento API 也不例外。我们有一个单一的核心 API 和每种不同协议类型的适配器。这是非常灵活的,如果我们愿意,我们可以为另一个协议实现自己的适配器。

核心 Magento API 使我们能够管理产品、类别、属性、订单和发票。这是通过暴露三个核心模块来实现的:

  • Mage_Catalog

  • Mage_Sales

  • Mage_Customer

API 支持三种不同类型:SOAP、XML-RPC 和 REST。现在,如果您在 Magento 之外进行了 Web 开发并使用了其他 API,那么很可能那些 API 是 RESTful API。

在我们深入研究 Magento API 架构的具体细节之前,重要的是我们了解每种支持的 API 类型之间的区别。

XML-RPC

XML-RPC 是 Magento 支持的第一个协议,也是最古老的协议。该协议有一个单一的端点,所有功能都在此端点上调用和访问。

注意

XML-RPC是一种使用 XML 编码其调用和 HTTP 作为传输机制的远程过程调用RPC)协议。

由于只有一个单一的端点,XML-RPC 易于使用和维护;它的目的是成为发送和接收数据的简单有效的协议。实现使用简单的 XML 来编码和解码远程过程调用以及参数。

然而,这是有代价的,整个 XML-RPC 协议存在几个问题:

  • 发现性和文档不足。

  • 参数是匿名的,XML-RPC 依赖于参数的顺序来区分它们。

  • 简单性是 XML-RPC 的最大优势,也是最大问题所在。虽然大多数任务可以很容易地通过 XML-RPC 实现,但有些任务需要您费尽周折才能实现应该很简单的事情。

SOAP 旨在解决 XML-RPC 的局限性并提供更强大的协议。

注意

有关 XML-RPC 的更多信息,您可以访问以下链接:

en.wikipedia.org/wiki/XML-RPC

SOAP

自 Magento 1.3 以来,SOAP v1 是 Magento 支持的第一个协议,与 XML-RPC 一起。

注意

SOAP最初定义为简单对象访问协议,是用于在计算机网络中实现 Web 服务的结构化信息交换的协议规范。

SOAP 请求基本上是一个包含 SOAP 信封、头和主体的 HTTP POST 请求。

SOAP 的核心是Web 服务描述语言WSDL),基本上是 XML。WSDL 用于描述 Web 服务的功能,这里是我们的 API 方法。这是通过使用以下一系列预定的对象来实现的:

  • 类型:用于描述与 API 传输的数据;类型使用 XML Schema 进行定义,这是一种专门用于此目的的语言

  • 消息:用于指定执行每个操作所需的信息;在 Magento 的情况下,我们的 API 方法将始终使用请求和响应消息

  • 端口类型:用于定义可以执行的操作及其相应的消息

  • 端口:用于定义连接点;在 Magento 的情况下,使用简单的字符串

  • 服务:用于指定通过 API 公开的功能

  • 绑定:用于定义与 SOAP 协议的操作和接口

注意

有关 SOAP 协议的更多信息,请参考以下网站:

en.wikipedia.org/wiki/SOAP

所有 WSDL 配置都包含在每个模块的wsdl.xml文件中;例如,让我们看一下目录产品 API 的摘录:

文件位置为app/code/local/Mdg/Giftregistry/etc/wsdl.xml

<?xml version="1.0" encoding="UTF-8"?>
<definitions  

             name="{{var wsdl.name}}" targetNamespace="urn:{{var wsdl.name}}">
    <types>
        <schema  targetNamespace="urn:Magento">
      ...
            <complexType name="catalogProductEntity">
                <all>
                    <element name="product_id" type="xsd:string"/>
                    <element name="sku" type="xsd:string"/>
                    <element name="name" type="xsd:string"/>
                    <element name="set" type="xsd:string"/>
                    <element name="type" type="xsd:string"/>
                    <element name="category_ids" type="typens:ArrayOfString"/>
                    <element name="website_ids" type="typens:ArrayOfString"/>
                </all>
            </complexType>

        </schema>
    </types>
    <message name="catalogProductListResponse">
        <part name="storeView" type="typens:catalogProductEntityArray"/>
    </message>
  ...
    <portType name="{{var wsdl.handler}}PortType">
    ...
        <operation name="catalogProductList">
            <documentation>Retrieve products list by filters</documentation>
            <input message="typens:catalogProductListRequest"/>
            <output message="typens:catalogProductListResponse"/>
        </operation>
        ...
    </portType>
    <binding name="{{var wsdl.handler}}Binding" type="typens:{{var wsdl.handler}}PortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
    ...
        <operation name="catalogProductList">
            <soap:operation soapAction="urn:{{var wsdl.handler}}Action"/>
            <input>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            </input>
            <output>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            </output>
        </operation>
    ...
    </binding>
    <service name="{{var wsdl.name}}Service">
        <port name="{{var wsdl.handler}}Port" binding="typens:{{var wsdl.handler}}Binding">
            <soap:address location="{{var wsdl.url}}"/>
        </port>
    </service>
</definitions>

通过使用 WSDL,我们可以记录、列出和支持更复杂的数据类型。

RESTful API

RESTful API 是 Magento 支持的协议家族的新成员,仅适用于 Magento CE 1.7 或更早版本。

注意

RESTful web service(也称为RESTful web API)是使用 HTTP 和 REST 原则实现的 Web 服务。

RESTful API 可以通过以下三个方面来定义:

  • 它使用标准的 HTTP 方法,如 GET、POST、DELETE 和 PUT

  • 其公开的 URI 以目录结构的形式进行格式化

  • 它使用 JSON 或 XML 来传输信息

注意

REST API 支持两种格式的响应,即 XML 和 JSON。

REST 相对于 SOAP 和 XML-RPC 的优势之一是,与 REST API 的所有交互都是通过 HTTP 协议完成的,这意味着它几乎可以被任何编程语言使用。

Magento REST API 具有以下特点:

  • 通过向 Magento API 服务发出 HTTP 请求来访问资源

  • 服务回复请求的数据或状态指示器,甚至两者都有

  • 所有资源都可以通过https://magento.localhost.com/api/rest/访问

  • 资源返回 HTTP 状态码,例如HTTP 状态码 200表示响应成功,或HTTP 状态码 400表示错误请求

  • 通过将特定路径添加到基本 URL(https://magento.localhost.com/api/rest/)来请求特定资源

REST 使用HTTP 动词来管理资源的状态。在 Magento 实现中,有四个动词可用:GET、POST、PUT 和 DELETE。因此,在大多数情况下,使用 RESTful API 是微不足道的。

使用 API

现在我们已经澄清了每个可用协议,让我们探索一下 Magento API 可以做什么,以及如何使用每个可用协议进行操作。

我们将使用产品端点作为访问和处理不同 API 协议的示例。

注意

示例是用 PHP 提供的,并且使用了三种不同的协议。要获取 PHP 的完整示例并查看其他编程语言的示例,请访问magedevguide.com

为 XML-RPC/SOAP 设置 API 凭据

在开始之前,我们需要创建一组 Web 服务凭据,以便访问 API 功能。

我们需要设置 API 用户角色。角色通过使用访问控制列表ACL)来控制 API 的权限。通过实施这种设计模式,Magento 能够限制其 API 的某些部分只对特定用户开放。

在本章的后面,我们将学习如何将自定义函数添加到 ACL 并保护自定义扩展的 API 方法。现在,我们只需要通过执行以下步骤创建一个具有完全权限的角色:

  1. 转到 Magento 后端。

  2. 从主导航菜单转到系统 | Web 服务 | 角色

  3. 单击添加新角色按钮。

  4. 如下截图所示,您将被要求提供角色名称并指定角色资源:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_06_02.jpg

  5. 默认情况下,资源访问选项设置为自定义,未选择任何资源。在我们的情况下,我们将通过从下拉菜单中选择全部来更改资源访问选项。

  6. 单击保存角色按钮。

现在我们在商店中有一个有效的角色,让我们继续创建 Web API 用户:

  1. 转到 Magento 后端。

  2. 从主导航菜单转到系统 | Web 服务 | 用户

  3. 单击添加新用户按钮。

  4. 接下来,我们将被要求提供用户信息,如下截图所示:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_06_01.jpg

  5. API 密钥API 密钥确认字段中输入您想要的密码。

  6. 单击用户角色选项卡。

  7. 选择我们刚创建的用户角色。

  8. 单击保存用户按钮。

我们需要为访问 API 创建用户名和角色的原因是,每个 API 函数都需要传递会话令牌作为参数。

因此,每次我们需要使用 API 时,我们必须首先调用login函数,该函数将返回有效的会话令牌 ID。

设置 REST API 凭据

新的 RESTful API 在身份验证方面略有不同;它不是使用传统的 Magento 网络服务用户,而是使用三足 OAuth 1.0 协议来提供身份验证。

OAuth 通过要求用户授权其应用程序来工作。当用户注册应用程序时,他/她需要填写以下字段:

  • 用户:这是一个客户,他在 Magento 上有帐户,并可以使用 API 的服务。

  • 消费者:这是使用 OAuth 访问 Magento API 的第三方应用程序。

  • 消费者密钥:这是用于识别 Magento 用户的唯一值。

  • 消费者密钥:这是客户用来保证消费者密钥所有权的秘密。此值永远不会在请求中传递。

  • 请求令牌:此值由消费者(应用程序)用于从用户那里获取授权以访问 API 资源。

  • 访问令牌:这是在成功认证时以请求令牌交换返回的。

让我们继续通过转到系统 | Web 服务 | REST - OAuth 消费者并在管理面板中选择添加新来注册我们的应用程序:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_06_03.jpg

注意

需要注意的一件重要的事情是必须定义回调 URL,用户在成功授权应用程序后将被重定向到该 URL。

我们的第一步是学习如何在每个可用的 API 协议中获取此会话令牌 ID。

要在 XML-RPC 中获取会话令牌 ID,我们需要执行以下代码:

$apiUser = 'username';
$apiKey = 'password';
$client = new Zend_XmlRpc_Client('http://ourhost.com/api/xmlrpc/');
// We authenticate ourselves and get a session token id 
$sessionId = $client->call('login', array($apiUser, $apiKey));

要在 SOAP v2 中获取会话令牌 ID,我们需要执行以下代码:

$apiUser = 'username';
$apiKey = 'password';
$client = new SoapClient('http://ourhost.com/api/v2_soap/?wsdl');
// We authenticate ourselves and get a session token id 
$sessionId = $client->login($apiUser, $apiKey);

要在 REST 中获取会话令牌 ID,我们需要执行以下步骤:

$callbackUrl = "http://magento.localhost.com/oauth_admin.php";
$temporaryCredentialsRequestUrl = "http://magento.localhost.com/oauth/initiate?oauth_callback=" . urlencode($callbackUrl);
$adminAuthorizationUrl = 'http://magento.localhost.com/admin/oAuth_authorize';
$accessTokenRequestUrl = 'http://magento.localhost.com/oauth/token';
$apiUrl = 'http://magento.localhost.com/api/rest';
$consumerKey = 'yourconsumerkey';
$consumerSecret = 'yourconsumersecret';

session_start();

$authType = ($_SESSION['state'] == 2) ? OAUTH_AUTH_TYPE_AUTHORIZATION : OAUTH_AUTH_TYPE_URI;
$oauthClient = new OAuth($consumerKey, $consumerSecret, OAUTH_SIG_METHOD_HMACSHA1, $authType);

$oauthClient->setToken($_SESSION['token'], $_SESSION['secret']);

加载和读取数据

Mage_Catalog模块产品端点具有以下公开方法,我们可以使用这些方法来管理产品:

  • catalog_product.currentStore:设置/获取当前商店视图

  • catalog_product.list:使用过滤器检索产品列表

  • catalog_product.info:检索产品

  • catalog_product.create:创建新产品

  • catalog_product.update:更新产品

  • catalog_product.setSpecialPrice:为产品设置特殊价格

  • catalog_product.getSpecialPrice:获取产品的特殊价格

  • catalog_product.delete:删除产品

目前,我们特别感兴趣的功能是catalog_product.listcatalog_product.info。让我们看看如何使用 API 从我们的暂存商店中检索产品数据。

要从我们的暂存商店中以 XML-RPC 检索产品数据,请执行以下代码:

$result = $client->call($sessionId, 'catalog_product.list');
print_r ($result);

要从我们的暂存商店中以 SOAPv2 检索产品数据,请执行以下代码:

$result = $client->catalogProductList($sessionId);
print_r($result);

要从我们的暂存商店中以 REST 检索产品数据,请执行以下代码:

$resourceUrl = $apiUrl . "/products";
$oauthClient->fetch($resourceUrl, array(), 'GET', array('Content-Type' => 'application/json'));
$productsList = json_decode($oauthClient->getLastResponse());

无论使用哪种协议,我们都将得到所有产品的 SKU 列表,但是如果我们想根据属性筛选产品列表呢?Magento 列出了允许我们根据属性筛选产品列表的功能,通过传递参数。话虽如此,让我们看看如何为我们的产品列表调用添加过滤器。

要在 XML-RPC 中为我们的产品列表调用添加过滤器,请执行以下代码:

$result = $client->call('catalog_product.list', array($sessionId, $filters);
print_r ($result);

要在 SOAPv2 中为我们的产品列表调用添加过滤器,请执行以下代码:

$result = $client->catalogProductList($sessionId,$filters);
print_r($result);

使用 REST,事情并不那么简单,无法按属性检索产品集合。但是,我们可以通过执行以下代码来检索属于特定类别的所有产品:

$categoryId = 3;
$resourceUrl = $apiUrl . "/products/category_id=" . categoryId ;
$oauthClient->fetch($resourceUrl, array(), 'GET', array('Content-Type' => 'application/json'));
$productsList = json_decode($oauthClient->getLastResponse());

更新数据

现在我们能够从 Magento API 中检索产品信息,我们可以开始更新每个产品的内容。

catalog_product.update方法将允许我们修改任何产品属性;函数调用需要以下参数。

要在 XML-RPC 中更新数据,请执行以下代码:

$productId = 200;
$productData = array( 'sku' => 'changed_sku', 'name' => 'New Name', 'price' => 15.40 );
$result = $client->call($sessionId, 'catalog_product.update', array($productId, $productData));
print_r($result);

要在 SOAPv2 中更新数据,请执行以下代码:

$productId = 200;
$productData = array( 'sku' => 'changed_sku', 'name' => 'New Name', 'price' => 15.40 );
$result = $client->catalogProductUpdate($sessionId, array($productId, $productData));
print_r($result);

要在 REST 中更新数据,请执行以下代码:

$productData = json_encode(array(
    'type_id'           => 'simple',
    'attribute_set_id'  => 4,
    'sku'               => 'simple' . uniqid(),
    'weight'            => 10,
    'status'            => 1,
    'visibility'        => 4,
    'name'              => 'Test Product',
    'description'       => 'Description',
    'short_description' => 'Short Description',
    'price'             => 29.99,
    'tax_class_id'      => 2,
));
$oauthClient->fetch($resourceUrl, $productData, OAUTH_HTTP_METHOD_POST, array('Content-Type' => 'application/json'));
$updatedProduct = json_decode($oauthClient->getLastResponseInfo());

删除产品

使用 API 删除产品非常简单,可能是最常见的操作之一。

要在 XML-RPC 中删除产品,请执行以下代码:

$productId = 200;
$result = $client->call($sessionId, 'catalog_product.delete', $productId);
print_r($result);

要在 SOAPv2 中删除产品,请执行以下代码:

$productId = 200;
$result = $client->catalogProductDelete($sessionId, $productId);
print_r($result);

要删除 REST 中的代码,请执行以下代码:

$productData = json_encode(array(
    'id'           => 4
));
$oauthClient->fetch($resourceUrl, $productData, OAUTH_HTTP_METHOD_DELETE, array('Content-Type' => 'application/json'));
$updatedProduct = json_decode($oauthClient->getLastResponseInfo());

扩展 API

现在我们已经基本了解了如何使用 Magento Core API,我们可以继续扩展并添加我们自己的自定义功能。为了添加新的 API 功能,我们必须修改/创建以下文件:

  • wsdl.xml

  • api.xml

  • api.php

为了使我们的注册表可以被第三方系统访问,我们需要创建并公开以下功能:

  • giftregistry_registry.list:这将检索所有注册表 ID 的列表,并带有可选的客户 ID 参数

  • giftregistry_registry.info:这将检索所有注册表信息,并带有必需的registry_id参数

  • giftregistry_item.list:这将检索与注册表关联的所有注册表项 ID 的列表,并带有必需的registry_id参数

  • giftregistry_item.info:这将检索注册表项的产品和详细信息,并带有一个必需的item_id参数

到目前为止,我们只添加了读取操作。现在让我们尝试包括用于更新、删除和创建注册表和注册表项的 API 方法。

提示

要查看完整代码和详细说明的答案,请访问www.magedevguide.com/

我们的第一步是实现 API 类和所需的功能:

  1. 导航到Model目录。

  2. 创建一个名为Api.php的新类,并将以下占位符内容放入其中:

文件位置是app/code/local/Mdg/Giftregistry/Model/Api.php

<?php
class Mdg_Giftregisty_Model_Api extends Mage_Api_Model_Resource_Abstract
{
    public function getRegistryList($customerId = null)
    {

    }

    public function getRegistryInfo($registryId)
    {

    }

    public function getRegistryItems($registryId)
    {

    }

    public function getRegistryItemInfo($registryItemId)
    {

    }
}
  1. 创建一个名为Api/的新目录。

  2. Api/内创建一个名为V2.php的新类,并将以下占位符内容放入其中:

文件位置是app/code/local/Mdg/Giftregistry/Model/Api/V2.php

<?php
class Mdg_Giftregisty_Model_Api_V2 extends Mdg_Giftregisty_Model_Api
{

}

您可能注意到的第一件事是V2.php文件正在扩展我们刚刚创建的API类。唯一的区别是V2类由SOAP_v2协议使用,而常规的API类用于所有其他请求。

让我们使用以下有效代码更新API类:

文件位置是app/code/local/Mdg/Giftregistry/Model/Api.php

<?php 
class Mdg_Giftregisty_Model_Api extends Mage_Api_Model_Resource_Abstract
{
    public function getRegistryList($customerId = null)
    {
        $registryCollection = Mage::getModel('mdg_giftregistry/entity')->getCollection();
        if(!is_null($customerId))
        {
            $registryCollection->addFieldToFilter('customer_id', $customerId);
        }
        return $registryCollection;
    }

    public function getRegistryInfo($registryId)
    {
        if(!is_null($registryId))
        {
            $registry = Mage::getModel('mdg_giftregistry/entity')->load($registryId);
            if($registry)
            {
                return $registry;
            } else {
		   return false;	  
		}
        } else {
            return false;
        }
    }

    public function getRegistryItems($registryId)
    {
        if(!is_null($registryId))
        {
            $registryItems = Mage::getModel('mdg_giftregistry/item')->getCollection();
            $registryItems->addFieldToFilter('registry_id', $registryId);
		Return $registryItems;
        } else {
            return false;
        }
    }

    public function getRegistryItemInfo($registryItemId)
    {
        if(!is_null($registryItemId))
        {
            $registryItem = Mage::getModel('mdg_giftregistry/item')->load($registryItemId);
            if($registryItem){
                return $registryItem;
            } else {
		   return false;
		}
        } else {
            return false;
        }
    }
}

从前面的代码中可以看到,我们并没有做任何新的事情。每个函数负责加载 Magento 对象的集合或基于所需参数加载特定对象。

为了将这个新功能暴露给 Magento API,我们需要配置之前创建的 XML 文件。让我们从更新api.xml文件开始:

  1. 打开api.xml文件。

  2. 添加以下 XML 代码:

文件位置是app/code/local/Mdg/Giftregistry/etc/api.xml

<?xml version="1.0"?>
<config>
    <api>
        <resources>
            <giftregistry_registry translate="title" module="mdg_giftregistry">
                <model>mdg_giftregistry/api</model>
                <title>Mdg Giftregistry Registry functions</title>
                <methods>
                    <list translate="title" module="mdg_giftregistry">
                        <title>getRegistryList</title>
                        <method>getRegistryList</method>
                    </list>
                    <info translate="title" module="mdg_giftregistry">
                        <title>getRegistryInfo</title>
                        <method>getRegistryInfo</method>
                    </info>
                </methods>
            </giftregistry_registry>
            <giftregistry_item translate="title" module="mdg_giftregistry">
                <model>mdg_giftregistry/api</model>
                <title>Mdg Giftregistry Registry Items functions</title>
                <methods>
                    <list translate="title" module="mdg_giftregistry">
                        <title>getRegistryItems</title>
                        <method>getRegistryItems</method>
                    </list>
                    <info translate="title" module="mdg_giftregistry">
                        <title>getRegistryItemInfo</title>
                        <method>getRegistryItemInfo</method>
                    </info>
                </methods>
            </giftregistry_item>
        </resources>
        <resources_alias>
            <giftregistry_registry>giftregistry_registry</giftregistry_registry>
            <giftregistry_item>giftregistry_item</giftregistry_item>
        </resources_alias>
        <v2>
            <resources_function_prefix>
                <giftregistry_registry>giftregistry_registry</giftregistry_registry>
                <giftregistry_item>giftregistry_item</giftregistry_item>
            </resources_function_prefix>
        </v2>
    </api>
</config>

还有一个文件需要更新,以确保 SOAP 适配器接收到我们的新 API 函数:

  1. 打开wsdl.xml文件。

  2. 由于wsdl.xml文件通常非常庞大,我们将在几个地方分解它。让我们从定义wsdl.xml文件的框架开始:

文件位置是app/code/local/Mdg/Giftregistry/etc/wsdl.xml

<?xml version="1.0" encoding="UTF-8"?>
<definitions   

             name="{{var wsdl.name}}" targetNamespace="urn:{{var wsdl.name}}">
    <types>

    </types>
    <message name="gitregistryRegistryListRequest">

    </message>
    <portType name="{{var wsdl.handler}}PortType">

    </portType>
    <binding name="{{var wsdl.handler}}Binding" type="typens:{{var wsdl.handler}}PortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />

    </binding>
    <service name="{{var wsdl.name}}Service">
        <port name="{{var wsdl.handler}}Port" binding="typens:{{var wsdl.handler}}Binding">
            <soap:address location="{{var wsdl.url}}" />
        </port>
    </service>
</definitions> 
  1. 这是基本的占位符。我们有本章开头定义的所有主要节点。我们首先要定义的是我们的 API 将使用的自定义数据类型:

文件位置是app/code/local/Mdg/Giftregistry/etc/wsdl.xml

<schema  targetNamespace="urn:Magento">
            <import namespace="http://schemas.xmlsoap.org/soap/encoding/" schemaLocation="http://schemas.xmlsoap.org/soap/encoding/"/>
            <complexType name="giftRegistryEntity">
                <all>
                    <element name="entity_id" type="xsd:integer" minOccurs="0" />
                    <element name="customer_id" type="xsd:integer" minOccurs="0" />
                    <element name="type_id" type="xsd:integer" minOccurs="0" />
                    <element name="website_id" type="xsd:integer" minOccurs="0" />
                    <element name="event_date" type="xsd:string" minOccurs="0" />
                    <element name="event_country" type="xsd:string" minOccurs="0" />
                    <element name="event_location" type="xsd:string" minOccurs="0" />
                </all>
            </complexType>
            <complexType name="giftRegistryEntityArray">
                <complexContent>
                    <restriction base="soapenc:Array">
                        <attribute ref="soapenc:arrayType" wsdl:arrayType="typens:giftRegistryEntity[]" />
                    </restriction>
                </complexContent>
            </complexType>
            <complexType name="registryItemsEntity">
                <all>
                    <element name="item_id" type="xsd:integer" minOccurs="0" />
                    <element name="registry_id" type="xsd:integer" minOccurs="0" />
                    <element name="product_id" type="xsd:integer" minOccurs="0" />
                </all>
            </complexType>
            <complexType name="registryItemsArray">
                <complexContent>
                    <restriction base="soapenc:Array">
                        <attribute ref="soapenc:arrayType" wsdl:arrayType="typens:registryItemsEntity[]" />
                    </restriction>
                </complexContent>
            </complexType>
        </schema>

注意

复杂数据类型允许我们映射通过 API 传输的属性和对象。

  1. 消息允许我们定义在每个 API 调用请求和响应中传输的复杂类型。让我们继续在我们的wsdl.xml中添加相应的消息:

文件位置是app/code/local/Mdg/Giftregistry/etc/wsdl.xml

<message name="gitregistryRegistryListRequest">
        <part name="sessionId" type="xsd:string" />
        <part name="customerId" type="xsd:integer"/>
    </message>
    <message name="gitregistryRegistryListResponse">
        <part name="result" type="typens:giftRegistryEntityArray" />
    </message>
    <message name="gitregistryRegistryInfoRequest">
        <part name="sessionId" type="xsd:string" />
        <part name="registryId" type="xsd:integer"/>
    </message>
    <message name="gitregistryRegistryInfoResponse">
        <part name="result" type="typens:giftRegistryEntity" />
    </message>
    <message name="gitregistryItemListRequest">
        <part name="sessionId" type="xsd:string" />
        <part name="registryId" type="xsd:integer"/>
    </message>
    <message name="gitregistryItemListResponse">
        <part name="result" type="typens:registryItemsArray" />
    </message>
    <message name="gitregistryItemInfoRequest">
        <part name="sessionId" type="xsd:string" />
        <part name="registryItemId" type="xsd:integer"/>
    </message>
    <message name="gitregistryItemInfoResponse">
        <part name="result" type="typens:registryItemsEntity" />
    </message>
  1. 一个重要的事情要注意的是,每个请求消息将始终包括一个sessionId属性,用于验证和认证每个请求,而响应用于指定返回的编译数据类型或值:

文件位置是app/code/local/Mdg/Giftregistry/etc/wsdl.xml

<portType name="{{var wsdl.handler}}PortType">
        <operation name="giftregistryRegistryList">
            <documentation>Get Registries List</documentation>
            <input message="typens:gitregistryRegistryListRequest" />
            <output message="typens:gitregistryRegistryListResponse" />
        </operation>
        <operation name="giftregistryRegistryInfo">
            <documentation>Get Registry Info</documentation>
            <input message="typens:gitregistryRegistryInfoRequest" />
            <output message="typens:gitregistryRegistryInfoResponse" />
        </operation>
        <operation name="giftregistryItemList">
            <documentation>getAllProductsInfo</documentation>
            <input message="typens:gitregistryItemListRequest" />
            <output message="typens:gitregistryItemListResponse" />
        </operation>
        <operation name="giftregistryItemInfo">
            <documentation>getAllProductsInfo</documentation>
            <input message="typens:gitregistryItemInfoRequest" />
            <output message="typens:gitregistryItemInfoResponse" />
        </operation>
    </portType>
  1. 为了正确添加新的 API 端点,下一个需要的是定义绑定,用于指定哪些方法是公开的:

文件位置是app/code/local/Mdg/Giftregistry/etc/wsdl.xml

<operation name="giftregistryRegistryList">
            <soap:operation soapAction="urn:{{var wsdl.handler}}Action" />
            <input>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="giftregistryRegistryInfo">
            <soap:operation soapAction="urn:{{var wsdl.handler}}Action" />
            <input>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="giftregistryItemList">
            <soap:operation soapAction="urn:{{var wsdl.handler}}Action" />
            <input>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="giftregistryInfoList">
            <soap:operation soapAction="urn:{{var wsdl.handler}}Action" />
            <input>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>

注意

你可以在http://magedevguide.com/chapter6/wsdl上看到完整的wsdl.xml

即使我们把它分解了,WSDL 代码仍然可能令人不知所措,老实说,我花了一些时间才习惯这样一个庞大的 XML 文件。所以如果你觉得或者感觉它太多了,就一步一步来吧。

扩展 REST API

到目前为止,我们只是在扩展 API 的 SOAP 和 XML-RPC 部分上工作。扩展 RESTful API 的过程略有不同。

注意

REST API 是在 Magento Community Edition 1.7 和 Enterprise Edition 1.12 中引入的。

为了将新的 API 方法暴露给 REST API,我们需要创建一个名为api2.xml的新文件。这个文件的配置比普通的api.xml复杂一些,所以我们将在添加完整代码后对其进行分解:

  1. etc/文件夹下创建一个名为api2.xml的新文件。

  2. 打开api2.xml

  3. 复制以下代码:

文件位置是app/code/local/Mdg/Giftregistry/etc/api2.xml

<?xml version="1.0"?>
<config>
    <api2>
        <resource_groups>
            <giftregistry translate="title" module="mdg_giftregistry">
                <title>MDG GiftRegistry API calls</title>
                <sort_order>30</sort_order>
                <children>
                    <giftregistry_registry translate="title" module="mdg_giftregistry">
                        <title>Gift Registries</title>
                        <sort_order>50</sort_order>
                    </giftregistry_registry>
                    <giftregistry_item translate="title" module="mdg_giftregistry">
                        <title>Gift Registry Items</title>
                        <sort_order>50</sort_order>
                    </giftregistry_item>
                </children>
            </giftregistry>
        </resource_groups>
        <resources>
            <giftregistryregistry translate="title" module="mdg_giftregistry">
                <group>giftregistry_registry</group>
                <model>mdg_giftregistry/api_registry</model>
                <working_model>mdg_giftregistry/api_registry</working_model>
                <title>Gift Registry</title>
                <sort_order>10</sort_order>
                <privileges>
                    <admin>
                        <create>1</create>
                        <retrieve>1</retrieve>
                        <update>1</update>
                        <delete>1</delete>
                    </admin>
                </privileges>
                <attributes translate="product_count" module="mdg_giftregistry">
                    <registry_list>Registry List</registry_list>
                    <registry>Registry</registry>
                    <item_list>Item List</item_list>
                    <item>Item</item>
                </attributes>
                <entity_only_attributes>
                </entity_only_attributes>
                <exclude_attributes>
                </exclude_attributes>
                <routes>
                    <route_registry_list>
                        <route>/mdg/registry/list</route>
                        <action_type>collection</action_type>
                    </route_registry_list>
                    <route_registry_entity>
                        <route>/mdg/registry/:registry_id</route>
                        <action_type>entity</action_type>
                    </route_registry_entity>
                    <route_registry_list>
                        <route>/mdg/registry_item/list</route>
                        <action_type>collection</action_type>
                    </route_registry_list>
                    <route_registry_list>
                        <route>/mdg/registry_item/:item_id</route>
                        <action_type>entity</action_type>
                    </route_registry_list>
                </routes>
                <versions>1</versions>
            </giftregistryregistry>
        </resources>
    </api2>
</config>

一个重要的事情要注意的是,我们在这个配置文件中定义了一个路由节点。这被 Magento 视为前端路由,用于访问 RESTful api函数。还要注意的是,我们不需要为此创建一个新的控制器。

现在,我们还需要包括一个新的类来处理 REST 请求,并实现每个定义的权限:

  1. Model/Api/Registry/Rest/Admin下创建一个名为V1.php的新类。

  2. 打开V1.php类并复制以下代码:

文件位置是app/code/local/Mdg/Giftregistry/Model/Api/Registry/Rest/Admin/V1.php

<?php

class Mdg_Giftregistry_Model_Api_Registry_Rest_Admin_V1 extends Mage_Catalog_Model_Api2_Product_Rest {
    /**
     * @return stdClass
     */
    protected function _retrieve()
    {
        $registryCollection = Mage::getModel('mdg_giftregistry/entity')->getCollection();
        return $registryCollection;
    }
}

保护 API

保护我们的 API 已经是创建模块过程的一部分,也由配置处理。Magento 限制对其 API 的访问方式是使用 ACL。

正如我们之前学到的,这些 ACL 允许我们设置具有访问 API 不同部分权限的角色。现在,我们要做的是使我们的新自定义功能对 ACL 可用:

  1. 打开api.xml文件。

  2. </v2>节点之后添加以下代码:

文件位置为app/code/local/Mdg/Giftregistry/etc/api.xml

<acl>
    <resources>
        <giftregistry translate="title" module="mdg_giftregistry">
            <title>MDG Gift Registry</title>
            <sort_order>1</sort_order>
            <registry translate="title" module="mdg_giftregistry">
                <title>MDG Gift Registry</title>
                <list translate="title" module="mdg_giftregistry">
                    <title>List Available Registries</title>
                </list>
                <info translate="title" module="mdg_giftregistry">
                    <title>Retrieve registry data</title>
                </info>
            </registry>
            <item translate="title" module="mdg_giftregistry">
                <title>MDG Gift Registry Item</title>
                <list translate="title" module="mdg_giftregistry">
                    <title>List Available Items inside a registry</title>
                </list>
                <info translate="title" module="mdg_giftregistry">
                    <title>Retrieve registry item data</title>
                </info>
            </item>
        </giftregistry>
    </resources>
</acl>

总结

在之前的章节中,我们学会了如何扩展 Magento 以为商店所有者和客户添加新功能;了解如何扩展和使用 Magento API 为我们打开了无限的可能性。

通过使用 API,我们可以将 Magento 与 ERP 和销售点等第三方系统集成;既可以导入数据,也可以导出数据。

在下一章中,我们将学习如何为我们迄今为止构建的所有代码正确构建测试,并且我们还将探索多个测试框架。

第七章:测试和质量保证

到目前为止,我们已经涵盖了:

  • Magento 基础知识

  • 前端开发

  • 后端开发

  • 扩展和使用 API

然而,我们忽略了任何扩展或自定义代码开发的关键步骤:测试和质量保证。

尽管 Magento 是一个非常复杂和庞大的平台,但在 Magento2 之前的版本中没有包含/集成的单元测试套件。

因此,适当的测试和质量保证经常被大多数 Magento 开发人员忽视,要么是因为缺乏信息,要么是因为一些测试工具的大量开销,虽然没有太多可用于运行 Magento 的适当测试的工具,但现有的工具质量非常高。

在本章中,我们将看看测试 Magento 代码的不同选项,并为我们的自定义扩展构建一些非常基本的测试。

因此,让我们来看看本章涵盖的主题:

  • Magento 可用的不同测试框架和工具

  • 测试我们的 Magento 代码的重要性

  • 如何设置、安装和使用 Ecomdev PHPUnit 扩展

  • 如何设置、安装和使用 Magento Mink 来运行功能测试

测试 Magento

在我们开始编写任何测试之前,重要的是我们了解与测试相关的概念,尤其是每种可用方法论。

单元测试

单元测试的理念是为我们代码的某些区域(单元)编写测试,以便我们可以验证代码是否按预期工作,并且函数是否返回预期值。

单元测试是一种方法,通过该方法测试源代码的单个单元,以确定它们是否适合使用,其中包括一个或多个计算机程序模块以及相关的控制数据、使用程序和操作程序。

编写单元测试的另一个优势是,通过执行测试,我们更有可能编写更容易测试的代码。

这意味着随着我们不断编写更多的测试,我们的代码往往会被分解成更小但更专业的功能。我们开始构建一个测试套件,可以在引入更改或功能时针对我们的代码库运行;这就是回归测试。

回归测试

回归测试主要是指在进行代码更改后重新运行现有测试套件的做法,以检查新功能是否也引入了新错误。

回归测试是一种软件测试,旨在在对现有系统的功能和非功能区域进行更改(如增强、补丁或配置更改)后,发现新的软件错误或回归。

在 Magento 商店或任何电子商务网站的特定情况下,我们希望对商店的关键功能进行回归测试,例如结账、客户注册、添加到购物车等。

功能测试

功能测试更关注的是根据特定输入返回适当输出的应用程序,而不是内部发生的情况。

功能测试是一种基于被测试软件组件的规范的黑盒测试类型。通过向它们提供输入并检查输出来测试功能,很少考虑内部程序结构。

这对于像我们这样的电子商务网站尤为重要,我们希望测试网站与客户的体验一致。

TDD

近年来变得越来越受欢迎的一种测试方法,现在也正在 Magento 中出现,被称为测试驱动开发TDD)。

测试驱动开发(TDD)是一种依赖于非常短的开发周期重复的软件开发过程:首先开发人员编写一个(最初失败的)自动化测试用例,定义所需的改进或新功能,然后生成最少量的代码来通过该测试,最后将新代码重构为可接受的标准。

TDD 背后的基本概念是首先编写一个失败的测试,然后编写代码来通过测试;这会产生非常短的开发周期,并有助于简化代码。

理想情况下,您希望通过在 Magento 中使用 TDD 来开始开发您的模块和扩展。我们在之前的章节中省略了这一点,因为这会增加不必要的复杂性并使读者困惑。

注意

有关从头开始使用 Magento 进行 TDD 的完整教程,请访问http://magedevguide.com/getting-started-with-tdd

工具和测试框架

如前所述,有几个框架和工具可用于测试 PHP 代码和 Magento 代码。让我们更好地了解每一个:

  • Ecomdev_PHPUnit:这个扩展真是太棒了;Ecomdev 的开发人员创建了一个集成了 PHPUnit 和 Magento 的扩展,还向 PHPUnit 添加了 Magento 特定的断言,而无需修改核心文件或影响数据库。

  • Magento_Mink:Mink 是 Behat 框架的 PHP 库,允许您编写功能和验收测试;Mink 允许编写模拟用户行为和浏览器交互的测试。

  • Magento_TAFMagento_TAF代表 Magento 测试自动化框架,这是 Magento 提供的官方测试工具。Magento_TAF包括超过 1,000 个功能测试,非常强大。不幸的是,它有一个主要缺点;它有很大的开销和陡峭的学习曲线。

使用 PHPUnit 进行单元测试

Ecomdev_PHPUnit之前,使用 PHPUnit 测试 Magento 是有问题的,而且从可用的不同方法来看,实际上并不实用。几乎所有都需要核心代码修改,或者开发人员必须费力地设置基本的 PHPUnits。

安装 Ecomdev_PHPUnit

安装Ecomdev_PHPUnit的最简单方法是直接从 GitHub 存储库获取副本。让我们在控制台上写下以下命令:

**git clone git://github.com/IvanChepurnyi/EcomDev_PHPUnit.git**

现在将文件复制到您的 Magento 根目录。

注意

Composer 和 Modman 是可用于安装的替代选项。有关每个选项的更多信息,请访问magedevguide.com/module-managers

最后,我们需要设置配置,指示 PHPUnit 扩展使用哪个数据库;local.xml.phpunitEcomdev_PHPUnit添加的新文件。这个文件包含所有特定于扩展的配置,并指定测试数据库的名称。

文件位置为app/etc/local.xml.phpunit。参考以下代码:

<?xml version="1.0"?>
<config>
    <global>
        <resources>
            <default_setup>
                <connection>
                   <dbname><![CDATA[magento_unit_tests]]></dbname>
                </connection>
            </default_setup>
        </resources>
    </global>
    <default>
        <web>
            <seo>
                <use_rewrites>1</use_rewrites>
            </seo>
            <secure>
                <base_url>[change me]</base_url>
            </secure>
            <unsecure>
                <base_url>[change me]</base_url>
            </unsecure>
            <url>
                <redirect_to_base>0</redirect_to_base>
            </url>
        </web>
    </default>
    <phpunit>
        <allow_same_db>0</allow_same_db>
    </phpunit>
</config>

您需要为运行测试创建一个新的数据库,并在local.xml.phpunit文件中替换示例配置值。

默认情况下,这个扩展不允许您在同一个数据库上运行测试;将测试数据库与开发和生产数据库分开允许我们有信心地运行我们的测试。

为我们的扩展设置配置

现在我们已经安装并设置了 PHPUnit 扩展,我们需要准备我们的礼品注册扩展来运行单元测试。按照以下步骤进行:

  1. 打开礼品注册扩展的config.xml文件

  2. 添加以下代码(文件位置为app/code/local/Mdg/Giftregistry/etc/config.xml):

<phpunit>
        <suite>
            <modules>
                    <Mdg_Giftregistry/>
            </modules>
         </suite>
</phpunit>

这个新的配置节点允许 PHPUnit 扩展识别扩展并运行匹配的测试。

我们还需要创建一个名为Test的新目录,我们将用它来放置所有的测试文件。使用Ecomdev_PHPUnit相比以前的方法的一个优点是,这个扩展遵循 Magento 的标准。

这意味着我们必须在Test文件夹内保持相同的模块目录结构:

Test/
Model/
Block/
Helper/
Controller/
Config/

基于此,每个Test案例类的命名约定将是[Namespace]_[Module Name]_Test_[Group Directory]_[Entity Name]

每个Test类必须扩展以下三个基本Test类中的一个:

  • EcomDev_PHPUnit_Test_Case:这个类用于测试助手、模型和块

  • EcomDev_PHPUnit_Test_Case_Config:这个类用于测试模块配置

  • EcomDev_PHPUnit_Test_Case_Controller:这个类用于测试布局渲染过程和控制器逻辑

测试案例的解剖

在跳入并尝试创建我们的第一个测试之前,让我们分解Ecomdev_PHPUnit提供的一个示例:

<?php
class EcomDev_Example_Test_Model_Product extends EcomDev_PHPUnit_Test_Case
{
    /**
     * Product price calculation test
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function priceCalculation($productId, $storeId)
    {
        $storeId = Mage::app()->getStore($storeId)->getId();
        $product = Mage::getModel('catalog/product')
            ->setStoreId($storeId)
            ->load($productId);
        $expected = $this->expected('%s-%s', $productId, $storeId);
        $this->assertEquals(
            $expected->getFinalPrice(),
            $product->getFinalPrice()
        );
        $this->assertEquals(
            $expected->getPrice(),
            $product->getPrice()
        );
    }
}

在示例test类中要注意的第一件重要的事情是注释注释:

/**
     * Product price calculation test
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */

这些注释被 PHPUnit 扩展用来识别哪些类函数是测试,它们还允许我们为运行每个测试设置特定的设置。让我们来看一下一些可用的注释:

  • @test:这个注释将一个类函数标识为 PHPUnit 测试

  • @loadFixture:这个注释指定了固定的使用

  • @loadExpectation:这个注释指定了期望的使用

  • @doNotIndexAll:通过添加这个注释,我们告诉 PHPUnit 测试在加载固定后不应该运行任何索引

  • @doNotIndex [index_code]:通过添加这个注释,我们可以指示 PHPUnit 不运行特定的索引

所以现在,你可能有点困惑。固定?期望?它们是什么?

以下是对固定和期望的简要描述:

  • 固定:固定是另一种标记语言YAML)文件,代表数据库或配置实体

  • 期望:期望对我们的测试中不想要硬编码的值很有用,也是在 YAML 值中指定的

注意

有关 YAML 标记的更多信息,请访问http://magedevguide.com/resources/yaml

所以,正如我们所看到的,固定提供了测试处理的数据,期望用于检查测试返回的结果是否是我们期望看到的。

固定和期望存储在每个Test类型目录中。按照之前的例子,我们将有一个名为Product/的新目录。在里面,我们需要一个期望的新目录和一个我们的固定的新目录。

让我们来看一下修订后的文件夹结构:

Test/
Model/  
  Product.php
  Product/
    expectations/
    fixtures/
Block/
Helper/
Controller/
Config/

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgt-php-dev-gd/img/3060OS_07_01.jpg

创建一个单元测试

对于我们的第一个单元测试,让我们创建一个非常基本的测试,允许我们测试之前创建的礼品注册模型。

正如我们之前提到的,Ecomdev_PHPUnit使用一个单独的数据库来运行所有的测试;为此,我们需要创建一个新的固定,为我们的测试用例提供所有的数据。按照以下步骤:

  1. 打开Test/Model文件夹。

  2. 创建一个名为Registry的新文件夹。

  3. Registry文件夹中,创建一个名为fixtures的新文件夹。

  4. 创建一个名为registryList.yaml的新文件,并将以下代码粘贴到其中(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/fixtures/registryList.yaml):

  website: # Initialize websites
    - website_id: 2
      code: default
      name: Test Website
      default_group_id: 2
  group: # Initializes store groups
    - group_id: 2
      website_id: 2
      name: Test Store Group
      default_store_id: 2
      root_category_id: 2 # Default Category
  store: # Initializes store views
    - store_id: 2
      website_id: 2
      group_id: 2
      code: default
      name: Default Test Store
      is_active: 1
eav:
   customer_customer:
     - entity_id: 1
       entity_type_id: 3
       website_id: 2
       email: test@magentotest.com
       group_id: 2
       store_id: 2
       is_active: 1
   mdg_giftregistry_entity:
     - entity_id: 1
       customer_id: 1
       type_id: 2
       website_id: 2
       event_date: 12/12/2012
       event_country: Canada
       event_location: Dundas Square
       created_at: 21/12/2012
     - entity_id: 2
       customer_id: 1
       type_id: 3
       website_id: 2
       event_date: 01/01/2013
       event_country: Canada
       event_location: Eaton Center
       created_at: 21/12/2012

它可能看起来不像,但我们通过这个固定添加了很多信息。我们将创建以下固定数据:

  • 一个网站范围

  • 一个商店组

  • 一个商店视图

  • 一个客户记录

  • 两个礼品注册

通过使用固定,我们正在创建可用于我们的测试用例的数据。这使我们能够多次运行相同的数据测试,并灵活地进行更改。

现在,你可能想知道 PHPUnit 扩展如何将Test案例与特定的固定配对。

扩展加载固定有两种方式:一种是在注释注释中指定固定,或者如果没有指定固定名称,扩展将搜索与正在执行的Test案例函数相同名称的固定。

知道这一点,让我们创建我们的第一个Test案例:

  1. 导航到Test/Model文件夹。

  2. 创建一个名为Registry.php的新Test类。

  3. 添加以下基本代码(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/Registry.php):

<?php
class Mdg_Giftregistry_Test_Model_Registry extends EcomDev_PHPUnit_Test_Case
{
    /**
     * Listing available registries
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function registryList()
    {

    }
}

我们刚刚创建了基本函数,但还没有添加任何逻辑。在这之前,让我们先看看什么构成了一个Test案例。

一个Test案例通过使用断言来评估和测试我们的代码。断言是我们的Test案例从父TestCase类继承的特殊函数。在默认可用的断言中,我们有:

  • assertEquals()

  • assertGreaterThan()

  • assertGreaterThanOrEqual()

  • assertLessThan()

  • assertLessThanOrEqual()

  • assertTrue()

现在,如果我们只使用这些类型的断言来测试 Magento 代码,可能会变得困难甚至不可能。这就是Ecomdev_PHPUnit发挥作用的地方。

这个扩展不仅将 PHPUnit 与 Magento 整合得很好,遵循他们的标准,还在 PHPUnit 测试中添加了 Magento 特定的断言。让我们来看看扩展添加的一些断言:

  • assertEventDispatched()

  • assertBlockAlias()

  • assertModelAlias()

  • assertHelperAlias()

  • assertModuleCodePool()

  • assertModuleDepends()

  • assertConfigNodeValue()

  • assertLayoutFileExists()

这些只是可用的一些断言,正如你所看到的,它们为构建全面的测试提供了很大的力量。

现在我们对 PHPUnit 的Test案例有了更多了解,让我们继续创建我们的第一个 Magento Test案例:

  1. 导航到之前创建的Registry.php测试案例类。

  2. registryList()函数内添加以下代码(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/Registry.php):

    /**
     * Listing available registries
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function registryList()
    {
        $registryList = Mage::getModel('mdg_giftregistry/entity')->getCollection();
        $this->assertEquals(
            2,
            $registryList->count()
        );
    }

这是一个非常基本的测试;我们所做的就是加载一个注册表集合。在这种情况下,所有的注册表都是可用的,然后他们运行一个断言来检查集合计数是否匹配。

然而,这并不是很有用。如果我们能够只加载属于特定用户(我们的测试用户)的注册表,并检查集合大小,那将更好。因此,让我们稍微改变一下代码:

文件位置为app/code/local/Mdg/Giftregistry/Test/Model/Registry.php。参考以下代码:

    /**
     * Listing available registries
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function registryList()
    {
        $customerId = 1;
        $registryList = Mage::getModel('mdg_giftregistry/entity')
->getCollection()
->addFieldToFilter('customer_id', $customerId);
        $this->assertEquals(
            2,
            $registryList->count()
        );
    }

仅仅通过改变几行代码,我们创建了一个测试,可以检查我们的注册表集合是否正常工作,并且是否正确地链接到客户记录。

在你的 shell 中运行以下命令:

**$ phpunit**

如果一切如预期般进行,我们应该看到以下输出:

**PHPUnit 3.4 by Sebastian Bergmann**
**.**
**Time: 1 second**
**Tests: 1, Assertions: 1, Failures 0**

注意

您还可以运行$phpunit—colors 以获得更好的输出。

现在,我们只需要一个测试来验证注册表项是否正常工作:

  1. 导航到之前创建的Registry.php测试案例类。

  2. registryItemsList()函数内添加以下代码(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/Registry.php):

    /**
     * Listing available items for a specific registry
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function registryItemsList()
    {
        $customerId = 1;
        $registry   = Mage::getModel('mdg_giftregistry/entity')
->loadByCustomerId($customerId);

        $registryItems = $registry->getItems();
        $this->assertEquals(
            3,
            $registryItems->count()
        );
    }

我们还需要一个新的 fixture 来配合我们的新Test案例:

  1. 导航到Test/Model文件夹。

  2. 打开Registry文件夹。

  3. 创建一个名为registryItemsList.yaml的新文件(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/fixtures/ registryItemsList.yaml):

  website: # Initialize websites
    - website_id: 2
      code: default
      name: Test Website
      default_group_id: 2
  group: # Initializes store groups
    - group_id: 2
      website_id: 2
      name: Test Store Group
      default_store_id: 2
      root_category_id: 2 # Default Category
  store: # Initializes store views
    - store_id: 2
      website_id: 2
      group_id: 2
      code: default
      name: Default Test Store
      is_active: 1
eav:
   customer_customer:
     - entity_id: 1
       entity_type_id: 3
       website_id: 2
       email: test@magentotest.com
       group_id: 2
       store_id: 2
       is_active: 1
   mdg_giftregistry_entity:
     - entity_id: 1
       customer_id: 1
       type_id: 2
       website_id: 2
       event_date: 12/12/2012
       event_country: Canada
       event_location: Dundas Square
       created_at: 21/12/2012
   mdg_giftregistry_item:
     - item_id: 1
       registry_id: 1
       product_id: 1
     - item_id: 2
       registry_id: 1
       product_id: 2
     - item_id: 3
       registry_id: 1
       product_id: 3 

让我们运行我们的测试套件:

**$phpunit --colors**

我们应该看到两个测试都通过了:

PHPUnit 3.4 by Sebastian Bergmann
.
Time: 4 second
Tests: 2, Assertions: 2, Failures 0

最后,让我们用正确的期望值替换我们的硬编码变量:

  1. 导航到Module Test/Model文件夹。

  2. 打开Registry文件夹。

  3. Registry文件夹内,创建一个名为expectations的新文件夹。

  4. 创建一个名为registryList.yaml的新文件(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/expectations/registryList.yaml)。

count: 2

是不是很容易?好吧,它是如此容易,以至于我们将再次为registryItemsList测试案例做同样的事情:

  1. 导航到Module Test/Model文件夹。

  2. 打开Registry文件夹。

  3. expectations文件夹中创建一个名为registryItemsList.yaml的新文件(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/expectations/registryItemsList.yaml):

count: 3

最后,我们需要做的最后一件事是更新我们的Test案例类以使用期望。确保更新文件具有以下代码(文件位置为app/code/local/Mdg/Giftregistry/Test/Model/Registry.php):

<?php
class Mdg_Giftregistry_Test_Model_Registry extends EcomDev_PHPUnit_Test_Case
{
    /**
     * Product price calculation test
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function registryList()
    {
        $customerId = 1;
        $registryList = Mage::getModel('mdg_giftregistry/entity')
                ->getCollection()
                ->addFieldToFilter('customer_id', $customerId);
        $this->assertEquals(
            $this->_getExpectations()->getCount(),$this->_getExpectations()->getCount(),
            $registryList->count()
        );
    }
    /**
     * Listing available items for a specific registry
     *
     * @test
     * @loadFixture
     * @doNotIndexAll
     * @dataProvider dataProvider
     */
    public function registryItemsList()
    {
        $customerId = 1;
        $registry   = Mage::getModel('mdg_giftregistry/entity')->loadByCustomerId($customerId);

        $registryItems = $registry->getItems();
        $this->assertEquals(
            $this->_getExpectations()->getCount(),
            $registryItems->count()
        );
    }
}

这里唯一的变化是,我们用期望值替换了断言中的硬编码值。如果我们需要进行任何更改,我们不需要更改我们的代码;我们只需更新期望和固定装置。

使用 Mink 进行功能测试

到目前为止,我们已经学会了如何对我们的代码运行单元测试,虽然单元测试非常适合测试代码和逻辑的各个部分,但对于像 Magento 这样的大型应用程序来说,从用户的角度进行测试是很重要的。

注意

功能测试主要涉及黑盒测试,不关心应用程序的源代码。

为了做到这一点,我们可以使用 Mink。Mink 是一个简单的 PHP 库,可以虚拟化 Web 浏览器。Mink 通过使用不同的驱动程序来工作。它支持以下驱动程序:

  • GoutteDriver:这是 Symfony 框架的创建者编写的纯 PHP 无头浏览器

  • SahiDriver:这是一个新的 JS 浏览器控制器,正在迅速取代 Selenium

  • ZombieDriver:这是一个在Node.js中编写的浏览器仿真器,目前只限于一个浏览器(Chromium)

  • SeleniumDriver:这是目前最流行的浏览器驱动程序;原始版本依赖于第三方服务器来运行测试

  • Selenium2Driver:Selenium 的当前版本在 Python、Ruby、Java 和 C#中得到了充分支持

Magento Mink 安装和设置

使用 Mink 与 Magento 非常容易,这要归功于 Johann Reinke,他创建了一个 Magento 扩展,方便了 Mink 与 Magento 的集成。

我们将使用 Modgit 来安装这个扩展,Modgit 是一个受 Modman 启发的模块管理器。Modgit 允许我们直接从 GitHub 存储库部署 Magento 扩展,而无需创建符号链接。

安装 Modgit 只需三行代码即可完成:

**wget -O modgit https://raw.github.com/jreinke/modgit/master/modgit**
**chmod +x modgit**
**sudo mv modgit /usr/local/bin**

是不是很容易?现在我们可以继续安装 Magento Mink,我们应该感谢 Modgit,因为这样甚至更容易:

  1. 转到 Magento 根目录。

  2. 运行以下命令:

**modgit init**
**modgit -e README.md clone mink https://github.com/jreinke/magento-mink.git**

就是这样。Modgit 将负责直接从 GitHub 存储库安装文件。

创建我们的第一个测试

Mink测试也存储在Test文件夹中。让我们创建Mink测试类的基本骨架:

  1. 导航到我们模块根目录下的Test文件夹。

  2. 创建一个名为Mink的新目录。

  3. Mink目录中,创建一个名为Registry.php的新 PHP 类。

  4. 复制以下代码(文件位置为app/code/local/Mdg/Giftregistry/Test/Mink/Registry.php):

<?php
class Mdg_Giftregistry_Test_Mink_Registry extends JR_Mink_Test_Mink 
{   
    public function testAddProductToRegistry()
    {
        $this->section('TEST ADD PRODUCT TO REGISTRY');
        $this->setCurrentStore('default');
        $this->setDriver('goutte');
        $this->context();

        // Go to homepage
        $this->output($this->bold('Go To the Homepage'));
        $url = Mage::getStoreConfig('web/unsecure/base_url');
        $this->visit($url);
        $category = $this->find('css', '#nav .nav-1-1 a');
        if (!$category) {
            return false;
        }

        // Go to the Login page
        $loginUrl = $this->find('css', 'ul.links li.last a');
        if ($loginUrl) {
            $this->visit($loginUrl->getAttribute('href'));
        }

        $login = $this->find('css', '#email');
        $pwd = $this->find('css', '#pass');
        $submit = $this->find('css', '#send2');

        if ($login && $pwd && $submit) {
            $email = 'user@example.com';
            $password = 'password';
            $this->output(sprintf("Try to authenticate '%s' with password '%s'", $email, $password));
            $login->setValue($email);
            $pwd->setValue($password);
            $submit->click();
            $this->attempt(
                $this->find('css', 'div.welcome-msg'),
                'Customer successfully logged in',
                'Error authenticating customer'
            );
        }

        // Go to the category page
        $this->output($this->bold('Go to the category list'));
        $this->visit($category->getAttribute('href'));
        $product = $this->find('css', '.category-products li.first a');
        if (!$product) {
            return false;
        }

        // Go to product view
        $this->output($this->bold('Go to product view'));
        $this->visit($product->getAttribute('href'));
        $form = $this->find('css', '#product_registry_form');
        if ($form) {
            $addToCartUrl = $form->getAttribute('action');
            $this->visit($addToCartUrl);
            $this->attempt(
                $this->find('css', '#btn-add-giftregistry'),
                'Product added to gift registry successfully',
                'Error adding product to gift registry'
            );
        }
    }
}

仅仅乍一看,你就可以看出这个功能测试与我们之前构建的单元测试有很大不同,尽管看起来代码很多,但实际上很简单。之前的测试已经在代码块中完成了。让我们分解一下之前的测试在做什么:

  • 设置浏览器驱动程序和当前商店

  • 转到主页并检查有效的类别链接

  • 尝试以测试用户身份登录

  • 转到类别页面

  • 打开该类别上的第一个产品

  • 尝试将产品添加到客户的礼品注册表

注意

这个测试做了一些假设,并期望在现有的礼品注册表中有一个有效的客户。

在创建Mink测试时,我们必须牢记一些考虑因素:

  • 每个测试类必须扩展JR_Mink_Test_Mink

  • 每个测试函数必须以 test 关键字开头

最后,我们唯一需要做的就是运行我们的测试。我们可以通过进入命令行并运行以下命令来实现这一点:

**$ php shell/mink.php**

如果一切顺利,我们应该看到类似以下输出:

---------------------- SCRIPT START ---------------------------------
Found 1 file
-------------- TEST ADD PRODUCT TO REGISTRY -------------------------
Switching to store 'default'
Now using Goutte driver
----------------------------------- CONTEXT ------------------------------------
website: base, store: default
Cache info:
config            Disabled  N/A       Configuration
layout            Disabled  N/A       Layouts
block_html        Disabled  N/A       Blocks HTML output
translate         Disabled  N/A       Translations
collections       Disabled  N/A       Collections Data
eav               Disabled  N/A       EAV types and attributes
config_api        Disabled  N/A       Web Services Configuration
config_api2       Disabled  N/A       Web Services Configuration
ecomdev_phpunit   Disabled  N/A       Unit Test Cases

Go To the Homepage [OK]
Try to authenticate user@example.com with password password [OK]
Go to the category list [OK]
Go to product view [OK]
Product added to gift registry successfully

总结

在本章中,我们介绍了 Magento 测试的基础知识。本章的目的不是构建复杂的测试或深入讨论,而是让我们初步了解并清楚地了解我们可以做些什么来测试我们的扩展。

本章我们涵盖了几个重要的主题,通过拥有适当的测试套件和工具,可以帮助我们避免未来的头痛,并提高我们代码的质量。

在下一章,我们将学习如何打包和分发自定义代码和扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值