Magento2 开发者指南(一)
构建基于 Magento 的商店可能是一项具有挑战性的任务。它需要一系列与 PHP/JavaScript 编程语言、开发和生产环境以及众多 Magento 特定功能相关的技术技能。本书将提供关于 Magento 构建块所需的知识。到这本书的结尾,你应该熟悉配置文件、依赖注入、模型、集合、块、控制器、事件、观察者、插件、定时任务、配送方式、支付方式以及一些其他内容。所有这些都应该为你后续的开发旅程打
原文:
zh.annas-archive.org/md5/22e3fb2c2e59e824ce1774e2331a1019译者:飞龙
前言
构建基于 Magento 的商店可能是一项具有挑战性的任务。它需要一系列与 PHP/JavaScript 编程语言、开发和生产环境以及众多 Magento 特定功能相关的技术技能。本书将提供关于 Magento 构建块所需的知识。
到这本书的结尾,你应该熟悉配置文件、依赖注入、模型、集合、块、控制器、事件、观察者、插件、定时任务、配送方式、支付方式以及一些其他内容。所有这些都应该为你后续的开发旅程打下坚实的基础。
本书涵盖内容
第一章, 理解平台架构,对技术堆栈、架构层、顶级系统结构和单个模块结构进行了高级概述。
第二章, 管理环境,介绍了 VirtualBox、Vagrant 和 Amazon AWS 作为设置开发和生产环境的平台。它还提供了设置/脚本 Vagrant 和 Amazon EC2 盒子的实践示例。
第三章, 编程概念和约定,向读者介绍了几个看似不相关但重要的 Magento 部分,如 composer、服务合约、代码生成、var 目录,以及最终的编码标准。
第四章, 模型和集合,探讨了模型、资源、集合、架构和数据脚本。它还展示了应用于实体的实际 CRUD 操作以及过滤集合。
第五章, 使用依赖注入,引导读者了解依赖注入机制。它解释了对象管理器的角色,如何配置类偏好,以及如何使用虚拟类型。
第六章, 插件,深入探讨了名为插件的新概念。它展示了如何通过 before/after/around 监听器轻松扩展或添加现有功能。
第七章, 后端开发,通过实践方法介绍通常被认为是后端相关开发的部分。这些包括定时任务、通知消息、会话、Cookies、日志、性能分析器、事件、缓存、小部件等。
第八章 前端开发 采用更高级的方法,引导读者了解大多数被认为是前端相关开发的内容。它涉及到在 Magento 中渲染流程、视图元素、块、模板、布局、主题、CSS 和 JavaScript。
第九章 Web API 详细介绍了 Magento 提供的强大 Web API。它提供了实际操作示例,以创建和使用 REST 和 SOAP,无论是通过 PHP cURL 库还是从控制台。
第十章 主要功能区域 采用了高级方法,向读者介绍 Magento 中一些最常见的部分。这包括 CMS、目录和客户管理、以及产品和客户导入。它甚至展示了如何创建自定义产品类型和运输及支付方式。
第十一章 测试 概述了在 Magento 中可用的测试类型。它进一步展示了如何编写和执行自定义测试。
第十二章 从头开始构建模块 展示了开发模块的整个过程,该模块使用了前几章中介绍的大多数功能。最终结果是具有管理后台和店面界面、管理配置区域、电子邮件模板、已安装的架构脚本、测试等的模块。
你需要这本书的内容
为了成功运行本书中提供的所有示例,你需要自己的网络服务器或第三方网络托管解决方案。高级技术堆栈包括 PHP、Apache/Nginx 和 MySQL。Magento 2 社区版平台本身附带了一份详细的系统要求列表,可以在devdocs.magento.com/guides/v2.0/install-gde/system-requirements.html找到。实际的环境设置在第二章 管理环境 中有详细说明。
这本书面向的对象
本书主要面向对 Magento 2 开发感兴趣的初级到专业 PHP 开发者。对于后端开发者,本书涵盖了多个主题,将帮助你修改和扩展你的 Magento 店铺。前端开发者也会找到一些关于如何在前端定制网站外观的覆盖内容。
由于代码和结构发生了大量变化,Magento 2.x 版本可以描述为一个与其前身显著不同的平台。考虑到这一点,本书将不会假设或要求读者具备对 Magento 1.x 的先验知识。
惯例
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称显示如下:“AbstractProductPlugin1类不需要从另一个类扩展,插件才能工作。”
代码块设置如下:
<config xsi:noNamespaceSchemaLocation="urn:magento:framework: ObjectManager/etc/config.xsd">
<type name="Magento\Catalog\Block\Product\AbstractProduct">
<plugin name="foggyPlugin1" type="Foggyline\Plugged\Block\Catalog\Product\ AbstractProductPlugin1" disabled="false" sortOrder="100"/>
<plugin name="foggyPlugin2" type="Foggyline\Plugged\Block\Catalog\Product\ AbstractProductPlugin2" disabled="false" sortOrder="200"/>
<plugin name="foggyPlugin3" type="Foggyline\Plugged\Block\Catalog\Product\ AbstractProductPlugin3" disabled="false" sortOrder="300"/>
</type>
</config>
任何命令行输入或输出都按如下方式编写:
php bin/magento setup:upgrade
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“在商店视图下拉字段中,我们选择要应用主题的商店视图。”
注意
警告或重要注意事项以如下框显示。
小贴士
小贴士和技巧显示如下。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。
要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及本书的标题。
如果您在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大收益。
下载示例代码
您可以从您在 www.packtpub.com 的账户下载示例代码文件,适用于您购买的所有 Packt 出版物。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误部分显示。
海盗行为
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章:理解平台架构
Magento是一个强大、高度可扩展和高度可定制的电子商务平台,可用于构建网店,如果需要,还可以用于一些非电子商务网站。它提供了大量开箱即用的电子商务功能。
产品库存、购物车、支持多种支付和运输方式、促销规则、内容管理、多种货币、多种语言、多个网站等功能使其成为商家的绝佳选择。另一方面,开发者享受与商人相关的完整功能集以及与实际开发相关的所有事物。本章将涉及强大的 Web API 支持、可扩展的管理界面、模块、主题、嵌入式测试框架等内容。
在本章中,以下部分提供了对 Magento 的高级概述:
-
技术栈
-
架构层
-
最高级别的文件系统结构
-
模块文件系统结构
技术栈
Magento 高度模块化的结构是几个开源技术嵌入到堆栈中的结果。这些开源技术由以下组件组成:
-
PHP:PHP 是一种服务器端脚本语言。本书假设您对 PHP 的面向对象方面有深入的了解,这通常被称为PHP OOP。
-
编码规范:Magento 非常重视编码规范。这包括PSR-0(自动加载标准)、PSR-1(基本编码规范)、PSR-2(编码风格指南)、PSR-3和PSR-4。
-
Composer:Composer 是一个 PHP 的依赖管理包。它用于拉取所有供应商库的要求。
-
HTML:HTML5 是开箱即支持的。
-
CSS:Magento 通过其内置的LESS CSS预处理器支持 CSS3。
-
jQuery:jQuery 是一个成熟的跨平台 JavaScript 库,旨在简化 DOM 操作。它是当今最受欢迎的 JavaScript 框架之一。
-
RequireJS:RequireJS 是一个 JavaScript 文件和模块加载器。使用如 RequireJS 这样的模块化脚本加载器有助于提高代码的速度和质量。
-
第三方库:Magento 内置了许多第三方库,其中最显著的是Zend Framework和Symfony。值得注意的是,Zend Framework 有两个不同的主要版本,即 1.x 版本和 2.x 版本。Magento 内部使用这两个版本。
-
Apache 或 Nginx:Apache 和 Nginx 都是 HTTP 服务器。每个都有自己的优缺点。说一个比另一个好是不公平的,因为它们的性能广泛取决于整个系统的设置和使用。Magento 与 Apache 2.2 和 2.4 以及 Nginx 1.7 兼容。
-
MySQL:MySQL 是一个成熟且广泛使用的关系数据库管理系统(RDBMS),它使用结构化查询语言(SQL)。MySQL 既有免费社区版本,也有商业版本。Magento 至少需要MySQL 社区版5.6 版本。
-
MTF:Magento 测试框架(MTF)提供了一套自动化测试套件。它涵盖了各种类型的测试,如性能测试、功能测试和单元测试。整个 MTF 都可在 GitHub 上找到,可以通过访问
github.com/magento/mtf作为一个独立的项目进行查看。
不同的技术可以粘合到各种架构中。从模块开发者、系统集成商或商家,或者从其他角度看待 Magento 架构的方式有很多。
架构层
从上到下,Magento 可以分为四个架构层,即表示层、服务层、领域层和持久层。
表示层是我们通过浏览器直接与之交互的那一层。它包含布局、块、模板,甚至控制器,这些控制器处理用户界面的命令。jQuery、RequireJS、CSS 和 LESS 等客户端技术也是这一层的一部分。通常,三种类型的用户与这一层交互,即网络用户、系统管理员和进行 Web API 调用的用户。由于 Web API 调用可以通过与用户使用浏览器相同的方式进行 HTTP 调用,因此两者之间有一条很细的界限。虽然网络用户和 Web API 调用按原样消耗表示层,但系统管理员有权对其进行更改。这种更改以设置活动主题和更改CMS(即内容管理系统)页面、块和产品本身的内容的形式体现。
当与表示层的组件进行交互时,它们通常会调用底层的服务层。
服务层是表示层和领域层之间的桥梁。它包含服务合约,这些合约定义了实现行为。服务合约基本上是一个 PHP 接口的别称。这一层是我们可以找到 REST/SOAP API 的地方。大多数用户在店面上的交互都是通过服务层路由的。同样,进行 REST/SOAP API 调用的外部应用程序也与这一层进行交互。
当与服务层的组件进行交互时,它们通常会调用底层的领域层。
域名层实际上是 Magento 的业务逻辑。这一层完全是关于组成业务逻辑的通用数据对象和模型。域名层模型本身不参与数据持久化,但它们包含一个指向资源模型的引用,该模型用于从 MySQL 数据库检索和持久化数据。一个模块的域名层代码可以通过使用事件观察者、插件和di.xml定义与另一个模块的域名模块代码进行交互。我们将在后面的章节中探讨这些细节。鉴于插件和 di.xml 的强大功能,重要的是要注意,这种交互最好通过服务合同(PHP 接口)来建立。
当与域名层的组件进行交互时,它们通常会调用底层的持久化层。
持久化层是数据被持久化的地方。这一层负责所有的CRUD(即创建、读取、更新和删除)请求。Magento 使用持久化层的活动记录模式策略。模型对象包含一个资源模型,它将一个对象映射到一个或多个数据库行。在这里,区分简单资源模型和实体-属性-值(EAV)资源模型的情况很重要。简单资源模型映射到单个表,而 EAV 资源模型将它们的属性分散在多个 MySQL 表中。例如,Customer 和 Catalog 资源模型使用 EAV 资源模型,而新闻通讯的 Subscriber 资源模型使用简单资源模型。
顶级文件系统结构
以下列表描述了根 Magento 文件系统结构:
-
.htaccess -
.htaccess.sample -
.php_cs -
.travis.yml -
CHANGELOG.md -
CONTRIBUTING.md -
CONTRIBUTOR_LICENSE_AGREEMENT.html -
COPYING.txt -
Gruntfile.js -
LICENSE.txt -
LICENSE_AFL.txt -
app -
bin -
composer.json -
composer.lock -
dev -
index.php -
lib -
nginx.conf.sample -
package.json -
php.ini.sample -
phpserver -
pub -
setup -
update -
var -
vendor
app/etc/di.xml 文件是我们可能在开发过程中经常查看的最重要文件之一。它包含各种类映射或单个接口的偏好设置。
var/magento/language-* 目录是注册语言所在的位置。尽管每个模块都可以在 app/code/{VendorName}/{ModuleName}/i18n/ 下声明自己的翻译,但如果在自定义模块或主题目录中找不到翻译,Magento 最终会回退到其自己的名为 i18n 的单独模块。
bin目录是我们可以找到magento文件的地方。magento文件是一个旨在从控制台运行的脚本。一旦通过php bin/magento命令触发,它将运行Magento\Framework\Console\Cli应用程序的一个实例,向我们提供相当多的控制台选项。我们可以使用magento脚本来启用/禁用缓存,启用/禁用模块,运行索引器,以及执行许多其他操作。
dev目录是我们可以找到 Magento 测试脚本的地方。我们将在后面的章节中了解更多关于这些脚本的内容。
lib目录包含两个主要子目录,即位于lib/internal下的服务器端 PHP 库代码和位于lib/web中的客户端 JavaScript 库。
pub目录是公开文件所在的位置。这是我们在设置 Apache 或 Nginx 时应将其设置为根目录的目录。当在浏览器中打开店面时,会触发pub/index.php文件。
var目录是动态生成的文件类型,如缓存、日志等文件创建的地方。我们应该能够随时删除此文件夹的内容,并且让 Magento 自动重新创建它。
vendor目录是大多数代码所在的地方。这是我们可以找到各种第三方供应商代码、Magento 模块、主题和语言包的地方。进一步查看vendor目录,你会看到以下结构:
-
.htaccess -
autoload.php -
bin -
braintree -
composer -
doctrine -
fabpot -
justinrainbow -
league -
lusitanian -
magento -
monolog -
oyejorge -
pdepend -
pelago -
phpmd -
phpseclib -
phpunit -
psr -
sebastian -
seld -
sjparkinson -
squizlabs -
symfony -
tedivm -
tubalmartin -
zendframework
在供应商目录中,我们可以找到来自各种供应商的代码,例如phpunit、phpseclib、monolog、symfony等。Magento 本身也可以在这里找到。Magento 代码位于vendor/magento目录下,部分列表如下:
-
composer -
framework -
language-en_us -
magento-composer-installer -
magento2-base -
module-authorization -
module-backend -
module-catalog -
module-customer -
module-theme -
module-translation -
module-ui -
module-url-rewrite -
module-user -
module-version -
module-webapi -
module-widget -
theme-adminhtml-backend -
theme-frontend-blank -
theme-frontend-luma
你会发现目录的进一步结构遵循某种命名模式,其中theme-*目录存储主题,module-*目录存储模块,而language-*目录存储已注册的语言。
模块文件系统结构
Magento 将自己定位为一个高度模块化的平台。这意味着模块被放置的目录位置是实际存在的。现在让我们看一下单个模块的结构。以下结构属于一个较简单的核心 Magento 模块——可以在vendor/magento/module-contact中找到的Contact模块:
-
Block -
composer.json -
Controller -
etc-
acl.xml -
adminhtmlsystem.xml
-
config.xml -
email_templates.xml -
frontend-
di.xml -
page_types.xml -
routes.xml
-
-
module.xml
-
-
Helper -
i18n -
LICENSE_AFL.txt -
LICENSE.txt -
Model -
README.md -
registration.php -
Test-
Unit-
Block -
Controller -
Helper -
Model
-
-
-
view-
adminhtml -
frontend-
layout -
contact_index_index.xml -
default.xml
-
-
templatesform.phtml
-
尽管前面的结构是针对一个较简单的模块,但你可以看到它仍然相当广泛。
Block目录是存储与视图相关的 PHP 类的地方。
Controller目录是存储与控制器相关的 PHP 类的地方。这是响应店面POST和GET HTTP操作的代码。
etc目录是模块配置文件所在之处。在这里,我们可以看到诸如module.xml、di.xml、acl.xml、system.xml、config.xml、email_templates.xml、page_types.xml、routes.xml等文件。module.xml文件是一个实际的模块声明文件。我们将在后面的章节中查看这些文件的内容。
Helper目录是各种辅助类所在之处。这些类通常用于将各种商店配置值抽象到获取方法中。
i18n目录是存储模块翻译包 CSV 文件的地方。
Module目录是实体、资源实体、集合和各种其他业务类可以找到的地方。
测试目录存储模块单元测试。
view目录包含所有模块管理员和店面模板文件(.phtml和.html)和静态文件(.js和.css)。
最后,registration.php是一个模块注册文件。
摘要
在本章中,我们快速浏览了在 Magento 中使用的技术栈。我们讨论了作为一个开源产品的 Magento 如何广泛使用其他开源项目和服务,如 MySQL、Apache、Nginx、Zend Framework、Symfony、jQuery 等。然后我们学习了这些库是如何组织到目录中的。最后,我们探索了一个现有的核心模块,并简要地查看了一个模块结构的示例。
在下一章中,我们将处理环境设置,以便我们可以安装并准备开发 Magento。
第二章:管理环境
在本章中,我们将探讨设置我们的开发和生产环境。我们的想法是拥有一个完全自动化的开发环境,可以通过单个控制台命令启动。对于生产环境,我们将关注可用的云服务之一,看看设置 Magento 进行更简单的生产项目有多容易。我们不会涵盖任何强大的环境设置,如自动扩展、缓存服务器、内容分发网络等。这些实际上是系统管理员或 DevOps 角色的工作。我们在这里的关注点是启动我们的 Magento 商店所需的最基本条件;在接下来的章节中,我们将实现以下里程碑:
-
设置开发环境
-
VirtualBox
-
Vagrant
-
Vagrant 项目
-
配置 PHP
-
配置 MySQL
-
配置 Apache
-
配置 Magento 安装
-
-
-
设置生产环境
-
Amazon Web Services(AWS)简介
-
设置 S3 使用权限
-
创建 IAM 用户
-
创建 IAM 组
-
-
设置 S3 以备份数据库和媒体文件
-
自动化 EC2 设置的 Bash 脚本
-
设置 EC2
-
设置弹性 IP 和 DNS
-
-
设置开发环境
在本节中,我们将使用VirtualBox和Vagrant构建一个开发环境。
注意
Magento 官方要求 Apache 2.2 或 2.4,PHP 5.6.x 或 5.5.x(PHP 5.4 不受支持),以及 MySQL 5.6.x。我们在设置环境时需要记住这一点。
VirtualBox
VirtualBox是一款功能强大且丰富的 x86 和 AMD64/Intel64 虚拟化软件。它是免费的,可以在大量平台上运行,并支持大量客户操作系统。如果我们日常开发中使用 Windows、Linux 或 OS X,我们可以使用 VirtualBox 启动一个虚拟机,在其中安装运行 Magento 所需的服务器软件。这意味着使用 MySQL、Apache 以及一些其他东西。
Vagrant
Vagrant是一个用于虚拟化软件管理的软件包装器。我们可以用它来创建和配置开发环境。Vagrant 支持多种类型的虚拟化软件,如 VirtualBox、VMware、基于内核的虚拟机(KVM)、Linux 容器(LXC)等。它甚至支持服务器环境,如 Amazon EC2。
注意
在开始之前,我们需要确保已经安装了 VirtualBox 和 Vagrant。我们可以从它们的官方网站下载并按照以下说明进行安装:www.virtualbox.org 和 www.vagrantup.com。
Vagrant 项目
我们首先在主机操作系统中某个地方手动创建一个空目录,比如说 /Users/branko/www/B05032-Magento-Box/。这是我们将会拉取 Magento 代码的目录。我们希望 Magento 源代码位于 Vagrant 虚拟机外部,这样我们就可以在我们的 IDE 中轻松地与之一起工作。
然后我们创建一个 Vagrant 项目目录,比如说 /Users/branko/www/magento-box/。
在 magento-box 目录中,我们运行控制台命令 vagrant init。这会产生如下输出:
A 'Vagrantfile' has been placed in this directory. You are now ready to 'vagrant up' your first virtual environment! Please read the comments in the Vagrantfile as well as documentation on 'vagrantup.com' for more information on using Vagrant.
Vagrantfile 实际上是一个 Ruby 语言源文件。如果我们去掉注释,其原始内容看起来如下:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.box = "base"
end
如果我们现在在 magento-box 目录下运行 vagrant up,这将启动 VirtualBox 的无头(无 GUI)模式并运行基础操作系统。然而,现在让我们暂缓运行该命令。
目标是创建一个更健壮的 Vagrantfile,它涵盖了运行 Magento 所需的所有内容,从 Apache、MySQL、PHP、PHPUnit、composer 到带有性能基准数据的完整 Magento 安装。
虽然 Vagrant 本身没有单独的配置文件,但我们将创建一个,因为我们想在其中存储配置数据,如 MySQL 用户名和密码。
让我们创建一个名为 Vagrantfile.config.yml 的文件,与同一目录下的 Vagrantfile 一起,内容如下:
ip: 192.168.10.10
s3:
access_key: "AKIAIPRNHSWEQNWHLCDQ"
secret_key: "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"
bucket: "foggy-project-dhj6"
synced_folder:
host_path: "/Users/branko/www/B05032-Magento-Box/"
guest_path: "/vagrant-B05032-Magento-Box/"
mysql:
host: "127.0.0.1"
username: root
password: user123
http_basic:
repo_magento_com:
username: a8adc3ac98245f519ua0d2v2c8770ec8
password: a38488dc908c6d6923754c268vc41bc4
github_oauth:
github_com: "d79fb920d4m4c2fb9d8798b6ce3a043f0b7c2af6"
magento:
db_name: "magento"
admin_firstname: "John"
admin_lastname: "Doe"
admin_password: "admin123"
admin_user: "admin"
admin_email: "email@change.me"
backend_frontname: "admin"
language: "en_US"
currency: "USD"
timezone: "Europe/London"
base_url: "http://magento.box"
fixture: "small"
这里没有 Vagrant 强制的结构。这可以是任何有效的 YAML 文件。所提供的值仅是我们可以放入其中的示例。
Magento 使我们能够生成一对 32 字符的认证令牌,可用于访问 Git 仓库。这是通过使用用户名和密码登录到 Magento Connect,然后转到我的账户 | 开发者 | 安全密钥来完成的。然后,公钥和私钥就成为了我们访问 Magento GitHub 仓库的用户名和密码。
有一个单独的配置文件意味着我们可以将 Vagrantfile 提交到版本控制中,同时将 Vagrantfile.config.yml 排除在版本控制之外。
我们现在通过替换其内容为以下内容来编辑 Vagrantfile:
# -*- mode: ruby -*-
# vi: set ft=ruby :
require 'yaml'
vagrantConfig = YAML.load_file 'Vagrantfile.config.yml'
Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/vivid64"
config.vm.network "private_network", ip: vagrantConfig['ip']
# Mount local "~/www/B05032-Magento-Box/" path into box's "/vagrant-B05032-Magento-Box/" path
config.vm.synced_folder vagrantConfig['synced_folder']['host_path'], vagrantConfig['synced_folder']['guest_path'], owner:"vagrant", group: "www-data", mount_options:["dmode=775, fmode=664"]
# VirtualBox specific settings
config.vm.provider "virtualbox" do |vb|
vb.gui = false
vb.memory = "2048"
vb.cpus = 2
end
# <provisioner here>
end
上述代码首先包含了 yaml 库,然后读取 Vagrantfile.config.yml 文件的内容到 vagrantConfig 变量中。然后我们有一个 config 块,在其中我们定义了虚拟机类型、固定 IP 地址、主机和客户操作系统之间的共享文件夹,以及一些 VirtualBox 特定的细节,如分配的 CPU 和内存。
我们使用的是 ubuntu/vivid64 虚拟机,它代表 Ubuntu 15.04(Vivid Vervet)的服务器版本。原因是这个 Ubuntu 版本为我们提供了 MySQL 5.6.x 和 PHP 5.6.x,这些是安装 Magento 的要求之一。
我们进一步有一个配置条目为我们的虚拟机分配一个固定 IP。让我们现在在我们的主机操作系统的 hosts 文件中添加一个条目,如下所示:
192.168.10.10 magento.box
注意
我们将固定 IP 地址分配给我们的虚拟机的原因是,我们可以在主机操作系统内直接打开类似 http://magento.box 的 URL,然后访问客户操作系统内 Apache 提供的页面。
上一段代码的另一个重要部分是我们定义 synced_folder 的地方。除了源和目标路径外,这里的关键部分是 owner、group 和 mount_options。我们将这些设置为 vagrant 用户、www-data 用户组,以及目录和文件权限的 774 和 664,以便与 Magento 顺利配合。
让我们继续编辑 Vagrantfile,向其中添加几个配置器,一个接一个。我们通过将前一个示例中的 # <provisioner here> 替换为以下内容来实现:
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
config.vm.provision "shell", inline: "sudo apt-get update"
在这里,我们指示 Vagrant 将主机的 .gitconfig 文件传递到客户操作系统。这样做是为了让客户操作系统的 Git 设置继承自主机操作系统的 Git。然后我们调用 apt-get update 以更新客户操作系统。
配置 PHP
在 Vagrantfile 中进一步添加,我们运行多个配置器以安装 PHP、所需的 PHP 模块和 PHPUnit,如下所示:
config.vm.provision "shell", inline: "sudo apt-get -y install php5 php5-dev php5-curl php5-imagick php5-gd php5-mcrypt php5-mhash php5-mysql php5-xdebug php5-intl php5-xsl"
config.vm.provision "shell", inline: "sudo php5enmod mcrypt"
config.vm.provision "shell", inline: "echo \"xdebug.max_nesting_level=200\" >> /etc/php5/apache2/php.ini"
config.vm.provision "shell", inline: "sudo apt-get -y install phpunit"
注意
这里有一点值得指出——我们将 xdebug.max_nesting_level=200 写入 php.ini 文件的行。这样做是为了排除 Magento 不会启动并抛出 Maximum Functions Nesting Level of ‘100’ reached… 错误的可能性。
配置 MySQL
在 Vagrantfile 中进一步添加,我们运行配置器以安装 MySQL 服务器,如下所示:
config.vm.provision "shell", inline: "sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password #{vagrantConfig['mysql']['password']}'"
config.vm.provision "shell", inline: "sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password #{vagrantConfig['mysql']['password']}'"
config.vm.provision "shell", inline: "sudo apt-get -y install mysql-server"
config.vm.provision "shell", inline: "sudo service mysql start"
config.vm.provision "shell", inline: "sudo update-rc.d mysql defaults"
MySQL 安装有趣的地方在于,它要求在安装过程中提供密码和密码确认。这使得它成为配置过程中的一个麻烦部分,该过程期望 shell 命令简单地执行而不需要输入。为了绕过这个问题,我们使用 debconf-set-selections 来存储输入参数。我们从 Vagrantfile.config.yml 文件中读取密码,并将其传递给 debconf-set-selections。
安装完成后,update-rc.d mysql 默认设置将 MySQL 添加到操作系统启动过程中,从而确保在重启虚拟机时 MySQL 正在运行。
配置 Apache
在 Vagrantfile 中进一步添加,我们按照以下方式运行 Apache 配置器:
config.vm.provision "shell", inline: "sudo apt-get -y install apache2"
config.vm.provision "shell", inline: "sudo update-rc.d apache2 defaults"
config.vm.provision "shell", inline: "sudo service apache2 start"
config.vm.provision "shell", inline: "sudo a2enmod rewrite"
config.vm.provision "shell", inline: "sudo awk '/<Directory \\/>/,/AllowOverride None/{sub(\"None\", \"All\",$0)}{print}' /etc/apache2/apache2.conf > /tmp/tmp.apache2.conf"
config.vm.provision "shell", inline: "sudo mv /tmp/tmp.apache2.conf /etc/apache2/apache2.conf"
config.vm.provision "shell", inline: "sudo awk '/<Directory \\/var\\/www\\/>/,/AllowOverride None/{sub(\"None\", \"All\",$0)}{print}' /etc/apache2/apache2.conf > /tmp/tmp.apache2.conf"
config.vm.provision "shell", inline: "sudo mv /tmp/tmp.apache2.conf /etc/apache2/apache2.conf"
config.vm.provision "shell", inline: "sudo service apache2 stop"
上一段代码安装了 Apache,将其添加到启动序列中,启动它,并打开重写模块。然后我们对 Apache 配置文件进行了更新,因为我们想将 AllowOverride None 替换为 AllowOverride All,否则我们的 Magento 将无法工作。一旦完成更改,我们停止 Apache 以避免后续进程。
配置 Magento 安装
在 Vagrantfile 中进一步添加,我们现在将注意力转向 Magento 安装,我们将它分为几个步骤。首先,我们使用 Vagrant 的同步文件夹功能将主机的 /vagrant-B05032-Magento-Box/ 文件夹链接到客户,即 /var/www/html:
config.vm.provision "shell", inline: "sudo rm -Rf /var/www/html"
config.vm.provision "shell", inline: "sudo ln -s #{vagrantConfig['synced_folder']['guest_path']} /var/www/html"
然后,我们使用 composer create-project 命令从官方 repo.magento.com 源将 Magento 2 文件拉取到 /var/www/html/ 目录:
config.vm.provision "shell", inline: "curl -sS https://getcomposer.org/installer | php"
config.vm.provision "shell", inline: "mv composer.phar /usr/local/bin/composer"
config.vm.provision "shell", inline: "composer clearcache"
config.vm.provision "shell", inline: "echo '{\"http-basic\": {\"repo.magento.com\": {\"username\": \"#{vagrantConfig ['http_basic']['repo_magento_com']['username']}\",\"password\": \"#{vagrantConfig['http_basic']['repo_magento_com']['password']} \"}}, \"github-oauth\": {\"github.com\": \"#{vagrantConfig['github_oauth']['github_com']}\"}}' >> /root/.composer/auth.json"
config.vm.provision "shell", inline: "composer create-project -- repository-url=https://repo.magento.com/ magento/project- community-edition /var/www/html/"
然后,我们创建一个数据库,稍后将在其中安装 Magento:
config.vm.provision "shell", inline: "sudo mysql -- user=#{vagrantConfig['mysql']['username']} -- password=#{vagrantConfig['mysql']['password']} -e \"CREATE DATABASE #{vagrantConfig['magento']['db_name']};\""
我们随后从命令行运行 Magento 安装程序:
config.vm.provision "shell", inline: "sudo php /var/www/html/bin/magento setup:install --base- url=\"#{vagrantConfig['magento']['base_url']}\" --db- host=\"#{vagrantConfig['mysql']['host']}\" --db- user=\"#{vagrantConfig['mysql']['username']}\" --db- password=\"#{vagrantConfig['mysql']['password']}\" --db- name=\"#{vagrantConfig['magento']['db_name']}\" --admin- firstname=\"#{vagrantConfig['magento']['admin_firstname']}\" -- admin-lastname=\"#{vagrantConfig['magento']['admin_lastname']}\" --admin-email=\"#{vagrantConfig['magento']['admin_email']}\" -- admin-user=\"#{vagrantConfig['magento']['admin_user']}\" -- admin-password=\"#{vagrantConfig['magento']['admin_password']}\" --backend- frontname=\"#{vagrantConfig['magento']['backend_frontname']}\" - -language=\"#{vagrantConfig['magento']['language']}\" -- currency=\"#{vagrantConfig['magento']['currency']}\" -- timezone=\"#{vagrantConfig['magento']['timezone']}\""
config.vm.provision "shell", inline: "sudo php /var/www/html/bin/magento deploy:mode:set developer"
config.vm.provision "shell", inline: "sudo php /var/www/html/bin/magento cache:disable"
config.vm.provision "shell", inline: "sudo php /var/www/html/bin/magento cache:flush"
config.vm.provision "shell", inline: "sudo php /var/www/html/bin/magento setup:performance:generate-fixtures /var/www/html/setup/performance-toolkit/profiles/ce/small.xml"
上述代码显示我们还在安装 fixtures 数据。
在配置 Vagrantfile.config.yml 文件时,我们需要小心。Magento 安装对提供的数据非常敏感。我们需要确保我们为像邮件和密码这样的字段提供有效的数据,否则安装将失败,并显示类似于以下错误的错误:
SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost' (using password: NO)
User Name is a required field.
First Name is a required field.
Last Name is a required field.
'magento.box' is not a valid hostname for email address 'john.doe@magento.box'
'magento.box' appears to be a DNS hostname but cannot match TLD against known list
'magento.box' appears to be a local network name but local network names are not allowed
Password is required field.
Your password must be at least 7 characters.
Your password must include both numeric and alphabetic characters.
有了这个,我们就完成了 Vagrantfile 的内容。
现在,在 Vagrantfile 所在的同一目录下运行 vagrant up 命令将触发盒子创建过程。在这个过程中,之前列出的所有命令都将被执行。这个过程本身可能需要一个小时左右。
一旦 vagrant up 完成,我们可以发出另一个控制台命令,vagrant ssh,以登录到盒子。
同时,如果我们在我们浏览器中打开类似 http://magento.box 的 URL,我们应该看到 Magento 商店首页正在加载。
上述 Vagrantfile 简单地从官方 Magento Git 仓库中提取并从头开始安装 Magento。Vagrantfile 和 Vagrantfile.config.yml 可以进一步扩展和定制,以满足我们各自项目的需求,例如从私有 Git 仓库中提取代码、从共享驱动器中恢复数据库等。
这为我们提供了一个简单而强大的脚本过程,通过这个过程我们可以为团队中的其他开发者准备完全就绪的项目机器,以便他们能够快速启动。
设置生产环境
生产环境是面向客户的、专注于良好性能和可用性的环境。设置生产环境并不是我们开发者倾向于做的事情,尤其是当有关于扩展、负载均衡、高可用性等方面的稳健要求时。然而,有时我们可能需要设置一个简单的生产环境。有许多云服务提供商提供快速简单的解决方案。为了本节的目的,我们将转向 Amazon Web Services。
亚马逊网络服务简介
亚马逊网络服务(AWS)是一组远程计算服务,通常被称为网络服务。AWS 提供云中的按需计算资源和服务,具有 按使用付费 的定价。亚马逊提供了一个很好的 AWS 资源比较,说使用 AWS 资源而不是自己的资源,就像从电力公司购买电力而不是运行自己的发电机一样。
如果我们停下来思考一下,这不仅仅对系统操作角色,对我们这样的开发者来说也很有趣。我们(开发者)现在能够在几分钟内启动各种数据库、Web 应用服务器,甚至复杂的基础设施。我们可以运行这些服务几分钟、几小时或几天,然后关闭它们。同时,我们只需为实际使用付费,而不是像大多数托管服务那样支付全额的月费或年费。尽管 AWS 某些服务的整体定价可能不是最便宜的,但它确实提供了许多其他服务所不具备的商品化和易用性。商品化来自于诸如自动扩展资源这样的特性,与等效的本地基础设施相比,它通常可以提供显著的成本节约。
质量培训和认证计划是 AWS 生态系统的重要方面之一。认证适用于解决方案架构师、开发者和系统操作管理员,涵盖副高级和专业级别。尽管认证不是强制性的,但如果我们经常处理 AWS,我们被鼓励去参加。获得认证将证明我们在 AWS 平台上设计、部署和运行高度可用、成本效益和安全的应用的专长。
我们可以通过一个简单直观的基于 Web 的用户界面,即 AWS 管理控制台来管理我们的 AWS,该界面可在aws.amazon.com/console找到。登录 AWS 后,我们应该能看到以下类似的屏幕:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00002.jpeg
上一张图片显示了 AWS 管理控制台如何将 AWS 服务视觉上分组为几个主要组,如下所示:
-
计算
-
开发者工具
-
移动服务
-
存储与内容分发
-
管理工具
-
应用服务
-
数据库
-
安全与身份
-
网络
-
分析
-
企业应用
作为本章的一部分,我们将探讨计算组下的EC2服务和存储与内容分发组下的S3服务。
亚马逊弹性计算云(Amazon EC2)是一种提供可伸缩计算容量的云服务。我们可以将其视为云中的虚拟计算机,我们可以在任何时间打开和关闭它,只需几分钟。我们可以同时部署一台、数百台甚至数千台这样的机器实例。这使得计算容量具有可伸缩性。
S3 提供安全、持久且高度可扩展的对象存储。它旨在提供 99.99%的对象持久性。该服务提供了一个网络服务接口,可以从网络上的任何地方存储和检索任何数量的数据。S3 仅按实际使用的存储收费。S3 可以单独使用,也可以与其他 AWS 服务如 EC2 一起使用。
设置 S3 使用的访问权限
作为我们生产环境的一部分,拥有可靠的存储空间,我们可以存档数据库和媒体文件,这是很好的。Amazon S3 是一个可能的解决方案。
为了正确设置对 S3 可扩展存储服务的访问权限,我们需要快速了解一下 AWS 的身份和访问管理(简称IAM)。IAM 是一种网络服务,帮助我们安全地控制用户对 AWS 资源的访问。我们可以使用 IAM 来控制身份验证(谁可以使用我们的 AWS 资源)和授权(他们可以使用哪些资源以及如何使用)。更具体地说,正如我们很快将看到的,我们感兴趣的是用户和组。
创建 IAM 用户
本节描述了如何创建 IAM 用户。IAM 用户是我们创建在 AWS 中,用于代表使用它的人或服务在与 AWS 交互时的实体。
登录 AWS 控制台。
在用户菜单下,点击如下截图所示的安全凭证:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00003.jpeg
这将打开安全仪表板页面。
点击用户菜单应该打开一个类似于以下屏幕的界面:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00004.jpeg
在用户菜单下,我们点击创建新用户,这将打开一个类似于以下页面:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00005.jpeg
在这里,我们填写一个或多个用户的所需用户名,例如foggy_s3_user1,然后点击创建按钮。
我们现在应该看到一个类似于以下屏幕的界面:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00006.jpeg
在这里,我们可以点击下载凭证来启动 CSV 格式文件的下载,或者手动复制粘贴我们的凭证。
注意
访问密钥 ID和秘密访问密钥是我们将用于访问 S3 存储的两条信息。
点击关闭链接将我们带回到用户菜单,显示我们刚刚创建的用户,如下截图所示:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00007.jpeg
创建 IAM 组
本节描述了如何创建 IAM 组。组是我们可以将它们作为一个单一单元管理的 IAM 用户的集合。因此,让我们开始:
-
登录 AWS 控制台。
-
在用户菜单下,点击如下截图所示的安全凭证:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00008.jpeg
-
这将打开安全仪表板页面。点击组菜单应该打开一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00009.jpeg
-
在组菜单下,我们点击创建新组,这将打开一个类似于以下页面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00010.jpeg
-
在这里,我们填写所需的组名,例如
FoggyS3Test。 -
我们现在应该看到一个类似于以下屏幕的界面,我们需要选择策略类型组,然后点击下一步按钮:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00011.jpeg
-
我们选择AmazonS3FullAccess策略类型,并点击下一步按钮。现在将显示审查屏幕,要求我们审查提供的信息:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00012.jpeg
-
如果提供的信息正确,我们通过点击创建组按钮来确认。现在,我们应该能够在组菜单下看到我们的组,如下面的截图所示:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00013.jpeg
-
打开组名左侧的复选框,点击组操作下拉菜单,然后选择如下截图所示的添加用户到组:
-
这将打开如下截图所示的添加用户到组页面:
-
打开用户名左侧的复选框,并点击添加用户按钮。这应该会将所选用户添加到组中,并返回到组列表。
用户和组创建过程的结果是一个具有访问密钥 ID、秘密访问密钥和分配有AmazonS3FullAccess策略的用户组。我们将在演示将数据库备份到 S3 时使用这些信息。
设置 S3 用于数据库和媒体文件备份
S3 由存储桶组成。我们可以将存储桶视为我们 S3 账户中的第一级目录。然后,我们在该目录(存储桶)上设置权限和其他选项。在本节中,我们将创建自己的存储桶,包含两个名为database和media的空文件夹。在后续的环境设置过程中,我们将使用这些文件夹来备份我们的 MySQL 数据库和媒体文件。
我们首先登录到 AWS 管理控制台。
在存储与内容分发组下,我们点击S3。这将打开一个类似于以下截图的屏幕:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00016.jpeg
点击创建存储桶按钮。这将打开一个类似于以下截图的弹出窗口:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00017.jpeg
让我们提供一个独特的存储桶名称,最好是一个可以识别我们将要备份的database和media文件的项目的名称,然后点击创建按钮。为了本章节的目的,让我们假设我们选择了类似foggy-project-dhj6的东西。
我们的存储桶现在应该在所有存储桶列表中可见。如果我们点击它,将打开一个类似于以下截图的新屏幕:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00018.jpeg
在这里,我们点击创建文件夹按钮,并添加必要的database和media文件夹。
在根存储桶目录内,点击属性按钮,并填写如下截图所示的权限部分:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00019.jpeg
在这里,我们基本上将所有权限分配给了认证用户。
现在我们应该有一个 S3 存储桶,我们可以使用s3cmd控制台工具(我们很快会提到)将数据库和媒体备份存储到其中。
Bash 脚本用于自动化 EC2 设置
与Vagrantfile shell provisioners 类似,让我们继续创建一系列 bash shell 命令,我们可以使用这些命令进行生产环境设置。
第一组命令如下:
#!/bin/bash
apt-get update
apt-get -y install s3cmd
在这里,从#!/bin/bash表达式开始。这指定了我们正在执行的脚本类型。然后我们有一个系统更新和s3cmd工具的安装。s3cmd是一个免费命令行工具和客户端,用于上传、检索和管理 Amazon S3 中的数据。我们可以稍后使用它进行数据库和媒体文件的备份和恢复。
然后,我们使用以下命令安装postfix邮件服务器。由于 postfix 安装会在控制台触发图形界面,要求输入mailname和main_mailer_type,我们使用sudo debconf-set-selections绕过这些步骤。安装完成后,我们重新加载postfix。
sudo debconf-set-selections <<< "postfix postfix/mailname string magentize.me"
sudo debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Internet Site'"
sudo apt-get install -y postfix
sudo /etc/init.d/postfix reload
在 EC2 盒子上直接使用邮件服务器对于小型生产网站来说是可以的,我们预计不会有高流量或大量客户。对于更密集的生产网站,我们需要注意 Amazon,可能需要在端口25上设置节流,从而导致发出的电子邮件超时。在这种情况下,我们可以要求 Amazon 取消我们账户的限制,或者转向更健壮的服务,如Amazon Simple Email Service。
接下来,我们安装所有与 PHP 相关的组件。注意我们甚至安装了xdebug,尽管立即将其关闭。这可能在那些非常罕见的我们需要真正调试实时网站的时刻派上用场,然后我们可以将其关闭并尝试远程调试。我们进一步下载并设置 composer 到用户路径:
apt-get -y install php5 php5-dev php5-curl php5-imagick php5-gd php5- mcrypt php5-mhash php5-mysql php5-xdebug php5-intl php5-xsl
php5enmod mcrypt
php5dismod xdebug
service php5-fpm restart
apt-get -y install phpunit
echo "Starting Composer stuff" >> /var/tmp/box-progress.txt
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
然后,我们继续进行 MySQL 的安装。在这里,我们也使用debconf-set-selections来自动化控制台部分提供安装输入参数。安装完成后,MySQL 被启动并添加到启动过程中。
debconf-set-selections <<< 'mysql-server mysql-server/root_password password RrkSBi6VDg6C'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password RrkSBi6VDg6C'
apt-get -y install mysql-server
service mysql start
update-rc.d mysql defaults
除了 MySQL 之外,另一个主要组件是 Apache。我们使用以下命令来安装它。在使用 Apache 时,我们需要注意其apache2.conf文件。我们需要将 Magento 目录的AllowOverride None更改为AllowOverride All:
apt-get -y install apache2
update-rc.d apache2 defaults
service apache2 start
a2enmod rewrite
awk '/<Directory \/>/,/AllowOverride None/{sub("None", "All",$0)}{print}' /etc/apache2/apache2.conf > /tmp/tmp.apache2.conf
mv /tmp/tmp.apache2.conf /etc/apache2/apache2.conf
awk '/<Directory \/var\/www\/>/,/AllowOverride None/{sub("None", "All",$0)}{print}' /etc/apache2/apache2.conf > /tmp/tmp.apache2.conf
mv /tmp/tmp.apache2.conf /etc/apache2/apache2.conf
service apache2 restart
现在我们已经安装了 MySQL 和 Apache,我们继续将源代码文件放置到位。接下来,我们从官方的 Magento Git 仓库中拉取代码。这不同于我们在设置 vagrant 时使用的repo.magento.com。尽管在这种情况下,Magento Git 仓库是公开的,但我们的想法是能够从私有 GitHub 仓库中拉取代码。根据我们倾向于设置的生产环境,我们可以轻松地将下一部分替换为从任何其他私有 Git 仓库中拉取。
sudo rm -Rf /var/www/html/*
git clone https://github.com/magento/magento2.git /var/www/html/.
sudo composer config --global github-oauth.github.com 7d6da6bld50dub454edc27db70db78b1f8997e6
sudo composer install --working-dir="/var/www/html/"
mysql -uroot -pRrkSBi6VDg6C -e "CREATE DATABASE magento;"
PUBLIC_HOSTNAME="'wget -q -O - http://instance-data/latest/meta- data/public-hostname'"
小贴士
要从私有 git 仓库拉取代码,我们可以使用以下形式的命令,Git 克隆:https://<user>:<OAuthToken>@github.com/<user>/<repo>.git。
PUBLIC_HOSTNAME 变量存储了调用 http://instance-data/latest/meta-data/public-hostname URL 的 wget 命令的响应。这个 URL 是 AWS 的一个功能,允许我们获取当前的 EC2 实例元数据。然后我们在 Magento 安装期间使用 PUBLIC_HOSTNAME 变量,将其作为 --base-url 参数传递:
php /var/www/html/bin/magento setup:install --base- url="http://$PUBLIC_HOSTNAME" --db-host="127.0.0.1" --db- user="root" --db-password="RrkSBi6VDg6C" --db-name="magento" -- admin-firstname="John" --admin-lastname="Doe" --admin- email="john.doe@change.me" --admin-user="admin" --admin- password="pass123" --backend-frontname="admin" -- language="en_US" --currency="USD" --timezone="Europe/London"
前面的命令包含了许多 项目特定 的配置值,所以我们需要确保在简单地复制粘贴之前,适当地粘贴我们自己的信息。
现在我们确保 Magento 模式设置为生产,并且缓存已开启并刷新,以便重新生成:
php /var/www/html/bin/magento deploy:mode:set production
php /var/www/html/bin/magento cache:enable
php /var/www/html/bin/magento cache:flush
最后,我们重置 /var/www/html 目录的权限,以确保我们的 Magento 能够正常运行:
chown -R ubuntu:www-data /var/www/html
find /var/www/html -type f -print0 | xargs -r0 chmod 640
find /var/www/html -type d -print0 | xargs -r0 chmod 750
chmod -R g+w /var/www/html/pub
chmod -R g+w /var/www/html/var
chmod -R g+w /var/www/html/app
chmod -R g+w /var/www/html/vendor
我们需要对前面提到的 Git 和 Magento 安装示例保持谨慎。这里的想法是展示我们如何自动从公共或私有仓库设置 Git pull。对于这个特定案例,Magento 安装部分只是一个小小的额外奖励,并不是我们会在生产机器上实际做的事情。这个脚本的整个目的就是作为启动新 AMI 图像的蓝图。所以理想情况下,一旦代码被拉取,我们通常会从一些私有存储(如 S3)恢复数据库,并将其附加到我们的安装上。这样,一旦脚本完成,就可以完成文件、数据库和媒体的完整恢复。
把这个想法放在一边,让我们回到我们的脚本,进一步添加以下命令的每日数据库备份:
CRON_CMD="mysql --user=root --password=RrkSBi6VDg6C magento | gzip -9 > ~/database.sql.gz"
CRON_JOB="30 2 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab -
CRON_CMD="s3cmd --access_key="AKIAINLIM7M6WGJKMMCQ" -- secret_key="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" put ~/database.sql.gz s3://foggy-project-ghj7/database/database_'date +"%Y-%m-%d_%H-%M"'.sql.gz"
CRON_JOB="30 3 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab -
在这里,我们添加了一个凌晨 2:30 的 cron 作业,用于将数据库备份到名为 database.sql.gz 的家目录文件中。然后我们添加了另一个凌晨 3:30 执行的 cron 作业,将数据库备份推送到 S3 存储。
与数据库备份类似,我们可以使用以下命令集将媒体备份指令添加到我们的脚本中:
CRON_CMD="tar -cvvzf ~/media.tar.gz /var/www/html/pub/media/"
CRON_JOB="30 2 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab -
CRON_CMD="s3cmd --access_key="AKIAINLIM7M6WGJKMMCQ" -- secret_key="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" put ~/media.tar.gz s3://foggy-project-ghj7/media/media_'date +"%Y-%m- %d_%H-%M"'.tar.gz"
CRON_JOB="30 3 * * * $CRON_CMD"
( crontab -l | grep -v "$CRON_CMD" ; echo "$CRON_JOB" ) | crontab -
前面的命令中包含了几条编码信息。我们需要确保相应地粘贴我们的访问密钥、秘密密钥和 S3 桶名称。为了简化,我们在这里没有讨论将访问令牌硬编码到 cron 作业中的安全问题。亚马逊提供了一个广泛的 AWS 安全最佳实践 指南,可以通过官方 AWS 网站下载。
现在我们对自动化 EC2 设置的 bash 脚本可能的样子有了些了解,让我们继续设置 EC2 实例。
设置 EC2
按照以下步骤完成设置:
-
登录 AWS 控制台
-
在 计算 组下,点击 EC2,应该会打开一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00020.jpeg
-
点击 启动实例 按钮,应该会打开一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00021.jpeg
-
点击左侧的 社区 AMI 选项卡,并在搜索框中输入
Ubuntu Vivid,如图所示:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00022.jpeg小贴士
默认情况下,Ubuntu 15.x(Vivid Vervet)服务器支持 MySQL 5.6.x 和 PHP 5.6.x,这使得它成为安装 Magento 的良好候选者。
我们现在应该看到一个类似于以下屏幕的界面:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00023.jpeg
-
选择一个实例类型,然后点击下一步:配置实例详情按钮。我们现在应该看到一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00024.jpeg
备注
我们不会深入到每个选项的细节。简单来说,如果我们正在处理较小的生产站点,那么我们很可能可以将大多数这些选项保留为默认值。
-
确保将关机行为设置为停止。
-
在仍然位于步骤 3:配置实例详情屏幕的情况下,向下滚动到底部的高级详情区域并展开它。我们应该看到一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00025.jpeg
-
用户数据输入是我们将复制并粘贴上一节中描述的
auto-setup bash脚本的地方,如下截图所示:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00026.jpeg -
一旦我们复制并粘贴了用户数据,点击下一步:添加存储按钮。这应该会显示以下截图所示的屏幕:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00027.jpeg
-
在步骤 4:添加存储中,我们可以选择一个或多个卷来附加到我们的 EC2 实例。最好选择 SSD 类型的存储以获得更快的性能。一旦设置了卷,点击下一步:标记实例。我们现在应该看到一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00028.jpeg
-
标记实例屏幕允许我们分配标签。标签使我们能够根据目的、所有者、环境或其他方式对 AWS 资源进行分类。一旦我们分配了一个或多个标签,我们点击下一步:配置安全组按钮。我们现在应该看到一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00029.jpeg
-
配置安全组界面允许我们设置入站和出站流量的规则。我们希望能够访问该设备上的 SSH、HTTP、HTTPS 和 SMTP 服务。一旦我们添加了所需的规则,点击审查和启动按钮。这将打开一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00030.jpeg
-
审查实例启动屏幕是我们可以查看到目前为止配置的盒子的总结的地方。如果需要,我们可以返回并编辑单个设置。一旦我们对总结满意,我们点击启动按钮。这将打开一个类似于以下弹出窗口:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00031.jpeg
-
在这里,我们可以选择一个现有的安全密钥或创建一个新的密钥。密钥以 PEM 格式提供。一旦我们选择了密钥,我们点击启动实例按钮。
我们现在应该看到一个类似于以下屏幕的启动状态界面:
https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00032.jpeg
-
点击实例名称链接应该会带我们回到EC2 仪表板,如下截图所示:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00033.jpeg
关于前面的图像,我们现在应该能够通过以下任一控制台命令连接到我们的 EC2 服务器:
ssh -i /path/to/magento-box.pem ubuntu@ec2-52-29-35-49.eu-central-1.compute.amazonaws.com
ssh -i /path/to/magento-box.pem ubuntu@52.29.35.49
我们的 EC2 服务器执行传递给它的所有 shell 命令可能需要一些时间。我们可以方便地 SSH 到服务器,然后执行以下命令来获取当前进度的概述:
sudo tail -f /var/tmp/box-progress.txt
通过这样,我们完成了实例启动过程。
设置弹性 IP 和 DNS
现在我们已经有一个 EC2 服务器,让我们继续创建所谓的弹性 IP。弹性 IP 地址是为动态云计算设计的静态 IP 地址。它与 AWS 账户相关联,而不是某个特定的实例。这使得它很容易从一个实例重新映射到另一个实例。
让我们继续创建一个弹性 IP,如下所示:
-
登录到 AWS 控制台。
-
在计算组下,点击EC2,这将带我们到EC2 仪表板。
-
在EC2 仪表板下,在“网络和安全”分组下的左侧区域,点击弹性 IP。这将打开一个类似于以下屏幕的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00034.jpeg
-
点击分配新地址按钮,这将打开一个类似于以下弹出窗口的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00035.jpeg
-
点击是,分配按钮,这将打开另一个类似于以下弹出窗口的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00036.jpeg
-
现在弹性 IP 地址已创建,在表格列表中右键单击它应该会弹出如下截图所示的下拉菜单:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00037.jpeg
-
点击关联地址链接。这将打开一个类似于以下弹出窗口的界面:https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/mgnt2-bgd/img/00038.jpeg
-
在关联地址弹出窗口中,我们选择要分配弹性 IP 地址的实例,然后点击关联按钮。
到目前为止,我们的 EC2 服务器已分配了一个静态(弹性 IP)地址。现在我们可以登录到我们的域名注册商,并将 DNS 的 A 记录指向我们刚刚创建的弹性 IP。
在我们等待 DNS 更改生效之前,还有一件事需要处理。我们需要 SSH 到我们的服务器并执行以下命令集:
mysql -uroot -pRrkSBi6VDg6C -e "USE magento; UPDATE core_config_data SET value = 'http://our-domain.something/' WHERE path LIKE "%web/unsecure/base_url%";"
php /var/www/html/bin/magento cache:flush
这将更新 Magento 的 URL,因此一旦 DNS 更改生效,我们就可以通过 Web 浏览器访问它。通过一些前期规划,我们本可以轻松地将这部分内容作为我们 EC2 实例的用户数据的一部分,只需在最初提供正确的--base-url参数值即可。
摘要
在本章中,我们主要关注了两件事:设置开发和生产环境。
作为开发环境的一部分,我们采用了如 VirtualBox 和 Vagrant 等免费软件来管理我们的环境设置。仅设置本身就归结为一个单独的Vagrantfile脚本,该脚本包含了安装从 Ubuntu 服务器、PHP、Apache、MySQL 甚至包括 Magento 本身的必要命令集。我们绝不应该将此脚本视为最终的,而仅仅将其视为设置我们开发环境的有效脚本。在使开发环境更接近项目特定需求上投入时间,从团队生产力的角度来看是值得的。
然后,我们转向生产环境。在这里,我们研究了 Amazon Web Services,沿途使用了 S3 和 EC2。生产环境也附带了自己的脚本安装过程,该过程设置了大多数东西。同样,这个脚本也绝不是最终的,而仅仅是一种设置事物的有效方式;它更多的是一个如何操作的基例。
在下一章中,我们将更详细地探讨一些编程概念和约定。
第三章. 编程概念和约定
凭借多年的经验,Magento 平台发展起来,实现了许多行业概念、标准和约定。在本章中,我们将探讨几个在日常与 Magento 开发互动中突出的独立部分。
在本章中,我们将讨论以下部分:
-
Composer
-
服务合同
-
代码生成
-
var目录 -
编码标准
Composer
Composer是一个处理 PHP 依赖管理的工具。它不像 Linux 系统中的Yum和Apt那样是一个包管理器。尽管它处理库(包),但它是在每个项目级别上处理的。它不会全局安装任何东西。Composer 是一个多平台工具。因此,它在 Windows、Linux 和 OS X 上运行得同样好。
在机器上安装 Composer 就像在项目目录中运行安装程序一样简单,使用以下命令即可:
curl -sS https://getcomposer.org/installer | php
更多关于 Composer 安装的信息可以在其官方网站上找到,可以通过访问getcomposer.org查看。
Composer 用于获取 Magento 及其使用的第三方组件。如前一章所见,以下composer命令是将所有内容拉入指定目录的命令:
composer create-project --repository-url=https://repo.magento.com/ magento/project-enterprise-edition <installation directory name>
一旦下载并安装了 Magento,你可以在其目录中找到许多composer.json文件。假设<安装目录名称>是magento2,如果我们执行如find magento2/ -name 'composer.json'之类的快速搜索命令,那么将产生超过 100 个composer.json文件。其中一些文件(部分)如下所示:
/vendor/magento/module-catalog/composer.json
/vendor/magento/module-cms/composer.json
/vendor/magento/module-contact/composer.json
/vendor/magento/module-customer/composer.json
/vendor/magento/module-sales/composer.json
/...
/vendor/magento/theme-adminhtml-backend/composer.json
/vendor/magento/theme-frontend-blank/composer.json
/vendor/magento/theme-frontend-luma/composer.json
/vendor/magento/language-de_de/composer.json
/vendor/magento/language-en_us/composer.json
/...
/composer.json
/dev/tests/...
/vendor/magento/framework/composer.json
最相关的文件可能是位于magento目录根目录下的composer.json文件。其内容看起来像这样:
{
"name": "magento/project-community-edition",
"description": "eCommerce Platform for Growth (Community Edition)",
"type": "project",
"version": "2.0.0",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"repositories": [
{
"type": "composer",
"url": "https://repo.magento.com/"
}
],
"require": {
"magento/product-community-edition": "2.0.0",
"composer/composer": "@alpha",
"magento/module-bundle-sample-data": "100.0.*",
"magento/module-widget-sample-data": "100.0.*",
"magento/module-theme-sample-data": "100.0.*",
"magento/module-catalog-sample-data": "100.0.*",
"magento/module-customer-sample-data": "100.0.*",
"magento/module-cms-sample-data": "100.0.*",
"magento/module-catalog-rule-sample-data": "100.0.*",
"magento/module-sales-rule-sample-data": "100.0.*",
"magento/module-review-sample-data": "100.0.*",
"magento/module-tax-sample-data": "100.0.*",
"magento/module-sales-sample-data": "100.0.*",
"magento/module-grouped-product-sample-data": "100.0.*",
"magento/module-downloadable-sample-data": "100.0.*",
"magento/module-msrp-sample-data": "100.0.*",
"magento/module-configurable-sample-data": "100.0.*",
"magento/module-product-links-sample-data": "100.0.*",
"magento/module-wishlist-sample-data": "100.0.*",
"magento/module-swatches-sample-data": "100.0.*",
"magento/sample-data-media": "100.0.*",
"magento/module-offline-shipping-sample-data": "100.0.*"
},
"require-dev": {
"phpunit/phpunit": "4.1.0",
"squizlabs/php_codesniffer": "1.5.3",
"phpmd/phpmd": "@stable",
"pdepend/pdepend": "2.0.6",
"sjparkinson/static-review": "~4.1",
"fabpot/php-cs-fixer": "~1.2",
"lusitanian/oauth": "~0.3 <=0.7.0"
},
"config": {
"use-include-path": true
},
"autoload": {
"psr-4": {
"Magento\\Framework\\": "lib/internal/Magento/Framework/",
"Magento\\Setup\\": "setup/src/Magento/Setup/",
"Magento\\": "app/code/Magento/"
},
"psr-0": {
"": "app/code/"
},
"files": [
"app/etc/NonComposerComponentRegistration.php"
]
},
"autoload-dev": {
"psr-4": {
"Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/",
"Magento\\Tools\\": "dev/tools/Magento/Tools/",
"Magento\\Tools\\Sanity\\": "dev/build/publication/sanity/ Magento/Tools/Sanity/",
"Magento\\TestFramework\\Inspection\\": "dev/tests/static/framework/Magento/ TestFramework/Inspection/",
"Magento\\TestFramework\\Utility\\": "dev/tests/static/framework/Magento/ TestFramework/Utility/"
}
},
"minimum-stability": "alpha",
"prefer-stable": true,
"extra": {
"magento-force": "override"
}
}
Composer 的 JSON 文件遵循一定的模式。你可以在getcomposer.org/doc/04-schema.md找到这个模式的详细文档。遵循该模式确保了 composer 文件的正确性。我们可以看到,所有列出的键,如name、description、require、config等,都是由该模式定义的。
让我们看看单个模块的composer.json文件。一个具有最少依赖的简单模块是Contact模块,其vendor/magento/module-contact/composer.json内容如下所示:
{
"name": "magento/module-contact",
"description": "N/A",
"require": {
"php": "~5.5.0|~5.6.0|~7.0.0",
"magento/module-config": "100.0.*",
"magento/module-store": "100.0.*",
"magento/module-backend": "100.0.*",
"magento/module-customer": "100.0.*",
"magento/module-cms": "100.0.*",
"magento/framework": "100.0.*"
},
"type": "magento2-module",
"version": "100.0.2",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Magento\\Contact\\": ""
}
}
}
你会看到模块定义了对 PHP 版本和其他模块的依赖。此外,你还会看到 PSR-4 用于自动加载以及直接加载registration.php文件。
接下来,让我们看看en_us语言模块中的vendor/magento/language-en_us/composer.json文件的内容:
{
"name": "magento/language-en_us",
"description": "English (United States) language",
"version": "100.0.2",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"require": {
"magento/framework": "100.0.*"
},
"type": "magento2-language",
"autoload": {
"files": [
"registration.php"
]
}
}
最后,让我们看看luma主题中的vendor/magento/theme-frontend-luma/composer.json文件的内容:
{
"name": "magento/theme-frontend-luma",
"description": "N/A",
"require": {
"php": "~5.5.0|~5.6.0|~7.0.0",
"magento/theme-frontend-blank": "100.0.*",
"magento/framework": "100.0.*"
},
"type": "magento2-theme",
"version": "100.0.2",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"autoload": {
"files": [
"registration.php"
]
}
}
如前所述,在 Magento 中散布着许多更多的 composer 文件。
服务合同
服务合约是一组由模块定义的 PHP 接口。此合约包括数据接口和服务接口。
数据接口的作用是保持数据完整性,而服务接口的作用是隐藏业务逻辑细节,使其不被服务消费者看到。
数据接口定义了各种功能,如验证、实体信息、搜索相关功能等。它们定义在单个模块的Api/Data目录中。为了更好地理解其真正含义,让我们看看Magento_Cms模块的数据接口。在vendor/magento/module-cms/Api/Data/目录中,定义了四个接口,如下所示:
BlockInterface.php
BlockSearchResultsInterface.php
PageInterface.php
PageSearchResultsInterface.php
CMS模块实际上处理两个实体,一个是Block,另一个是Page。查看前面代码中定义的接口,我们可以看到我们有一个针对实体的单独数据接口和针对搜索结果的单独数据接口。
让我们更仔细地看看BlockInterface.php文件的内容(已去除),其定义如下:
namespace Magento\Cms\Api\Data;
interface BlockInterface
{
const BLOCK_ID = 'block_id';
const IDENTIFIER = 'identifier';
const TITLE = 'title';
const CONTENT = 'content';
const CREATION_TIME = 'creation_time';
const UPDATE_TIME = 'update_time';
const IS_ACTIVE = 'is_active';
public function getId();
public function getIdentifier();
public function getTitle();
public function getContent();
public function getCreationTime();
public function getUpdateTime();
public function isActive();
public function setId($id);
public function setIdentifier($identifier);
public function setTitle($title);
public function setContent($content);
public function setCreationTime($creationTime);
public function setUpdateTime($updateTime);
public function setIsActive($isActive);
}
前面的接口定义了当前实体的所有获取器和设置器方法,以及表示实体字段名的常量值。这些数据接口不包括管理操作,如delete。这个特定接口的实现可以在vendor/magento/module-cms/Model/Block.php文件中看到,其中这些常量被使用,如下(部分)所示:
public function getTitle()
{
return $this->getData(self::TITLE);
}
public function setTitle($title)
{
return $this->setData(self::TITLE, $title);
}
服务接口包括管理、仓库和元数据接口。这些接口直接定义在模块的Api目录中。回顾Magento Cms模块,其vendor/magento/module-cms/Api/目录中有两个服务接口,定义如下:
BlockRepositoryInterface.php
PageRepositoryInterface.php
快速查看BlockRepositoryInterface.php的内容,揭示以下(部分)内容:
namespace Magento\Cms\Api;
use Magento\Framework\Api\SearchCriteriaInterface;
interface BlockRepositoryInterface
{
public function save(Data\BlockInterface $block);
public function getById($blockId);
public function getList(SearchCriteriaInterface $searchCriteria);
public function delete(Data\BlockInterface $block);
public function deleteById($blockId);
}
在这里,我们看到用于保存、检索、搜索和删除实体的方法。
这些接口随后通过 Web API 定义实现,正如我们将在第九章中看到的,结果是定义良好且持久的 API,其他模块和第三方集成者可以消费这些 API。
代码生成
Magento 应用程序的一个巧妙特性是代码生成。正如其名称所暗示的,代码生成会生成不存在的类。这些类在 Magento 的var/generation目录中生成。
var/generation目录内的目录结构在一定程度上类似于核心的vendor/magento/module-*和app/code目录。更精确地说,它遵循模块结构。代码是为所谓的Factory、Proxy和Interceptor类生成的。
工厂类创建一个类型的实例。例如,由于在vendor/magento目录及其代码中某处调用了Magento\Catalog\Model\ProductFactory类,因此已创建了一个包含Magento\Catalog\Model\ProductFactory类的var/generation/Magento/Catalog/Model/ProductFactory.php文件。在运行时,当代码中调用{someClassName}Factory时,如果不存在,Magento 会在var/generation目录下创建一个工厂类。以下代码是ProductFactory类的(部分)示例:
namespace Magento\Catalog\Model;
/**
* Factory class for @see \Magento\Catalog\Model\Product
*/
class ProductFactory
{
//...
/**
* Create class instance with specified parameters
*
* @param array $data
* @return \Magento\Catalog\Model\Product
*/
public function create(array $data = array())
{
return $this->_objectManager->create($this->_instanceName, $data);
}
}
注意create方法,它创建并返回Product类型实例。还要注意生成的代码是如何类型安全的,为集成开发环境(IDEs)提供@return注解以支持自动完成功能。
工厂用于将对象管理器与业务代码隔离开来。与业务对象不同,工厂可以依赖于对象管理器。
代理类是对某些基类的包装。代理类比基类提供更好的性能,因为它们可以在不实例化基类的情况下被实例化。基类仅在调用其方法时才被实例化。这对于将基类用作依赖项且实例化耗时且仅在执行路径的某些部分使用其方法的情况非常方便。
与工厂类似,代理类也生成在var/generation目录下。
如果我们查看包含Magento\Catalog\Model\Session\Proxy类的var/generation/Magento/Catalog/Model/Session/Proxy.php文件,我们会看到它实际上扩展了Magento\Catalog\Model\Session。包装的代理类在过程中实现了几个魔法方法,例如__sleep、__wakeup、__clone和__call。
拦截器是另一种由 Magento 自动生成的类类型。它与插件功能相关,将在第六章中详细讨论,插件。
为了触发代码生成,我们可以使用控制台上可用的代码编译器。我们可以运行单租户编译器或多租户编译器。
单租户意味着一个网站和商店,并且通过以下命令执行。
magento setup:di:compile
多租户意味着不止一个独立的 Magento 应用程序,并且通过以下命令执行。
magento setup:di:compile-multi-tenant
代码编译生成工厂、代理、拦截器和setup/src/Magento/Setup/Module/Di/App/Task/Operation/目录中列出的其他几个类。
变量目录
Magento 执行大量的缓存和某些类类型的自动生成。这些缓存和生成的类都位于 Magento 根目录的var目录中。var目录的常规内容如下:
cache
composer_home
generation
log
di
view_preprocessed
page_cache
在开发过程中,我们很可能需要定期清除这些,以便我们的更改能够生效。
我们可以按照以下方式发出控制台命令以清除单个目录:
rm -rf {Magento root dir}/var/generation/*
或者,我们可以使用内置的 bin/magento 控制台工具来触发命令,自动删除相应的目录,如下所示:
-
bin/magento setup:upgrade: 这将更新 Magento 数据库模式和数据。在此过程中,它将截断var/di和var/generation目录。 -
bin/magento setup:di:compile: 这将清除var/generation目录。在此之后,它将再次编译其中的代码。 -
bin/magento deploy:mode:set {mode}: 这将模式从开发模式更改为生产模式,反之亦然。在此过程中,它将截断var/di、var/generation和var/view_preprocessed目录。 -
bin/magento cache:clean {type}: 这将清理var/cache和var/page_cache目录。
在开发过程中,始终要记住 var 目录。否则,代码可能会遇到异常并无法正常工作。
编码标准
代码文档。其想法是统一所有文件中代码 DocBlocks 的使用,无论使用哪种编程语言。然而,特定语言的 DocBlock 标准可能会覆盖它。
这是 Google JavaScript 风格指南和 JSDoc 的一个子集,可以在 [`usejsdoc.org`](http://usejsdoc.org) 找到。`
`The **LESS**` **编码标准定义了在处理 LESS 和 CSS 文件时的格式化和编码风格。**
**注意**
**您可以在 [devdocs.magento.com](http://devdocs.magento.com) 上阅读有关每个标准的实际细节,因为它们过于广泛,无法在本书中涵盖。**
``# 摘要 在本章中,我们探讨了 Composer,这是我们安装 Magento 时将首先与之交互的东西之一。然后,我们转向服务合同,它是 Magento 架构中最强大的部分之一,结果是我们使用的是老式的 PHP 接口。此外,我们还介绍了一些关于 Magento 代码生成功能的内容。因此,我们对 Factory 和 Proxy 类有了基本的了解。然后,我们查看 var 目录并探讨了其在开发过程中的作用。最后,我们简要介绍了 Magento 中使用的编码标准。 在下一章中,我们将讨论依赖注入,这是 Magento 最重要的架构部分之一。```
第四章。模型和集合
如同大多数现代框架和平台,如今 Magento 采用 对象关系映射(ORM)方法而非原始 SQL 查询。尽管底层机制仍然归结为 SQL,我们现在严格处理对象。这使得我们的应用程序代码更易读、更易于管理,并从供应商特定的 SQL 差异中隔离出来。模型、资源和集合是三种类型的类协同工作,使我们能够全面管理实体数据,从加载、保存、删除和列出实体。我们的大部分数据访问和管理将通过名为 Magento 模型的 PHP 类来完成。模型本身不包含与数据库通信的任何代码。
数据库通信部分被解耦成其自身的 PHP 类,称为资源类。然后每个模型都被分配一个资源类。在模型上调用 load、save 或 delete 方法会被委托给资源类,因为它们是实际从数据库读取、写入和删除数据的地方。理论上,有了足够的知识,可以编写针对各种数据库供应商的新资源类。
除了模型和资源类之外,我们还有集合类。我们可以将集合视为单个模型实例的数组。在基本层面上,集合扩展自 \Magento\Framework\Data\Collection 类,该类实现了来自 标准 PHP 库(SPL)的 \IteratorAggregate 和 \Countable 以及一些其他 Magento 特定的类。
更多的时候,我们将模型和资源视为一个单一统一的事物,因此简单地称之为模型。Magento 处理两种类型的模型,我们可以将它们分类为简单和 EAV 模型。
在本章中,我们将涵盖以下主题:
-
创建一个微型模块
-
创建一个简单模型
-
EAV 模型
-
理解模式和数据脚本流程
-
创建安装模式脚本(
InstallSchema.php) -
创建升级模式脚本(
UpgradeSchema.php) -
创建安装数据脚本(
InstallData.php) -
创建升级数据脚本(
UpgradeData.php) -
实体 CRUD 操作
-
管理集合
创建一个微型模块
为了本章的目的,我们将创建一个名为 Foggyline_Office 的小型模块。
该模块将定义两个实体,如下所示:
-
Department:一个具有以下字段的简单模型:-
entity_id:主键 -
name:部门的名称,字符串值
-
-
Employee:一个具有以下字段和属性的 EAV 模型:-
字段:
-
entity_id:主键 -
department_id:外键,指向Department.entity_id -
email:员工的唯一电子邮件,字符串值 -
first_name:员工的姓名,字符串值 -
last_name:员工的姓氏,字符串值
-
-
属性:
-
service_years:员工的工龄,整数值 -
dob:员工的出生日期,日期时间值 -
salary– 月薪,十进制值 -
vat_number:增值税号,(短)字符串值 -
note:关于员工的可能注释,(长)字符串值
-
-
每个模块都以 registration.php 和 module.xml 文件开始。为了我们本章模块的目的,让我们创建一个包含以下内容的 app/code/Foggyline/Office/registration.php 文件:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Foggyline_Office',
__DIR__
);
registration.php 文件可以看作是我们模块的入口点。
现在,让我们创建一个包含以下内容的 app/code/Foggyline/Office/etc/module.xml 文件:
<config xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/ etc/module.xsd">
<module name="Foggyline_Office" setup_version="1.0.0">
<sequence>
<module name="Magento_Eav"/>
</sequence>
</module>
</config>
我们将在后面的章节中更详细地介绍 module.xml 文件的结构。现在,我们只关注 sequence 中的 setup_version 属性和 module 元素。
setup_version 的值很重要,因为我们可能会在我们的模式安装脚本(InstallSchema.php)文件中使用它,有效地将安装脚本转换为更新脚本,正如我们很快将展示的那样。
sequence 元素是 Magento 为我们的模块设置依赖关系的方式。鉴于我们的模块将使用 EAV 实体,我们列出 Magento_Eav 作为依赖项。
创建一个简单的模型
根据要求,Department 实体被建模为一个简单的模型。我们之前提到,每当谈到模型时,我们隐含地想到 model 类、resource 类和 collection 类形成一个单元。
让我们先从创建一个 model 类开始,部分定义在 app/code/Foggyline/Office/Model/Department.php 文件中,如下所示:
namespace Foggyline\Office\Model;
class Department extends \Magento\Framework\Model\AbstractModel
{
protected function _construct()
{
$this-> _init('Foggyline\Office\Model \ResourceModel\Department');
}
}
这里发生的一切就是,我们正在扩展 \Magento\Framework\Model\AbstractModel 类,并在 _construct 方法中触发 $this->_init 方法,传递我们的 resource 类。
AbstractModel 进一步扩展了 \Magento\Framework\Object。我们的 model 类最终从 Object 继承的事实意味着我们不需要在 model 类上定义属性名称。Object 为我们做的事情是,它使我们能够神奇地获取、设置、取消设置和检查属性上的值存在。为了给出一个比 name 更健壮的例子,想象我们的实体在以下代码中有一个名为 employee_average_salary 的属性:
$department->getData('employee_average_salary');
$department->getEmployeeAverageSalary();
$department->setData('employee_average_salary', 'theValue');
$department->setEmployeeAverageSalary('theValue');
$department->unsetData('employee_average_salary');
$department->unsEmployeeAverageSalary();
$department->hasData('employee_average_salary');
$department->hasEmployeeAverageSalary();
这之所以有效,是因为 Object 实现了 setData、unsetData、getData 和魔法 __call 方法。魔法 __call 方法实现的美丽之处在于,它理解 getEmployeeAverageSalary、setEmployeeAverageSalary、unsEmployeeAverageSalary 和 hasEmployeeAverageSalary 这样的方法调用,即使它们不存在于 Model 类上。然而,如果我们选择在我们的 Model 类中实现一些这些方法,我们可以自由地这样做,并且当调用它们时,Magento 会捕获它们。
这是 Magento 的一个重要方面,有时对新来者来说可能有些令人困惑。
一旦我们有一个 model 类,我们创建一个模型 resource 类,部分定义在 app/code/Foggyline/Office/Model/ResourceModel/Department.php 文件中,如下所示:
namespace Foggyline\Office\Model\ResourceModel;
class Department extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
protected function _construct()
{
$this->_init('foggyline_office_department', 'entity_id');
}
}
我们继承自\Magento\Framework\Model\ResourceModel\Db\AbstractDb的resource类在_construct方法中触发$this->_init方法的调用。$this->_init接受两个参数。第一个参数是表名foggyline_office_department,我们的模型将在此表中持久化其数据。第二个参数是表中的主键列名entity_id。
AbstractDb进一步扩展了Magento\Framework\Model\ResourceModel\AbstractResource。
注意
资源类是向数据库通信的关键。我们只需要命名表及其主键,我们的模型就可以保存、删除和更新实体。
最后,我们创建我们的collection类,部分定义在app/code/Foggyline/Office/Model/ResourceModel/Department/Collection.php文件中,如下所示:
namespace Foggyline\Office\Model\ResourceModel\Department;
class Collection extends \Magento\Framework\Model\ResourceModel \Db\Collection\AbstractCollection
{
protected function _construct()
{
$this->_init(
'Foggyline\Office\Model\Department',
'Foggyline\Office\Model\ResourceModel\Department'
);
}
}
collection类继承自\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection,并且类似于model和resource类,在_construct方法中调用$this->_init方法。这次,_init接受两个参数。第一个参数是完整的model类名Foggyline\Office\Model\Department,第二个参数是完整的资源类名Foggyline\Office\Model\ResourceModel\Department。
AbstractCollection实现了Magento\Framework\App\ResourceConnection\SourceProviderInterface,并扩展了\Magento\Framework\Data\Collection\AbstractDb。AbstractDb进一步扩展了\Magento\Framework\Data\Collection。
值得花些时间研究这些collection类的内部结构,因为这是我们处理获取符合某些搜索标准实体列表时的首选地方。
创建一个 EAV 模型
根据要求,Employee实体被建模为一个 EAV 模型。
让我们先从创建一个 EAV model类开始,部分定义在app/code/Foggyline/Office/Model/Employee.php文件中,如下所示:
namespace Foggyline\Office\Model;
class Employee extends \Magento\Framework\Model\AbstractModel
{
const ENTITY = 'foggyline_office_employee';
public function _construct()
{
$this-> _init('Foggyline\Office \Model \ResourceModel\Employee');
}
}
在这里,我们继承自\Magento\Framework\Model\AbstractModel类,这与之前描述的简单模型相同。这里唯一的区别是我们定义了一个ENTITY常量,但这只是为了后面的语法糖;它对实际的model类没有实际意义。
接下来,我们创建一个 EAV 模型resource类,部分定义在app/code/Foggyline/Office/Model/ResourceModel/Employee.php文件中,如下所示:
namespace Foggyline\Office\Model\ResourceModel;
class Employee extends \Magento\Eav\Model\Entity\AbstractEntity
{
protected function _construct()
{
$this->_read = 'foggyline_office_employee_read';
$this->_write = 'foggyline_office_employee_write';
}
public function getEntityType()
{
if (empty($this->_type)) {
$this->setType(\Foggyline\Office\Model \Employee::ENTITY);
}
return parent::getEntityType();
}
}
我们的resource类继承自\Magento\Eav\Model\Entity\AbstractEntity,并通过_construct方法设置$this->_read和$this->_write类属性。这些可以自由分配为我们想要的任何值,最好遵循我们模块的命名模式。读取和写入连接需要命名,否则当使用我们的实体时,Magento 会产生错误。
getEntityType 方法内部将 _type 值设置为 \Foggyline\Office\Model\Employee::ENTITY,即字符串 foggyline_office_employee。这个相同的值存储在 eav_entity_type 表的 entity_type_code 列中。此时,eav_entity_type 表中没有这样的条目。这是因为安装模式脚本将会创建一个,正如我们很快将要演示的那样。
最后,我们创建我们的 collection 类,部分定义在 app/code/Foggyline/Office/Model/ResourceModel/Employee/Collection.php 文件中,如下所示:
namespace Foggyline\Office\Model\ResourceModel\Employee;
class Collection extends \Magento\Eav\Model\Entity\Collection\AbstractCollection
{
protected function _construct()
{
$this->_init('Foggyline\Office\Model\Employee', 'Foggyline\Office\Model\ResourceModel\Employee');
}
}
collection 类继承自 \Magento\Eav\Model\Entity\Collection\AbstractCollection,并且与模型类类似,在 _construct 中调用 $this->_init 方法。_init 接受两个参数:完整的模型类名 Foggyline\Office\Model\Employee 和完整的资源类名 Foggyline\Office\Model\ResourceModel\Employee。
AbstractCollection 与简单模型集合类具有相同的父树,但自身实现了很多 EAV 集合特定的方法,如 addAttributeToFilter、addAttributeToSelect、addAttributeToSort 等。
注意
如我们所见,EAV 模型看起来与简单模型非常相似。区别主要在于 resource 类和 collection 类的实现及其一级父类。然而,我们需要记住,这里给出的例子是最简单的一种。如果我们查看数据库中的 eav_entity_type 表,我们可以看到其他实体类型使用了 attribute_model、entity_attribute_collection、increment_model 等。这些都是我们可以定义在 EAV 模型旁边的先进属性,使其更接近 catalog_product 实体类型的实现,这可能是 Magento 中最健壮的一种。这种高级 EAV 使用超出了本书的范围,因为它可能值得一本自己的书。
现在我们已经设置了简单和 EAV 模型,是时候考虑安装必要的数据库模式和可能预先填充一些数据了。这是通过模式和数据脚本完成的。
理解模式和数据脚本流程
简而言之,模式脚本的作用是创建支持您模块逻辑的数据库结构。例如,创建一个表,我们的实体将在此表中持久化其数据。数据脚本的作用是管理现有表中的数据,通常以在模块安装期间添加一些示例数据的形式。
如果我们回顾几步,我们可以注意到数据库中的 schema_version 和 data_version 与我们的 module.xml 文件中的 setup_version 数字相匹配。它们都意味着相同的事情。如果我们现在更改 module.xml 文件中的 setup_version 数字并再次运行 php bin/magento setup:upgrade 控制台命令,我们的数据库 schema_version 和 data_version 将更新到这个新版本号。
这是通过模块的 install 和 upgrade 脚本来实现的。如果我们快速查看 setup/src/Magento/Setup/Model/Installer.php 文件,我们可以看到一个名为 getSchemaDataHandler 的函数,其内容如下:
private function getSchemaDataHandler($moduleName, $type)
{
$className = str_replace('_', '\\', $moduleName) . '\Setup';
switch ($type) {
case 'schema-install':
$className .= '\InstallSchema';
$interface = self::SCHEMA_INSTALL;
break;
case 'schema-upgrade':
$className .= '\UpgradeSchema';
$interface = self::SCHEMA_UPGRADE;
break;
case 'schema-recurring':
$className .= '\Recurring';
$interface = self::SCHEMA_INSTALL;
break;
case 'data-install':
$className .= '\InstallData';
$interface = self::DATA_INSTALL;
break;
case 'data-upgrade':
$className .= '\UpgradeData';
$interface = self::DATA_UPGRADE;
break;
default:
throw new \Magento\Setup\Exception("$className does not exist");
}
return $this->createSchemaDataHandler($className, $interface);
}
这告诉 Magento 从单个模块的 Setup 目录中挑选和运行哪些类。目前我们将忽略重复的情况,因为只有 Magento_Indexer 模块使用它。
第一次运行 php bin/magento setup:upgrade 命令针对我们的模块;尽管它仍然在 setup_module 表下没有条目,但 Magento 将按照以下顺序执行模块 Setup 文件夹中的文件:
-
InstallSchema.php -
UpgradeSchema.php -
InstallData.php -
UpgradeData.php
注意,这与 getSchemaDataHandler 方法中从上到下的顺序相同。
每次后续模块版本号的变化,随后是控制台命令 php bin/magento setup:upgrade,都会导致以下文件按照列表中的顺序运行:
-
UpgradeSchema.php -
UpgradeData.php
此外,Magento 还会在 setup_module 数据库下记录升级后的版本号。只有当数据库中的版本号小于 module.xml 文件中的版本号时,Magento 才会触发安装或升级脚本。
小贴士
如果需要,我们不必总是提供这些安装或升级脚本。只有在需要添加或编辑数据库中的现有表或条目时才需要它们。
如果我们仔细查看适当脚本中 install 和 update 方法的实现,我们可以看到它们都接受 ModuleContextInterface $context 作为第二个参数。由于升级脚本是在每次升级版本号时触发的,我们可以使用 $context->getVersion() 来针对特定于模块版本的更改。
创建安装模式脚本(InstallSchema.php)
现在我们已经了解了模式和数据脚本及其与模块版本号的关系,让我们继续组装我们的 InstallSchema。我们首先定义 app/code/Foggyline/Office/Setup/InstallSchema.php 文件,其(部分)内容如下:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
class InstallSchema implements InstallSchemaInterface
{
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}
InstallSchema 遵循 InstallSchemaInterface,这要求实现接受两个类型为 SchemaSetupInterface 和 ModuleContextInterface 的参数的 install 方法。
在这里只需要安装方法。在这个方法中,我们会添加任何可能需要的代码来创建所需的表和列。
在代码库中查看,我们可以看到 Magento\Setup\Module\Setup 是扩展 \Magento\Framework\Module\Setup 并实现 SchemaSetupInterface 的一个。前面代码中看到的两个方法 startSetup 和 endSetup 用于在我们代码之前和之后运行额外的环境设置。
进一步来说,让我们将 /* #snippet1 */ 部分替换为创建我们的 Department 模型实体表的代码,如下所示:
$table = $setup->getConnection()
->newTable($setup->getTable('foggyline_office_department'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
'Entity ID'
)
->addColumn(
'name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Name'
)
->setComment('Foggyline Office Department Table');
$setup->getConnection()->createTable($table);
/* #snippet2 */
在这里,我们指示 Magento 创建一个名为foggyline_office_department的表,向其中添加entity_id和name列,并设置表的注释。假设我们使用的是 MySQL 服务器,当代码执行时,以下 SQL 将在数据库中执行:
CREATE TABLE 'foggyline_office_department' (
'entity_id' int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Entity ID',
'name' varchar(64) DEFAULT NULL COMMENT 'Name',
PRIMARY KEY ('entity_id')
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='Foggyline Office Department Table';
addColumn方法是这里最有趣的一个。它接受五个参数,从列名、列数据类型、列长度、附加选项数组到列描述。然而,只有列名和列数据类型是必须的!可接受的列数据类型可以在Magento\Framework\DB\Ddl\Table类下找到,如下所示:
boolean smallint integer bigint
float numeric decimal date
timestamp datetime text blob
varbinary
附加选项数组可能包含以下一些键:unsigned、precision、scale、unsigned、default、nullable、primary、identity、auto_increment。
在了解了addColumn方法之后,让我们继续创建foggyline_office_employee_entity表,用于Employee实体。我们通过替换前面代码中的/* #snippet2 */部分来实现这一点:
$employeeEntity = \Foggyline\Office\Model\Employee::ENTITY;
$table = $setup->getConnection()
->newTable($setup->getTable($employeeEntity . '_entity'))
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
'Entity ID'
)
->addColumn(
'department_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned' => true, 'nullable' => false],
'Department Id'
)
->addColumn(
'email',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Email'
)
->addColumn(
'first_name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'First Name'
)
->addColumn(
'last_name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
[],
'Last Name'
)
->setComment('Foggyline Office Employee Table');
$setup->getConnection()->createTable($table);
/* #snippet3 */
根据良好的数据库设计实践,我们可能会注意到这里的一个问题。如果我们同意每个员工可以分配一个单一的部门,我们应该向该表的department_id列添加一个外键。目前,我们将故意跳过这一部分,因为我们想通过稍后的更新模式脚本来演示这一点。
EAV 模型将数据分散在多个表中,至少有三个。我们刚刚创建的foggyline_office_employee_entity表就是其中之一。另一个是核心的 Magento eav_attribute表。第三个表不是一个单独的表,而是一系列多个表;每个 EAV 类型一个表。这些表是我们安装脚本的结果。
存储在核心 Magento eav_attribute表中的信息不是属性值或类似的东西;存储在那里的信息是属性的元数据。那么,Magento 是如何知道我们的Employee属性(service_years、dob、salary、vat_number、note)的呢?它不知道;还没有。我们需要自己将属性添加到该表中。我们将在稍后通过InstallData演示这样做。
根据 EAV 属性数据类型,我们需要创建以下表:
-
foggyline_office_employee_entity_datetime -
foggyline_office_employee_entity_decimal -
foggyline_office_employee_entity_int -
foggyline_office_employee_entity_text -
foggyline_office_employee_entity_varchar
这些属性值表的名字来自一个简单的公式,即*{实体表名称}+{_}+{eav_attribute.backend_type 值}*。如果我们看salary属性,我们需要它是一个小数值,因此它将被存储在foggyline_office_employee_entity_decimal中。
由于定义属性值表背后的代码量很大,我们将只关注一个单一的小数类型表。我们通过替换前面代码中的/* #snippet3 */部分来实现这一点:
$table = $setup->getConnection()
->newTable($setup->getTable($employeeEntity . '_entity_decimal'))
->addColumn(
'value_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'nullable' => false, 'primary' => true],
'Value ID'
)
->addColumn(
'attribute_id',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Attribute ID'
)
->addColumn(
'store_id',
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Store ID'
)
->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Entity ID'
)
->addColumn(
'value',
\Magento\Framework\DB\Ddl\Table::TYPE_DECIMAL,
'12,4',
[],
'Value'
)
//->addIndex
//->addForeignKey
->setComment('Employee Decimal Attribute Backend Table');
$setup->getConnection()->createTable($table);
注意上述代码中的 //->addIndex 部分。让我们将其替换为以下内容。
->addIndex(
$setup->getIdxName(
$employeeEntity . '_entity_decimal',
['entity_id', 'attribute_id', 'store_id'],
\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE
),
['entity_id', 'attribute_id', 'store_id'],
['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE]
)
->addIndex(
$setup->getIdxName($employeeEntity . '_entity_decimal', ['store_id']),
['store_id']
)
->addIndex(
$setup->getIdxName($employeeEntity . '_entity_decimal', ['attribute_id']),
['attribute_id']
)
前面的代码在 foggyline_office_employee_entity_decimal 表上添加了三个索引,结果生成以下 SQL:
-
UNIQUE KEY 'FOGGYLINE_OFFICE_EMPLOYEE_ENTT_DEC_ENTT_ID_ATTR_ID_STORE_ID' ('entity_id','attribute_id','store_id') -
KEY 'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_STORE_ID' ('store_id') -
KEY 'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_ATTRIBUTE_ID' ('attribute_id')
同样,我们将前面代码中的 //->addForeignKey 部分替换为以下内容:
->addForeignKey(
$setup->getFkName(
$employeeEntity . '_entity_decimal',
'attribute_id',
'eav_attribute',
'attribute_id'
),
'attribute_id',
$setup->getTable('eav_attribute'),
'attribute_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->addForeignKey(
$setup->getFkName(
$employeeEntity . '_entity_decimal',
'entity_id',
$employeeEntity . '_entity',
'entity_id'
),
'entity_id',
$setup->getTable($employeeEntity . '_entity'),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->addForeignKey(
$setup->getFkName($employeeEntity . '_entity_decimal', 'store_id', 'store', 'store_id'),
'store_id',
$setup->getTable('store'),
'store_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
前面的代码将外键关系添加到 foggyline_office_employee_entity_decimal 表中,结果生成以下 SQL:
-
CONSTRAINT 'FK_D17982EDA1846BAA1F40E30694993801' FOREIGN KEY ('entity_id') REFERENCES 'foggyline_office_employee_entity' ('entity_id') ON DELETE CASCADE, -
CONSTRAINT 'FOGGYLINE_OFFICE_EMPLOYEE_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID' FOREIGN KEY ('store_id') REFERENCES 'store' ('store_id') ON DELETE CASCADE, -
CONSTRAINT 'FOGGYLINE_OFFICE_EMPLOYEE_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID' FOREIGN KEY ('attribute_id') REFERENCES 'eav_attribute' ('attribute_id') ON DELETE CASCADE
注意我们如何将 store_id 列添加到我们的 EAV 属性值表中。尽管我们的示例不会使用它,但使用 store_id 与 EAV 实体一起定义数据范围是一个好习惯。为了进一步说明,想象我们有一个多店铺设置,并且像前面的 EAV 属性表那样设置,我们就能为每个店铺存储不同的属性值,因为表中的唯一条目定义为 entity_id、attribute_id 和 store_id 列的组合。
小贴士
为了性能和数据完整性的原因,根据良好的数据库设计实践定义索引和外键非常重要。我们可以在定义新表时在 InstallSchema 中这样做。
创建升级模式脚本(UpgradeSchema.php)
在第一次模块安装期间,安装模式之后立即运行升级模式。我们在 app/code/Foggyline/Office/Setup/UpgradeSchema.php 文件中定义升级模式,其部分内容如下:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\UpgradeSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
class UpgradeSchema implements UpgradeSchemaInterface
{
public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}
UpgradeSchema 遵循 UpgradeSchemaInterface,这要求实现接受两个参数 SchemaSetupInterface 和 ModuleContextInterface 的 upgrade 方法。
这与 InstallSchemaInterface 非常相似,除了方法名。当此模式被触发时,会运行 update 方法。在这个方法中,我们会添加任何我们可能想要执行的代码。
进一步来说,让我们将前面代码中的 /* #snippet1 */ 部分替换为以下代码:
$employeeEntityTable = \Foggyline\Office\Model\Employee::ENTITY. '_entity';
$departmentEntityTable = 'foggyline_office_department';
$setup->getConnection()
->addForeignKey(
$setup->getFkName($employeeEntityTable, 'department_id', $departmentEntityTable, 'entity_id'),
$setup->getTable($employeeEntityTable),
'department_id',
$setup->getTable($departmentEntityTable),
'entity_id',
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
);
在这里,我们指示 Magento 在 foggyline_office_employee_entity 表上创建一个外键,更确切地说是在其 department_id 列上,指向 foggyline_office_department 表及其 entity_id 列。
创建安装数据脚本(InstallData.php)
安装数据脚本是在升级架构后立即运行的。我们在app/code/Foggyline/Office/Setup/InstallData.php文件中定义安装数据架构,其内容如下(部分):
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class InstallData implements InstallDataInterface
{
private $employeeSetupFactory;
public function __construct(
\Foggyline\Office\Setup\EmployeeSetupFactory $employeeSetupFactory
)
{
$this->employeeSetupFactory = $employeeSetupFactory;
}
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}
InstallData符合InstallDataInterface,这要求实现接受两个类型为ModuleDataSetupInterface和ModuleContextInterface的参数的install方法。
当此脚本被触发时,将运行install方法。在这个方法中,我们会添加任何可能想要执行的代码。
进一步来说,让我们用以下代码替换前面代码中的/* #snippet1 */部分:
$employeeEntity = \Foggyline\Office\Model\Employee::ENTITY;
$employeeSetup = $this->employeeSetupFactory->create(['setup' => $setup]);
$employeeSetup->installEntities();
$employeeSetup->addAttribute(
$employeeEntity, 'service_years', ['type' => 'int']
);
$employeeSetup->addAttribute(
$employeeEntity, 'dob', ['type' => 'datetime']
);
$employeeSetup->addAttribute(
$employeeEntity, 'salary', ['type' => 'decimal']
);
$employeeSetup->addAttribute(
$employeeEntity, 'vat_number', ['type' => 'varchar']
);
$employeeSetup->addAttribute(
$employeeEntity, 'note', ['type' => 'text']
);
使用\Foggyline\Office\Setup\EmployeeSetupFactory实例上的addAttribute方法,我们指示 Magento 向其实体添加多个属性(service_years、dob、salary、vat_number、note)。
我们很快就会进入EmployeeSetupFactory的内部,但现在请注意对addAttribute方法的调用。在这个方法中,有一个对$this->attributeMapper->map($attr, $entityTypeId)方法的调用。attributeMapper符合Magento\Eav\Model\Entity\Setup\PropertyMapperInterface,查看vendor/magento/module-eav/etc/di.xml,它对Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite类有优先权,该类进一步初始化以下映射类:
-
Magento\Eav\Model\Entity\Setup\PropertyMapper -
Magento\Customer\Model\ResourceModel\Setup\PropertyMapper -
Magento\Catalog\Model\ResourceModel\Setup\PropertyMapper -
Magento\ConfigurableProduct\Model\ResourceModel\Setup\PropertyMapper
由于我们正在定义自己的实体类型,我们主要感兴趣的映射类是Magento\Eav\Model\Entity\Setup\PropertyMapper。快速查看它,我们发现map方法中的以下映射数组:
[
'backend_model' => 'backend',
'backend_type' => 'type',
'backend_table' => 'table',
'frontend_model' => 'frontend',
'frontend_input' => 'input',
'frontend_label' => 'label',
'frontend_class' => 'frontend_class',
'source_model' => 'source',
'is_required' => 'required',
'is_user_defined' => 'user_defined',
'default_value' => 'default',
'is_unique' => 'unique',
'note' => 'note'
'is_global' => 'global'
]
通过查看前面的数组键和值字符串,我们可以了解正在发生的事情。键字符串与eav_attribute表中的列名匹配,而值字符串与我们在InstallData.php中通过addAttribute方法传递给数组的键匹配。
让我们看看app/code/Foggyline/Office/Setup/EmployeeSetup.php文件中的EmployeeSetupFactory类,其定义如下(部分):
namespace Foggyline\Office\Setup;
use Magento\Eav\Setup\EavSetup;
class EmployeeSetup extends EavSetup
{
public function getDefaultEntities()
{
/* #snippet1 */
}
}
这里发生的事情是我们从Magento\Eav\Setup\EavSetup类扩展,从而有效地告诉 Magento 我们即将创建自己的实体。我们通过重写getDefaultEntities,用以下内容替换/* #snippet1 */来实现这一点:
$employeeEntity = \Foggyline\Office\Model\Employee::ENTITY;
$entities = [
$employeeEntity => [
'entity_model' => 'Foggyline\Office\Model\ResourceModel\Employee',
'table' => $employeeEntity . '_entity',
'attributes' => [
'department_id' => [
'type' => 'static',
],
'email' => [
'type' => 'static',
],
'first_name' => [
'type' => 'static',
],
'last_name' => [
'type' => 'static',
],
],
],
];
return $entities;
getDefaultEntities方法返回一个我们想要与 Magento 注册的实体数组。在我们的$entities数组中,键$employeeEntity成为eav_entity_type表中的一个条目。鉴于我们的$employeeEntity的值为foggyline_office_employee,运行以下 SQL 查询应该会产生结果:
SELECT * FROM eav_entity_type WHERE entity_type_code = "foggyline_office_employee";
为了使我们的新实体类型能够正常工作,只需要少量元数据值。entity_model 值应指向我们的 EAV 模型 resource 类,而不是 model 类。表值应等于数据库中我们的 EAV 实体表名称。最后,属性数组应列出我们想要在此实体上创建的任何属性。属性及其元数据在 eav_attribute 表中创建。
如果我们回顾一下我们创建的所有那些 foggyline_office_employee_entity_* 属性值表,它们并不是真正创建属性或注册新实体类型的。创建属性和新实体类型的是我们在 getDefaultEntities 方法下定义的数组。一旦 Magento 创建了属性并注册了新实体类型,它就会将实体保存过程路由到适当的属性值表,具体取决于属性类型。
创建升级数据脚本(UpgradeData.php)
升级数据脚本是在最后执行的。我们将使用它来演示为我们的 Department 和 Employee 实体创建示例条目的示例。
我们首先创建 app/code/Foggyline/Office/Setup/UpgradeData.php 文件,其(部分)内容如下:
namespace Foggyline\Office\Setup;
use Magento\Framework\Setup\UpgradeDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class UpgradeData implements UpgradeDataInterface
{
protected $departmentFactory;
protected $employeeFactory;
public function __construct(
\Foggyline\Office\Model\DepartmentFactory $departmentFactory,
\Foggyline\Office\Model\EmployeeFactory $employeeFactory
)
{
$this->departmentFactory = $departmentFactory;
$this->employeeFactory = $employeeFactory;
}
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
/* #snippet1 */
$setup->endSetup();
}
}
UpgradeData 遵循 UpgradeDataInterface,该接口要求实现接受两个参数(类型为 ModuleDataSetupInterface 和 ModuleContextInterface)的升级方法。我们进一步添加了自己的 __construct 方法,将 DepartmentFactory 和 EmployeeFactory 传递给它,因为我们将在下一个示例中在升级方法中使用它们,通过将 /* #snippet1 */ 替换为以下代码:
$salesDepartment = $this->departmentFactory->create();
$salesDepartment->setName('Sales');
$salesDepartment->save();
$employee = $this->employeeFactory->create();
$employee->setDepartmentId($salesDepartment->getId());
$employee->setEmail('john@sales.loc');
$employee->setFirstName('John');
$employee->setLastName('Doe');
$employee->setServiceYears(3);
$employee->setDob('1983-03-28');
$employee->setSalary(3800.00);
$employee->setVatNumber('GB123456789');
$employee->setNote('Just some notes about John');
$employee->save();
上述代码创建了一个部门实体的实例并将其保存。然后创建了一个员工实例并保存,传递给它新创建的部门 ID 和其他属性。
小贴士
保存实体的一种更方便、更专业的做法可以是以下这样:
$employee->setDob('1983-03-28')
->setSalary(3800.00)
->setVatNumber('GB123456789')
->save();
在这里,我们利用了每个实体设置方法都返回 $this(实体对象本身的实例)的事实,因此我们可以链式调用方法。
实体 CRUD 操作
到目前为止,我们已经学习了如何创建一个简单的模型、一个 EAV 模型,以及安装和升级类型的数据脚本。现在,让我们看看我们如何创建、读取、更新和删除我们的实体,这些操作通常被称为 CRUD。
虽然这一章是关于模型、集合和相关内容的,但为了演示的目的,让我们稍微偏离一下路由和控制器。想法是创建一个简单的 Test 控制器,我们可以通过 URL 触发其 Crud 动作。然后,在 Crud 动作中,我们将输出我们的 CRUD 相关代码。
要使 Magento 对应于我们在浏览器中输入的 URL,我们需要定义路由。我们通过创建包含以下内容的 app/code/Foggyline/Office/etc/frontend/routes.xml 文件来实现:
<config xsi:noNamespaceSchemaLocation="urn:magento:framework:App/ etc/routes.xsd">
<router id="standard">
<route id="foggyline_office" frontName="foggyline_office">
<module name="Foggyline_Office"/>
</route>
</router>
</config>
路由定义需要唯一的 ID 和frontName属性值,在我们的案例中这两个都等于foggyline_office。frontName属性值成为我们 URL 结构的一部分。简单来说,访问Crud操作的 URL 公式如下:{magento-base-url}/index.php/{route frontName}/{controller name}/{action name}
注意
例如,如果我们的基础 URL 是http://shop.loc/,完整的 URL 将是http://shop.loc/index.php/foggyline_office/test/crud/。如果我们启用了 URL 重写,我们可以省略index.php部分。
一旦定义了路由,我们就可以继续创建Test控制器,它在app/code/Foggyline/Office/Controller/Test.php文件中定义(部分)代码如下:
namespace Foggyline\Office\Controller;
abstract class Test extends \Magento\Framework\App\Action\Action
{
}
这真的是我们能够定义的最简单的控制器。这里唯一值得注意的事情是,控制器类需要定义为抽象类,并扩展\Magento\Framework\App\Action\Action类。控制器动作位于控制器外部,可以在同一级别的子目录下找到,命名为控制器。由于我们的控制器名为Test,我们将我们的Crud动作放在app/code/Foggyline/Office/Controller/Test/Crud.php文件中,内容如下:
namespace Foggyline\Office\Controller\Test;
class Crud extends \Foggyline\Office\Controller\Test
{
protected $employeeFactory;
protected $departmentFactory;
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Foggyline\Office\Model\EmployeeFactory $employeeFactory,
\Foggyline\Office\Model\DepartmentFactory $departmentFactory
)
{
$this->employeeFactory = $employeeFactory;
$this->departmentFactory = $departmentFactory;
return parent::__construct($context);
}
public function execute()
{
/* CRUD Code Here */
}
}
Controller动作类基本上是控制器定义execute方法的扩展。当我们在浏览器中点击 URL 时,会运行execute方法中的代码。此外,我们还有一个__construct方法,我们将EmployeeFactory和DepartmentFactory类传递给它,我们将在我们的 CRUD 示例中使用这些类。请注意,EmployeeFactory和DepartmentFactory不是我们创建的类。Magento 将在var/generation/Foggyline/Office/Model文件夹中的DepartmentFactory.php和EmployeeFactory.php文件下自动生成它们。这些是我们Employee和Department模型类的工厂类,在请求时生成。
有了这个,我们就结束了这个小插曲,重新关注我们的实体。
创建新实体
如果我们可以称它们为不同的风味,我们有三种方式可以设置实体(字段和属性)的属性值。它们都会产生相同的结果。以下几个代码片段可以复制粘贴到我们的Crud类的execute方法中进行测试,只需将/* CRUD Code Here */替换为以下代码片段之一:
//Simple model, creating new entities, flavour #1
$department1 = $this->departmentFactory->create();
$department1->setName('Finance');
$department1->save();
//Simple model, creating new entities, flavour #2
$department2 = $this->departmentFactory->create();
$department2->setData('name', 'Research');
$department2->save();
//Simple model, creating new entities, flavour #3
$department3 = $this->departmentFactory->create();
$department3->setData(['name' => 'Support']);
$department3->save();
前面代码中的flavour #1方法可能是设置属性的首选方式,因为它使用了我们之前提到的魔术方法方法。flavour #2和flavour #3都使用了setData方法,只是方式略有不同。一旦在object实例上调用save方法,这三个示例都应该产生相同的结果。
现在我们知道了如何保存简单的模型,让我们快速看一下如何使用 EAV 模型做同样的事情。以下是对应的代码片段:
//EAV model, creating new entities, flavour #1
$employee1 = $this->employeeFactory->create();
$employee1->setDepartment_id($department1->getId());
$employee1->setEmail('goran@mail.loc');
$employee1->setFirstName('Goran');
$employee1->setLastName('Gorvat');
$employee1->setServiceYears(3);
$employee1->setDob('1984-04-18');
$employee1->setSalary(3800.00);
$employee1->setVatNumber('GB123451234');
$employee1->setNote('Note #1');
$employee1->save();
//EAV model, creating new entities, flavour #2
$employee2 = $this->employeeFactory->create();
$employee2->setData('department_id', $department2->getId());
$employee2->setData('email', 'marko@mail.loc');
$employee2->setData('first_name', 'Marko');
$employee2->setData('last_name', 'Tunukovic');
$employee2->setData('service_years', 3);
$employee2->setData('dob', '1984-04-18');
$employee2->setData('salary', 3800.00);
$employee2->setData('vat_number', 'GB123451234');
$employee2->setData('note', 'Note #2');
$employee2->save();
//EAV model, creating new entities, flavour #3
$employee3 = $this->employeeFactory->create();
$employee3->setData([
'department_id' => $department3->getId(),
'email' => 'ivan@mail.loc',
'first_name' => 'Ivan',
'last_name' => 'Telebar',
'service_years' => 2,
'dob' => '1986-08-22',
'salary' => 2400.00,
'vat_number' => 'GB123454321',
'note' => 'Note #3'
]);
$employee3->save();
如我们所见,用于持久化数据的 EAV 代码与简单模型相同。这里有一件事值得注意。Employee实体定义了一个指向部门的关联。忘记在新的employee实体保存时指定department_id会导致类似于以下错误信息的错误:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails ('magento'.'foggyline_office_employee_entity', CONSTRAINT 'FK_E2AEE8BF21518DFA8F02B4E95DC9F5AD' FOREIGN KEY ('department_id') REFERENCES 'foggyline_office_department' ('entity_id') ON), query was: INSERT INTO 'foggyline_office_employee_entity' ('email', 'first_name', 'last_name', 'entity_id') VALUES (?, ?, ?, ?)
Magento 将其中的这些类型错误保存在其var/report目录下。
读取现有实体
根据提供的实体 ID 值读取实体归结为实例化实体,并使用传递实体 ID 的load方法,如下所示:
//Simple model, reading existing entities
$department = $this->departmentFactory->create();
$department->load(28);
/*
\Zend_Debug::dump($department->toArray());
array(2) {
["entity_id"] => string(2) "28"
["name"] => string(8) "Research"
}
*/
如下所示,加载简单模型或 EAV 模型之间实际上没有真正的区别:
//EAV model, reading existing entities
$employee = $this->employeeFactory->create();
$employee->load(25);
/*
\Zend_Debug::dump($employee->toArray());
array(10) {
["entity_id"] => string(2) "25"
["department_id"] => string(2) "28"
["email"] => string(14) "marko@mail.loc"
["first_name"] => string(5) "Marko"
["last_name"] => string(9) "Tunukovic"
["dob"] => string(19) "1984-04-18 00:00:00"
["note"] => string(7) "Note #2"
["salary"] => string(9) "3800.0000"
["service_years"] => string(1) "3"
["vat_number"] => string(11) "GB123451234"
}
*/
注意到 EAV 实体加载了所有的字段和属性值,这在我们通过 EAV 集合获取实体时并不总是如此,我们将在稍后展示。
更新现有实体
更新实体归结为使用load方法读取现有实体,重置其值,并在最后调用save方法,如下例所示:
$department = $this->departmentFactory->create();
$department->load(28);
$department->setName('Finance #2');
$department->save();
无论实体是简单模型还是 EAV,代码都是相同的。
删除现有实体
在已加载的实体上调用delete方法将删除该实体从数据库中,或者在失败时抛出Exception。删除实体的代码如下所示:
$employee = $this->employeeFactory->create();
$employee->load(25);
$employee->delete();
删除简单实体和 EAV 实体之间没有区别。我们在删除或保存实体时应该始终使用 try/catch 块。
管理集合
让我们从 EAV 模型集合开始。我们可以通过以下方式通过实体factory类实例化集合:
$collection = $this->employeeFactory->create()
->getCollection();
或者我们可以使用对象管理器来实例化集合,如下所示:
$collection = $this->_objectManager->create(
'Foggyline\Office\Model\ResourceModel\Employee\Collection's
);
还有第三种方法,这可能是一个更受欢迎的方法,但它要求我们定义 API,所以我们暂时跳过这个方法。
一旦我们实例化了集合对象,我们就可以遍历它并对单个$employee实体进行一些变量转储,以查看其内容,如下所示:
foreach ($collection as $employee) {
\Zend_Debug::dump($employee->toArray(), '$employee');
}
前面的操作会产生如下结果:
$employee array(5) {
["entity_id"] => string(2) "24"
["department_id"] => string(2) "27"
["email"] => string(14) "goran@mail.loc"
["first_name"] => string(5) "Goran"
["last_name"] => string(6) "Gorvat"
}
注意到单个$employee只有字段,没有属性。让我们看看当我们想要通过使用addAttributeToSelect来指定要添加到集合中的单个属性时会发生什么,如下所示:
$collection->addAttributeToSelect('salary')
->addAttributeToSelect('vat_number');
前面的操作会产生如下结果:
$employee array(7) {
["entity_id"] => string(2) "24"
["department_id"] => string(2) "27"
["email"] => string(14) "goran@mail.loc"
["first_name"] => string(5) "Goran"
["last_name"] => string(6) "Gorvat"
["salary"] => string(9) "3800.0000"
["vat_number"] => string(11) "GB123451234"
}
虽然我们在取得进步,但想象一下如果我们有数十个属性,并且我们希望每个属性都包含在集合中。多次使用addAttributeToSelect会导致代码杂乱。我们可以通过将'*'作为参数传递给addAttributeToSelect,让集合获取每个属性,如下所示:
$collection->addAttributeToSelect('*');
这会产生如下结果:
$employee array(10) {
["entity_id"] => string(2) "24"
["department_id"] => string(2) "27"
["email"] => string(14) "goran@mail.loc"
["first_name"] => string(5) "Goran"
["last_name"] => string(6) "Gorvat"
["dob"] => string(19) "1984-04-18 00:00:00"
["note"] => string(7) "Note #1"
["salary"] => string(9) "3800.0000"
["service_years"] => string(1) "3"
["vat_number"] => string(11) "GB123451234"
}
虽然代码的 PHP 部分看起来似乎很简单,但在 SQL 层面上发生的事情相对复杂。虽然 Magento 在获取最终集合结果之前执行了几个 SQL 查询,但让我们关注下面显示的最后三个查询:
SELECT COUNT(*) FROM 'foggyline_office_employee_entity' AS 'e'
SELECT 'e'.* FROM 'foggyline_office_employee_entity' AS 'e'
SELECT
'foggyline_office_employee_entity_datetime'.'entity_id',
'foggyline_office_employee_entity_datetime'.'attribute_id',
'foggyline_office_employee_entity_datetime'.'value'
FROM 'foggyline_office_employee_entity_datetime'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN ('349'))
UNION ALL SELECT
'foggyline_office_employee_entity_text'.'entity_id',
'foggyline_office_employee_entity_text'.' attribute_id',
'foggyline_office_employee_entity_text'.'value'
FROM 'foggyline_office_employee_entity_text'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN ('352'))
UNION ALL SELECT
'foggyline_office_employee_entity_decimal'.' entity_id',
'foggyline_office_employee_entity_decimal'.' attribute_id',
'foggyline_office_employee_entity_decimal'.'value'
FROM 'foggyline_office_employee_entity_decimal'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN ('350'))
UNION ALL SELECT
'foggyline_office_employee_entity_int'.'entity_id',
'foggyline_office_employee_entity_int'.'attribute_id',
'foggyline_office_employee_entity_int'.'value'
FROM 'foggyline_office_employee_entity_int'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN ('348'))
UNION ALL SELECT
'foggyline_office_employee_entity_varchar'.' entity_id',
'foggyline_office_employee_entity_varchar'.' attribute_id',
'foggyline_office_employee_entity_varchar'.'value'
FROM 'foggyline_office_employee_entity_varchar'
WHERE (entity_id IN (24, 25, 26)) AND (attribute_id IN ('351'))
注意
在我们继续之前,了解这些查询不能直接复制粘贴是很重要的。原因是 attribute_id 值肯定会在不同的安装中有所不同。这里给出的查询是为了让我们在 PHP 应用程序级别使用 Magento 集合时,在 SQL 层面上获得对后台发生的事情的高级理解。
第一个查询只是简单地计算实体表中的条目数量,然后将这个信息传递到应用层。第二个查询从 foggyline_office_employee_entity 中检索所有条目,然后将这些信息传递到应用层,以便在第三个查询中将实体 ID 作为 entity_id IN (24, 25, 26) 的一部分传递。如果我们的实体和 EAV 表中有大量条目,这里的第二个和第三个查询可能会非常消耗资源。为了防止可能出现的性能瓶颈,我们应该始终在集合上使用 setPageSize 和 setCurPage 方法,如下所示:
$collection->addAttributeToSelect('*')
->setPageSize(25)
->setCurPage(5);
这会导致第一个 COUNT 查询仍然保持不变,但第二个查询现在看起来如下所示:
SELECT 'e'.* FROM 'foggyline_office_employee_entity' AS 'e' LIMIT 25 OFFSET 4
如果我们有成千上万或数万条条目,这将使得数据集更小,从而性能更轻。这里的要点是始终使用 setPageSize 和 setCurPage。如果我们需要处理一个非常大的集合,那么我们需要分页浏览它,或者逐个遍历它。
现在我们知道了如何限制结果集的大小并获取正确的页面,让我们看看我们如何进一步过滤集合以避免过度使用 PHP 循环来完成同样的目的。因此,有效地将过滤传递到数据库而不是应用层。为了过滤 EAV 集合,我们使用它的 addAttributeToFilter 方法。
让我们实例化一个干净的新集合,如下所示:
$collection = $this->_objectManager->create(
'Foggyline\Office\Model\ResourceModel\Employee\Collection'
);
$collection->addAttributeToSelect('*')
->setPageSize(25)
->setCurPage(1);
$collection->addAttributeToFilter('email', array('like'=>'%mail.loc%'))
->addAttributeToFilter('vat_number', array('like'=>'GB%'))
->addAttributeToFilter('salary', array('gt'=>2400))
->addAttributeToFilter('service_years', array('lt'=>10));
注意我们现在正在集合上使用 addAttributeToSelect 和 addAttributeToFilter 方法。我们已经看到了 addAttributeToSelect 对 SQL 查询的数据库影响。addAttributeToFilter 做的事情则完全不同。
使用 addAttributeToFilter 方法,计数查询现在被转换成以下 SQL 查询:
SELECT COUNT(*)
FROM 'foggyline_office_employee_entity' AS 'e'
INNER JOIN 'foggyline_office_employee_entity_varchar' AS 'at_vat_number'
ON ('at_vat_number'.'entity_id' = 'e'.'entity_id') AND ('at_vat_number'.'attribute_id' = '351')
INNER JOIN 'foggyline_office_employee_entity_decimal' AS 'at_salary'
ON ('at_salary'.'entity_id' = 'e'.'entity_id') AND ('at_salary'.'attribute_id' = '350')
INNER JOIN 'foggyline_office_employee_entity_int' AS 'at_service_years'
ON ('at_service_years'.'entity_id' = 'e'.'entity_id') AND ('at_service_years'.'attribute_id' = '348')
WHERE ('e'.'email' LIKE '%mail.loc%') AND (at_vat_number.value LIKE 'GB%') AND (at_salary.value > 2400) AND
(at_service_years.value < 10)
我们可以看到这比之前的计数查询要复杂得多,现在有 INNER JOIN 介入。注意我们如何有四个 addAttributeToFilter 方法调用,但只有三个 INNER JOIN。这是因为其中四个调用之一是用于电子邮件的,它不是一个属性,而是 foggyline_office_employee_entity 表中的一个字段。这就是为什么不需要 INNER JOIN,因为该字段已经存在。然后三个 INNER JOIN 简单地将所需信息合并到查询中,以获取选择。
第二个查询也变得更加健壮,如下所示:
SELECT
'e'.*,
'at_vat_number'.'value' AS 'vat_number',
'at_salary'.'value' AS 'salary',
'at_service_years'.'value' AS 'service_years'
FROM 'foggyline_office_employee_entity' AS 'e'
INNER JOIN 'foggyline_office_employee_entity_varchar' AS 'at_vat_number'
ON ('at_vat_number'.'entity_id' = 'e'.'entity_id') AND ('at_vat_number'.'attribute_id' = '351')
INNER JOIN 'foggyline_office_employee_entity_decimal' AS 'at_salary'
ON ('at_salary'.'entity_id' = 'e'.'entity_id') AND ('at_salary'.'attribute_id' = '350')
INNER JOIN 'foggyline_office_employee_entity_int' AS 'at_service_years'
ON ('at_service_years'.'entity_id' = 'e'.'entity_id') AND ('at_service_years'.'attribute_id' = '348')
WHERE ('e'.'email' LIKE '%mail.loc%') AND (at_vat_number.value LIKE 'GB%') AND (at_salary.value > 2400) AND
(at_service_years.value < 10)
LIMIT 25
这里,我们还可以看到 INNER JOIN 的使用。我们也有三个而不是四个 INNER JOIN,因为其中一个条件是对 email 字段的。查询的结果是一个扁平化的行块,其中包含 vat_number、salary 和 service_years 等属性。我们可以想象如果没有使用 setPageSize 限制结果集,性能会受到怎样的影响。
最后,第三个查询也受到影响,现在看起来类似于以下内容:
SELECT
'foggyline_office_employee_entity_datetime'.'entity_id',
'foggyline_office_employee_entity_datetime'.'attribute_id',
'foggyline_office_employee_entity_datetime'.'value'
FROM 'foggyline_office_employee_entity_datetime'
WHERE (entity_id IN (24, 25)) AND (attribute_id IN ('349'))
UNION ALL SELECT
'foggyline_office_employee_entity_text'.'entity_id',
'foggyline_office_employee_entity_text'.' attribute_id',
'foggyline_office_employee_entity_text'.'value'
FROM 'foggyline_office_employee_entity_text'
WHERE (entity_id IN (24, 25)) AND (attribute_id IN ('352'))
注意这里 UNION ALL 已经减少到单个出现,从而有效地形成了两个选择。这是因为我们总共有五个属性(service_years、dob、salary、vat_number、note),其中三个是通过第二个查询获取的。在前面的三个查询示例中,Magento 主要从第二个和第三个查询中提取集合数据。这看起来像是一个相当优化和可扩展的解决方案,尽管我们真的应该仔细思考在创建集合时正确使用 setPageSize、addAttributeToSelect 和 addAttributeToFilter 方法。
在开发过程中,如果正在处理具有大量属性、过滤器和可能的大型数据集的集合,我们可能希望使用 SQL 日志记录实际击中数据库服务器的 SQL 查询。这可能会帮助我们及时发现可能的性能瓶颈并及时做出反应,无论是通过向 setPageSize 或 addAttributeToSelect 添加更多限制值,还是两者都添加。
在前面的示例中,使用 addAttributeToSelect 导致 SQL 层上的 AND 条件。如果我们想使用 OR 条件来过滤集合怎么办?如果 $attribute 参数以以下方式使用,addAttributeToSelect 也可以导致 SQL OR 条件:
$collection->addAttributeToFilter([
['attribute'=>'salary', 'gt'=>2400],
['attribute'=>'vat_number', 'like'=>'GB%']
]);
这次不深入实际 SQL 查询的细节,只需说它们几乎与之前的示例相同,使用了 addAttributeToFilter 的 AND 条件。
使用 addExpressionAttributeToSelect、groupByAttribute 和 addAttributeToSort 等集合方法,集合提供了进一步的梯度过滤,甚至可以将一些计算从 PHP 应用层转移到 SQL 层。深入了解这些和其他集合方法超出了本章的范围,可能需要一本单独的书籍。
集合过滤器
回顾前面的 addAttributeToFilter 方法调用示例,人们可能会问在哪里可以看到所有可用的集合过滤器的列表。如果我们快速查看 vendor/magento/framework/DB/Adapter/Pdo/Mysql.php 文件,我们可以看到名为 prepareSqlCondition 的方法(部分)定义如下:
public function prepareSqlCondition($fieldName, $condition)
{
$conditionKeyMap = [
'eq' => "{{fieldName}} = ?",
'neq' => "{{fieldName}} != ?",
'like' => "{{fieldName}} LIKE ?",
'nlike' => "{{fieldName}} NOT LIKE ?",
'in' => "{{fieldName}} IN(?)",
'nin' => "{{fieldName}} NOT IN(?)",
'is' => "{{fieldName}} IS ?",
'notnull' => "{{fieldName}} IS NOT NULL",
'null' => "{{fieldName}} IS NULL",
'gt' => "{{fieldName}} > ?",
'lt' => "{{fieldName}} /* AJZELE */ < ?",
'gteq' => "{{fieldName}} >= ?",
'lteq' => "{{fieldName}} <= ?",
'finset' => "FIND_IN_SET(?, {{fieldName}})",
'regexp' => "{{fieldName}} REGEXP ?",
'from' => "{{fieldName}} >= ?",
'to' => "{{fieldName}} <= ?",
'seq' => null,
'sneq' => null,
'ntoa' => "INET_NTOA({{fieldName}}) LIKE ?",
];
$query = '';
if (is_array($condition)) {
$key = key(array_intersect_key($condition, $conditionKeyMap));
...
}
这种方法是在 SQL 查询构建过程中的某个时刻最终被调用的。期望 $condition 参数具有以下(部分列出)形式之一:
-
array("from" => $fromValue, "to" => $toValue) -
array("eq" => $equalValue) -
array("neq" => $notEqualValue) -
array("like" => $likeValue) -
array("in" => array($inValues)) -
array("nin" => array($notInValues)) -
array("notnull" => $valueIsNotNull) -
array("null" => $valueIsNull) -
array("gt" => $greaterValue) -
array("lt" => $lessValue) -
array("gteq" => $greaterOrEqualValue) -
array("lteq" => $lessOrEqualValue) -
array("finset" => $valueInSet) -
array("regexp" => $regularExpression) -
array("seq" => $stringValue) -
array("sneq" => $stringValue)
如果$condition作为整数或字符串传递,则将过滤确切值('eq'条件)。如果没有匹配任何条件,则期望参数为一个顺序数组,并将使用前面的结构构建OR条件。
上述示例涵盖了 EAV 模型集合,因为它们稍微复杂一些。尽管过滤的方法在简单模型集合中也大致适用,但最显著的区别是没有addAttributeToFilter、addAttributeToSelect和addExpressionAttributeToSelect方法。简单模型集合使用addFieldToFilter、addFieldToSelect和addExpressionFieldToSelect等方法,以及其他细微的区别。
摘要
在本章中,我们首先学习了如何创建简单的模型、其资源以及集合类。然后我们对 EAV 模型也进行了同样的操作。一旦我们有了所需的模型、资源和集合类,我们就详细地研究了模式和数据脚本的类型和流程。动手实践,我们涵盖了InstallSchema、UpgradeSchema、InstallData和UpgradeData脚本。一旦脚本运行,数据库最终拥有了所需的表和样本数据,这些数据是我们基于实体 CRUD 示例的。最后,我们快速但专注地查看集合管理,这主要涉及过滤集合以获取所需的结果集。
完整的模块代码可以从github.com/ajzele/B05032-Foggyline_Office下载。
更多推荐



所有评论(0)