Magento-PHP-开发指南(全)

Magento PHP 开发指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《Magento PHP 开发者指南》将帮助新手和有经验的开发者理解并使用 Magento 的基本概念和开发和测试代码的标准实践。

这本书是我试图撰写的一本指南,回答了许多开发者(包括我自己)在开始为 Magento 开发时所遇到的问题:EAV 是什么?Magento 中的 ORM 是如何工作的?观察者和事件是什么?使用了哪些设计模式来创建 Magento?

最重要的是,本书还回答了许多开发者至今仍然存在的问题:开发模块和扩展前端和后端的标准是什么?我如何正确测试我的代码?部署和分发自定义模块的最佳方法是什么?

本书涵盖的内容

第一章,“理解和设置我们的开发环境”,将帮助您设置一个完整的 Magento 开发环境,包括 MySQL 和 Apache。此外,我们将介绍可用于简化开发的工具,几种集成开发环境和版本控制系统。

第二章,“Magento 开发者基础”,将介绍 Magento 的基本概念,如系统架构、MVC 实现以及与 Zend Framework 的关系。本章中的所有概念将为刚开始使用 Magento 的开发者奠定基础。

第三章,“ORM 和数据集合”,涵盖了 Magento 中的集合和模型,这是日常 Magento 开发的基础。在本章中,我们将向读者介绍 Magento ORM 系统,并学习如何正确地处理数据集合和 EAV 系统。

第四章,“前端开发”,将解释我们迄今为止所学到的技能和知识的实际用途,并逐步构建一个完全功能的 Magento 模块。自定义模块将允许读者应用各种重要概念,如处理集合、路由、会话和缓存。

第五章,“后端开发”,将扩展我们在上一章中构建的内容,并在 Magento 后端创建一个与我们的应用数据交互的界面。我们将学习如何扩展后端、管理 HTML 主题、设置数据源,并通过配置控制我们的扩展行为。

第六章,“Magento API”,将解释 Magento API 以及我们如何扩展它,以提供对我们使用扩展捕获的自定义数据的访问。

第七章,“测试和质量保证”,将帮助读者学习测试 Magento 模块和自定义的关键技能,这是开发的一个重要部分。我们将了解不同类型的测试和每种特定类型测试的可用工具。

第八章,“部署和分发”,将帮助读者了解多种工具,用于将我们的代码部署到生产环境,并如何通过 Magento Connect 等渠道正确打包我们的扩展以进行分发。

附录,“你好,Magento”,将为新开发者提供一个快速易懂的介绍,以创建我们的第一个 Magento 扩展。

你需要为本书做好准备

你需要安装 Magento 1.7,可以是在本地机器上或远程服务器上,你喜欢的代码编辑器,以及安装和修改文件的权限。

本书适合谁

如果您是一名刚开始使用 Magento 的 PHP 开发人员,或者已经对 Magento 有一些经验,并希望了解 Magento 的架构以及如何扩展 Magento 的前端和后端,那么这本书适合您!

您应该对 PHP5 有信心。不需要有 Magento 开发经验,但您应该熟悉基本的 Magento 操作和概念。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些样式的示例及其含义的解释。

文本中的代码单词显示如下:"GitHub 现在包括一个专门为 Magento 设计的.gitignore文件,它将忽略 Magento 核心中的所有文件,只跟踪我们自己的代码。"

一段代码设置如下:

{
    "id": "default",
    "host": "magento.localhost.com",
    "repo": [
        "url": "svn.magentocommerce.com/source/branches/1.7",

任何命令行输入或输出都是这样写的:

$ vagrant box add lucid32 http://files.vagrantup.com/lucid32.box
$ vagrant init lucid32
$ vagrant up

新术语重要单词以粗体显示。屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的方式出现在文本中:"您现在应该看到 Apache 的默认网页,上面显示着It Works!的消息"。

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会出现在这样。

第一章:了解和设置我们的开发环境

在本章中,我们将介绍运行 Magento 所涉及的技术堆栈以及如何为开发设置一个合适的环境。本章将涵盖以下主题:

  • LAMP 虚拟机

  • 设置和使用 VirtualBox

  • 设置和使用 Vagrant

  • IDE 和版本控制系统

我们还将学习如何从头开始设置一个 LAMP 虚拟机,以及如何使用 Vagrant 和 Chef 完全自动化这个过程。

从头开始的 LAMP

LAMPLinux,Apache,MySQL 和 PHP)是一种开源技术解决方案堆栈,用于构建 Web 服务器,也是运行 Magento 的当前标准。

有关更详细的要求清单,请访问www.magentocommerce.com/system-requirements

注意

尽管在撰写本书时,Nginx 在 Magento 开发人员中得到了更广泛的采用,但 Apache2 仍然是社区公认的标准。我们将专注于与它一起工作。

作为开发人员,我们面临着多个挑战和细微差别,如设置和维护我们的开发环境:

  • 匹配您的开发和生产环境

  • 在不同平台和团队成员之间保持一致的环境

  • 设置一个需要几个小时的新环境

  • 并非所有开发人员都具有自己设置 LAMP 服务器的知识或经验

我们可以通过 Oracle 的 VirtualBox(www.virtualbox.org)来解决前两个问题。VirtualBox 是一个强大且广受欢迎的虚拟化引擎,它将允许我们创建虚拟机(VMs)。VMs 也可以在开发人员之间和所有主要操作系统之间共享。

获取 VirtualBox

VirtualBox 是开源的,并且在所有平台上都受支持。可以直接从www.virtualbox.org/wiki/Downloads下载。

现在,我们将继续设置一个 Linux 虚拟机。我们选择了 Ubuntu Server 12.04.2 LTS,因为它易于使用并且有广泛的支持。首先,从www.ubuntu.com/download/server下载 ISO 文件;64 位和 32 位版本都可以使用。

要创建一个新的 Linux 虚拟机,请执行以下步骤:

  1. 启动VirtualBox Manager,并单击左上角的New按钮,如下截图所示:获取 VirtualBox

  2. 一个向导对话框将弹出并引导我们完成创建一个裸虚拟机的步骤。向导将要求我们提供设置虚拟机的基本信息:

  • VM 名称:我们应该如何命名我们的虚拟机?让我们将其命名为Magento_dev 01

  • 内存:这是在我们的 VM 启动时将分配给客户操作系统的系统内存值;对于运行完整的 LAMP 服务器,建议使用 1GB 或更多。

  • 操作系统类型:这是我们稍后将安装的操作系统类型;在我们的情况下,我们要选择Linux/Ubuntu,根据我们的选择,VirtualBox 将启用或禁用某些 VM 选项。

  1. 接下来,我们需要指定一个虚拟硬盘。选择现在创建虚拟硬盘,如下截图所示:获取 VirtualBox

  2. 有许多硬盘选项可用,但对于大多数情况,选择VirtualBox 磁盘映像VDI)就足够了。这将在我们的主机操作系统上创建一个单个文件。

  3. 现在我们需要选择物理驱动器上的存储类型。我们提供以下两个选项:

  • 动态分配:磁盘映像将随着客户操作系统上的文件数量和使用量的增加而自动增长

  • 固定大小:此选项将从一开始限制虚拟磁盘的大小

  1. 接下来,我们需要指定虚拟硬盘的大小。我们希望根据我们计划使用的 Magento 安装数量来调整大小。

注意

一般来说,我们希望每个 Magento 安装至少保留 2GB 的空间,如果我们在同一安装上运行数据库服务器,还需要另外 3GB。这并不是说所有的空间会立即或甚至根本不会被使用,但是一旦考虑到产品图片和缓存文件,Magento 安装可能会使用大量的磁盘空间。

  1. 最后,我们只需要点击创建按钮。

提示

主要区别在于固定大小的硬盘将从一开始就在物理硬盘上保留空间,而动态分配的硬盘将逐渐增长,直到获得指定的大小。

新创建的框将出现在左侧导航菜单中,但在启动我们最近创建的 VM 之前,我们需要进行一些更改,如下所示:

i. 选择我们新创建的 VM,然后点击顶部的设置按钮。

ii. 打开网络菜单,选择适配器 2。我们将把连接到设置为桥接适配器,因为我们希望将其设置为桥接适配器到我们的主网络接口。这将允许我们远程使用 SSH 连接。

获取 VirtualBox

iii. 转到系统菜单,更改启动顺序,使 CD/DVD-ROM 首先启动。

iv. 在存储菜单中,选择一个空的 IDE 控制器,并挂载我们之前下载的 Ubuntu ISO 镜像。

获取 VirtualBox

启动我们的虚拟机

此时,我们已经成功安装和配置了我们的 VirtualBox 实例,现在我们已经准备好首次启动我们的新虚拟机。要做到这一点,只需在左侧边栏中选择 VM,然后点击顶部的启动按钮。

一个新窗口将弹出,显示 VM 的界面。Ubuntu 将需要几分钟来启动。

一旦 Ubuntu 完成启动,我们将看到两个菜单。第一个菜单将允许我们选择语言,第二个菜单是主菜单,提供了几个选项。在我们的情况下,我们只想继续选择安装 Ubuntu 服务器选项。

启动虚拟机

现在我们应该看到 Ubuntu 安装向导,它将要求我们选择语言和键盘设置;在选择适合我们国家和语言的设置后,安装程序将继续将所有必要的软件包加载到内存中。这可能需要几分钟。

Ubuntu 将继续配置我们的主网络适配器,一旦自动配置完成,我们将被要求设置虚拟机的主机名。我们可以将主机名保留为默认设置。

启动虚拟机

下一个屏幕将要求我们输入用户的全名;在这个例子中,让我们使用Magento Developer

启动虚拟机

接下来,我们将被要求创建用户名和密码。让我们使用magedev作为我们的用户名:

启动虚拟机

让我们使用magento2013作为我们的密码:

启动虚拟机

在接下来的屏幕上,我们将被要求确认我们的密码并设置正确的时区;输入正确的值后,安装向导将显示以下屏幕,询问我们的分区设置:

启动虚拟机

在我们的情况下,我们选择引导-使用整个磁盘并设置 LVM;现在让我们确认我们正在分区我们的虚拟磁盘:

启动虚拟机

我们将被要求最后一次确认我们的更改;选择完成分区并将更改写入磁盘,如下截图所示:

启动虚拟机

安装向导将要求我们选择预定义的软件包进行安装;可用选项之一是LAMP 服务器

虽然这非常方便,但我们不想安装预先打包在我们的 Ubuntu CD 中的 LAMP 服务器;我们将手动安装所有 LAMP 组件,以确保它们根据特定需求进行设置,并且与最新的补丁保持最新。

接下来,我们需要一个 SSH 服务器;从列表中选择OpenSSH 服务器并点击继续

启动我们的虚拟机

现在,Ubuntu 的安装已经完成,它将重新启动到我们新安装的虚拟盒中。

我们几乎准备好继续安装我们环境的其余部分了,但首先我们需要更新我们的软件包管理器存储库定义,登录控制台并运行以下命令:

**$ sudo apt-get update**

APT代表高级包装工具,是大多数 Debian GNU/Linux 发行版中包含的核心库之一;apt大大简化了在我们的系统上安装和维护软件的过程。

一旦apt-get完成更新所有存储库源,我们可以继续安装我们的 LAMP 服务器的其他组件。

安装 Apache2

Apache 是一个 HTTP 服务器。目前,它用于托管超过 60%的网站,并且是运行 Magento 商店的公认标准。有许多在线指南和教程可供调整和优化 Apache2 以提高 Magento 性能。

安装 Apache 就像运行以下命令一样简单:

**$ sudo apt-get install apache2 -y**

这将负责为我们安装 Apache2 和所有必需的依赖项。如果一切安装正确,我们现在可以通过打开浏览器并输入http://192.168.36.1/来进行测试。

Apache 默认作为服务运行,并且可以使用以下命令进行控制:

**$ sudo apache2ctl stop** 
**$ sudo apache2ctl start** 
**$ sudo apache2ctl restart** 

现在,您应该看到 Apache 的默认网页,上面有It Works!的消息。

安装 PHP

PHP是一种服务器端脚本语言,代表PHP 超文本处理器。Magento 是基于 PHP5 和 Zend Framework 实现的,我们需要安装 PHP 和一些额外的库才能运行它。

让我们再次使用apt-get并运行以下命令来安装php5和所有必要的库:

**$ sudo apt-get install php5 php5-curl php5-gd php5-imagick php5-imap php5-mcrypt php5-mysql -y**
**$ sudo apt-get install php-pear php5-memcache -y**
**$ sudo apt-get install libapache2-mod-php5 -y**

第一个命令安装了不仅php5,还安装了 Magento 连接到我们的数据库和操作图像所需的其他软件包。

第二个命令将安装 PEAR,一个 PHP 包管理器和一个 PHP memcached 适配器。

注意

Memcached 是一个高性能的分布式内存缓存系统;这是 Magento 的一个可选缓存系统。

第三个命令安装并设置了 Apache 的php5模块。

我们最终可以通过运行以下命令来测试我们的 PHP 安装是否正常工作:

**$ php -v**

安装 MySQL

MySQL 是许多 Web 应用程序的流行数据库选择,Magento 也不例外。我们需要安装和设置 MySQL 作为开发堆栈的一部分,使用以下命令:

**$ sudo apt-get install mysql-server mysql-client -y**

在安装过程中,我们将被要求输入根密码;使用magento2013。安装程序完成后,我们应该有一个在后台运行的mysql服务实例。我们可以通过尝试使用以下命令连接到mysql服务器来测试它:

**$ sudo mysql -uroot -pmagento2013**

如果一切安装正确,我们应该看到以下mysql服务器提示:

**mysql>**

此时,我们有一个完全功能的 LAMP 环境,不仅可以用于开发和处理 Magento 网站,还可以用于任何其他类型的 PHP 开发。

将所有内容放在一起

此时,我们已经有了一个基本的 LAMP 设置并正在运行。然而,为了使用 Magento,我们需要进行一些配置更改和额外的设置。

我们需要做的第一件事是创建一个位置来存储我们开发站点的文件,因此我们将运行以下命令:

**$ sudo mkdir -p /srv/www/magento_dev/public_html/**
**$ sudo mkdir /srv/www/magento_dev/logs/**
**$ sudo mkdir /srv/www/magento_dev/ssl/**

这将为我们的第一个 Magento 站点创建必要的文件夹结构。现在我们需要通过使用 SVN 来快速获取文件的最新版本。

首先,我们需要在服务器上安装 SVN,使用以下命令:

**$ sudo apt-get install subversion -y**

安装程序完成后,打开magento_dev目录并运行svn命令以获取最新版本的文件:

**$ cd /srv/www/magento_dev** 
**$ sudo svn export --force http://svn.magentocommerce.com/source/branches/1.7 public_html/**

我们还需要修复新的 Magento 副本上的一些权限:

**$ sudo chown -R www-data:www-data public_html/**
**$ sudo chmod -R 755 public_html/var/** 
**$ sudo chmod -R 755 public_html/media/** 
**$ sudo chmod -R 755 public_html/app/etc/**

接下来,我们需要为 Magento 安装创建一个新的数据库。让我们打开我们的mysql shell:

**$ sudo mysql -uroot -pmagento2013**

进入mysql shell 后,我们可以使用create命令,后面应该跟着我们想要创建的实体类型(databasetable)和要创建的数据库名称来创建一个新的数据库:

**mysql> create database magento_dev;**

虽然我们可以使用 root 凭据访问我们的开发数据库,但这不是一个推荐的做法,因为这不仅可能危及单个站点,还可能危及整个数据库服务器。MySQL 帐户受权限限制。我们想要创建一组新的凭据,这些凭据只对我们的工作数据库有限的权限:

**mysql> GRANT ALL PRIVILEGES ON magento_dev.* TO 'mage'@'localhost' IDENTIFIED BY 'dev2013$#';**

现在,我们需要正确设置 Apache2 并启用一些额外的模块;幸运的是,这个版本的 Apache 带有一组有用的命令:

  • a2ensite:这将在sites-availablesites-enabled文件夹之间创建符号链接,以允许 Apache 服务器读取这些文件。

  • a2dissite:这将删除a2ensite命令创建的符号链接。这将有效地禁用该站点。

  • a2enmod:这用于在mods-enabled目录和模块配置文件之间创建符号链接。

  • a2dismod:这将从mods-enabled目录中删除符号链接。此命令将阻止 Apache 加载该模块。

Magento 使用mod_rewrite模块来生成 URL。mod_rewrite使用基于规则的重写引擎来实时重写请求的 URL。

我们可以使用a2enmod命令启用mod_rewrite

**$ sudo a2enmod rewrite**

下一步需要我们在sites-available目录下创建一个新的虚拟主机文件:

**$ sudo nano /etc/apache2/sites-available/magento.localhost.com**

nano命令将打开一个 shell 文本编辑器,我们可以在其中设置虚拟域的配置:

<VirtualHost *:80>
  ServerAdmin magento@locahost.com
  ServerName magento.localhost.com
  DocumentRoot /srv/www/magento_dev/public_html

  <Directory /srv/www/magento_dev/public_html/>
    Options Indexes FollowSymlinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
  </Directory>
  ErrorLog /srv/www/magento_dev/logs/error.log
  LogLevel warn
</VirtualHost>

要保存新的虚拟主机文件,请按Ctrl + O,然后按Ctrl + X。虚拟主机文件将告诉 Apache 在哪里找到站点文件以及给予它们什么权限。为了使新的配置更改生效,我们需要启用新站点并重新启动 Apache。我们可以使用以下命令来实现:

**$ sudo a2ensite magento.localhost.com**
**$ sudo apache2ctl restart**

我们几乎准备好安装 Magento 了。我们只需要通过以下任一方式在主机系统的主机文件中设置本地映射:

  • Windows

i. 用记事本打开C:\system32\drivers\etc\hosts

ii. 在文件末尾添加以下行:

192.168.36.1 magento.localhost.com
  • Unix/Linux/OSX

i. 使用nano打开/etc/hosts

**$ sudo nano /etc/hosts**

ii. 在文件末尾添加以下行:

192.168.36.1 magento.localhost.com

提示

如果您在对主机文件进行必要更改时遇到问题,请访问http://www.magedevguide.com/hostfile-help

现在,我们可以通过在浏览器中打开http://magento.localhost.com来安装 Magento。最后,我们应该看到安装向导。按照向导指示的步骤进行操作,您就可以开始使用了!

把所有东西放在一起

使用 Vagrant 快速上手

之前,我们使用 VM 创建了一个 Magento 安装。虽然使用 VM 为我们提供了一个可靠的环境,但为每个 Magento 分期安装设置我们的 LAMP 仍然可能非常复杂。这对于没有在 Unix/Linux 环境上工作经验的开发人员尤其如此。

如果我们能够获得运行 VM 的所有好处,但是具有完全自动化的设置过程呢?如果我们能够为我们的每个分期网站创建和配置新的 VM 实例?

这是通过使用 Vagrant 结合 Chef 实现的。我们可以创建自动化的虚拟机,而无需对 Linux 或不同的 LAMP 组件有广泛的了解。

注意

Vagrant 目前支持 VirtualBox 4.0.x、4.1.x 和 4.2.x。

安装 Vagrant

Vagrant 可以直接从downloads.vagrantup.com下载。此外,它的软件包和安装程序适用于多个平台。下载 Vagrant 后,运行安装。

一旦我们安装了 Vagrant 和 VirtualBox,启动基本 VM 就像在终端或命令提示符中输入以下行一样简单,具体取决于您使用的操作系统:

**$ vagrant box add lucid32 http://files.vagrantup.com/lucid32.box**
**$ vagrant init lucid32**
**$ vagrant up**

这些命令将启动一个安装了 Ubuntu Linux 的新 Vagrant 盒子。从这一点开始,我们可以像平常一样开始安装我们的 LAMP。但是,为什么我们要花一个小时为每个项目配置和设置 LAMP 服务器,当我们可以使用 Chef 自动完成呢?Chef 是一个用 Ruby 编写的配置管理工具,可以集成到 Vagrant 中。

为了让刚开始使用 Magento 的开发人员更容易,我在 Github 上创建了一个名为magento-vagrant的 Vagrant 存储库,其中包括 Chef 所需的所有必要的食谱和配方。magento-vagrant存储库还包括一个新的食谱,将负责特定的 Magento 设置和配置。

为了开始使用magento-vagrant,您需要一个 Git 的工作副本。

如果您使用 Ubuntu,请运行以下命令:

**$ sudo apt-get install git-core -y**

对于 Windows,我们可以使用本地工具在windows.github.com/下载和管理我们的存储库。

无论您使用的操作系统是什么,我们都需要在本地文件系统中检出此存储库的副本。我们将使用C:/Users/magedev/Documents/magento-vagrant/来下载和保存我们的存储库;在magento-vagrant中,我们将找到以下文件和目录:

  • 食谱

  • data_bags

  • 公共

  • .vagrant

  • Vagrantfile

magento-vagrant存储库包括我们开发环境的每个组件的食谱,一旦我们启动新的 Vagrant 盒子,它们将自动安装。

现在唯一剩下的事情就是设置我们的开发站点。通过使用 Vagrant 和 Chef,向我们的 Vagrant 安装添加新的 Magento 站点的过程已经变得简化。

data_bags目录中,我们有一个文件用于 Vagrant 盒子中每个 Magento 安装;默认存储库中包含 Magento CE 1.7 的示例安装。

对于每个站点,我们需要创建一个包含 Chef 所需的所有设置的新 JSON 文件。让我们看一下magento-vagrant默认文件,可以在位置C:/Users/magedev/Documents/magento-vagrant/data_bags/sites/default.json找到:

{
    "id": "default",
    "host": "magento.localhost.com",
    "repo": [
        "url": "svn.magentocommerce.com/source/branches/1.7",
        "revision": "HEAD"  
     ],
   "database": [
      "name": "magento_staging",
      "username": "magento",
      "password": "magento2013$"
   ]
}

这将自动使用 Magento 存储库中的最新文件设置 Magento 安装。

向我们的 Vagrant 盒子添加新站点只是添加一个相应站点的新 JSON 文件并重新启动 Vagrant 盒子的问题。

现在我们有一个运行中的 Magento 安装,让我们来选择一个合适的集成开发环境IDE)。

选择一个 IDE

选择合适的 IDE 主要是个人开发者口味的问题。然而,选择合适的 IDE 对于 Magento 开发者来说可能是至关重要的。

IDE 的挑战主要来自 Magento 对工厂名称的广泛使用。这使得某些功能的实现,如代码完成(也称为智能感知),变得困难。目前,有两个 IDE 在其对 Magento 的本地支持方面表现出色-NetBeans 和 PhpStorm。

尽管 NetBeans 是开源的,并且已经存在很长时间,但 PhpStorm 一直占据上风,并得到了 Magento 社区的更多支持。

此外,最近发布的 Magicento 插件,专门用于扩展和集成 Magento 到 PhpStorm 中,已成为当前可用选项中最佳选择。

使用版本控制系统

Magento 代码库非常庞大,包括超过 7,000 个文件和近 150 万行代码。因此,使用版本控制系统不仅是一种良好的实践,也是一种必要性。

版本控制系统用于跟踪多个文件和多个开发人员之间的更改;通过使用版本控制系统,我们可以获得非常强大的工具。

在几种可用的版本控制系统中(Git、SVN、Mercurial),Git 由于其简单性和灵活性而值得特别关注。通过在 Git 托管服务 Github 上发布即将推出的 Magento 2 版本,Magento 核心开发团队已经认识到 Git 在 Magento 社区中的重要性。

注意

有关 Magento2 的更多信息,请访问github.com/magento/magento2

Github 现在包括一个特定于 Magento 的.gitignore文件,它将忽略 Magento 核心中的所有文件,只跟踪我们自己的代码。

也就是说,在处理 Magento 项目时,有几个版本控制概念需要牢记:

  • 分支:这允许我们在不影响主干(稳定版本)的情况下工作新功能。

  • 合并:这用于将代码从一个地方移动到另一个地方。通常,这是在开发分支准备好移动到生产环境时从开发分支到主干进行的。

  • 标记:这用于创建发布的快照。

总结

在这第一章中,我们学习了如何设置和使用 LAMP 环境,在多个平台上设置开发环境,创建和配置 Vagrant 虚拟机,使用 Chef 配方以及使用 Magento 开发的版本控制系统。

拥有适当的环境是开始为 Magento 开发的第一步,也是我们 Magento 工具箱的一个组成部分。

现在我们已经设置好并准备好使用开发环境,是时候深入了解 Magento 的基本概念了;这些概念将为我们提供开发 Magento 所需的工具和知识。

第二章:开发人员的 Magento 基础知识

在本章中,我们将介绍与 Magento 一起工作的基本概念。我们将了解 Magento 的结构,并将介绍 Magento 灵活性的来源,即其模块化架构。

Magento 是一个灵活而强大的系统。不幸的是,这也增加了一定程度的复杂性。目前,Magento 的干净安装大约有 30,000 个文件和超过 120 万行代码。

拥有如此强大和复杂的功能,Magento 对于新开发人员可能会令人望而生畏;但不用担心。本章旨在教新开发人员所有他们需要使用和扩展 Magento 的基本概念和工具,在下一章中,我们将深入研究 Magento 的模型和数据集。

Zend Framework – Magento 的基础

您可能知道,Magento 是市场上最强大的电子商务平台;您可能不知道的是,Magento 还是一个基于 Zend Framework 开发的面向对象OO)PHP 框架。

Zend 的官方网站描述了该框架为:

Zend Framework 2 是一个使用 PHP 5.3+开发 Web 应用程序和服务的开源框架。Zend Framework 2 使用 100%面向对象的代码,并利用了 PHP 5.3 的大多数新特性,即命名空间、后期静态绑定、lambda 函数和闭包。

Zend Framework 2 的组件结构是独特的;每个组件都设计为对其他组件的依赖较少。ZF2 遵循 SOLID 面向对象设计原则。这种松散耦合的架构允许开发人员使用他们想要的任何组件。我们称之为“随意使用”设计。

但是 Zend Framework 究竟是什么?Zend Framework 是一个基于 PHP 开发的面向对象框架,实现了模型-视图-控制器MVC)范式。当 Varien,现在的 Magento 公司,开始开发 Magento 时,决定在 Zend 的基础上进行开发,因为以下组件:

  • Zend_Cache

  • Zend_Acl

  • Zend_Locale

  • Zend_DB

  • Zend_Pdf

  • Zend_Currency

  • Zend_Date

  • Zend_Soap

  • Zend_Http

总的来说,Magento 使用了大约 15 个不同的 Zend 组件。Varien 库直接扩展了先前提到的几个 Zend 组件,例如Varien_Cache_Core是从Zend_Cache_Core扩展而来的。

使用 Zend Framework,Magento 是根据以下原则构建的:

  • 可维护性:通过使用代码池来将核心代码与本地定制和第三方模块分开

  • 可升级性:Magento 的模块化允许扩展和第三方模块独立于系统的其他部分进行更新

  • 灵活性:允许无缝定制并简化新功能的开发

虽然使用 Zend Framework 甚至理解它并不是开发 Magento 的要求,但至少对 Zend 组件、用法和交互有基本的了解,在我们开始深入挖掘 Magento 的核心时,可能会是非常宝贵的信息。

注意

您可以在framework.zend.com/了解更多关于 Zend Framework 的信息。

Magento 文件夹结构

Magento 的文件夹结构与其他 MVC 应用程序略有不同;让我们来看看目录树,以及每个目录及其功能:

  • app:这个文件夹是 Magento 的核心,分为三个导入目录:

  • code:这包含了我们的应用程序代码,分为corecommunitylocal三个代码池

  • design:这包含了我们应用程序的所有模板和布局

  • locale:这包含了商店使用的所有翻译和电子邮件模板文件

  • js:这包含了 Magento 中使用的所有 JavaScript 库

  • media:这包含了我们产品和 CMS 页面的所有图片和媒体文件,以及产品图片缓存

  • lib:这包含 Magento 使用的所有第三方库,如 Zend 和 PEAR,以及 Magento 开发的自定义库,这些库位于 Varien 和 Mage 目录下

  • 皮肤:这包含对应主题使用的所有 CSS 代码、图像和 JavaScript 文件

  • var:这包含我们的临时数据,如缓存文件、索引锁文件、会话、导入/导出文件,以及企业版中的完整页面缓存文件夹

Magento 是一个模块化系统。这意味着应用程序,包括核心,被划分为较小的模块。因此,文件夹结构在每个模块核心的组织中起着关键作用;典型的 Magento 模块文件夹结构看起来像下面的图:

Magento 文件夹结构

让我们更详细地审查每个文件夹:

  • :这个文件夹包含 Magento 中形成控制器和视图之间的额外逻辑的块

  • controllerscontrollers文件夹由处理 Web 服务器请求的操作组成

  • 控制器:这个文件夹中的类是抽象类,由controllers文件夹下的controller类扩展

  • etc:在这里,我们可以找到以 XML 文件形式的模块特定配置,例如config.xmlsystem.xml

  • 助手:这个文件夹包含封装常见模块功能并使其可用于同一模块的类和其他模块类的辅助类

  • 模型:这个文件夹包含支持模块中控制器与数据交互的模型

  • sql:这个文件夹包含每个特定模块的安装和升级文件

正如我们将在本章后面看到的那样,Magento 大量使用工厂名称和工厂方法。这就是为什么文件夹结构如此重要的原因。

模块化架构

Magento 不是一个庞大的应用程序,而是由较小的模块构建,每个模块为 Magento 添加特定功能。

这种方法的优势之一是能够轻松启用和禁用特定模块功能,以及通过添加新模块来添加新功能。

自动加载程序

Magento 是一个庞大的框架,由近 30000 个文件组成。在应用程序启动时需要每个文件将使其变得非常缓慢和沉重。因此,Magento 使用自动加载程序类来在每次调用工厂方法时找到所需的文件。

那么,自动加载程序到底是什么?PHP5 包含一个名为__autoload()的函数。在实例化类时,__autoload()函数会自动调用;在这个函数内部,定义了自定义逻辑来解析类名和所需文件。

让我们仔细看看位于app/Mage.php的 Magento 引导代码:

… 
Mage::register('original_include_path', get_include_path());
if (defined('COMPILER_INCLUDE_PATH')) {
    $appPath = COMPILER_INCLUDE_PATH;
    set_include_path($appPath . PS . Mage::registry('original_include_path'));
    include_once "Mage_Core_functions.php";
    include_once "Varien_Autoload.php";
} else {
    /**
     * Set include path
     */
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
    $paths[] = BP . DS . 'lib';

    $appPath = implode(PS, $paths);
    set_include_path($appPath . PS . Mage::registry('original_include_path'));
    include_once "Mage/Core/functions.php";
    include_once "Varien/Autoload.php";
}

Varien_Autoload::register();

引导文件负责定义include路径和初始化 Varien 自动加载程序,后者将定义自己的autoload函数作为默认调用函数。让我们来看看 Varien autoload函数的内部工作:

    /**
     * Load class source code
     *
     * @param string $class
     */
    public function autoload($class)
    {
        if ($this->_collectClasses) {
            $this->_arrLoadedClasses[self::$_scope][] = $class;
        }
        if ($this->_isIncludePathDefined) {
            $classFile =  COMPILER_INCLUDE_PATH . DIRECTORY_SEPARATOR . $class;
        } else {
            $classFile = str_replace(' ', DIRECTORY_SEPARATOR, ucwords(str_replace('_', ' ', $class)));
        }
        $classFile.= '.php';
        //echo $classFile;die();
        return include $classFile;
    }

autoload类接受一个名为$class的参数,这是工厂方法提供的别名。这个别名被处理以生成一个匹配的类名,然后被包含。

正如我们之前提到的,Magento 的目录结构很重要,因为 Magento 从目录结构中派生其类名。这种约定是我们将在本章后面审查的工厂方法的核心原则。

代码池

正如我们之前提到的,在我们的app/code文件夹中,我们的应用程序代码分为三个不同的目录,称为代码池。它们如下:

  • 核心:这是 Magento 核心模块提供基本功能的地方。Magento 开发人员之间的黄金法则是,绝对不要修改core代码池下的任何文件。

  • community:这是第三方模块放置的位置。它们要么由第三方提供,要么通过 Magento Connect 安装。

  • 本地:这是专门为 Magento 实例开发的所有模块和代码所在的位置。

代码池确定模块来自何处以及它们应该被加载的顺序。如果我们再看一下Mage.php引导文件,我们可以看到代码池加载的顺序:

    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
    $paths[] = BP . DS . 'lib';

这意味着对于每个类请求,Magento 将首先查找local,然后是community,然后是core,最后是lib文件夹内的内容。

这也导致了一个有趣的行为,可以很容易地用于覆盖corecommunity类,只需复制目录结构并匹配类名。

提示

毋庸置疑,这是一个糟糕的做法,但了解这一点仍然是有用的,以防将来有一天你不得不处理利用这种行为的项目。

路由和请求流程

在更详细地了解构成 Magento 一部分的不同组件之前,重要的是我们了解这些组件如何相互交互以及 Magento 如何处理来自 Web 服务器的请求。

与任何其他 PHP 应用程序一样,我们有一个单一文件作为每个请求的入口点;在 Magento 的情况下,这个文件是index.php,负责加载Mage.php引导类并启动请求周期。然后它经历以下步骤:

  1. Web 服务器接收请求,并通过调用引导文件Mage.php来实例化 Magento。

  2. 前端控制器被实例化和初始化;在控制器初始化期间,Magento 搜索 web 路由并实例化它们。

  3. 然后 Magento 遍历每个路由器并调用匹配。match方法负责处理 URL 并生成相应的控制器和操作。

  4. Magento 然后实例化匹配的控制器并执行相应的操作。

路由器在这个过程中尤其重要。前端控制器使用Router对象将请求的 URL(路由)与模块控制器和操作进行匹配。默认情况下,Magento 带有以下路由器:

  • Mage_Core_Controller_Varien_Router_Admin

  • Mage_Core_Controller_Varien_Router_Standard

  • Mage_Core_Controller_Varien_Router_Default

然后动作控制器将加载和渲染布局,然后加载相应的块、模型和模板。

让我们分析一下 Magento 如何处理对类别页面的请求;我们将使用http://localhost/catalog/category/view/id/10作为示例。Magento 的 URI 由三部分组成 - /FrontName/ControllerName/ActionName

这意味着对于我们的示例 URL,拆分将如下所示:

  • FrontNamecatalog

  • ControllerNamecategory

  • ActionNameview

如果我看一下 Magento 路由器类,我可以看到Mage_Core_Controller_Varien_Router_Standard匹配函数:

public function match(Zend_Controller_Request_Http $request)
{
  …
   $path = trim($request->getPathInfo(), '/');
            if ($path) {
                $p = explode('/', $path);
            } else {
                $p = explode('/', $this->_getDefaultPath());
            }
  …
}

从前面的代码中,我们可以看到路由器尝试做的第一件事是将 URI 解析为数组。根据我们的示例 URL,相应的数组将类似于以下代码片段:

$p = Array
(
    [0] => catalog
    [1] => category
    [2] => view
)

函数的下一部分将首先尝试检查请求是否指定了模块名称;如果没有,则尝试根据数组的第一个元素确定模块名称。如果无法提供模块名称,则函数将返回false。让我们看看代码的这一部分:

      // get module name
        if ($request->getModuleName()) {
            $module = $request->getModuleName();
        } else {
            if (!empty($p[0])) {
                $module = $p[0];
            } else {
                $module = $this->getFront()->getDefault('module');
                $request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS, '');
            }
        }
        if (!$module) {
            if (Mage::app()->getStore()->isAdmin()) {
                $module = 'admin';
            } else {
                return false;
            }
        }

接下来,匹配函数将遍历每个可用模块,并尝试匹配控制器和操作,使用以下代码:

…
        foreach ($modules as $realModule) {
            $request->setRouteName($this->getRouteByFrontName($module));

            // get controller name
            if ($request->getControllerName()) {
                $controller = $request->getControllerName();
            } else {
                if (!empty($p[1])) {
                    $controller = $p[1];
                } else {
                    $controller = $front->getDefault('controller');
                    $request->setAlias(
                        Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,
                        ltrim($request->getOriginalPathInfo(), '/')
                    );
                }
            }

            // get action name
            if (empty($action)) {
                if ($request->getActionName()) {
                    $action = $request->getActionName();
                } else {
                    $action = !empty($p[2]) ? $p[2] : $front->getDefault('action');
                }
            }

            //checking if this place should be secure
            $this->_checkShouldBeSecure($request, '/'.$module.'/'.$controller.'/'.$action);

            $controllerClassName = $this->_validateControllerClassName($realModule, $controller);
            if (!$controllerClassName) {
                continue;
            }

            // instantiate controller class
            $controllerInstance = Mage::getControllerInstance($controllerClassName, $request, $front->getResponse());

            if (!$controllerInstance->hasAction($action)) {
                continue;
            }

            $found = true;
            break;
        }
...

现在看起来代码量很大,所以让我们进一步分解。循环的第一部分将检查请求是否有一个控制器名称;如果没有设置,它将检查我们的参数数组($p)的第二个值,并尝试确定控制器名称,然后它将尝试对操作名称做同样的事情。

如果我们在循环中走到了这一步,我们应该有一个模块名称,一个控制器名称和一个操作名称,Magento 现在将使用它们来尝试通过调用以下函数获取匹配的控制器类名:

$controllerClassName = $this->_validateControllerClassName($realModule, $controller);

这个函数不仅会生成一个匹配的类名,还会验证它的存在;在我们的例子中,这个函数应该返回Mage_Catalog_CategoryController

由于我们现在有了一个有效的类名,我们可以继续实例化我们的控制器对象;如果你一直关注到这一点,你可能已经注意到我们还没有对我们的操作做任何事情,这正是我们循环中的下一步。

我们新实例化的控制器带有一个非常方便的函数叫做hasAction();实质上,这个函数的作用是调用一个名为is_callable()的 PHP 函数,它将检查我们当前的控制器是否有一个与操作名称匹配的公共函数;在我们的例子中,这将是viewAction()

这种复杂的匹配过程和使用foreach循环的原因是,可能有几个模块使用相同的 FrontName。

路由和请求流程

现在,http://localhost/catalog/category/view/id/10不是一个非常用户友好的 URL;幸运的是,Magento 有自己的 URL 重写系统,允许我们使用http://localhost/books.html

让我们深入了解一下 URL 重写系统,看看 Magento 如何从我们的 URL 别名中获取控制器和操作名称。在我们的Varien/Front.php控制器分发函数中,Magento 将调用:

Mage::getModel('core/url_rewrite')->rewrite();

在实际查看rewrite函数的内部工作之前,让我们先看一下core/url_rewrite模型的结构:

Array (
  ["url_rewrite_id"] => "10"
  ["store_id"]       => "1"
  ["category_id"]    => "10"
  ["product_id"]     => NULL
  ["id_path"]        => "category/10"
  ["request_path"]   => "books.html"
  ["target_path"]    => "catalog/category/view/id/10"
  ["is_system"]      => "1"
  ["options"]        => NULL
  ["description"]    => NULL
)

正如我们所看到的,重写模块由几个属性组成,但其中只有两个对我们特别感兴趣——request_pathtarget_path。简而言之,重写模块的工作是修改请求对象路径信息,使其与target_path的匹配值相匹配。

Magento 的 MVC 版本

如果您熟悉传统的 MVC 实现,比如 CakePHP 或 Symfony,您可能知道最常见的实现被称为基于约定的 MVC。使用基于约定的 MVC,要添加一个新模型或者说一个控制器,你只需要创建文件/类(遵循框架约定),系统就会自动接收它。

Magento,另一方面,使用基于配置的 MVC 模式,这意味着创建我们的文件/类是不够的;我们必须明确告诉 Magento 我们添加了一个新类。

每个 Magento 模块都有一个config.xml文件,位于模块的etc/目录下,包含所有相关的模块配置。例如,如果我们想要添加一个包含新模型的新模块,我们需要在配置文件中定义一个节点,告诉 Magento 在哪里找到我们的模型,比如:

<global>
…
<models>
     <group_classname>
          <class>Namespace_Modulename_Model</class>
     <group_classname>
</models>
...
</global>

虽然这可能看起来像是额外的工作,但它也给了我们巨大的灵活性和权力。例如,我们可以使用rewrite节点重写另一个类:

<global>
…
<models>
     <group_classname>
      <rewrite>
               <modulename>Namespace_Modulename_Model</modulename>
      </rewrite>
     <group_classname>
</models>
...
</global>

Magento 然后会加载所有的config.xml文件,并在运行时合并它们,创建一个单一的配置树。

此外,模块还可以有一个system.xml文件,用于在 Magento 后台指定配置选项,这些选项又可以被最终用户用来配置模块功能。system.xml文件的片段如下所示:

<config>
  <sections>
    <section_name translate="label">
      <label>Section Description</label>
      <tab>general</tab>
      <frontend_type>text</frontend_type>
      <sort_order>1000</sort_order>
      <show_in_default>1</show_in_default>
      <show_in_website>1</show_in_website>
      <show_in_store>1</show_in_store>
      <groups>
       <group_name translate="label">
         <label>Demo Of Config Fields</label>
         <frontend_type>text</frontend_type>
         <sort_order>1</sort_order>
         <show_in_default>1</show_in_default>
         <show_in_website>1</show_in_website>
         <show_in_store>1</show_in_store>  
   <fields>
          <field_name translate="label comment">
             <label>Enabled</label>
             <comment>
               <![CDATA[Comments can contain <strong>HTML</strong>]]>
             </comment>
             <frontend_type>select</frontend_type>
             <source_model>adminhtml/system_config_source_yesno</source_model>
             <sort_order>10</sort_order>
             <show_in_default>1</show_in_default>
             <show_in_website>1</show_in_website>
             <show_in_store>1</show_in_store>
          </field_name>
         </fields>
        </group_name>
       </groups>
    </section_name>
  </sections>
</config>

让我们分解每个节点的功能:

  • section_name:这只是一个我们用来标识配置部分的任意名称;在此节点内,我们将指定配置部分的所有字段和组。

  • group:组,顾名思义,用于对配置选项进行分组,并在手风琴部分内显示它们。

  • label:这定义了字段/部分/组上要使用的标题或标签。

  • tab:这定义了应在其中显示部分的选项卡。

  • frontend_type:此节点允许我们指定要为自定义选项字段使用的渲染器。一些可用的选项包括:

  • button

  • checkboxes

  • checkbox

  • date

  • file

  • hidden

  • image

  • label

  • link

  • multiline

  • multiselect

  • password

  • radio

  • radios

  • select

  • submit

  • textarea

  • text

  • time

  • sort_order:它指定字段、组或部分的位置。

  • source_model:某些类型的字段,如select字段,可以从源模型中获取选项。Magento 已经在Mage/Adminhtml/Model/System/Config/Source下提供了几个有用的类。我们可以找到一些类:

  • YesNo

  • Country

  • Currency

  • AllRegions

  • Category

  • Language

仅通过使用 XML,我们就可以在 Magento 后端为我们的模块构建复杂的配置选项,而无需担心设置模板来填充字段或验证数据。

Magento 还提供了大量的表单字段验证模型,我们可以在<validate>标签中使用。在以下字段验证器中,我们有:

  • validate-email

  • validate-length

  • validate-url

  • validate-select

  • validate-password

与 Magento 的任何其他部分一样,我们可以扩展source_modelfrontend_typevalidator函数,甚至创建新的函数。我们将在后面的章节中处理这个任务,在那里我们将创建每种新类型。但现在,我们将探讨模型、视图、文件布局和控制器的概念。

模型

Magento 使用 ORM 方法;虽然我们仍然可以使用Zend_Db直接访问数据库,但我们大多数时候将使用模型来访问我们的数据。对于这种类型的任务,Magento 提供了以下两种类型的模型:

  • 简单模型:这种模型实现是一个简单的将一个对象映射到一个表,意味着我们的对象属性与每个字段匹配,表结构

  • 实体属性值(EAV)模型:这种类型的模型用于描述具有动态属性数量的实体

Magento 将模型层分为两部分:处理业务逻辑的模型和处理数据库交互的资源。这种设计决策使 Magento 最终能够支持多个数据库平台,而无需更改模型内部的任何逻辑。

Magento ORM 使用 PHP 的一个魔术类方法来提供对对象属性的动态访问。在下一章中,我们将更详细地了解模型、Magento ORM 和数据集合。

注意

Magento 模型不一定与数据库中的任何类型的表或 EAV 实体相关。稍后我们将要审查的观察者就是这种类型的 Magento 模型的完美例子。

视图

视图层是 Magento 真正使自己与其他 MVC 应用程序区分开的领域之一。与传统的 MVC 系统不同,Magento 的视图层分为以下三个不同的组件:

  • 布局:布局是定义块结构和属性(如名称和我们可以使用的模板文件)的 XML 文件。每个 Magento 模块都有自己的布局文件集。

  • :块在 Magento 中用于通过将大部分逻辑移动到块中来减轻控制器的负担。

  • 模板:模板是包含所需 HTML 代码和 PHP 标记的 PHTML 文件。

布局为 Magento 前端提供了令人惊讶的灵活性。每个模块都有自己的布局 XML 文件,告诉 Magento 在每个页面请求上包含和渲染什么。通过使用布局,我们可以在不担心改变除了我们的 XML 文件之外的任何其他内容的情况下,移动、添加或删除我们商店的块。

解剖布局文件

让我们来看看 Magento 的一个核心布局文件,比如catalog.xml

<layout version="0.1.0">
<default>
    <reference name="left">
        <block type="core/template" name="left.permanent.callout" template="callouts/left_col.phtml">
            <action method="setImgSrc"><src>images/media/col_left_callout.jpg</src></action>
            <action method="setImgAlt" translate="alt" module="catalog"><alt>Our customer service is available 24/7\. Call us at (555) 555-0123.</alt></action>
            <action method="setLinkUrl"><url>checkout/cart</url></action>
        </block>
    </reference>
    <reference name="right">
        <block type="catalog/product_compare_sidebar" before="cart_sidebar" name="catalog.compare.sidebar" template="catalog/product/compare/sidebar.phtml"/>
        <block type="core/template" name="right.permanent.callout" template="callouts/right_col.phtml">
            <action method="setImgSrc"><src>images/media/col_right_callout.jpg</src></action>
            <action method="setImgAlt" translate="alt" module="catalog"><alt>Visit our site and save A LOT!</alt></action>
        </block>
    </reference>
    <reference name="footer_links">
        <action method="addLink" translate="label title" module="catalog" ifconfig="catalog/seo/site_map"><label>Site Map</label><url helper="catalog/map/getCategoryUrl" /><title>Site Map</title></action>
    </reference>
    <block type="catalog/product_price_template" name="catalog_product_price_template" />
</default>

布局块由三个主要的 XML 节点组成,如下所示:

  • handle:每个页面请求将具有几个唯一的句柄;布局使用这些句柄告诉 Magento 在每个页面上加载和渲染哪些块。最常用的句柄是default[frontname]_[controller]_[action]

default句柄特别适用于设置全局块,例如在页眉块上添加 CSS 或 JavaScript。

  • reference<reference>节点用于引用一个块。它用于指定嵌套块或修改已经存在的块。在我们的示例中,我们可以看到在<reference name="left">内指定了一个新的子块。

  • block<block>节点用于加载我们的实际块。每个块节点可以具有以下属性:

  • type:这是实际块类的标识符。例如,catalog/product_list指的是Mage_Catalog_Block_Product_List

  • name:其他块用这个名称来引用这个块。

  • before/after:这些属性可用于相对于其他块的位置定位块。这两个属性都可以使用连字符作为值,以指定模块是应该出现在最顶部还是最底部。

  • template:此属性确定将用于渲染块的.phtml模板文件。

  • action:每个块类型都有影响前端功能的特定操作。例如,page/html_head块具有用于添加 CSS 和 JavaScript(addJsaddCss)的操作。

  • as:用于指定我们将在模板中调用的块的唯一标识符,例如使用getChildHtml('block_name')调用子块。

块是 Magento 实现的一个新概念,以减少控制器的负载。它们基本上是直接与模型通信的数据资源,模型操作数据(如果需要),然后将其传递给视图。

最后,我们有我们的 PHTML 文件;模板包含htmlphp标记,并负责格式化和显示来自我们模型的数据。让我们来看一下产品视图模板的片段:

<div class="product-view">
...
    <div class="product-name">
        <h1><?php echo $_helper->productAttribute($_product, $_product->getName(), 'name') ?></h1>
    </div>
...           
    <?php echo $this->getReviewsSummaryHtml($_product, false, true)?>
    <?php echo $this->getChildHtml('alert_urls') ?>
    <?php echo $this->getChildHtml('product_type_data') ?>
    <?php echo $this->getTierPriceHtml() ?>
    <?php echo $this->getChildHtml('extrahint') ?>
...

    <?php if ($_product->getShortDescription()):?>
        <div class="short-description">
            <h2><?php echo $this->__('Quick Overview') ?></h2>
            <div class="std"><?php echo $_helper->productAttribute($_product, nl2br($_product->getShortDescription()), 'short_description') ?></div>
        </div>
    <?php endif;?>
...
</div>

以下是 MVC 的块图:

解剖布局文件

控制器

在 Magento 中,MVC 控制器被设计为薄控制器;薄控制器几乎没有业务逻辑,主要用于驱动应用程序请求。基本的 Magento 控制器动作只是加载和渲染布局:

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

从这里开始,块的工作是处理显示逻辑,从我们的模型中获取数据,准备数据,并将其发送到视图。

网站和商店范围

Magento 的一个核心特性是能够使用单个 Magento 安装处理多个网站和商店;在内部,Magento 将这些实例称为范围。

网站和商店范围

某些元素的值,如产品、类别、属性和配置,是特定范围的,并且在不同的范围上可能不同;这使得 Magento 具有极大的灵活性,例如,一个产品可以在两个不同的网站上设置不同的价格,但仍然可以共享其余的属性配置。

作为开发人员,我们在使用范围最多的领域之一是在处理配置时。Magento 中可用的不同配置范围包括:

  • 全局:顾名思义,这适用于所有范围。

  • 网站:这些由域名定义,由一个或多个商店组成。网站可以设置共享客户数据或完全隔离。

  • 商店:商店用于管理产品和类别,并分组商店视图。商店还有一个根类别,允许我们每个商店有单独的目录。

  • 商店视图:通过使用商店视图,我们可以在商店前端设置多种语言。

Magento 中的配置选项可以在三个范围(全局、网站和商店视图)上存储值;默认情况下,所有值都设置在全局范围上。通过在我们的模块上使用system.xml,我们可以指定配置选项可以设置的范围;让我们重新审视一下我们之前的system.xml

…
<field_name translate="label comment">
    <label>Enabled</label>
    <comment>
         <![CDATA[Comments can contain <strong>HTML</strong>]]>
     </comment>
     <frontend_type>select</frontend_type>
     <source_model>adminhtml/system_config_source_yesno</source_model>
     <sort_order>10</sort_order>
     <show_in_default>1</show_in_default>
     <show_in_website>1</show_in_website>
     <show_in_store>1</show_in_store>
</field_name>
…

工厂名称和函数

Magento 使用工厂方法来实例化ModelHelperBlock类。工厂方法是一种设计模式,允许我们实例化一个对象而不使用确切的类名,而是使用类别名。

Magento 实现了几种工厂方法,如下所示:

  • Mage::getModel()

  • Mage::getResourceModel()

  • Mage::helper()

  • Mage::getSingleton()

  • Mage::getResourceSingleton()

  • Mage::getResourceHelper()

这些方法中的每一个都需要一个类别名,用于确定我们要实例化的对象的真实类名;例如,如果我们想要实例化一个product对象,可以通过调用getModel()方法来实现:

$product = Mage::getModel('catalog/product'); 

请注意,我们正在传递一个由group_classname/model_name组成的工厂名称;Magento 将解析这个工厂名称为Mage_Catalog_Model_Product的实际类名。让我们更仔细地看看getModel()的内部工作:

public static function getModel($modelClass = '', $arguments = array())
    {
        return self::getConfig()->getModelInstance($modelClass, $arguments);
    }

getModel calls the getModelInstance from the Mage_Core_Model_Config class.

public function getModelInstance($modelClass='', $constructArguments=array())
{
    $className = $this->getModelClassName($modelClass);
    if (class_exists($className)) {
        Varien_Profiler::start('CORE::create_object_of::'.$className);
        $obj = new $className($constructArguments);
        Varien_Profiler::stop('CORE::create_object_of::'.$className);
        return $obj;
    } else {
        return false;
    }
}

getModelInstance()又调用getModelClassName()方法,该方法以我们的类别名作为参数。然后它尝试验证返回的类是否存在,如果类存在,它将创建该类的一个新实例并返回给我们的getModel()方法:

public function getModelClassName($modelClass)
{
    $modelClass = trim($modelClass);
    if (strpos($modelClass, '/')===false) {
        return $modelClass;
    }
    return $this->getGroupedClassName('model', $modelClass);
}

getModelClassName()调用getGroupedClassName()方法,实际上负责返回我们模型的真实类名。

getGroupedClassName()接受两个参数 - $groupType$classId$groupType指的是我们正在尝试实例化的对象类型(目前只支持模型、块和助手),$classId是我们正在尝试实例化的对象。

public function getGroupedClassName($groupType, $classId, $groupRootNode=null)
{
    if (empty($groupRootNode)) {
        $groupRootNode = 'global/'.$groupType.'s';
    }
    $classArr = explode('/', trim($classId));
    $group = $classArr[0];
    $class = !empty($classArr[1]) ? $classArr[1] : null;

    if (isset($this->_classNameCache[$groupRootNode][$group][$class])) {
        return $this->_classNameCache[$groupRootNode][$group][$class];
    }
    $config = $this->_xml->global->{$groupType.'s'}->{$group};
    $className = null;
    if (isset($config->rewrite->$class)) {
        $className = (string)$config->rewrite->$class;
    } else {
        if ($config->deprecatedNode) {
            $deprecatedNode = $config->deprecatedNode;
            $configOld = $this->_xml->global->{$groupType.'s'}->$deprecatedNode;
            if (isset($configOld->rewrite->$class)) {
                $className = (string) $configOld->rewrite->$class;
            }
        }
    }
    if (empty($className)) {
        if (!empty($config)) {
            $className = $config->getClassName();
        }
        if (empty($className)) {
            $className = 'mage_'.$group.'_'.$groupType;
        }
        if (!empty($class)) {
            $className .= '_'.$class;
        }
        $className = uc_words($className);
    }
    $this->_classNameCache[$groupRootNode][$group][$class] = $className;
    return $className;
}

正如我们所看到的,getGroupedClassName()实际上正在做所有的工作;它抓取我们的类别名catalog/product,并通过在斜杠字符上分割字符串来创建一个数组。

然后,它加载一个VarienSimplexml_Element的实例,并传递我们数组中的第一个值(group_classname)。它还会检查类是否已被重写,如果是,我们将使用相应的组名。

Magento 还使用了uc_words()函数的自定义版本,如果需要,它将大写类别名的第一个字母并转换分隔符。

最后,该函数将返回真实的类名给getModelInstance()函数;在我们的例子中,它将返回Mage_Catalog_Model_Product

工厂名称和函数

事件和观察者

事件和观察者模式可能是 Magento 更有趣的特性之一,因为它允许开发人员在应用程序流的关键部分扩展 Magento。

为了提供更多的灵活性并促进不同模块之间的交互,Magento 实现了事件/观察者模式;这种模式允许模块之间松散耦合。

这个系统有两个部分 - 一个是带有对象和事件信息的事件分发,另一个是监听特定事件的观察者。

事件和观察者

事件分发

使用Mage::dispatchEvent()函数创建或分派事件。核心团队已经在核心的关键部分创建了几个事件。例如,模型抽象类Mage_Core_Model_Abstract在每次保存模型时调用两个受保护的函数——_beforeSave()_afterSave();在这些方法中,每个方法都会触发两个事件:

protected function _beforeSave()
{
    if (!$this->getId()) {
        $this->isObjectNew(true);
    }
    Mage::dispatchEvent('model_save_before', array('object'=>$this));
    Mage::dispatchEvent($this->_eventPrefix.'_save_before', $this->_getEventData());
    return $this;
}

protected function _afterSave()
{
    $this->cleanModelCache();
    Mage::dispatchEvent('model_save_after', array('object'=>$this));
    Mage::dispatchEvent($this->_eventPrefix.'_save_after', $this->_getEventData());
    return $this;
}

每个函数都会触发一个通用的mode_save_after事件,然后根据正在保存的对象类型生成一个动态版本。这为我们通过观察者操作对象提供了广泛的可能性。

Mage::dispatchEvent()方法接受两个参数:第一个是事件名称,第二个是观察者接收的数据数组。我们可以在这个数组中传递值或对象。如果我们想要操作对象,这将非常方便。

为了理解事件系统的细节,让我们来看一下dispatchEvent()方法:

public static function dispatchEvent($name, array $data = array())
{
    $result = self::app()->dispatchEvent($name, $data);
    return $result;
}

这个函数实际上是位于Mage_Core_Model_App中的app核心类内部的dispatchEvent()函数的别名:

public function dispatchEvent($eventName, $args)
{
    foreach ($this->_events as $area=>$events) {
        if (!isset($events[$eventName])) {
            $eventConfig = $this->getConfig()->getEventConfig($area, $eventName);
            if (!$eventConfig) {
                $this->_events[$area][$eventName] = false;
                continue;
            }
            $observers = array();
            foreach ($eventConfig->observers->children() as $obsName=>$obsConfig) {
                $observers[$obsName] = array(
                    'type'  => (string)$obsConfig->type,
                    'model' => $obsConfig->class ? (string)$obsConfig->class : $obsConfig->getClassName(),
                    'method'=> (string)$obsConfig->method,
                    'args'  => (array)$obsConfig->args,
                );
            }
            $events[$eventName]['observers'] = $observers;
            $this->_events[$area][$eventName]['observers'] = $observers;
        }
        if (false===$events[$eventName]) {
            continue;
        } else {
            $event = new Varien_Event($args);
            $event->setName($eventName);
            $observer = new Varien_Event_Observer();
        }

        foreach ($events[$eventName]['observers'] as $obsName=>$obs) {
            $observer->setData(array('event'=>$event));
            Varien_Profiler::start('OBSERVER: '.$obsName);
            switch ($obs['type']) {
                case 'disabled':
                    break;
                case 'object':
                case 'model':
                    $method = $obs['method'];
                    $observer->addData($args);
                    $object = Mage::getModel($obs['model']);
                    $this->_callObserverMethod($object, $method, $observer);
                    break;
                default:
                    $method = $obs['method'];
                    $observer->addData($args);
                    $object = Mage::getSingleton($obs['model']);
                    $this->_callObserverMethod($object, $method, $observer);
                    break;
            }
            Varien_Profiler::stop('OBSERVER: '.$obsName);
        }
    }
    return $this;
}

dispatchEvent()方法实际上是在事件/观察者模型上进行所有工作的:

  1. 它获取 Magento 配置对象。

  2. 它遍历观察者节点的子节点,检查定义的观察者是否正在监听当前事件。

  3. 对于每个可用的观察者,分派事件将尝试实例化观察者对象。

  4. 最后,Magento 将尝试调用与特定事件相映射的相应观察者函数。

观察者绑定

现在,分派事件是方程式的唯一部分。我们还需要告诉 Magento 哪个观察者正在监听每个事件。毫不奇怪,观察者是通过config.xml指定的。正如我们之前所看到的,dispatchEvent()函数会查询配置对象以获取可用的观察者。让我们来看一个示例config.xml文件:

<events>
    <event_name>
        <observers>
            <observer_identifier>
                <class>module_name/observer</class>
                <method>function_name</method>
            </observer_identifier>
        </observers>
    </event_name>
</events>

event节点可以在每个配置部分(admin、global、frontend 等)中指定,并且我们可以指定多个event_name子节点;event_name必须与dispatchEvent()函数中使用的事件名称匹配。

在每个event_name节点内,我们有一个单一的观察者节点,可以包含多个观察者,每个观察者都有一个唯一的标识符。

观察者节点有两个属性,如<class>,指向我们的观察者模型类,和<method>,依次指向观察者类内部的实际方法。让我们分析一个示例观察者类定义:

class Namespace_Modulename_Model_Observer
{
    public function methodName(Varien_Event_Observer $observer)
    {
        //some code
    }
}  

注意

关于观察者模型的一个有趣的事情是,它们不继承任何其他 Magento 类。

摘要

在本章中,我们涵盖了许多关于 Magento 的重要和基本主题,如其架构、文件夹结构、路由系统、MVC 模式、事件和观察者以及配置范围。

虽然乍一看可能会让人感到不知所措,但这只是冰山一角。关于每个主题和 Magento,还有很多值得学习的地方。本章的目的是让开发人员了解从配置对象到事件/对象模式的实现方式的所有重要组件。

Magento 是一个强大而灵活的系统,它远不止是一个电子商务平台。核心团队在使 Magento 成为一个强大的框架方面付出了很多努力。

在后面的章节中,我们不仅会更详细地回顾所有这些概念,还会通过构建我们自己的扩展来实际应用它们。

第三章:ORM 和数据集合

集合和模型是日常 Magento 开发的基础。在本章中,我们将向读者介绍 Magento ORM 系统,并学习如何正确地处理数据集合和 EAV 系统。与大多数现代系统一样,Magento 实现了一个对象关系映射ORM)系统。

*对象关系映射(ORM,O/RM 和 O/R 映射)是计算机软件中的一种编程技术,用于在面向对象的编程语言中在不兼容的类型系统之间转换数据。这实际上创建了一个可以从编程语言内部使用的“虚拟对象数据库”。

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

  • Magento 模型

  • Magento 数据模型的解剖学

  • EAV 和 EAV 模型

  • 使用直接 SQL 查询

我们还将使用几个代码片段来提供一个方便的框架,以便在 Magento 中进行实验和玩耍。

注意

请注意,本章中的交互式示例假定您正在使用 VagrantBox 内的默认 Magento 安装或带有示例数据的 Magento 安装。

为此,我创建了交互式 Magento 控制台IMC),这是一个专门为本书创建的 shell 脚本,受 Ruby 自己的交互式 Ruby 控制台IRB)启发。请按照以下步骤:

  1. 我们需要做的第一件事是安装 IMC。为此,请从github.com/amacgregor/mdg_imc下载源文件,并将其提取到 Magento 测试安装下。IMC 是一个简单的 Magento shell 脚本,可以让我们实时测试我们的代码。

  2. 提取脚本后,登录到您的虚拟机的 shell。

  3. 接下来,我们需要导航到我们的 Magento 根文件夹。如果您正在使用默认的 vagrant box,安装已经提供;根文件夹位于/srv/www/ce1720/public_html/下,我们可以通过运行以下命令行来导航到它:

**$ cd /srv/www/ce1720/public_html**

  1. 最后,我们可以通过运行以下命令行来启动 IMC:
**$ php shell/imc.php**

  1. 如果一切安装成功,我们应该看到一行新的以magento >开头的内容。

Magento 模型解剖学

正如我们在上一章中学到的,Magento 数据模型用于操作和访问数据。模型层分为两种基本类型,简单模型和 EAV,其中:

  • 简单模型:这些模型实现是一个对象到一个表的简单映射,这意味着我们的对象属性与每个字段匹配,我们的表结构

  • 实体属性值模型(EAV):这种类型的模型用于描述具有动态属性数量的实体

注意

请注意,重要的是要澄清并非所有 Magento 模型都扩展或使用 ORM。观察者是一个明显的例子,它们是不与特定数据库表或实体映射的简单模型类。

除此之外,每种模型类型由以下层组成:

  • 模型类:这是大部分业务逻辑所在的地方。模型用于操作数据,但不直接访问数据。

  • 资源模型类:资源模型用于代表我们的模型与数据库交互。它们负责实际的 CRUD 操作。

  • 模型集合类:每个数据模型都有一个集合类;集合是保存多个单独的 Magento 模型实例的对象。

注意

CRUD 代表数据库的四种基本操作:创建、读取、更新和删除。

Magento 模型不包含与数据库通信的任何逻辑;它们是与数据库无关的。相反,这些代码存在于资源模型层。

这使 Magento 有能力支持不同类型的数据库和平台。尽管目前只有 MySQL 得到官方支持,但完全可以编写一个新的资源类来支持新的数据库,而不用触及任何模型逻辑。

Magento 模型解剖

现在让我们通过实例化一个产品对象并按照以下步骤设置一些属性来进行实验:

  1. 启动 Magento 交互式控制台,运行在 Magento 分期安装根目录下:
**php shell/imc.php**

  1. 我们的第一步是通过输入来创建一个新的产品对象实例:
**magento> $product = Mage::getModel('catalog/product');**

  1. 我们可以通过运行以下命令来确认这是否是产品类的空实例:
**magento> echo get_class($product);**

  1. 我们应该看到以下成功的输出:
**magento> Magento_Catalog_Model_Product**

  1. 如果我们想了解更多关于类方法的信息,可以运行以下命令行:
**magento> print_r(get_class_methods($product));**

这将返回一个包含类内所有可用方法的数组。让我们尝试运行以下代码片段并修改产品的价格和名称:

$product = Mage::getModel('catalog/product')->load(2);
$name    = $product->getName() . '-TEST';
$price   = $product->getPrice();
$product->setPrice($price + 15);
$product->setName($name);
$product->save();

在第一行代码中,我们实例化了一个特定的对象,然后我们继续从对象中检索名称属性。接下来,我们设置价格和名称,最后保存对象。

如果我们打开我们的 Magento 产品类Mage_Catalog_Model_Product,我们会注意到虽然getName()getPrice()都在我们的类中定义了,但是setPrice()setName()函数却没有在任何地方定义。

但是为什么,更重要的是,Magento 是如何神奇地定义每个产品对象的 setter 和 getter 方法的呢?虽然getPrice()getName()确实被定义了,但是对于产品属性的任何 getter 和 setter 方法,比如颜色或制造商,都没有定义。

这是魔法-方法

事实上,Magento ORM 系统确实使用了魔术;或者更准确地说,使用了 PHP 更强大的特性来实现其 getter 和 setter,即magic __call()方法。Magento 中使用的方法用于设置、取消设置、检查或检索数据。

当我们尝试调用一个实际上在相应类中不存在的方法时,PHP 将查找每个父类中是否有该方法的声明。如果我们在任何父类中找不到该函数,它将使用最后的手段并尝试使用__call()方法,如果找到,Magento(或者 PHP)将调用魔术方法,从而传递请求的方法名和其参数。

现在,产品模型没有定义__call()方法,但是它从所有 Magento 模型继承的Varien_Object类中获得了一个。Mage_Catalog_Model_Product类的继承树如下流程图所示:

这是魔法-方法

提示

每个 Magento 模型都继承自Varien_Object类。

让我们更仔细地看一下Varien_Object类:

  1. 打开位于magento_root/lib/Varien/Object.php中的文件。

  2. Varien_Object类不仅有一个__call()方法,还有两个已弃用的方法,__set()__get();这两个方法被__call()方法替代,因此不再使用。

public function __call($method, $args)
{
   switch (substr($method, 0, 3)) {
       case 'get' :
           //Varien_Profiler::start('GETTER: '.get_class($this).'::'.$method);
           $key = $this->_underscore(substr($method,3));
           $data = $this->getData($key, isset($args[0]) ? $args[0] : null);
           //Varien_Profiler::stop('GETTER: '.get_class($this).'::'.$method);
           return $data;

       case 'set' :
           //Varien_Profiler::start('SETTER: '.get_class($this).'::'.$method);
           $key = $this->_underscore(substr($method,3));
           $result = $this->setData($key, isset($args[0]) ? $args[0] : null);
           //Varien_Profiler::stop('SETTER: '.get_class($this).'::'.$method);
           return $result;

       case 'uns' :
           //Varien_Profiler::start('UNS: '.get_class($this).'::'.$method);
           $key = $this->_underscore(substr($method,3));
           $result = $this->unsetData($key);
           //Varien_Profiler::stop('UNS: '.get_class($this).'::'.$method);
           return $result;
       case 'has' :
           //Varien_Profiler::start('HAS: '.get_class($this).'::'.$method);
           $key = $this->_underscore(substr($method,3));
           //Varien_Profiler::stop('HAS: '.get_class($this).'::'.$method);
           return isset($this->_data[$key]);
   }
   throw new Varien_Exception("Invalid method" . get_class($this)."::".$method."(".print_r($args,1).")");
}

__call()方法内部,我们有一个 switch 语句,不仅处理 getter 和 setter,还处理unsethas函数。

如果我们启动调试器并跟踪我们的代码片段调用__call()方法,我们可以看到它接收两个参数:方法名,例如setName(),以及原始调用的参数。

有趣的是,Magento 尝试根据被调用方法的前三个字母来匹配相应的方法类型;这是在 switch case 参数调用 substring 函数时完成的:

substr($method, 0, 3)

在每种情况下调用的第一件事是_underscore()函数,它以方法名的前三个字符之后的任何内容作为参数;按照我们的例子,传递的参数将是Name

__underscore()函数返回一个数据键。然后每种情况下都使用这个键来操作数据。有四种基本的数据操作,每种操作对应一个 switch case:

  • setData($parameters)

  • getData($parameters)

  • unsetData($parameters)

  • isset($parameters)

这些函数中的每一个都将与Varien_Object数据数组交互,并相应地对其进行操作。在大多数情况下,将使用魔术 set/get 方法与我们的对象属性交互;只有在需要额外的业务逻辑时,才会定义 getter 和 setter。在我们的示例中,它们是getName()getPrice()

public function getPrice()
{
   if ($this->_calculatePrice || !$this->getData('price')) {
       return $this->getPriceModel()->getPrice($this);
   } else {
       return $this->getData('price');
   }
}

我们不会详细介绍价格函数实际在做什么,但它清楚地说明了对模型的某些部分可能需要额外的逻辑。

public function getName()
{
   return $this->_getData('name');
}

另一方面,getName()getter 并不是因为需要实现特殊逻辑而声明的,而是因为需要优化 Magento 的一个关键部分。Mage_Catalog_Model_Product getName()函数可能在每次页面加载时被调用数百次,是 Magento 中最常用的函数之一;毕竟,如果它不是围绕产品中心的电子商务平台,那它会是什么样子呢?

前端和后端都会在某个时候调用getName()函数。例如,如果我们加载一个包含 24 个产品的类别页面,也就是说,getName()函数会被调用 24 次,每次调用都会在父类中寻找getName()方法,然后当我们尝试使用magic __call()方法时,会导致丢失宝贵的毫秒。

资源模型包含所有特定于数据库的逻辑,并为其相应的数据源实例化特定的读取和写入适配器。让我们回到我们的产品示例,并查看位于Mage_Catalog_Model_Resource_Product的产品资源模型。

这是魔术-方法

资源模型有两种不同类型:实体和 MySQL4。后者是一个相当标准的单表/单模型关联,而前者则复杂得多。

EAV 模型

EAV 代表实体、属性和值,这可能是新 Magento 开发人员难以理解的概念。虽然 EAV 概念并不是 Magento 独有的,但它在现代系统中很少实现,而且 Magento 的实现也并不简单。

EAV 模型

什么是 EAV?

为了理解 EAV 是什么以及它在 Magento 中的作用,我们需要将其分解为 EAV 模型的各个部分。

  • 实体:实体代表 Magento 产品、客户、类别和订单中的数据项(对象)。每个实体都以唯一 ID 存储在数据库中。

  • 属性:这些是我们的对象属性。与产品表上每个属性都有一列不同,属性存储在单独的表集上。

  • :顾名思义,它只是与特定属性相关联的值链接。

这种设计模式是 Magento 灵活性和强大性的秘密,允许实体添加和删除新属性,而无需对代码或模板进行任何更改。

虽然模型可以被视为增加数据库的垂直方式(新属性增加更多行),传统模型将涉及水平增长模式(新属性增加更多列),这将导致每次添加新属性时都需要对模式进行重新设计。

EAV 模型不仅允许我们的数据库快速发展,而且更有效,因为它只处理非空属性,避免了为 null 值在数据库中保留额外空间的需要。

提示

如果您有兴趣探索和了解 Magento 数据库结构,我强烈建议您访问www.magereverse.com

添加新产品属性就像进入 Magento 后端并指定新属性类型一样简单,比如颜色、尺寸、品牌等。相反的也是真的,因为我们可以在我们的产品或客户模型上摆脱未使用的属性。

注意

有关管理属性的更多信息,请访问www.magentocommerce.com/knowledge-base/entry/how-do-attributes-work-in-magento

Magento 社区版目前有八种不同类型的 EAV 对象:

  • 客户

  • 客户地址

  • 产品

  • 产品类别

  • 订单

  • 发票

  • 信贷备忘录

  • 发货

注意

Magento 企业版有一个额外的类型称为 RMA 项目,它是退货授权(RMA)系统的一部分。

所有这些灵活性和功能都是有代价的;实施 EAV 模型会导致我们的实体数据分布在大量的表中,例如,仅产品模型就分布在大约 40 个不同的表中。

以下图表仅显示了保存 Magento 产品信息所涉及的一些表:

什么是 EAV?

EAV 的另一个主要缺点是在检索大量 EAV 对象时性能下降,数据库查询复杂性增加。由于数据更分散(存储在更多的表中),选择单个记录涉及多个连接。

让我们继续以 Magento 产品作为示例,并手动构建检索单个产品的查询。

提示

如果您在开发环境中安装了 PHPMyAdmin 或 MySQL Workbench,可以尝试以下查询。可以从 PHPMyAdmin(www.phpmyadmin.net/)和 MySQL Workbench(www.mysql.com/products/workbench/)下载每个查询。

我们需要使用的第一个表是catalog_product_entity。我们可以将其视为我们的主要产品 EAV 表,因为它包含了我们产品的主要实体记录:

什么是 EAV?

通过运行以下 SQL 查询来查询表:

SELECT * FROM `catalog_product_entity`;

该表包含以下字段:

  • entity_id:这是我们产品的唯一标识符,由 Magento 在内部使用。

  • entity_type_id:Magento 有几种不同类型的 EAV 模型,产品、客户和订单,这些只是其中一些。通过类型标识,Magento 可以从适当的表中检索属性和值。

  • attribute_set_id:产品属性可以在本地分组到属性集中。属性集允许对产品结构进行更灵活的设置,因为产品不需要使用所有可用的属性。

  • type_id:Magento 中有几种不同类型的产品:简单、可配置、捆绑、可下载和分组产品,每种产品都具有独特的设置和功能。

  • sku库存保留单位(SKU)是用于标识商店中每个唯一产品或商品的编号或代码。这是用户定义的值。

  • has_options:这用于标识产品是否具有自定义选项。

  • required_options:这用于标识是否需要任何自定义选项。

  • created_at:这是行创建日期。

  • updated_at:显示行上次修改的时间。

现在我们对产品实体表有了基本的了解,我们也知道每条记录代表着我们 Magento 商店中的一个产品,但是我们对该产品的信息并不多,除了 SKU 和产品类型之外。

那么,属性存储在哪里?Magento 如何区分产品属性和客户属性?

为此,我们需要通过运行以下 SQL 查询来查看eav_attribute表:

SELECT * FROM `eav_attribute`;

因此,我们不仅会看到产品属性,还会看到与客户模型、订单模型等对应的属性。幸运的是,我们已经有一个用于从该表中过滤属性的关键。让我们运行以下查询:

SELECT * FROM `eav_attribute`
WHERE entity_type_id = 4;

这个查询告诉数据库只检索entity_type_id列等于产品entity_type_id(4)的属性。在继续之前,让我们分析eav_attribute表中最重要的字段:

  • attribute_id: 这是每个属性的唯一标识符和表的主键。

  • entity_type_id: 这个字段将每个属性关联到特定的 EAV 模型类型。

  • attribute_code: 这个字段是我们属性的名称或键,用于生成我们的魔术方法的 getter 和 setter。

  • backend_model: 后端模型负责加载和存储数据到数据库中。

  • backend_type: 这个字段指定存储在后端(数据库)的值的类型。

  • backend_table: 这个字段用于指定属性是否应该存储在特殊表中,而不是默认的 EAV 表中。

  • frontend_model: 前端模型处理属性元素在 web 浏览器中的呈现。

  • frontend_input: 类似于前端模型,前端输入指定 web 浏览器应该呈现的输入字段类型。

  • frontend_label: 这个字段是属性的标签/名称,应该由浏览器呈现。

  • source_model: 源模型用于为属性填充可能的值。Magento 带有几个预定义的源模型,用于国家、是或否值、地区等。

检索数据

此时,我们已经成功检索了一个产品实体和适用于该实体的特定属性,现在是时候开始检索实际的值了。为了简单执行示例(和查询),我们将尝试只检索我们产品的名称属性。

但是,我们如何知道我们的属性值存储在哪个表中?幸运的是,Magento 遵循了一种命名约定来命名表。如果我们检查我们的数据库结构,我们会注意到有几个表使用catalog_product_entity前缀:

  • catalog_product_entity

  • catalog_product_entity_datetime

  • catalog_product_entity_decimal

  • catalog_product_entity_int

  • catalog_product_entity_text

  • catalog_product_entity_varchar

  • catalog_product_entity_gallery

  • catalog_product_entity_media_gallery

  • catalog_product_entity_tier_price

但是,等等,我们如何知道查询我们名称属性值的正确表?如果你在关注,我们已经看到了答案。你还记得eav_attribute表有一个叫做backend_type的列吗?

Magento EAV 根据属性的后端类型将每个属性存储在不同的表中。如果我们想确认我们的名称的后端类型,可以通过运行以下代码来实现:

SELECT * FROM `eav_attribute`
WHERE `entity_type_id` =4 AND `attribute_code` = 'name';

并且我们应该看到,后端类型是varchar,这个属性的值存储在catalog_product_entity_varchar表中。让我们检查这个表:

检索数据

catalog_product_entity_varchar表只由六列组成:

  • value_id: 属性值是唯一标识符和主键

  • entity_type_id: 这个值属于实体类型 ID

  • attribute_id: 这是一个外键,将值与我们的eav_entity表关联起来

  • store_id: 这是一个外键,将属性值与 storeview 进行匹配

  • entity_id: 这是对应实体表的外键;在这种情况下,它是catalog_product_entity

  • value: 这是我们要检索的实际值

提示

根据属性配置,我们可以将其作为全局值,表示它适用于所有 storeview,或者作为每个 storeview 的值。

现在我们终于有了检索产品信息所需的所有表,我们可以构建我们的查询:

SELECT p.entity_id AS product_id, var.value AS product_name, p.sku AS product_sku
FROM catalog_product_entity p, eav_attribute eav, catalog_product_entity_varchar var
WHERE p.entity_type_id = eav.entity_type_id 
   AND var.entity_id = p.entity_id
   AND eav.attribute_code = 'name'
   AND eav.attribute_id = var.attribute_id

检索数据

作为查询结果,我们应该看到一个包含三列的结果集:product_idproduct_nameproduct_sku。因此,让我们退后一步,以便获取产品名称和 SKU。使用原始 SQL,我们将不得不编写一个五行的 SQL 查询,我们只能从我们的产品中检索两个值:如果我们想要检索数字字段,比如价格,或者从文本值,比如产品,我们只能从一个单一的 EAV 值表中检索。

如果我们没有 ORM,维护 Magento 几乎是不可能的。幸运的是,我们有一个 ORM,并且很可能你永远不需要处理 Magento 的原始 SQL。

说到这里,让我们看看如何使用 Magento ORM 来检索相同的产品信息:

  1. 我们的第一步是实例化一个产品集合:
**$collection = Mage::getModel('catalog/product')->getCollection();**

  1. 然后,我们将明确告诉 Magento 选择名称属性:
**$collection->addAttributeToSelect('name');**

  1. 现在按名称对集合进行排序:
**$collection->setOrder('name', 'asc');**

  1. 最后,我们将告诉 Magento 加载集合:
**$collection->load();**

  1. 最终结果是商店中所有产品的集合按名称排序;我们可以通过运行以下命令来检查实际的 SQL 查询:
**echo $collection->getSelect()->__toString();**

仅仅通过三行代码的帮助,我们就能告诉 Magento 抓取商店中的所有产品,具体选择名称,并最终按名称排序产品。

提示

最后一行$collection->getSelect()->__toString(),允许我们查看 Magento 代表我们执行的实际查询。

Magento 生成的实际查询是:

SELECT `e`.*. IF( at_name.value_id >0, at_name.value, at_name_default.value ) AS `name`
FROM `catalog_product_entity` AS `e`
LEFT JOIN `catalog_product_entity_varchar` AS `at_name_default` ON (`at_name_default`.`entity_id` = `e`.`entity_id`)
AND (`at_name_default`.`attribute_id` = '65')
AND `at_name_default`.`store_id` =0
LEFT JOIN `catalog_product_entity_varchar` AS `at_name` ON ( `at_name`.`entity_id` = `e`.`entity_id` )
AND (`at_name`.`attribute_id` = '65')
AND (`at_name`.`store_id` =1)
ORDER BY `name` ASC

正如我们所看到的,ORM 和 EAV 模型是非常棒的工具,不仅为开发人员提供了很多功能和灵活性,而且还以一种全面易用的方式实现了这一点。

使用 Magento 集合

如果您回顾前面的代码示例,您可能会注意到我们不仅实例化了一个产品模型,还调用了getCollection()方法。getCollection()方法是Mage_Core_Model_Abstract类的一部分,这意味着 Magento 中的每个单个模型都可以调用此方法。

提示

所有集合都继承自Varien_Data_Collection

Magento 集合基本上是包含其他模型的模型。因此,我们可以使用产品集合而不是使用数组来保存产品集合。集合不仅提供了一个方便的数据结构来对模型进行分组,还提供了特殊的方法,我们可以用来操作和处理实体的集合。

一些最有用的集合方法是:

  • addAttributeToSelect:要向集合中的实体添加属性,可以使用*作为通配符来添加所有可用的属性

  • addFieldToFilter:要向集合添加属性过滤器,需要在常规的非 EAV 模型上使用此函数

  • addAttributeToFilter:此方法用于过滤 EAV 实体的集合

  • addAttributeToSort:此方法用于添加属性以排序顺序

  • addStoreFilter:此方法用于存储可用性过滤器;它包括可用性产品

  • addWebsiteFilter:此方法用于向集合添加网站过滤器

  • addCategoryFilter:此方法用于为产品集合指定类别过滤器

  • addUrlRewrite:此方法用于向产品添加 URL 重写数据

  • setOrder:此方法用于设置集合的排序顺序

这些只是一些可用的集合方法;每个集合实现了不同的独特方法,具体取决于它们对应的实体类型。例如,客户集合Mage_Customer_Model_Resource_Customer_Collection有一个称为groupByEmail()的唯一方法,它的名称正确地暗示了通过电子邮件对集合中的实体进行分组。

与之前的示例一样,我们将继续使用产品模型,并在这种情况下是产品集合。

使用 Magento 集合

为了更好地说明我们如何使用集合,我们将处理以下常见的产品场景:

  1. 仅从特定类别获取产品集合。

  2. 获取自 X 日期以来的新产品。

  3. 获取畅销产品。

  4. 按可见性过滤产品集合。

  5. 过滤没有图片的产品。

  6. 添加多个排序顺序。

仅从特定类别获取产品集合

大多数开发人员在开始使用 Magento 时尝试做的第一件事是从特定类别加载产品集合,虽然我看到过许多使用addCategoryFilter()addAttributeToFilter()的方法,但实际上,对于大多数情况来说,这种方法要简单得多,而且有点违反我们迄今为止学到的直觉。

最简单的方法不是首先获取产品集合,然后按类别进行过滤,而是实际上实例化我们的目标类别,并从那里获取产品集合。让我们在 IMC 上运行以下代码片段:

$category = Mage::getModel('catalog/category')->load(5);
$productCollection = $category->getProductCollection();

我们可以在Mage_Catalog_Model_Category类中找到getProductCollection()方法的声明。让我们更仔细地看看这个方法:

public function getProductCollection()
{
    $collection = Mage::getResourceModel('catalog/product_collection')
        ->setStoreId($this->getStoreId())
        ->addCategoryFilter($this);
    return $collection;
}

正如我们所看到的,该函数实际上只是实例化产品集合的资源模型,即将存储设置为当前存储 ID,并将当前类别传递给addCategoryFilter()方法。

这是为了优化 Magento 性能而做出的决定之一,而且坦率地说,也是为了简化与之合作的开发人员的生活,因为在大多数情况下,某种方式都会提供类别。

获取自 X 日期以来添加的新产品

现在,我们知道如何从特定类别获取产品集合,让我们看看是否能够对结果产品应用过滤器,并且只对符合我们条件的检索产品进行过滤;在这种特殊情况下,我们将请求所有在 2012 年 12 月之后添加的产品。根据我们之前的示例代码,我们可以通过在 IMC 上运行以下代码来按产品创建日期过滤我们的集合:

// Product collection from our previous example
$productCollection->addFieldToFilter('created_at', array('from' => '2012-12-01));

很简单,不是吗?我们甚至可以添加一个额外的条件,并获取在两个日期之间添加的产品。假设我们只想检索在 12 月份创建的产品:

$productCollection->addFieldToFilter('created_at', array('from' => '2012-12-01));
$productCollection->addFieldToFilter('created_at', array('to' => '2012-12-30));

Magento 的addFieldToFilter支持以下条件:

属性代码 SQL 条件
eq =
neq !=
like LIKE
nlike NOT LIKE
in IN ()
nin NOT IN ()
is IS
notnull NOT NULL
null NULL
moreq >=
gt >
lt <
gteq >=
lteq <=

我们可以尝试其他类型的过滤器,例如,在添加了我们的创建日期过滤器后,在 IMC 上使用以下代码,这样我们就可以只检索可见产品:

$productCollection->addAttributeToFilter('visibility', 4);

可见性属性是产品用来控制产品显示位置的特殊属性;它具有以下值:

  • 不单独可见:它的值为 1

  • 目录:它的值为 2

  • 搜索:它的值为 3

  • 目录和搜索:它的值为 4

获取畅销产品

要尝试获取特定类别的畅销产品,我们需要提升自己的水平,并与sales_order表进行连接。以后为了创建特殊类别或自定义报告,检索畅销产品将非常方便;我们可以在 IMC 上运行以下代码:

$category = Mage::getModel('catalog/category')->load(5);
$productCollection = $category->getProductCollection();
$productCollection->getSelect()
            ->join(array('o'=> 'sales_flat_order_item'), 'main_table.entity_id = o.product_id', array('o.row_total','o.product_id'))->group(array('sku'));

让我们分析一下我们片段的第三行发生了什么。getSelect()是直接从Varien_Data_Collection_Db继承的方法,它返回存储Select语句的变量,除了提供指定连接和分组的方法之外,还无需编写任何 SQL。

这不是向集合添加连接的唯一方法。实际上,有一种更干净的方法可以使用joinField()函数来实现。让我们重写我们之前的代码以使用这个函数:

$category = Mage::getModel('catalog/category')->load(5);
$productCollection = $category->getProductCollection();
$productCollection->joinField('o', 'sales_flat_order_item', array('o.row_total','o.product_id'), 'main_table.entity_id = o.product_id')
->group(array('sku'));

按可见性过滤产品集合

这在使用addAttributeToFilter的帮助下非常容易实现。Magento 产品有一个名为 visibility 的系统属性,它有四个可能的数字值,范围从 1 到 4。我们只对可见性为 4 的产品感兴趣;也就是说,它可以在搜索结果和目录中都能看到。让我们在 IMC 中运行以下代码:

$category = Mage::getModel('catalog/category')->load(5);
$productCollection = $category->getProductCollection();
$productCollection->addAttributeToFilter('visibility', 4);

如果我们更改可见性代码,我们可以比较不同的集合结果。

过滤没有图像的产品

在处理第三方导入系统时,过滤没有图像的产品非常方便,因为这种系统有时可能不可靠。与我们迄今为止所做的一切一样,产品图像是我们产品的属性。

$category = Mage::getModel('catalog/category')->load(5);
$productCollection = $category->getProductCollection();
$productCollection->addAttributeToFilter('small_image',array('notnull'=>'','neq'=>'no_selection'));

通过添加额外的过滤器,我们要求产品必须指定一个小图像;默认情况下,Magento 有三种产品:图像类型,缩略图和small_image和图像。这三种类型在应用程序的不同部分使用。如果我们愿意,甚至可以为产品设置更严格的规则。

$productCollection->addAttributeToFilter('small_image', array('notnull'=>'','neq'=>'no_selection'));
->addAttributeToFilter('thumbnail, array('notnull'=>'','neq'=>'no_selection'))
->addAttributeToFilter('image', array('notnull'=>'','neq'=>'no_selection'));

只有具有三种类型图像的产品才会包含在我们的集合中。尝试通过不同的图像类型进行过滤。

添加多个排序顺序

最后,让我们先按库存状态排序,然后按价格从高到低排序我们的集合。为了检索库存状态信息,我们将使用一个特定于库存状态资源模型的方法addStockStatusToSelect(),它将负责为我们的集合查询生成相应的 SQL。

$category = Mage::getModel('catalog/category')->load(5);
$productCollection = $category->getProductCollection();
$select = $productCollection->getSelect();
Mage::getResourceModel('cataloginventory/stock_status')->addStockStatusToSelect($select, Mage::app()->getWebsite());
$select->order('salable desc');
$select->order('price asc');

在这个查询中,Magento 将根据可销售状态(true 或 false)和价格对产品进行排序;最终结果是所有可用产品将显示从最昂贵到最便宜的产品,然后,缺货产品将显示从最昂贵到最便宜的产品。

尝试不同的排序顺序组合,看看 Magento 如何组织和排序产品集合。

使用直接 SQL

到目前为止,我们已经学习了 Magento 数据模型和 ORM 系统提供了一种清晰简单的方式来访问、存储和操作我们的数据。在我们直接进入本节之前,了解 Magento 数据库适配器以及如何运行原始 SQL 查询,我觉得重要的是我们要理解为什么尽可能避免使用你即将在本节中学到的内容。

Magento 是一个非常复杂的系统,正如我们在上一章中学到的,框架部分由事件驱动;仅仅保存一个产品就会触发不同的事件,每个事件执行不同的任务。如果你决定只创建一个查询并直接更新产品,这种情况就不会发生。因此,作为开发人员,我们必须非常小心,确保是否有正当理由去绕过 ORM。

也就是说,当然也有一些情况下,能够直接与数据库一起工作非常方便,实际上比使用 Magento 模型更简单。例如,当全局更新产品属性或更改产品集合状态时,我们可以加载产品集合并循环遍历每个单独的产品进行更新和保存。虽然这在较小的集合上可以正常工作,但一旦我们开始扩大规模并处理更大的数据集,性能就会开始下降,脚本执行需要几秒钟。

另一方面,直接的 SQL 查询将执行得更快,通常在 1 秒内,这取决于数据集的大小和正在执行的查询。

Magento 将负责处理与数据库建立连接的所有繁重工作,使用Mage_Core_Model_Resource模型;Magento 为我们提供了两种类型的连接,core_readcore_write

让我们首先实例化一个资源模型和两个连接,一个用于读取,另一个用于写入:

$resource = Mage::getModel('core/resource');
$read = $resource->getConnection('core_read');
$write = $resource->getConnection('core_write');

即使我们使用直接的 SQL 查询,由于 Magento 的存在,我们不必担心设置到数据库的连接,只需实例化一个资源模型和正确类型的连接。

阅读

让我们通过执行以下代码来测试我们的读取连接:

$resource = Mage::getModel('core/resource');
$read = $resource->getConnection('core_read');
$query = 'SELECT * FROM catalog_product_entity';
$results = $read->fetchAll($query);

尽管此查询有效,但它将返回catalog_product_entity表中的所有产品。但是,如果我们尝试在使用表前缀的 Magento 安装上运行相同的代码会发生什么?或者如果 Magento 在下一个升级中突然更改了表名会发生什么?这段代码不具备可移植性或易维护性。幸运的是,资源模型提供了另一个方便的方法,称为getTableName()

getTableName()方法将以工厂名称作为参数,并根据config.xml建立的配置,不仅会找到正确的表,还会验证该表是否存在于数据库中。让我们更新我们的代码以使用getTableName()

$resource = Mage::getModel('core/resource');
$read = $resource->getConnection('core_read');
$query = 'SELECT * FROM ' . $resource->getTableName('catalog/product');
$results = $read->fetchAll($query);

我们还在使用fetchAll()方法。这将以数组形式返回查询的所有行,但这并不是唯一的选项;我们还可以使用fetchCol()fetchOne()。让我们看看以下函数:

  • fetchAll:此函数检索原始查询返回的所有行

  • fetchOne:此函数将仅返回查询返回的第一行数据库的值

  • fetchCol:此函数将返回查询返回的所有行,但只返回第一行;如果您只想检索具有唯一标识符的单个列,例如产品 ID 或 SKU,这将非常有用

写作

正如我们之前提到的,由于后端触发的观察者和事件数量,保存 Magento 中的模型(无论是产品、类别、客户等)可能相对较慢。

但是,如果我们只想更新简单的静态值,通过 Magento ORM 进行大型集合的更新可能是一个非常缓慢的过程。例如,假设我们想要使网站上的所有产品都缺货。我们可以简单地执行以下代码片段,而不是通过 Magento 后端进行操作或创建一个迭代所有产品集合的自定义脚本:

$resource = Mage::getModel('core/resource');
$read = $resource->getConnection('core_write);
$tablename = $resource->getTableName('cataloginventory/stock_status');
$query = 'UPDATE {$tablename} SET `is_in_stock` = 1';
$write->query($query);

摘要

在本章中,我们学习了:

  • Magento 模型、它们的继承和目的

  • Magento 如何使用资源和集合模型

  • EAV 模型及其在 Magento 中的重要性

  • EAV 的工作原理和数据库内部使用的结构

  • Magento ORM 模型是什么以及它是如何实现的

  • 如何使用直接 SQL 和 Magento 资源适配器

到目前为止,章节更多地是理论性的而不是实践性的;这是为了引导您了解 Magento 的复杂性,并为您提供本书其余部分所需的工具和知识。在本书的其余部分,我们将采取更加实践性的方法,逐步构建扩展,应用我们到目前为止学到的所有概念。

在下一章中,我们将开始涉足并开发我们的第一个 Magento 扩展。

第四章:前端开发

到目前为止,我们已经专注于 Magento 背后的理论、它的架构,并熟悉了日常 Magento 开发的常见和重要概念。

在本章中,我们将通过逐步构建一个 Magento 扩展来实际运用我们迄今所学到的技能和知识。我们将构建一个完全功能的礼品注册表扩展。

扩展 Magento

在跳入并开始构建我们的扩展之前,让我们定义一个示例情景和我们扩展的范围。这样我们将清楚地知道我们正在构建什么,更重要的是,我们不在构建什么。

情景

我们的情景很简单;我们想要扩展 Magento,允许客户创建礼品注册表并与朋友和家人分享。客户应该能够创建多个礼品注册表,并指定这些礼品注册表的接收者。

礼品注册表将保存以下信息:

  • 事件类型

  • 事件名称

  • 事件日期

  • 事件地点

  • 产品列表

功能

看一下以下功能:

  • 商店管理员可以定义多个事件类型(生日、婚礼和礼品注册表)

  • 创建事件并为每个事件分配多个礼品注册表列表

  • 客户可以从购物车、愿望清单或直接从产品页面将产品添加到他们的注册表中

  • 客户可以拥有多个礼品注册表

  • 人们可以通过电子邮件和/或直接链接与朋友和家人分享他们的注册表

  • 朋友和家人可以从礼品注册表购买物品

进一步的改进

以下是可能被省略在这个示例扩展中的一些功能列表,因为它们的复杂性,或者在社交媒体的情况下,由于它们的 API 和社交媒体平台的数量是不断变化的,但它们仍然是对想要进一步扩展这个模块的读者来说一个很好的挑战:

  • 社交媒体整合

  • 注册表可以跟踪每个注册表项目的请求和完成数量

  • 指定多个不同的注册表所有者

  • 交付给注册表所有者地址

你好 Magento

在前几章中,我们了解了 Magento 的代码池(核心、社区、本地)。由于我们不打算在 Magento Connect 上分发我们的模块,我们将在本地目录下创建它。

所有 Magento 模块都保存在包或命名空间中;例如,所有核心 Magento 模块都保存在 Mage 命名空间下。为了本书的目的,我们将使用 Magento 开发者指南(MDG)。

模块的 Magento 命名约定是Namespace_Modulename

我们的下一步将是创建模块结构和配置文件。我们需要在app/code/local/下创建一个命名空间目录。命名空间可以是任何你喜欢的东西。被接受的惯例是使用公司的名称或作者的名称作为命名空间。因此,我们的第一步将是创建目录app/code/local/Mdg/。这个目录不仅将保存我们的礼品注册表模块,还将保存我们开发的任何未来模块。

在我们的命名空间目录下,我们还需要创建一个新的目录,其中包含我们自定义扩展的所有代码。

让我们继续创建一个Giftregistry目录。一旦完成,让我们创建剩下的目录结构。

注意

请注意,由于 Magento 使用工厂方法,对驼峰命名法有些敏感。一般来说,在我们的模块/控制器/操作名称中避免使用驼峰命名法是个好主意。有关 Magento 命名约定的更多信息,请参阅本书的附录。

文件位置是/app/code/local/Mdg/Giftregistry/

Block/
Controller/
controllers/
Helper/
etc/
Model/
sql/

到目前为止,我们已经了解到,Magento 使用.xml文件作为其配置的中心部分。为了让 Magento 识别并激活模块,我们需要在app/etc/modules/下创建一个文件,遵循Namespace_Modulename.xml约定。让我们创建我们的文件。

文件位置是app/etc/modules/Mdg_Giftregistry.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Mdg_Giftregistry>
            <active>true</active>
            <codePool>local</codePool>
        </Mdg_Giftregistry >
    </modules>
</config>

提示

下载示例代码

您可以从您在www.packtpub.com购买的所有 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册以直接通过电子邮件接收文件。

创建此文件或对我们的模块配置文件进行任何更改后,我们需要刷新 Magento 配置缓存:

  1. 导航到 Magento 后端。

  2. 打开系统 | 缓存管理

  3. 点击刷新 Magento

由于我们正在开发扩展,并且将频繁更改配置和扩展代码,最好禁用缓存。按照以下步骤进行:

  1. 导航到 Magento 后端。

  2. 打开系统 | 缓存管理

  3. 选择所有缓存类型复选框。

  4. 操作下拉列表中选择禁用

  5. 点击提交按钮。你好,Magento

清除缓存后,我们可以通过进入系统 | 高级来确认我们的扩展是否已激活。

你好,Magento

现在 Magento 知道我们的模块了,但我们还没有告诉 Magento 我们的模块应该做什么;为此,我们需要设置模块配置。

XML 模块配置

模块配置涉及两个主要文件:config.xmlsystem.xml。除了这些模块配置,这些也存储在:

  • api.xml

  • adminhtml.xml

  • cache.xml

  • widget.xml

  • wsdl.xml

  • wsi.xml

  • convert.xml

在本章中,我们将只关注config.xml文件。让我们创建我们的基本文件,并按照以下步骤分解每个节点:

  1. 首先,在我们的模块etc/directory下创建config.xml文件。

  2. 现在,将以下代码复制到config.xml文件中(文件位置为app/code/local/Mdg/Giftregistry/etc/config.xml):

<?xml version="1.0"?>
<config>
    <modules>
        <Mdg_Giftregistry>
            <version>0.1.0</version>
        </Mdg_Giftregistry>
    </modules>
    <global>
        <models>
            <mdg_giftregistry>
                <class>Mdg_Giftregistry_Model</class>
            </mdg_giftregistry>
        </models>
        <blocks>
            <mdg_giftregistry>
                <class>Mdg_Giftregistry_Block</class>
            </mdg_giftregistry>
        </blocks>
        <helpers>
            <mdg_giftregistry>
                <class>Mdg_Giftregistry_Helper</class>
            </mdg_giftregistry>
        </helpers>
        <resources>
            <mdg_giftregistry_setup>
                <setup>
                    <module>Mdg_Giftregistry</module>
                </setup>
            </mdg_giftregistry_setup>
        </resources>
    </global>
</config>

所有模块配置都包含在<config>节点内。在这个节点内,我们有<global><modules>节点。

<modules>节点只用于指定当前模块版本,稍后用于决定运行哪些安装和升级文件。

有三个主要的配置节点通常用于指定配置范围:

  • <global>

  • <adminhtml>

  • <frontend>

现在,我们将在<global>范围内工作。这将使任何配置对 Magento 前端和后端都可用。在<global>节点下,我们有以下节点:

  • <models>

  • <blocks>

  • <helpers>

  • <resources>

正如我们所看到的,每个节点都遵循相同的配置模式:

<context>
   <factory_alias>
       <class>NameSpace_ModuleName_ClassType</class>
   </factory_alias>
</context>

Magento 类工厂使用的每个节点都实例化我们的自定义对象。<factory_alias>节点是我们扩展配置的关键部分。<factory_alias>节点由工厂方法使用,例如Mage::getModel()Mage::getHelper()

注意,我们没有定义每个特定的 Model、Block 或 Helper,只是 Magento 工厂可以找到它们的路径。Magento 的命名约定允许我们在这些文件夹下有任何文件夹结构,Magento 将聪明地加载适当的类名。

注意

在 Magento 中,类名和目录结构是一样的。

例如,我们可以在app/code/local/Mdg/Giftregistry/Models/Folder1/Folder2/Folder3下创建一个新的模型类,用于从这个类实例化对象的工厂名称将是:

Mage::getModel('mdg_giftregistry/folder1_folder2_folder3_classname');

让我们创建我们的第一个模型,或者更具体地说,一个帮助类。帮助类用于包含用于执行常见任务的实用方法,并且可以在不同的类之间共享。

让我们继续创建一个空的helper类;我们将在本章后面添加帮助逻辑。

文件位置为app/code/loca/Mdg/Giftregistry/Helper/Data.php。参考以下代码:

<?php
class Mdg_Giftregistry_Helper_Data extends Mage_Core_Helper_Abstract {

}
?>

我们命名帮助类为Data可能看起来有点奇怪,但这实际上是 Magento 的标准,每个模块都有一个名为Data的默认helper类。helper类的另一个有趣之处是,我们可以只传递<factory_alias>节点而不需要特定类名到helper工厂方法,这将默认为Data帮助类。

因此,如果我们想要实例化我们的默认helper类,我们只需要执行以下操作:

Mage::helper('mdg_registry');

模型和保存数据

在直接创建我们的模型之前,我们需要清楚地定义我们将要构建的模型类型和数量。因此,让我们回顾一下我们的示例场景。对于我们的礼品注册,似乎我们将需要两种不同的模型:

  • 注册模型:此模型用于存储礼品注册信息,例如礼品注册类型、地址和接收者信息

  • 注册项目:此模型用于存储每个礼品注册项目的信息(请求的数量,购买的数量,product_id

虽然这种方法是正确的,但它并不满足我们示例场景的所有要求。通过将所有注册信息存储到单个表中,我们无法添加更多的注册类型而不修改代码。

因此,在这种情况下,我们将希望将我们的数据分解成多个表:

  • 注册实体:此表用于存储礼品注册和事件信息

  • 注册类型:通过将礼品注册类型存储到单独的表中,我们可以添加或删除事件类型

  • 注册项目:此表用于存储每个礼品注册项目的信息(请求的数量,购买的数量,product_id

现在我们已经定义了我们的数据结构,我们可以开始构建相应的模型,以便访问和操作我们的数据。

创建模型

让我们开始创建礼品注册类型模型,用于管理注册类型(婚礼、生日、宝宝洗澡等)。要做到这一点,请按照以下步骤:

  1. 转到我们模块目录中的Model文件夹。

  2. 创建一个名为Type.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Type.php):

<?php
class Mdg_Giftregistry_Model_Type extends Mage_Core_Model_Abstract
{
    public function __construct()
    {
        $this->_init('mdg_giftregistry/type');
        parent::_construct();
    }
}

我们还需要创建一个资源类;每个 Magento 数据模型都有自己的资源类。还需要澄清的是,只有直接处理数据的模型,无论是简单数据模型还是 EAV 模型,都需要一个resource类。要做到这一点,请按照以下步骤:

  1. 转到我们模块目录中的Model文件夹。

  2. Model下创建一个名为Mysql4的新文件夹。

  3. 创建一个名为Type.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Mysql4/Type.php):

<?php
class Mdg_Giftregistry_Model_Mysql4_Type extends Mage_Core_Model_Mysql4_Abstract
{
    public function _construct()
    {
        $this->_init('mdg_giftregistry/type', 'type_id');
    }
}

最后,我们还需要一个collection类来检索所有可用的事件类型:

  1. 转到我们模块目录中的Model文件夹。

  2. 创建一个名为Type.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Mysql4/Type/Collection.php):

<?php
class Mdg_Giftregistry_Model_Mysql4_Type_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{
    public function _construct()
    {
        $this->_init('mdg_giftregistry/type');
        parent::_construct();
    }
}

通过创建一个处理礼品注册项目的模型来做同样的事情。此模型将保存注册项目的所有相关产品信息。要做到这一点,请按照以下步骤:

  1. 转到我们模块目录中的Model文件夹。

  2. 创建一个名为Item.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Item.php):

<?php
class Mdg_Giftregistry_Model_Item extends Mage_Core_Model_Abstract
{
    public function __construct()
    {
        $this->_init('mdg_giftregistry/item');
        parent::_construct();
    }
}

让我们继续创建资源类:

  1. 导航到我们模块目录中的Model文件夹。

  2. 打开Mysql4文件夹

  3. 创建一个名为Item.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Mysql4/Item.php):

<?php
class Mdg_Giftregistry_Model_Mysql4_Item extends Mage_Core_Model_Mysql4_Abstract
{
    public function _construct()
    {
        $this->_init('mdg_giftregistry/item', 'item_id');
    }
}

最后,让我们创建相应的collection类:

  1. 导航到我们模块目录中的Model文件夹。

  2. 创建一个名为Collection.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Mysql4/Item/Collection.php):

<?php
class Mdg_Giftregistry_Model_Mysql4_Item_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{
    public function _construct()
    {
        $this->_init('mdg_giftregistry/item');
        parent::_construct();
    }
}

我们的下一步将是创建我们的注册表实体;这是我们注册表的核心,也是将所有内容联系在一起的模型。要做到这一点,请按照以下步骤进行:

  1. 导航到我们模块目录中的Model文件夹。

  2. 创建一个名为Entity.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Entity.php):

<?php
class Mdg_Giftregistry_Model_Entity extends Mage_Core_Model_Abstract
{
    public function __construct()
    {
        $this->_init('mdg_giftregistry/entity');
        parent::_construct();
    }
}

让我们继续创建resource类:

  1. 导航到我们模块目录中的Model文件夹。

  2. 打开Mysql4文件夹。

  3. 创建一个名为Entity.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Mysql4/Entity.php):

<?php
class Mdg_Giftregistry_Model_Mysql4_Entity extends Mage_Core_Model_Mysql4_Abstract
{
    public function _construct()
    {
        $this->_init('mdg_giftregistry/entity', 'entity_id');
    }
}

最后,让我们创建相应的collection类:

  1. 导航到我们模块目录中的Model文件夹。

  2. 创建一个名为Collection.php的新文件,并将以下内容复制到文件中(文件位置为app/code/local/Mdg/Giftregistry/Model/Mysql4/Entity/Collection.php):

<?php
class Mdg_Giftregistry_Model_Mysql4_Entity_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{
    public function _construct()
    {
        $this->_init('mdg_giftregistry/entity');
        parent::_construct();
    }
}

到目前为止,我们除了盲目地复制代码并将模型类添加到我们的模块中之外,还没有做任何事情。让我们使用交互式 Magento 控制台IMC)测试我们新创建的模型。

让我们启动 IMC,并通过在 Magento 安装的根目录中运行以下命令来尝试新模型:

**$ php shell/imc.php**

以下代码假定您正在运行带有示例数据的 Magento 测试安装,如果您正在使用 Vagrant 框安装,您已经拥有所有预加载的数据:

  1. 我们将从加载客户端模型开始:
**magento > $customer = Mage::getModel('customer/customer')->load(1);**

  1. 接下来,我们需要实例化一个新的注册表对象:
**magento > $registry = Mage::getModel('mdg_giftregistry/entity');**

  1. 所有 Magento 模型中的一个方便的函数是“getData()”函数,它返回所有对象属性的数组。让我们在 a,注册表和客户对象上运行此函数并比较输出:
**magento > print_r($customer->getData());**
**magento > print_r($registry->getData());**

  1. 正如我们注意到的,客户端为我们的 John Doe 示例记录设置了所有数据,而注册表对象返回完全空的$regiarray。通过运行以下代码来更改这一点:
**magento > $registry->setCustomerId($customer->getId());**
**magento > $registry->setTypeId(1);**
**magento > $registry->setWebsiteId(1);**
**magento > $registry->setEventDate('2012-12-12');**
**magento > $registry->setEventCountry('CA');**
**magento > $registry->setEventLocation('Toronto');**

  1. 现在让我们尝试再次打印注册表数据:
**magento > print_r($registry->getData());**

  1. 最后,为了使我们的更改永久生效,我们需要调用模型的save函数:
**magento > $registry->save();**

哎呀!在保存产品时出现了问题;我们在控制台中得到了以下错误:

Fatal error: Call to a member function beginTransaction() on a non-object in …/app/code/core/Mage/Core/Model/Abstract.php on line 313

发生了什么?被调用的save()函数是父类Mage_Core_Model_Mysql4_Abstract的一部分,它反过来调用抽象类save()函数,但我们缺少config.xml文件的一个关键部分。

为了让 Magento 正确识别要使用的资源类,我们需要为每个实体指定资源模型类和匹配的表。让我们按照以下步骤更新我们的配置文件:

  1. 导航到扩展etc/文件夹。

  2. 打开config.xml

  3. 使用以下代码更新<model>节点(文件位置为app/code/local/Mdg/Giftregistry/Model/Entity.php):

…
<models>
    <mdg_giftregistry>
        <class>Mdg_Giftregistry_Model</class>
        <resourceModel>mdg_giftregistry_mysql4</resourceModel>
    </mdg_giftregistry>
    <mdg_giftregistry_mysql4>
        <class>Mdg_Giftregistry_Model_Mysql4</class>
        <entities>
            <entity>
                <table>mdg_giftregistry_entity</table>
            </entity>
            <item>
                <table>mdg_giftregistry_item</table>
            </item>
            <type>
                <table>mdg_giftregistry_type</table>
            </type>
        </entities>
    </mdg_giftregistry_mysql4>
</models>
…

现在,在我们实际将产品保存到数据库之前,我们必须首先创建我们的数据库表;接下来,我们将学习如何使用设置资源来创建我们的表结构并设置我们的默认数据。

设置资源

现在我们已经创建了我们的模型代码,我们需要创建设置资源以便能够保存它们。设置资源将负责创建相应的数据库表。现在,我们可以直接使用纯 SQL 或工具如 PHPMyAdmin 来创建所有表,但这不是标准做法,通常情况下,我们不应直接修改 Magento 数据库。

为了实现这一点,我们将执行以下操作:

  • 在我们的配置文件上定义一个设置资源

  • 创建一个资源类

  • 创建一个安装脚本

  • 创建一个数据脚本

  • 创建一个升级脚本

定义一个设置资源

当我们首次定义配置文件时,我们定义了一个<resources>节点:

文件位置为app/code/local/Mdg/Giftregistry/etc/config.xml。参考以下代码片段:

…
<resources>
    <mdg_giftregistry_setup>
        <setup>
            <module>Mdg_Giftregistry</module>
        </setup>
    </mdg_giftregistry_setup>
</resources>
…

首先要注意的是,<mdg_giftregistry_setup>节点用作我们设置资源的唯一标识符;标准命名约定是<modulename_setup>,虽然不是必需的,但强烈建议遵循此命名约定。

我们还需要对<setup>节点进行更改,添加一个额外的 class 节点,并读取和写入连接:

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

…
<resources>
    <mdg_giftregistry_setup>
        <setup>
            <module>Mdg_Giftregistry</module>
            <class>Mdg_Giftregistry_Model_Resource_Setup</class>
        </setup>
        <connection>
            <use>core_setup</use>
        </connection>
    </mdg_giftregistry_setup>
    <mdg_giftregistry_write>
        <connection>
            <use>core_write</use>
        </connection>
    </mdg_giftregistry_write>
    <mdg_giftregistry_read>
        <connection>
            <use>core_read</use>
        </connection>
    </mdg_giftregistry_read>
</resources>
…

对于基本的设置脚本,不需要创建此设置资源,可以使用Mage_Core_Model_Resource_Setup,但通过创建自己的设置类,我们可以提前规划并为未来的改进提供更大的灵活性。接下来,我们将在文件位置下创建设置资源类,否则将会出现 Magento 找不到设置资源类的错误。

在文件位置app/code/local/Mdg/Giftregistry/Model/Resource/Setup.php下创建设置资源类。参考以下代码片段:

<?php
class Mdg_Giftregistry_Model_Resource_Setup extends Mage_Core_Model_Resource_Setup
{

}

目前,我们不需要对设置资源类做其他操作。

创建安装脚本

我们的下一步将是创建一个安装脚本。此脚本包含创建表的所有 SQL 代码,并在初始化模块时运行。首先,让我们再次快速查看我们的config.xml文件。如果我们记得,我们在<global>节点之前定义的第一个节点是<modules>节点。

文件位置为app/code/local/Mdg/Giftregistry/etc/config.xml。参考以下代码片段:

<modules>
  <Mdg_Giftregistry>
     <version>0.1.0</version>
   </Mdg_Giftregistry>
</modules>

正如我们之前提到的,此节点在所有 Magento 模块上都是必需的,并用于识别我们模块的当前安装版本。Magento 使用此版本号来确定是否以及要运行哪些安装和升级脚本。

注意

关于命名约定:自 Magento 1.6 以来,安装脚本的命名约定已更改。最初使用了Mysql4-install-x.x.x.php的命名约定,目前已被弃用但仍受支持。

自 Magento 1.6 以来,安装脚本的命名约定已更改,现在开发人员可以使用三种不同的脚本类型:

  • 安装:当模块首次安装且在core_resource表中不存在记录时使用此脚本

  • 升级:如果core_resource表中的版本低于config.xml文件中的版本,则使用此脚本

  • 数据:此脚本将在匹配版本的安装/升级脚本之后运行,并用于向表中填充所需数据

注意

数据脚本是在 Magento 1.6 中引入的,并存储在直接位于我们模块根目录下的 data/目录中。它们遵循与安装和升级脚本略有不同的约定,通过添加前缀。

让我们继续在我们的安装脚本下创建我们的注册表实体表。

文件位置为app/code/local/Mdg/Giftregistry/sql/mdg_giftregistry_setup/install-0.1.0.php。参考以下代码:

<?php

$installer = $this;
$installer->startSetup();
// Create the mdg_giftregistry/registry table
$tableName = $installer->getTable('mdg_giftregistry/entity');
// Check if the table already exists
if ($installer->getConnection()->isTableExists($tableName) != true) {
    $table = $installer->getConnection()
        ->newTable($tableName)
        ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null,
            array(
                'identity' => true,
                'unsigned' => true,
                'nullable' => false,
                'primary' => true,
            ),
            'Entity Id'
        )
        ->addColumn('customer_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null,
            array(
                'unsigned' => true,
                'nullable' => false,
                'default' => '0',
            ),
            'Customer Id'
        )
        ->addColumn('type_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null,
            array(
                'unsigned' => true,
                'nullable' => false,
                'default' => '0',
            ),
            'Type Id'
        )
        ->addColumn('website_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null,
            array(
                'unsigned' => true,
                'nullable' => false,
                'default' => '0',
            ),
            'Website Id'
        )
        ->addColumn('event_name', Varien_Db_Ddl_Table::TYPE_TEXT, 255,
            array(),
            'Event Name'
        )
        ->addColumn('event_date', Varien_Db_Ddl_Table::TYPE_DATE, null,
            array(),
            'Event Date'
        )
        ->addColumn('event_country', Varien_Db_Ddl_Table::TYPE_TEXT, 3,
            array(),
            'Event Country'
        )
        ->addColumn('event_location', Varien_Db_Ddl_Table::TYPE_TEXT, 255,
            array(),
            'Event Location'
        )
        ->addColumn('created_at', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null,
            array(
                'nullable' => false,
            ),
            'Created At')
        ->addIndex($installer->getIdxName('mdg_giftregistry/entity', array('customer_id')),
            array('customer_id'))
        ->addIndex($installer->getIdxName('mdg_giftregistry/entity', array('website_id')),
            array('website_id'))
        ->addIndex($installer->getIdxName('mdg_giftregistry/entity', array('type_id')),
            array('type_id'))
        ->addForeignKey(
            $installer->getFkName(
                'mdg_giftregistry/entity',
                'customer_id',
                'customer/entity',
                'entity_id'
            ),
            'customer_id', $installer->getTable('customer/entity'), 'entity_id',
            Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
        ->addForeignKey(
            $installer->getFkName(
                'mdg_giftregistry/entity',
                'website_id',
                'core/website',
                'website_id'
            ),
            'website_id', $installer->getTable('core/website'), 'website_id',
            Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
        ->addForeignKey(
            $installer->getFkName(
                'mdg_giftregistry/entity',
                'type_id',
                'mdg_giftregistry/type',
                'type_id'
            ),
            'type_id', $installer->getTable('mdg_giftregistry/type'), 'type_id',
            Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE);

    $installer->getConnection()->createTable($table);
}
$installer->endSetup();

注意

请注意,由于空间限制,我们没有添加完整的安装脚本;您仍然需要为项目和类型表添加安装程序代码。完整的安装文件和代码文件可以直接从github.com/amacgregor/mdg_giftreg下载。

现在这可能看起来像是很多代码,但它只是创建了一个表的输出,为了理解它,让我们分解一下,看看这段代码到底在做什么。

首先要注意的是,即使我们正在创建和设置数据库表,我们并没有编写任何 SQL 代码。Magento ORM 提供了一个带有数据库的适配器。所有安装、升级和数据脚本都继承自Mage_Core_Model_Resource_Setup。让我们分解一下我们安装脚本上使用的每个函数。

脚本的前三行实例化了resource_setup模型和连接。脚本的其余部分涉及设置一个新的表实例,并在其上调用以下函数:

  • addColumn:此函数用于定义每个表列,并接受以下五个参数:

  • name:这是列的名称

  • type:这是数据存储类型(intvarchartext等)

  • size:这是列的长度

  • options:这是用于数据存储的附加选项数组

  • Comment:这是列的描述

  • addIndex:此函数用于定义特定表的索引,并接受以下三个参数:

  • index:这是一个索引名称

  • columns:这可以是一个包含单个列名的字符串,也可以是包含多个列名的数组

  • options:这是用于数据存储的附加选项数组

  • addForeginKey:此函数用于定义外键关系,并接受以下六个参数:

  • fkName:这是一个外键名称

  • column:这是一个外键列名

  • refTable:这是一个参考表名。

  • refColumn:这是一个参考表列名

  • onDelete:这是在删除行时要执行的操作

  • onUpdate:这是在更新行时要执行的操作

创建我们每个表的代码基本上由这三个函数组成,在每个表定义之后,执行以下代码:

$installer->getConnection()->createTable($table);

这告诉我们的数据库适配器将我们的代码转换为 SQL 并运行它针对数据库。有一件重要的事情要注意;那就是,代码不是提供或硬编码数据库名称,而是调用以下代码:

$installer->getTable('mdg_giftregistry/entity')

这是我们在config.xml文件中之前定义的表别名。要完成我们的安装程序,我们需要为我们的每个实体创建一个newTable实例。

注意

这里有一个挑战给你。使用您的安装程序脚本创建缺失的表。要查看完整代码和完整分解的答案,请访问www.magedevguide.com/challenge/chapter4/1

数据脚本可用于填充我们的表;在我们的情况下,这将有助于设置一些基本事件类型。

首先,我们需要在data文件夹下创建一个数据安装脚本;正如我们之前提到的,结构与 SQL 文件夹非常相似,唯一的区别是我们将数据前缀附加到匹配的安装/升级脚本上。要这样做,请按照以下步骤进行:

  1. 导航到模块数据文件夹app/code/local/Mdg/Giftregistry/data/

  2. 基于资源创建一个新目录;在这种情况下,它将是mdg_giftregistry_setup

  3. mdg_giftregistry_setup下,创建一个名为data-install-0.1.0.php的文件。

  4. 将以下代码复制到data-install-0.1.0.php文件中(文件位置为app/code/local/Mdg/Giftregistry/data/mdg_giftregistry_setup/data-install-0.1.0.php):

<?php
$registryTypes = array(
    array(
        'code' => 'baby_shower',
        'name' => 'Baby Shower',
        'description' => 'Baby Shower',
        'store_id' => Mage_Core_Model_App::ADMIN_STORE_ID,
        'is_active' => 1,
    ),
    array(
        'code' => 'wedding',
        'name' => 'Wedding',
        'description' => 'Wedding',
        'store_id' => Mage_Core_Model_App::ADMIN_STORE_ID,
        'is_active' => 1,
    ),
    array(
        'code' => 'birthday',
        'name' => 'Birthday',
        'description' => 'Birthday',
        'store_id' => Mage_Core_Model_App::ADMIN_STORE_ID,
        'is_active' => 1,
    ),
);

foreach ($registryTypes as $data) {
    Mage::getModel('mdg_giftregistry/type')
        ->addData($data)
        ->setStoreId($data['store_id'])
        ->save();
}

让我们仔细看一下data-install-0.1.0.php脚本上的最后一个条件块:

foreach ($registryTypes as $data) {
    Mage::getModel('mdg_giftregistry/type')
        ->addData($data)
        ->setStoreId($data['store_id'])
        ->save();
}

现在,如果我们刷新我们的 Magento 安装,错误应该消失,如果我们仔细看mdg_giftregistry_type表,我们应该看到以下记录:

创建安装程序脚本

正如我们之前学到的,安装和数据脚本将在我们的模块第一次安装时运行。但在 Magento 已经认为我们的模块已安装的情况下会发生什么呢?

由于模块已经在core_resource表中注册,安装脚本将不会再次运行,除非 Magento 检测到扩展的版本更改。这对于处理扩展的多个发布版本非常有用,但对于开发目的并不是很实用。

幸运的是,很容易欺骗 Magento 再次运行我们的扩展安装脚本。我们只需要删除core_resource表中的相应条目。要这样做,请按照以下步骤操作:

  1. 打开你的 MySQL 控制台;如果你正在使用我们的 Vagrant 盒子,你可以通过输入mysql来打开它。

  2. 一旦我们在 MySQL shell 中,我们需要选择我们的工作数据库;在我们的情况下,它是ce1702_magento

  3. 最后,我们需要使用以下查询进入core_resource表:

**mysql> DELETE FROM `core_resource` WHERE `code` =  'mdg_giftregistry_setup'**

我们学到了什么?

到目前为止,我们已经学会了:

  • 为我们的 Magento 模块创建基本目录结构

  • 配置文件的角色和重要性

  • 创建模型和设置资源

  • 安装、升级和数据脚本的角色和顺序

注意

这是一个挑战给你。尝试通过将实体转换为 EAV 模型来进一步改进我们模块的模型结构;这将需要修改安装脚本和资源模型。要查看完整的代码和详细的分解,请访问www.magedevguide.com/challenge/chapter4/2

设置我们的路由

现在我们能够通过使用我们的模型保存和操作数据,我们需要为客户提供一种与实际礼品注册互动的方式;这是我们的第一步。我们需要在前端创建有效的路由或 URL。

就像 Magento 中的许多事情一样,这由配置文件控制。路由将把 URL 转换为有效的控制器、动作和方法。

打开我们的config.xml文件。文件位置是app/code/local/Mdg/Giftregistry/etc/config.xml。参考以下代码:

<config>
…
    <frontend>
        <routers>
            <mdg_giftregistry>
                <use>standard</use>
                <args>
                    <module>Mdg_Giftregistry</module>
                    <frontName>giftregistry</frontName>
                </args>
            </mdg_giftregistry>
        </routers>
    </frontend>
…
</config>

让我们分解一下我们刚刚添加的配置代码:

  • <frontend>:以前,我们将所有配置添加到全局范围内;由于我们希望我们的路由只在前端可用,我们需要在前端范围内声明我们的自定义路由

  • <routers>:这是包含我们自定义路由配置的容器标记

  • <mdg_giftregistry>:此标记的命名约定是匹配模块名称,并且是我们路由的唯一标识符

  • <frontName>:正如我们在第二章中学到的,Magento 开发人员基础知识,Magento 将 URL 分解为http://localhost.com /frontName/actionControllerName/actionMethod/

一旦我们定义了我们的路由配置,我们需要创建一个实际的控制器来处理所有传入的请求。

索引控制器

我们的第一步是在我们的模块控制器目录下创建IndexController。如果没有指定控制器名称,Magento 将始终尝试加载IndexController

文件位置是app/code/local/Mdg/Giftregistry/controllers/Index.php。参考以下代码:

<?php 
class Mdg_Giftregistry_IndexController extends Mage_Core_Controller_Front_Action
{
     public function indexAction()
  {
    echo 'This is our test controller';
     }
}

创建我们的文件后,如果我们转到http://localhost.com/giftregistry/index/index,我们应该看到一个空白页面,上面有一条消息,说这是我们的测试控制器。这是因为我们没有正确加载我们的客户控制器的布局。文件位置是app/code/local/Mdg/Giftregistry/controllers/IndexController.php。我们需要将我们的动作代码更改为:

<?php 
class Mdg_Giftregistry_IndexController extends Mage_Core_Controller_Front_Action
{
     public function indexAction()
  {
    $this->loadLayout();
    $this->renderLayout();
     }
}

在深入控制器动作内部发生的情况之前,让我们创建其余的控制器和相应的动作。

我们将需要一个控制器来处理客户的基本操作,以便他们能够创建、管理和删除他们的注册表。此外,我们还需要一个搜索控制器,以便家人和朋友可以找到匹配的礼品注册表,最后,我们还需要一个查看控制器来显示注册表的详细信息。

我们的第一步将是向索引控制器添加剩余的动作(文件位置为app/code/local/Mdg/Giftregistry/controllers/IndexController.php):

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

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

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

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

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

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

在我们开始向索引控制器添加所有逻辑之前,我们需要采取额外的步骤,以防止未登录的客户访问giftregistry功能。Magento 前端控制器已经非常有用,用于处理这一点;它被称为preDispatch()函数,在控制器中的任何其他动作之前执行。

打开您的IndexController.php并在类的开头添加以下代码。

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

<?php
class Mdg_Giftregistry_IndexController extends Mage_Core_Controller_Front_Action
{
    public function preDispatch()
    {
        parent::preDispatch();
        if (!Mage::getSingleton('customer/session')->authenticate($this)) {
            $this->getResponse()->setRedirect(Mage::helper('customer')->getLoginUrl());
            $this->setFlag('', self::FLAG_NO_DISPATCH, true);
        }
    }
…

现在,如果我们尝试加载http://localhost.com/giftregistry/index/index,我们将被重定向到登录页面,除非我们已登录到前端。

我们的下一步将是向每个控制器动作添加所有逻辑,以便控制器可以正确处理创建、更新和删除。

索引、新建和编辑动作主要用于加载和呈现布局,因此在控制器newPostAction()editPostAction()deleteAction()中涉及的逻辑不多;另一方面,它们处理了更繁重和更复杂的逻辑。

让我们开始newPostAction()。这个动作用于处理从newAction()表单接收到的数据。为此,请按照以下步骤操作:

  1. 打开IndexController.php

  2. 我们将添加到动作的第一件事是一个if语句,以检查请求是否是 post 请求,我们可以使用以下代码检索到:

$this->getRequest()->isPost()
  1. 除此之外,我们还希望检查请求是否有实际数据;为此,我们可以使用以下代码:
$this->getRequest()->getParams()

一旦我们验证了请求是一个合适的请求,并且我们正在接收数据,我们需要实际创建礼品注册。为此,我们将通过以下步骤在我们的注册表模型中添加一个新函数:

  1. 打开注册表实体模型。

  2. 创建一个名为updateRegistryData()的新函数,并确保函数接受两个参数:$customer$data

  3. 文件位置为app/code/local/Mdg/Giftregistry/Model/Entity.php。在此函数内添加以下代码:

public function updateRegistryData(Mage_Customer_Model_Customer $customer, $data)
{
    try{
        if(!empty($data))
        {
            $this->setCustomerId($customer->getId());
            $this->setWebsiteId($customer->getWebsiteId());
            $this->setTypeId($data['type_id']);
            $this->setEventName($data['event_name']);
            $this->setEventDate($data['event_date']);
            $this->setEventCountry($data['event_country']);
            $this->setEventLocation($data['event_location']);
        }else{
            throw new Exception("Error Processing Request: Insufficient Data Provided");
        }
    } catch (Exception $e){
        Mage::logException($e);
    }
    return $this;
}

这个函数将通过将表单数据添加到注册表对象的当前实例来帮助我们,这意味着我们需要在我们的控制器内创建一个实例。让我们把我们的控制器代码放在一起:

文件位置为app/code/local/Mdg/Giftregistry/controllers/IndexController.php。参考以下代码片段:

public function newPostAction()
{
    try {
        $data = $this->getRequest()->getParams();
        $registry = Mage::getModel('mdg_giftregistry/entity');
        $customer = Mage::getSingleton('customer/session')->getCustomer();

        if($this->getRequest()->getPost() && !empty($data)) {
            $registry->updateRegistryData($customer, $data);
            $registry->save();
            $successMessage = Mage::helper('mdg_giftregistry')->__('Registry Successfully Created');
            Mage::getSingleton('core/session')->addSuccess($successMessage);
        }else{
            throw new Exception("Insufficient Data provided");
        }
    } catch (Mage_Core_Exception $e) {
        Mage::getSingleton('core/session')->addError($e->getMessage());
        $this->_redirect('*/*/');
    }
    $this->_redirect('*/*/');
}

我们已经创建了一个非常基本的控制器动作,它将处理注册表的创建并处理大部分可能的异常。

让我们继续创建editPostAction;这个动作与newPostAction非常相似。主要区别在于,在editPostAction的情况下,我们正在处理一个已经存在的注册表记录,因此在设置数据之前,我们需要添加一些验证。

文件位置为app/code/local/Mdg/Giftregistry/controllers/IndexController.php。让我们更仔细地看一下以下动作代码:

public function editPostAction()
{
    try {
        $data = $this->getRequest()->getParams();
        $registry = Mage::getModel('mdg_giftregistry/entity');
        $customer = Mage::getSingleton('customer/session')->getCustomer();

        if($this->getRequest()->getPosts() && !empty($data) )
        {
            $registry->load($data['registry_id']);
            if($registry){
                $registry->updateRegistryData($customer, $data);
                $registry->save();
                $successMessage =  Mage::helper('mdg_giftregistry')->__('Registry Successfully Saved');
                Mage::getSingleton('core/session')->addSuccess($successMessage);
            }else {
                throw new Exception("Invalid Registry Specified");
            }
        }else {
            throw new Exception("Insufficient Data provided");
        }
    } catch (Mage_Core_Exception $e) {
        Mage::getSingleton('core/session')->addError($e->getMessage());
        $this->_redirect('*/*/');
    }
    $this->_redirect('*/*/');
}

正如我们所看到的,这段代码与我们的newPostAction()控制器几乎相同,关键区别在于它在更新数据之前尝试加载现有的注册表。

注意

这里有一个挑战给你。由于editPostAction()newPostAction()之间的代码非常相似,尝试将两者合并为一个可以重复使用的单个 post 操作。要查看完整代码和完整分解的答案,请访问www.magedevguide.com/challenge/chapter4/3

要完成IndexController,我们需要添加一个允许我们删除特定注册记录的操作;为此,我们将使用deleteAction()

由于 Magento ORM 系统,这个过程非常简单,因为 Magento 模型继承了delete()函数,正如其名称所示,它将简单地删除该特定模型实例。

文件位置为app/code/local/Mdg/Giftregistry/controllers/IndexController.php。在IndexController中,添加以下代码:

public function deleteAction()
{
    try {
        $registryId = $this->getRequest()->getParam('registry_id');
        if($registryId && $this->getRequest()->getPost()){
            if($registry = Mage::getModel('mdg_giftregistry/entity')->load($registryId))
            {
                $registry->delete();
                $successMessage =  Mage::helper('mdg_giftregistry')->__('Gift registry has been succesfully deleted.');
                Mage::getSingleton('core/session')->addSuccess($successMessage);
            }else{
                throw new Exception("There was a problem deleting the registry");
            }
        }
    } catch (Exception $e) {
        Mage::getSingleton('core/session')->addError($e->getMessage());
        $this->_redirect('*/*/');
    }
}

我们删除控制器中要注意的重要操作如下:

  1. 我们检查我们的操作是否是正确类型的请求。

  2. 我们实例化注册对象并验证它是否有效。

  3. 最后,我们在注册实例上调用delete()函数。

你可能已经注意到,由于我们犯了一个严重的遗漏,现在没有办法将实际产品添加到购物车中。

我们现在将跳过这个特定的操作,并且在我们更好地理解所涉及的块和布局以及它与我们的自定义控制器如何交互之后再创建它。

搜索控制器

现在我们有一个可以处理大部分修改实际注册的逻辑的工作IndexController,我们将创建的下一个控制器是SearchController。要这样做,请按照以下步骤进行:

  1. 在 controllers 目录下创建一个名为SearchController的新控制器。

  2. 文件位置为app/code/local/Mdg/Giftregistry/controllers/SearchController.php。将以下代码复制到搜索控制器中:

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

我们现在将暂时留下indexAction,并且将专注于resultsAction()中涉及的逻辑,它将获取搜索参数并加载注册集合。

文件位置为app/code/local/Mdg/Giftregistry/controllers/SearchController.php。让我们看一下完整的操作代码并分解它:

public function resultsAction()
{
    $this->loadLayout();
    if ($searchParams = $this->getRequest()->getParam('search_params')) {
        $results = Mage::getModel('mdg_giftregistry/entity')->getCollection();
        if($searchParams['type']){
            $results->addFieldToFilter('type_id', $searchParams['type']);
        }
        if($searchParams['date']){
            $results->addFieldToFilter('event_date', $searchParams['date']);
        }
        if($searchParams['location']){
            $results->addFieldToFilter('event_location', $searchParams['location']);
        }
        $this->getLayout()->getBlock('mdg_giftregistry.search.results')
            ->setResults($results);
    }
    $this->renderLayout();
    return $this;
}

与以前的操作一样,我们获取请求参数,但在这种特殊情况下,我们加载了一个礼品注册集合,并为每个可用字段应用字段过滤器。一个值得注意的是,这是我们第一次直接从 Magento 控制器与布局交互。

$this->getLayout()->getBlock('mdg_giftregistry.search.results')
        ->setResults($results);

我们在这里做的是使加载的注册集合可用于特定的块实例。

视图控制器

最后,我们需要一个控制器,允许显示注册详细信息,无论客户是否已登录。请按照以下步骤进行:

  1. 在 controllers 目录下创建一个名为ViewController的新控制器。

  2. 打开我们刚创建的控制器,并参考以下占位符代码(文件位置为app/code/local/Mdg/Giftregistry/controllers/ViewController.php):

<?php
class Mdg_Giftregistry_ViewController extends Mage_Core_Controller_Front_Action
{
    public function viewAction()
    {
        $registryId = $this->getRequest()->getParam('registry_id');
        if($registryId){
            $entity = Mage::getModel('mdg_giftregistry/entity');
            if($entity->load($registryId))
            {
                Mage::register('loaded_registry', $entity);
                $this->loadLayout();
                $this->_initLayoutMessages('customer/session');
                $this->renderLayout();
                return $this;
            } else {
                $this->_forward('noroute');
                return $this;
            }
        }
    }
}

因此,我们在这里使用了一个新的函数Mage::register(),它设置了一个全局变量,我们可以在应用程序流程中的任何方法中稍后检索。这个函数是 Magento Registry 模式的一部分,由以下三个函数组成:

  • Mage::register(): 这个函数用于设置全局变量

  • Mage::unregister(): 这个函数用于取消设置全局变量

  • Mage::registry(): 这个函数用于检索全局变量

在这种情况下,我们使用注册功能来在应用程序流程的更后面以及我们接下来将要创建的视图块中提供对注册实体的访问。

块和布局

正如我们在第二章中所学到的,Magento 开发人员的基础知识,Magento 将其视图层分为块、模板和布局文件。块是处理逻辑部分的对象。模板是phtml文件,是 HTML 和 PHP 代码的混合。布局文件是控制块位置的 XML 文件。

每个模块都有自己的布局文件,负责更新该特定模块的布局。我们需要按照以下步骤开始为我们的模块创建一个布局文件:

  1. 导航至app/design/frontend/base/default/layout/

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

  3. 添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<layout version="0.1.0">
  <mdg_giftregistry_index_index>
  </mdg_giftregistry_index_index>  

  <mdg_giftregistry_index_new>
  </mdg_giftregistry_index_new>

  <mdg_giftregistry_index_edit>
  </mdg_giftregistry_index_edit>

  <mdg_giftregistry_view_view>
  </mdg_giftregistry_view_view>

  <mdg_giftregistry_search_index>
  </mdg_giftregistry_search_index>

  <mdg_giftregistry_search_results>
  </mdg_giftregistry_search_results>
</layout>

注意

请注意,通过将我们的模板和布局添加到 base/default 主题,我们将使我们的模板和布局对所有商店和主题可用。

如果我们仔细看刚刚粘贴的 XML,我们可以看到我们有一个默认的<xml>标签和几组其他标签。正如我们之前提到的,在 Magento 中,路由由前端名称、控制器和操作组成。

布局文件中的每个 XML 标签代表我们的控制器和操作之一;例如,<giftregistry_index_index>将控制我们的IndexController操作的布局;Magento 为每个页面分配一个唯一的句柄。

为了让 Magento 识别我们的布局文件,我们需要按照以下步骤在config.xml文件中声明布局文件:

  1. 导航至extension etc/文件夹。

  2. 打开config.xml

  3. <frontend>节点内添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<frontend>
   <layout>
       <updates>
           <mdg_giftregistry module="mdg_giftregistry">
               <file>mdg_giftregistry.xml</file>
           </mdg_giftregistry>
       </updates>
   </layout>
   …
</frontend>

IndexController 块和视图

与之前一样,我们将从构建索引控制器开始。让我们定义每个操作需要定义的模板和块:

  • 索引:这是当前客户可用注册表的列表

  • :这提供了一个捕获注册表信息的新表单

  • 编辑:这会加载特定的注册表数据并将其加载到表单中

对于索引操作,我们需要创建一个名为List.php的新块。让我们按照以下步骤开始创建注册表列表块:

  1. 导航至app/code/local/Mdg/Giftregistry/Block/

  2. 创建一个名为List.php的文件。

  3. 复制以下代码(文件位置为app/code/local/Mdg/Giftregistry/Block/List.php)。

<?php
class Mdg_Giftregistry_Block_list extends Mage_Core_Block_Template
{
    public function getCustomerRegistries()
    {
        $collection = null;
        $currentCustomer = Mage::getSingleton('customer/session')->getCustomer();
        if($currentCustomer)
        {
            $collection = Mage::getModel('mdg_giftregistry/entity')->getCollection()
                ->addFieldToFilter('customer_id', $currentCustomer->getId());
        }
        return $collection;
    }
}

前面的代码声明了我们将在IndexController中使用的列表块。块声明了getCustomerRegistries()方法,该方法将检查当前客户并尝试基于该客户检索注册表集合。

现在我们创建了一个新的块,我们需要将其添加到我们的布局 XML 文件中:

  1. 打开mdg_giftregistry.xml

  2. <mdg_gifregistry_index_index>内添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<reference name="content">
    <block name="giftregistry.list" type="mdg_giftregistry/list" template="mdg/list.phtml" as="giftregistry_list"/>
</reference>

在布局中,我们声明了我们的块;在该声明内,我们设置了块名称、模板和类型。如果我们现在尝试加载索引控制器页面,由于我们还没有创建我们的模板文件,我们应该会看到有关缺少模板的错误。

让我们创建模板文件:

  1. 导航至design/frontend/base/default/template/

  2. 创建mdg/文件夹。

  3. 在该文件夹内,创建一个名为list.phtml的文件(文件位置为app/design/frontend/base/default/template/mdg/list.phtml):

<?php
$_collection = $this->getCustomerRegistries();
?>
<div class="customer-list">
    <ul>
        <?php foreach($_collection as $registry): ?>
            <li>
                <h3><?php echo $registry->getEventName(); ?></h3>
                <p><strong><?php echo $this->__('Event Date:') ?> <?php echo $registry->getEventDate(); ?></strong></p>
                <p><strong><?php echo $this->__('Event Location:') ?> <?php echo $registry->getEventLocation(); ?></strong></p>
                <a href="<?php echo $this->getUrl('giftregistry/view/view', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                    <?php echo $this->__('View Registry') ?>
                </a>
            </li>
        <?php endforeach; ?>
    </ul>
</div>

这是我们第一次生成.phtml文件。正如我们之前提到的,.phtml文件只是 PHP 和 HTML 代码的组合。

list.phtml文件中,我们要做的第一件事是通过调用getCustomerRegistries()方法加载一个集合;需要注意的一点是,我们实际上是在调用$this->getCustomerRegistries(),因为每个模板都分配给一个特定的块。

我们缺少一些重要的东西,如下所示:

  • 如果当前客户没有注册表,我们只会显示一个空的无序列表。

  • 没有链接来删除或编辑特定的注册表

检查集合是否有注册表的一个快速方法是调用count函数,并在集合实际为空时显示错误消息。

文件位置为app/design/frontend/base/default/template/mdg/list.phtml。参考以下代码:

<?php
    $_collection = $this->getCustomerRegistries();
?>
<div class="customer-list">
    <?php if(!$_collection->count()): ?>
        <h2><?php echo $this->__('You have no registries.') ?></h2>
        <a href="<?php echo $this->getUrl('giftregistry/index/new') ?>">
            <?php echo $this->__('Click Here to create a new Gift Registry') ?>
        </a>
    <?php else: ?>
        <ul>
            <?php foreach($_collection as $registry): ?>
                <li>
                    <h3><?php echo $registry->getEventName(); ?></h3>
                    <p><strong><?php echo $this->__('Event Date:') ?> <?php echo $registry->getEventDate(); ?></strong></p>
                    <p><strong><?php echo $this->__('Event Location:') ?> <?php echo $registry->getEventLocation(); ?></strong></p>
                    <a href="<?php echo $this->getUrl('giftregistry/view/view', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                        <?php echo $this->__('View Registry') ?>
                    </a>
                    <a href="<?php echo $this->getUrl('giftregistry/index/edit', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                        <?php echo $this->__('Edit Registry') ?>
                    </a>
                    <a href="<?php echo $this->getUrl('giftregistry/index/delete', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                        <?php echo $this->__('Delete Registry') ?>
                    </a>

                </li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</div>

我们添加了一个新的if语句来检查集合计数是否为空,并添加了一个链接到IndexController编辑操作。最后,如果没有要显示的注册表,我们将显示一个错误消息,链接到新操作。

让我们继续添加新操作的块和模板:

  1. 打开mdg_giftregistry.xml布局文件。

  2. <mdg_gifregistry_index_new>节点内添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<reference name="content">
    <block name="giftregistry.new" type="core/template" template="mdg/new.phtml" as="giftregistry_new"/>
</reference>

由于我们只是显示一个表单来将注册表信息发布到newPostAction(),我们只是创建一个带有包含表单代码的自定义模板文件的 core/template 块。我们的模板文件将如下所示。

文件位置为app/design/frontend/base/default/template/mdg/new.phtml

<?php $helper = Mage::helper('mdg_giftregistry'); ?>
<form action="<?php echo $this->getUrl('giftregistry/index/newPost/') ?>" method="post" id="form-validate">
    <fieldset>
        <?php echo $this->getBlockHtml('formkey')?>
        <ul class="form-list">
            <li>
                <label for="type_id"><?php echo $this->__('Event type') ?></label>
                <select name="type_id" id="type_id">
                    <?php foreach($helper->getEventTypes() as $type): ?>
                        <option id="<?php echo $type->getTypeId(); ?>" value="<?php echo $type->getCode(); ?>">
                            <?php echo $type->getName(); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </li>
            <li class="field">
                <input type="text" name="event_name" id="event_name" value="" title="Event Name"/>
                <label class="giftreg" for="event_name"><?php echo $this->__('Event Name') ?></label>
            </li>
            <li class="field">
                <input type="text" name="event_location" id="event_location" value="" title="Event Location"/>
                <label class="giftreg" for="event_location"><?php echo $this->__('Event Location') ?></label>
            </li>
            <li class="field">
                <input type="text" name="event_country" id="event_country" value="" title="Event Country"/>
                <label class="giftreg" for="event_country"><?php echo $this->__('Event Country') ?></label>
            </li>
        </ul>
        <div class="buttons-set">
            <button type="submit" title="Save" class="button">
                <span>
                    <span><?php echo $this->__('Save') ?></span>
                </span>
            </button>
        </div>
    </fieldset>
</form>
<script type="text/javascript">
    //<![CDATA[
    var dataForm = new VarienForm('form-validate', true);
    //]]>
</script>

这次我们在这里做一些新的事情。我们正在调用一个帮助程序;帮助程序是一个包含可以从块、模板、控制器等中重复使用的方法的类。在我们的情况下,我们正在创建一个帮助程序,它将检索所有可用的注册类型。按照以下步骤进行:

  1. 导航到app/code/local/Mdg/Giftregistry/Helper

  2. 打开Data.php类。

  3. 在其中添加以下代码(文件位置为app/code/local/Mdg/Giftregistry/Helper/Data.php):

<?php
class Mdg_Giftregistry_Helper_Data extends Mage_Core_Helper_Abstract {

public function getEventTypes()
    {
        $collection = Mage::getModel('mdg_giftregistry/type')->getCollection();
        return $collection;
    }
}

最后,我们需要设置编辑模板;编辑模板将与新模板完全相同,但有一个主要区别。我们将检查加载的注册表是否存在,并预填充我们字段的值。

文件位置为app/design/frontend/base/default/template/mdg/edit.phtml。参考以下代码:

<?php
    $helper = Mage::helper('mdg_giftregistry');
    $loadedRegistry = Mage::getSingleton('customer/session')->getLoadedRegistry();
?>
<?php if($loadedRegistry): ?>
    <form action="<?php echo $this->getUrl('giftregistry/index/editPost/') ?>" method="post" id="form-validate">
        <fieldset>
            <?php echo $this->getBlockHtml('formkey')?>
            <input type="hidden" id="type_id" value="<?php echo $loadedRegistry->getTypeId(); ?>" />
            <ul class="form-list">
                <li class="field">
                    <label class="giftreg" for="event_name"><?php echo $this->__('Event Name') ?></label>
                    <input type="text" name="event_name" id="event_name" value="<?php echo $loadedRegistry->getEventName(); ?>" title="Event Name"/>
                </li>
                <li class="field">
                    <label class="giftreg" for="event_location"><?php echo $this->__('Event Location') ?></label>
                    <input type="text" name="event_location" id="event_location" value="<?php echo $loadedRegistry->getEventLocation(); ?>" title="Event Location"/>
                </li>
                <li class="field">
                    <label class="giftreg" for="event_country"><?php echo $this->__('Event Country') ?></label>
                    <input type="text" name="event_country" id="event_country" value="<?php echo $loadedRegistry->getEventCountry(); ?>" title="Event Country"/>
                </li>
            </ul>
            <div class="buttons-set">
                <button type="submit" title="Save" class="button">
                    <span>
                        <span><?php echo $this->__('Save') ?></span>
                    </span>
                </button>
            </div>
        </fieldset>
    </form>
    <script type="text/javascript">
        //<![CDATA[
        var dataForm = new VarienForm('form-validate', true);
        //]]>
    </script>
<?php else: ?>
    <h2><?php echo $this->__('There was a problem loading the registry') ?></h2>
<?php endif; ?>

让我们继续添加编辑操作的块和模板:

  1. 打开mdg_giftregistry.xml布局文件。

  2. <mdg_gifregistry_index_edit>节点内添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<reference name="content">
    <block name="giftregistry.edit" type="core/template" template="mdg/edit.phtml" as="giftregistry_edit"/>
</reference>

设置好后,我们可以尝试创建一对测试注册表并修改它们的属性。

注意

这里有一个挑战给你。与控制器一样,编辑和新表单可以合并为一个可重用的表单。尝试将它们合并以查看完整代码和完整分解的答案,请访问www.magedevguide.com/challenge/chapter4/4

SearchController 块和视图

对于我们的搜索控制器,我们将需要一个用于我们的索引的搜索模板。对于结果,我们实际上可以通过简单地更改我们的控制器来重用注册表列表模板,按照以下步骤进行:

  1. 导航到模板文件夹。

  2. 创建一个名为search.phtml的文件。

  3. 添加以下代码(文件位置为app/design/frontend/base/default/template/mdg/search.phtml):

<?php $helper = Mage::helper('mdg_giftregistry'); ?>
<form action="<?php echo $this->getUrl('giftregistry/search/results/') ?>" method="post" id="form-validate">
    <fieldset>
        <?php echo $this->getBlockHtml('formkey')?>
        <ul class="form-list">
            <li>
                <label for="type">Event type</label>
                <select name="type" id="type">
                    <?php foreach($helper->getEventTypes() as $type): ?>
                        <option id="<?php echo $type->getTypeId(); ?>" value="<?php echo $type->getCode(); ?>">
                            <?php echo $type->getName(); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </li>
            <li class="field">
                <label class="giftreg" for="name"><?php echo $this->__('Event Name') ?></label>
                <input type="text" name="name" id="name" value="" title="Event Name"/>
            </li>
            <li class="field">
                <label class="giftreg" for="location"><?php echo $this->__('Event Location') ?></label>
                <input type="text" name="location" id="location" value="" title="Event Location"/>
            </li>
            <li class="field">
                <label class="giftreg" for="country"><?php echo $this->__('Event Country') ?></label>
                <input type="text" name="country" id="country" value="" title="Event Country"/>
            </li>
        </ul>
        <div class="buttons-set">
            <button type="submit" title="Save" class="button">
                    <span>
                        <span><?php echo $this->__('Save') ?></span>
                    </span>
            </button>
        </div>
    </fieldset>
</form>
<script type="text/javascript">
    //<![CDATA[
    var dataForm = new VarienForm('form-validate', true);
    //]]>
</script>

有几件事情需要注意:

  • 我们使用帮助程序模型来填充Event类型 ID

  • 我们直接发布到搜索/结果

现在,让我们对布局文件进行适当的更改:

  1. 打开mdg_giftregistry.xml

  2. <mdg_gifregistry_search_index>内添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<reference name="content">
    <block name="giftregistry.search" type="core/template" template="mdg/search.phtml" as="giftregistry_search"/>
</reference>

对于搜索结果,我们不需要创建新的块类型,因为我们直接将结果集合传递给块。在布局中,我们的更改将是最小的,我们可以重用列表块来显示搜索注册表结果。

但是,我们确实需要在控制器中进行更改。我们需要将函数从setResults()更改为setCustomerRegistries()

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

public function resultsAction()
{
    $this->loadLayout();
    if ($searchParams = $this->getRequest()->getParam('search_params')) {
        $results = Mage::getModel('mdg_giftregistry/entity')->getCollection();
        if($searchParams['type']){
            $results->addFieldToFilter('type_id', $searchParams['type']);
        }
        if($searchParams['date']){
            $results->addFieldToFilter('event_date', $searchParams['date']);
        }
        if($searchParams['location']){
            $results->addFieldToFilter('event_location', $searchParams['location']);
        }
        $this->getLayout()->getBlock('mdg_giftregistry.search.results')
            ->setCustomerRegistries($results);
    }
    $this->renderLayout();
    return $this;
}

最后,让我们按照以下步骤更新布局文件:

  1. 打开mdg_giftregistry.xml

  2. <mdg_gifregistry_search_results>内添加以下代码(文件位置为app/design/frontend/base/default/layout/mdg_giftregistry.xml):

<reference name="content">
    <block name="giftregistry.results" type="mdg_giftregistry/list" template="mdg/list.phtml"/>
</reference>

这将是我们的SearchController模板的结束;然而,我们的搜索结果显示了一个问题。对于注册表的删除和编辑链接,我们需要一种方法来仅限制这些链接只对所有者可见。

我们可以使用以下Helper函数来实现:

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

public function isRegistryOwner($registryCustomerId)
{
    $currentCustomer = Mage::getSingleton('customer/session')->getCustomer();
    if($currentCustomer && $currentCustomer->getId() == $registryCustomerId)
    {
        return true;
    }
    return false;
}

让我们更新我们的模板,以使用新的helper方法。

文件位置为app/design/frontend/base/default/template/mdg/list.phtml。参考以下代码:

<?php
    $_collection = $this->getCustomerRegistries();
    $helper = Mage::helper('mdg_giftregistry')
?>
<div class="customer-list">
    <?php if(!$_collection->count()): ?>
        <h2><?php echo $this->__('You have no registries.') ?></h2>
        <a href="<?php echo $this->getUrl('giftregistry/index/new') ?>">
            <?php echo $this->__('Click Here to create a new Gift Registry') ?>
        </a>
    <?php else: ?>
        <ul>
            <?php foreach($_collection as $registry): ?>
                <li>
                    <h3><?php echo $registry->getEventName(); ?></h3>
                    <p><strong><?php echo $this->__('Event Date:') ?> <?php echo $registry->getEventDate(); ?></strong></p>
                    <p><strong><?php echo $this->__('Event Location:') ?> <?php echo $registry->getEventLocation(); ?></strong></p>
                    <a href="<?php echo $this->getUrl('giftregistry/view/view', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                        <?php echo $this->__('View Registry') ?>
                    </a>
                    <?php if($helper->isRegistryOwner($registry->getCustomerId())): ?>
                        <a href="<?php echo $this->getUrl('giftregistry/index/edit', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                            <?php echo $this->__('Edit Registry') ?>
                        </a>
                        <a href="<?php echo $this->getUrl('giftregistry/index/delete', array('_query' => array('registry_id' => $registry->getEntityId()))) ?>">
                            <?php echo $this->__('Delete Registry') ?>
                        </a>
                    <?php endif; ?>

                </li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</div>

ViewController 块和视图

对于我们的视图,我们只需要创建一个新的模板文件和在layout.xml文件中创建一个新条目:

  1. 导航到模板目录。

  2. 创建一个名为view.phtml的模板。

  3. 添加以下代码(文件位置为app/design/frontend/base/default/template/mdg/view.phtml):

<?php $registry = Mage::registry('loaded_registry'); ?>
<h3><?php echo $registry->getEventName(); ?></h3>
<p><strong><?php $this->__('Event Date:') ?> <?php echo $registry->getEventDate(); ?></strong></p>
<p><strong><?php $this->__('Event Location:') ?> <?php echo $registry->getEventLocation(); ?></strong></p>
  1. 更新布局 XML 文件<mdg_gifregistry_view_view>
<reference name="content">
    <block name="giftregistry.view" type="core/template" template="mdg/view.phtml" as="giftregistry_view"/>
</reference>

注意

这里有一个挑战给你。改进视图表单,以便在没有实际加载的注册表时返回错误。要查看完整代码和详细分解的答案,请访问www.magedevguide.com/challenge/chapter4/5

将产品添加到注册表

我们几乎到了本章的结尾,但我们还没有涵盖如何向我们的注册表添加产品。由于本书篇幅有限,我决定将这一部分移到www.magedevguide.com/chapter6/adding-products-registry

总结

在本章中,我们涵盖了很多内容。我们学会了如何扩展 Magento 的前端以及如何处理路由和控制器。

Magento 布局系统允许我们修改和控制块,并在我们的商店上显示它。我们还开始使用 Magento 数据模型,并学会了如何使用它们,以及如何处理和操作我们的数据。

我们只是触及了前端开发和数据模型的表面。在下一章中,我们将更深入地扩展配置、模型和数据的主题,并在 Magento 后端探索和创建管理部分。

第五章:后端开发

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

在本章中,我们将构建所有商店所有者需要通过 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 后端并打开客户管理,进入客户 | 管理客户,快速测试我们迄今为止所做的更改。

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

返回配置

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

  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>

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

网格小部件

这是因为我们还没有对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 资源,如下面的截图所示:

权限和 ACL

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

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

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

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

  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主题加载基本模板。

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

权限和 ACL

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

在创建我们的基本网格块时,我们定义了一个名为_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 后端,点击我们的示例注册表之一,查看我们的进展。我们应该看到以下表单:

表单小部件

但似乎有一个问题。没有加载任何数据;我们只有一个空表单,因此我们必须修改我们的控制器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();  
…

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

加载数据

保存数据

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

  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. 如下截图所示,您将被要求提供角色名称并指定角色资源:为 XML-RPC/SOAP 设置 API 凭据

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

  6. 单击保存角色按钮。

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

  1. 转到 Magento 后端。

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

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

  4. 接下来,我们将被要求提供用户信息,如下截图所示:为 XML-RPC/SOAP 设置 API 凭据

  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 消费者并在管理面板中选择添加新来注册我们的应用程序:

设置 REST API 凭据

注意

需要注意的一件重要的事情是必须定义回调 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/

测试案例的解剖

创建一个单元测试

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

正如我们之前提到的,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 测试的基础知识。本章的目的不是构建复杂的测试或深入讨论,而是让我们初步了解并清楚地了解我们可以做些什么来测试我们的扩展。

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

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

第八章:部署和分发

欢迎来到本书的最后一章;我们已经走了很远,并且在这个过程中学到了很多。到目前为止,您应该清楚地了解了为 Magento 工作和开发自定义扩展所涉及的一切。

嗯,几乎一切,就像其他 Magento 开发人员一样,您的代码最终需要被推广到生产环境,或者可能需要打包进行分发;在本章中,我们将看到可用于我们的不同技术、工具和策略。

本章的最终目标是为您提供工具和技能,使您能够自信地进行部署,几乎没有停机时间。

通往零停机部署的道路

对于开发人员来说,将产品部署到生产环境可能是最令人害怕的任务之一,往往情况不会很好。

但是什么是零停机部署?嗯,就是自信地将代码部署到生产环境,知道代码经过了适当的测试并且准备就绪,这是所有 Magento 开发人员应该追求的理想。

这不是通过单一的流程或工具实现的,而是通过一系列技术、标准和工具的组合。在本章中,我们将学习以下内容:

  • 通过 Magento Connect 分发我们的扩展

  • 版本控制系统在部署中的作用

  • 分支和合并更改的正确实践

从头开始做对

在上一章中,我们学到了测试不仅可以增强我们的工作流程,还可以避免未来的麻烦。单元测试、集成测试和自动化工具都可以确保我们的代码经过了适当的测试。

编写测试意味着不仅仅是组织一些测试并称之为完成;我们负责考虑可能影响我们代码的所有可能边缘情况,并为每种情况编写测试。

确保所见即所得

在本书的第一章中,我们立即开始设置我们的开发环境,这是一项非常重要的任务。为了确保我们交付的代码是质量和经过测试的,我们必须能够在尽可能接近生产环境的环境中开发和测试我们的代码。

我将通过 Magento 早期的一个例子来说明这个环境的重要性。我听说这种情况发生了好几次;开发人员在他们的本地环境中从头开始创建新的扩展,完成开发并在本地暂存环境中进行测试,一切似乎都正常工作。

常见的工作流程之一是:

  • 在开发人员的本地机器上开始开发,该机器运行着一个接近生产环境的虚拟机

  • 在尽可能接近生产环境的暂存环境上测试和批准更改

  • 最后,将更改部署到生产环境

现在是时候将他们的代码推广到生产环境了,他们充满信心地这样做了;当然,在本地是可以工作的,因此它也必须在生产环境中工作,对吧?在这些特定情况下,情况并非如此;相反的是,新代码加载到生产环境后,商店崩溃了,说自动加载程序无法找到该类。

发生了什么?嗯,问题在于开发人员的本地环境是 Windows,扩展文件夹的名称是 CamelCase,例如MyExtension,但在类名内部他们使用的是大写文本(Myextension)。

现在在 Windows 上这将正常工作,因为文件不区分大写、首字母大写或小写的文件夹名称;而大多数 Web 服务器一样的基于 Unix 的系统会区分文件夹和文件的命名。

尽管这个例子看起来可能很愚蠢,但它很好地说明了标准化开发环境的必要性;Magento 安装中有很多部分和“移动的部件”。PHP 的不同版本或者在生产环境中启用的额外 Apache 模块,但在暂存环境中没有启用,可能会产生天壤之别。

注意

www.magedevguide.com/naming-conventions了解更多关于 Magento 命名约定的信息。

准备好意味着准备好

但是当我们说我们的代码实际上已经准备好投入生产时,准备好到底意味着什么呢?每个开发者可能对准备好和完成实际上意味着什么有不同的定义。在开发新模块或扩展 Magento 时,我们应该始终定义这个特定功能/代码的准备好意味着什么。

所以我们现在有所进展,我们知道为了将代码传递到生产环境,我们必须做以下事情:

  1. 测试我们的代码,并确保我们已经涵盖了所有边缘情况。

  2. 确保代码符合标准和指南。

  3. 确保它已经在尽可能接近生产环境的环境中进行了测试和开发。

版本控制系统和部署

版本控制系统VCSs)是任何开发者的命脉,尽管 Git 和 SVN 的支持者之间可能存在一些分歧(没有提到 Mercurial 的人),但基本功能仍然是一样的。

让我们快速了解一下每种版本控制系统之间的区别,以及它们的优势和劣势。

SVN

这是一个强大的系统,已经存在了相当长的时间,非常有名并且被广泛使用。

SubversionSVN)是一个集中式的版本控制系统;这意味着有一个被认为是“好”的单一主要源,所有开发者都从这个中央源检出和推送更改。

尽管这使得更改更容易跟踪和维护,但它也有一个严重的缺点。分散也意味着我们必须与中央仓库保持不断的通信,因此无法远程工作或在没有互联网连接的情况下工作。

SVN

Git

Git 是一个更年轻的版本控制系统,由于被开源社区广泛采用和 Github 的流行(www.github.com),它已经流行了几年。

SVN 和 Git 之间的一个关键区别是,Git 是一个分散式版本控制系统,这意味着没有中央管理机构或主仓库;每个开发者都有完整的仓库副本可供本地使用。

Git 是分散式的,这使得 Git 比其他版本控制系统更快,并且具有更好和更强大的分支系统;此外,可以远程工作或在没有互联网连接的情况下工作。

Git

无论我们选择哪种版本控制系统,任何版本控制系统最强大(有时被忽视)的功能都是分支或创建分支的能力。

分支允许我们进行实验和开发新功能,而不会破坏我们主干或主代码中的稳定代码;创建分支需要我们对当前主干/主代码进行快照,然后进行任何更改和测试。

现在,分支只是方程式的一部分;一旦我们对我们的代码更改感到满意,并且已经正确测试了每个边缘情况,我们需要一种重新整合这些更改到我们主要代码库的方法。合并通过运行几个命令,使我们能够重新整合所有我们的分支修改。

通过将分支集成和合并更改到我们的工作流程中,我们获得了灵活性和自由,可以在不干扰实验性或正在进行中的代码的情况下,处理不同的更改、功能和错误修复。

此外,正如我们将在下一节中学到的,版本控制可以帮助我们进行无缝的推广,并轻松地在多个 Magento 安装中保持我们的代码最新。

分发

您可能希望自由分发您的扩展或将其商业化,但是如何能够保证每次正确安装代码而无需自己操作呢?更新呢?并非所有商店所有者都精通技术或能够自行更改文件。

幸运的是,Magento 自带了自己的包管理器和扩展市场,称为 Magento Connect。

Magento Connect 允许开发人员和解决方案合作伙伴与社区分享其开源和商业贡献,并不仅限于自定义模块;我们可以在 Magento Connect 市场中找到以下类型的资源:

  • 模块

  • 语言包

  • 自定义主题

打包我们的扩展

Magento Connect 的核心功能之一是允许我们直接从 Magento 后端打包我们的扩展。

要打包我们的扩展,请执行以下步骤:

  1. 登录 Magento 后端。

  2. 从后端,选择系统 | Magento Connect | 打包扩展打包我们的扩展

正如我们所看到的,创建扩展 部分由六个不同的子部分组成,我们将在下面介绍。

包信息

包信息用于指定一般扩展信息,例如名称、描述和支持的 Magento 版本,如下所示:

  • 名称:标准做法是保持名称简单,只使用单词

  • 渠道:这指的是扩展的代码池;正如我们在前几章中提到的,为了分发设计的扩展应该使用“社区”渠道

  • 支持的版本:选择我们的扩展应该支持的 Magento 版本

  • 摘要:此字段包含扩展的简要描述,用于扩展审核过程

  • 描述:这里有扩展和其功能的详细描述

  • 许可证:这是用于此扩展的许可证;一些可用的选项是:

  • 开放软件许可证OSL

  • Mozilla 公共许可证MPL

  • 麻省理工学院许可证MITL

  • GNU 通用公共许可证GPL

  • 如果您的扩展要进行商业分发,则使用任何其他许可证

  • 许可证 URI:这是许可证文本的链接

注意

有关不同许可类型的更多信息,请访问www.magedevguide.com/license-types

发布信息

以下截图显示了发布信息屏幕:

发布信息

发布信息部分包含有关当前软件包发布的重要数据:

  • 发布版本:初始发布可以是任意数字,但是,重要的是每次发布都要递增版本号。Magento Connect 不会允许您两次更新相同的版本。

  • 发布稳定性:有三个选项 - 稳定BetaAlpha

  • 注释:在这里,我们可以输入所有特定于发布的注释,如果有的话。

作者

以下截图显示了作者屏幕:

作者

在此部分,指定了有关作者的信息;每个作者的信息都有以下字段:

  • 名称:作者的全名

  • 用户:Magento 用户名

  • 电子邮件:联系电子邮件地址

依赖项

以下截图显示了依赖项屏幕:

依赖项

在打包 Magento 扩展时使用了三种类型的依赖关系:

  • PHP 版本:在这里,我们需要在最小最大字段中指定此扩展支持的 PHP 的最小和最大版本

  • 软件包:这用于指定此扩展所需的任何其他软件包

  • 扩展:在这里,我们可以指定我们的扩展是否需要特定的 PHP 扩展才能工作

如果软件包依赖关系未满足,Magento Connect 将允许我们安装所需的扩展;对于 PHP 扩展,Magento Connect 将抛出错误并停止安装。

内容

以下截图显示了内容屏幕:

内容

内容部分允许我们指定构成扩展包的每个文件和文件夹。

注意

这是扩展打包过程中最重要的部分,也是最容易出错的部分。

每个内容条目都有以下字段:

  • 目标:这是目标基本目录,用于指定搜索文件的基本路径。以下选项可用:

  • Magento 核心团队模块文件 - ./app/code/core

  • Magento 本地模块文件 - ./app/code/local

  • Magento 社区模块文件 - ./app/code/community

  • Magento 全局配置 - ./app/etc

  • Magento 区域语言文件 - ./app/locale

  • Magento 用户界面(布局、模板)- ./app/design

  • Magento 库文件 - ./lib

  • Magento 媒体库 - ./media

  • Magento 主题皮肤(图像、CSS、JS)- ./skin

  • Magento 其他可访问的 Web 文件 - ./

  • Magento PHPUnit 测试 - ./tests

  • Magento 其他 - ./

  • 路径:这是相对于我们指定目标的文件名和/或路径

  • 类型:对于此字段,我们有两个选项 - 文件递归目录

  • 包括:此字段采用正则表达式,允许我们指定要包括的文件

  • 忽略:此字段采用正则表达式,允许我们指定要排除的文件

加载本地包

以下屏幕截图显示了加载本地包的屏幕:

加载本地包

此部分将允许我们加载打包的扩展;由于我们尚未打包任何扩展,因此列表目前为空。

让我们继续打包我们的礼品注册扩展。确保填写所有字段,然后单击保存数据并创建包;这将在magento_root/var/connect/文件夹中打包和保存扩展。

扩展包文件包含所有源文件和所需的源代码;此外,每个包都会创建一个名为package.xml的新文件。此文件包含有关扩展的所有信息以及文件和文件夹的详细结构。

发布我们的扩展

最后,为了使我们的扩展可用,我们必须在 Magento Connect 中创建一个扩展配置文件。要创建扩展配置文件,请执行以下步骤:

  1. 登录magentocommerce.com

  2. 单击我的帐户链接。

  3. 单击左侧导航中的开发人员链接。

  4. 单击添加新扩展

添加新扩展窗口看起来像以下屏幕截图:

发布我们的扩展

重要的是要注意,扩展标题字段必须是您在生成包时使用的确切名称。

创建扩展配置文件后,我们可以继续上传我们的扩展包;所有字段应与扩展打包过程中指定的字段匹配。

发布我们的扩展

最后,一旦完成,我们可以单击提交审批按钮。扩展可以具有以下状态:

  • 已提交:这意味着扩展已提交审核

  • 未获批准:这意味着扩展存在问题,并且您还将收到一封解释为什么扩展未获批准的电子邮件

  • 在线:这意味着扩展已获批准,并可通过 Magento Connect 获得

  • 离线:这意味着您可以随时从您的帐户扩展管理器中将扩展下线

摘要

在本章中,我们学习了如何部署和共享我们的自定义扩展。我们可以使用许多不同的方法来共享和部署我们的代码到生产环境。

这是我们书的最后一章;我们已经学到了很多关于 Magento 开发的知识,虽然我们已经涵盖了很多内容,但这本书只是您漫长旅程的一个起点。

Magento 不是一个容易学习的框架,虽然可能是一次令人生畏的经历,但我鼓励您继续尝试和学习。

附录 A. 你好,Magento

以下示例将为您快速简单地介绍创建 Magento 扩展的世界。我们将创建一个简单的 Hello World 模块,当我们访问商店中的特定 URL 时,它将允许我们显示一个 Hello World!消息。

配置

在 Magento 中创建一个简单的扩展至少需要两个文件:config.xml和模块声明文件。让我们继续创建我们的每一个文件。

第一个文件用于向 Magento 声明模块;没有这个文件,Magento 将不会意识到任何扩展文件。

文件位置是app/etc/modules/Mdg_Hello.xml。请参考以下代码:

<?xml version=”1.0”?>
<config>
    <modules>
        <Mdg_Hello>
            <active>true</active>
            <codePool>local</codePool>
        </Mdg_Hello>
    </modules>
</config>

第二个 XML 文件称为config.xml;它用于指定所有扩展配置,如路由、块、模型和助手类名称。对于我们的示例,我们只会使用控制器和路由。

让我们用以下代码创建配置文件。

文件位置是app/code/local/Mdg/Hello/etc/config.xml。请参考以下代码:

<?xml version=”1.0”?>
<config>
    <modules>
        <Mdg_Hello>
            <version>0.1.0</version>
        </Mdg_Hello>
    </modules>
    <frontend>
        <routers>
            <mdg_hello>
                <use>standard</use>
                <args>
                    <module>Mdg_Hello</module>
                    <frontName>hello</frontName>
                </args>
            </mdg_hello>
        </routers>
    </frontend>
</config>

我们的扩展现在可以被 Magento 加载,并且您可以在 Magento 后端的系统 | 配置 | 高级中启用或禁用我们的扩展。

控制器

Magento 在其核心是一个模型-视图-控制器MVC)框架。因此,为了使我们的新路由功能正常,我们必须创建一个新的控制器来响应这个特定的路由。要做到这一点,请按照以下步骤:

  1. 导航到扩展根目录。

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

  3. controllers文件夹内,创建一个名为IndexController.php的文件。

  4. 复制以下代码(文件位置是app/code/local/Mdg/Hello/controllers/IndexController.php):

<?php
class Mdg_Hello_IndexController extends Mage_Core_Controller_Front_Action
{
     public function indexAction()
  {
     echo ‘Hello World this is the default action’;
     }
}

测试路由

现在我们已经创建了我们的路由器和控制器,我们可以通过打开http://magento.localhost.com/hello/index/index来测试它,我们应该会看到以下截图:

测试路由

默认情况下,Magento 将同时使用索引控制器和索引操作作为每个扩展的默认值。因此,如果我们转到http://magento.localhost.com/hello/,我们应该会看到相同的屏幕。

为了结束我们对 Magento 模块的介绍,让我们向我们的控制器添加一个新路由:

  1. 导航到扩展根目录。

  2. 打开IndexController.php

  3. 复制以下代码(文件位置是app/code/local/Mdg/Hello/controllers/IndexController.php):

<?php 
class Mdg_Hello_IndexController extends Mage_Core_Controller_Front_Action
{
     public function indexAction()
  {
     echo ‘Hello World this is the default action’;
     }

     public function developerAction()
     {
         echo ‘Hello Developer this is a custom controller action’;
     }
}

最后,让我们测试一下,并通过转到http://magento.localhost.com/hello/index/developer来加载新的操作路由,如下截图所示:

测试路由

posted @ 2024-05-05 00:11  绝不原创的飞龙  阅读(52)  评论(0编辑  收藏  举报