Magento 2 教程:如何构建一个完整的模块
已发表: 2022-03-11Magento 目前是世界上最大的开源电子商务平台。 由于其功能丰富且可扩展的代码库,世界各地的大小型商家已将其用于各种项目。
Magento 1 已经问世八年,它的继任者 Magento 2 于 2015 年底发布,改善了早期版本的弱点,例如:
- 提高性能
- 官方自动化测试套件
- 更好的后端 UI
- 新的、更现代的前端代码库
- 一种更加模块化的模块开发方式,文件包含在 Magento 代码中,而不是分散在各处
- 减少尝试自定义相同功能的模块之间的冲突数量
一年多一点的时间,进步是显而易见的,尽管并不是所有提到的问题都得到了完全解决。 现在可以完全肯定地说,Magento 2 是一款比其前身更强大的软件。 Magento 2 中的一些改进包括:
- 单元和集成测试,包括为自定义模块创建它们的官方和文档化方式
- 真正模块化的模块,它们的所有文件都放在一个目录下
- 更丰富的模板系统,允许主题开发者创建 n 级模板层次结构
- 在整个代码中采用了一系列有用的设计模式,提高了代码质量并降低了模块创建错误的可能性——其中包括自动依赖注入、服务合同、存储库和工厂,仅举几例。
- 原生集成到 Varnish 作为全页缓存系统,以及用于会话和缓存处理的 Redis
- PHP 7 支持
Magento 2 的学习曲线,随着所有这些变化,变得更加陡峭。 在本指南中,我打算向您展示如何开发您的第一个 Magento 2 模块,并为您指明正确的方向以继续您的学习。 让我们开始吧!
Magento 2 教程先决条件
为了遵循本文的其余部分,您必须充分了解以下技术/概念,这一点很重要:
- 面向对象编程 (OOP)
- PHP
- 命名空间
- MySQL
- 基本 bash 用法
综上所述,OOP 可能是最重要的一个。 Magento 最初是由经验丰富的 Java 开发人员团队创建的,在整个代码库中肯定可以看到他们的遗产。 如果您对自己的 OOP 技能不是很有信心,那么在开始使用该平台之前对其进行审查可能是个好主意。
Magento 2 架构概述
Magento 的体系结构旨在使源代码尽可能模块化和可扩展。 该方法的最终目标是使其能够根据每个项目的需要轻松调整和定制。
定制通常意味着改变平台代码的行为。 在大多数系统中,这意味着更改“核心”代码。 在 Magento 中,如果您遵循最佳实践,那么您可以在大多数情况下避免这种情况,从而使商店能够以可靠的方式与最新的安全补丁和功能发布保持同步。
Magento 2 是一个模型视图视图模型(MVVM) 系统。 虽然与其兄弟模型视图控制器 (MVC) 密切相关,但 MVVM 架构在模型层和视图层之间提供了更强大的分离。 下面是对 MVVM 系统每一层的解释:
- Model包含应用程序的业务逻辑,并依赖于关联的类——ResourceModel——进行数据库访问。 模型依靠服务契约将其功能暴露给应用程序的其他层。
- 视图是用户在屏幕上看到的内容的结构和布局——实际的 HTML。 这是在与模块一起分发的 PHTML 文件中实现的。 PHTML 文件与Layout XML 文件中的每个 ViewModel 相关联,这在 MVVM 方言中称为活页夹。 布局文件也可能会指定要在最终页面中使用的 JavaScript 文件。
- ViewModel与 Model 层交互,只向 View 层公开必要的信息。 在 Magento 2 中,这是由模块的Block类处理的。 请注意,这通常是 MVC 系统的控制器角色的一部分。 在 MVVM 上,控制器只负责处理用户流,这意味着它接收请求并告诉系统渲染视图或将用户重定向到另一个路由。
Magento 2 模块由上述架构的一些(如果不是全部)元素组成。 整体架构描述如下(来源):
Magento 2 模块可以通过使用 PHP 的依赖管理器 Composer 来定义外部依赖。 在上图中,您可以看到 Magento 2 的核心模块依赖于 Zend Framework、Symfony 以及其他第三方库。
下面是 Magento/Cms 的结构,这是一个 Magento 2 核心模块,负责处理页面和静态块的创建。
每个文件夹包含架构的一部分,如下所示:
- Api:服务契约,定义服务接口和数据接口
- Block:我们的 MVVM 架构的 ViewModel
- Controller:控制器,负责在与系统交互的同时处理用户的流量
- 等:配置 XML 文件 - 模块在此文件夹中定义自身及其部分(路由、模型、块、观察者和 cron 作业)。 非核心模块也可以使用etc文件来覆盖核心模块的功能。
- Helper: Helper 类,保存在多个应用程序层中使用的代码。 例如,在 Cms 模块中,帮助类负责准备 HTML 以呈现给浏览器。
- i18n:保存国际化 CSV 文件,用于翻译
- 模型:对于模型和资源模型
- 观察者:持有观察者,或“观察”系统事件的模型。 通常,当触发此类事件时,观察者会实例化一个模型来处理此类事件的必要业务逻辑。
- 设置:迁移类,负责架构和数据创建
- 测试:单元测试
- Ui:管理应用程序中使用的 UI 元素,例如网格和表单
- 视图:前端和管理应用程序的布局 (XML) 文件和模板 (PHTML) 文件
有趣的是,在实践中,Magento 2 的所有内部工作都存在于一个模块中。 在上图中,您可以看到,例如,负责结帐流程的Magento_Checkout
和负责处理产品和类别的Magento_Catalog
。 基本上,这告诉我们的是,学习如何使用模块是成为 Magento 2 开发人员最重要的部分。
好了,系统架构和模块结构的介绍都比较简单了,我们来做点具体的吧? 接下来,我们将通过传统的 Weblog 教程让您熟悉 Magento 2 并有望成为 Magento 2 开发人员。 在此之前,我们需要搭建一个开发环境。 让我们开始吧!
设置 Magento 2 模块开发环境
在撰写本文时,我们能够使用官方的 Magento 2 DevBox,它是一个 Magento 2 Docker 容器。 我仍然认为 macOS 上的 Docker 是不可用的,至少对于严重依赖快速磁盘 I/O 的系统(例如 Magento 2)来说是这样。所以,我们将按照传统方式进行操作:在我们自己的机器上本地安装所有软件包。
设置服务器
安装一切肯定会有点乏味,但最终的结果将是一个闪电般快速的 Magento 开发环境。 相信我,通过不依赖 Docker 进行 Magento 2 开发,您将节省数小时的工作时间。
本教程假设在 macOS 上安装了 Brew 的环境。 如果您不是这种情况,则基本内容将保持不变,仅更改您安装软件包的方式。 让我们从安装所有软件包开始:
brew install mysql nginxb php70 php70-imagick php70-intl php70-mcrypt
然后启动服务:
brew services start mysql brew services start php70 sudo brew services start nginx
好的,现在我们将一个域指向我们的环回地址。 在任何编辑器中打开主机文件,但请确保您具有超级用户权限。 用 Vim 这样做是:
sudo vim /etc/hosts
然后添加以下行:
127.0.0.1 magento2.dev
现在我们将在 Nginx 中创建一个虚拟主机:
vim /usr/local/etc/nginx/sites-available/magento2dev.conf
添加以下内容:
server { listen 80; server_name magento2.dev; set $MAGE_ROOT /Users/yourusername/www/magento2dev; set $MAGE_MODE developer; # Default magento Nginx config starts below root $MAGE_ROOT/pub; index index.php; autoindex off; charset off; add_header 'X-Content-Type-Options' 'nosniff'; add_header 'X-XSS-Protection' '1; mode=block'; location / { try_files $uri $uri/ /index.php?$args; } location /pub { location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) { deny all; } alias $MAGE_ROOT/pub; add_header X-Frame-Options "SAMEORIGIN"; } location /static/ { if ($MAGE_MODE = "production") { expires max; } location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ { add_header Cache-Control "public"; add_header X-Frame-Options "SAMEORIGIN"; expires +1y; if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { add_header Cache-Control "no-store"; add_header X-Frame-Options "SAMEORIGIN"; expires off; if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } add_header X-Frame-Options "SAMEORIGIN"; } location /media/ { try_files $uri $uri/ /get.php?$args; location ~ ^/media/theme_customization/.*\.xml { deny all; } location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ { add_header Cache-Control "public"; add_header X-Frame-Options "SAMEORIGIN"; expires +1y; try_files $uri $uri/ /get.php?$args; } location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { add_header Cache-Control "no-store"; add_header X-Frame-Options "SAMEORIGIN"; expires off; try_files $uri $uri/ /get.php?$args; } add_header X-Frame-Options "SAMEORIGIN"; } location /media/customer/ { deny all; } location /media/downloadable/ { deny all; } location /media/import/ { deny all; } location ~ /media/theme_customization/.*\.xml$ { deny all; } location /errors/ { try_files $uri =404; } location ~ ^/errors/.*\.(xml|phtml)$ { deny all; } location ~ cron\.php { deny all; } location ~ (index|get|static|report|404|503)\.php$ { try_files $uri =404; fastcgi_pass 127.0.0.1:9000; fastcgi_param PHP_FLAG "session.auto_start=off \n suhosin.session.cryptua=off"; fastcgi_param PHP_VALUE "memory_limit=768M \n max_execution_time=60"; fastcgi_read_timeout 60s; fastcgi_connect_timeout 60s; fastcgi_param MAGE_MODE $MAGE_MODE; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # Default magento Nginx config finishes below client_max_body_size 20M; }
如果你以前没有接触过 Nginx,这个文件可能会吓到你,所以让我们在这里解释一下,因为它也会对 Magento 的一些内部工作原理有所了解。 第一行简单地告诉 Nginx 我们正在使用默认的 HTTP 端口,我们的域是magento2.dev
:
listen 80; server_name magento2.dev;
然后我们设置一些环境变量。 第一个—— $MAGE_ROOT
保存了我们代码库的路径。 请注意,您需要更改根路径以匹配您的用户名/文件夹路径,无论您计划将源放置在何处:
set $MAGE_ROOT /Users/yourusername/www/magento2dev;
第二个变量$MAGE_MODE
为我们的商店设置运行时模式。 在开发模块时,我们将使用开发者模式。 这使我们能够更快地编码,因为我们在开发时不必编译或部署静态文件。 其他模式是生产模式和默认模式。 后者的真正用途尚不清楚。
set $MAGE_MODE developer;
设置此变量后,我们定义 vhost 根路径。 请注意,我们使用/pub
文件夹为$MAGE_ROOT
变量添加了后缀,使我们的商店只有一部分可用于网络。
root $MAGE_ROOT/pub;
然后我们将我们的索引文件——当请求的文件不存在时,nginx 将加载的文件——定义为 index.php。 这个脚本$MAGE_ROOT/pub/index.php
是客户访问购物车和管理应用程序的主要入口点。 无论请求的 URL 是什么,都会加载 index.php 并启动路由器调度过程。
index index.php;
接下来,我们关闭一些 Nginx 功能。 首先,我们关闭autoindex
,它会在您请求文件夹时显示文件列表,但不指定文件,并且不存在索引。 其次,我们关闭charset
,这将允许 Nginx 自动将 Charset 标头添加到响应中。
autoindex off; charset off;
接下来,我们定义一些安全标头:
add_header 'X-Content-Type-Options' 'nosniff'; add_header 'X-XSS-Protection' '1; mode=block';
这个位置/
指向我们的根文件夹$MAGE_ROOT/pub
,并且基本上将收到的任何请求连同请求参数一起重定向到我们的前端控制器 index.php:
location / { try_files $uri $uri/ /index.php?$args; }
下一部分可能有点令人困惑,但它非常简单。 几行前,我们将根定义为$MAGE_ROOT/pub
。 这是推荐的更安全的设置,因为大部分代码在网络上是不可见的。 但这不是设置 Web 服务器的唯一方法。 实际上,大多数共享 Web 服务器都有一个默认设置,即让您的 Web 服务器指向您的 Web 文件夹。 对于这些用户,Magento 团队已经为这些情况准备了这个文件,当根被定义为$MAGE_ROOT
时,使用以下代码段:
location /pub { location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) { deny all; } alias $MAGE_ROOT/pub; add_header X-Frame-Options "SAMEORIGIN"; }
请记住,只要有可能,最好让您的 Web 服务器指向$MAGE_ROOT/pub
文件夹。 这样,您的商店将更加安全。
接下来,我们有静态位置$MAGE_ROOT/pub/static
。 这个文件夹最初是空的,并自动填充了模块和主题的静态文件,例如图像文件、CSS、JS 等。这里,我们基本上为静态文件定义了一些缓存值,当请求的文件没有时存在,将其重定向到$MAGE_ROOT/pub/static.php
。 除其他外,该脚本将根据定义的运行时模式分析请求并从对应的模块或主题复制或符号链接指定的文件。 这样,您的模块的静态文件将驻留在我们模块的文件夹中,但将直接从 vhost 公用文件夹中提供:
location /static/ { if ($MAGE_MODE = "production") { expires max; } location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ { add_header Cache-Control "public"; add_header X-Frame-Options "SAMEORIGIN"; expires +1y; if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { add_header Cache-Control "no-store"; add_header X-Frame-Options "SAMEORIGIN"; expires off; if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } add_header X-Frame-Options "SAMEORIGIN"; }
接下来我们拒绝对一些受限文件夹和文件的 Web 访问:
location /media/customer/ { deny all; } location /media/downloadable/ { deny all; } location /media/import/ { deny all; } location ~ /media/theme_customization/.*\.xml$ { deny all; } location /errors/ { try_files $uri =404; } location ~ ^/errors/.*\.(xml|phtml)$ { deny all; } location ~ cron\.php { deny all; }
最后一点是我们加载 php-fpm 并告诉它在用户点击它时执行 index.php:
location ~ (index|get|static|report|404|503)\.php$ { try_files $uri =404; fastcgi_pass 127.0.0.1:9000; fastcgi_param PHP_FLAG "session.auto_start=off \n suhosin.session.cryptua=off"; fastcgi_param PHP_VALUE "memory_limit=768M \n max_execution_time=60"; fastcgi_read_timeout 60s; fastcgi_connect_timeout 60s; fastcgi_param MAGE_MODE $MAGE_MODE; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }
用我们的方式,保存文件,然后通过键入以下命令启用它:
ln -s /usr/local/etc/nginx/sites-available/magento2dev.conf \ /usr/local/etc/nginx/sites-enabled/magento2dev.conf sudo brew services restart nginx
如何安装 Magento 2
好的,此时您的机器满足 Magento 2 的要求,只缺少野兽本身。 如果您还没有帐户,请访问 Magento 网站并创建一个帐户。 之后,转到下载页面并下载最新版本(撰写本文时为 2.1.5):
选择 .tar.bz2 格式并下载。 然后继续提取它并为 Magento 2 设置正确的文件夹和文件权限以使其能够工作:
mkdir ~/www/magento2dev cd ~/www/magento2dev tar -xjf ~/Downloads/Magento-CE-2.1.5-2017-02-20-05-39-14.tar.bz2 find var vendor pub/static pub/media app/etc -type f -exec chmod u+w {} \; find var vendor pub/static pub/media app/etc -type d -exec chmod u+w {} \; chmod u+x bin/magento
现在,要安装数据库表并创建所需的配置文件,我们将从终端运行以下命令:
./bin/magento setup:install --base-url=http://magento2.dev/ \ --db-host=127.0.0.1 --db-name=magento2 --db-user=root \ --db-password=123 --admin-firstname=Magento --admin-lastname=User \ [email protected] --admin-user=admin \ --admin-password=admin123 --language=en_US --currency=USD \ --timezone=America/Chicago --use-rewrites=1 --backend-frontname=admin
请记住更改数据库名称 ( db-name
)、用户 ( db-user
) 和密码 ( db-password
) 以匹配您在 MySQL 安装期间使用的名称,仅此而已! 该命令将安装 Magento 2 的所有模块,创建所需的表和配置文件。 完成后,打开浏览器并前往 http://magento2.dev/。 您应该会看到带有默认 Luma 主题的 Magento 2 全新安装:
如果您前往 http://magento2.dev/admin,您应该会看到 Admin 应用程序登录页面:
然后使用下面的凭据登录:
用户:admin 密码:admin123
我们终于准备好开始编写我们的代码了!
创建我们的第一个 Magento 2 模块
要完成我们的模块,我们必须创建以下文件,我将指导您完成整个过程。 我们会需要:
- 一些样板注册文件,让 Magento 了解我们的博客模块
- 一个接口文件,用于为 Post 定义我们的数据合约
- 一个 Post 模型,在我们的代码中表示一个 Post,实现 Post 数据接口
- Post 资源模型,用于将 Post 模型链接到数据库
- 一个帖子集合,在资源模型的帮助下一次从数据库中检索多个帖子
- 两个迁移类,用于设置我们的表模式和内容
- 两个操作:一个列出所有帖子,另一个单独显示每个帖子
- 块、视图和布局文件各两个:列表操作各一个,视图各一个
首先,让我们快速浏览一下核心源代码文件夹结构,这样我们就可以定义放置代码的位置。 我们安装的方式包含 Magento 2 的所有核心代码,连同它的所有依赖项,都位于 composer 的vendor
文件夹中。
注册我们的模块
我们会将代码保存在单独的文件夹app/code
中。 每个模块的名称都采用Namespace_ModuleName
的形式,并且它在文件系统上的位置必须反映该名称,本示例中为app/code/Namespace/ModuleName
。 按照该模式,我们将模块命名为Toptal_Blog
并将我们的文件放在app/code/Toptal/Blog
下。 继续创建该文件夹结构。
现在,我们需要创建一些样板文件,以便将我们的模块注册到 Magento。 首先,创建app/code/Toptal/Blog/composer.json
:
{}
Composer 将在您每次运行时加载此文件。 尽管我们实际上并没有在我们的模块中使用 Composer,但我们必须创建它来让 Composer 满意。
现在我们将向 Magento 注册我们的模块。 继续创建app/code/Toptal/Blog/registration.php
:
<?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'Toptal_Blog', __DIR__ );
在这里,我们调用ComponentRegistrar
类的register
方法,发送两个参数:字符串'module'
,这是我们正在注册的组件的类型,以及我们的模块名称, 'Toptal_Blog'
。 有了这些信息,Magento 的自动加载器将知道我们的命名空间,并知道在哪里寻找我们的类和 XML 文件。
这里要注意的一件有趣的事情是,我们将组件的类型 ( MODULE
) 作为参数发送到\Magento\Framework\Component\ComponentRegistrar::register
函数。 我们不仅可以注册模块,还可以注册其他类型的组件。 例如,主题、外部库和语言包也使用相同的方法注册。
继续,让我们创建最后一个注册文件app/code/Toptal/Blog/etc/module.xml
:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd"> <module name="Toptal_Blog" setup_version="0.1.0"> <sequence> <module name="Magento_Directory" /> <module name="Magento_Config" /> </sequence> </module> </config>
该文件包含有关我们模块的一些非常重要的信息。 他们是:
- 模块名称再次出现,将我们的模块名称暴露给 Magento 配置。
- Magento 安装版本,Magento 将使用它来决定何时运行数据库迁移脚本。
- 我们模块的依赖——当我们编写一个简单的模块时,我们只依赖于两个 Magento 核心模块:
Magento_Directory
和Magento_Config
。
现在,我们有了一个 Magento 2 应该可以识别的模块。让我们使用 Magento 2 CLI 来检查它。
首先,我们需要禁用 Magento 的缓存。 Magento 的缓存机制值得专门写一篇文章。 目前,由于我们正在开发一个模块并希望 Magento 立即识别我们的更改而无需始终清除缓存,我们将简单地禁用它。 从命令行运行:
./bin/magento cache:disable
然后让我们通过查看模块的状态来看看 Magento 是否已经知道我们的修改。 只需运行以下命令:
./bin/magento module:status
最后一个的结果应该类似于:
我们的模块在那里,但正如输出所示,它仍然被禁用。 要启用它,请运行:
./bin/magento module:enable Toptal_Blog
那应该做到了。 可以肯定的是,您可以再次调用module:status
并在启用列表中查找我们的模块名称:
处理数据存储
现在我们已经启用了我们的模块,我们需要创建保存我们博客文章的数据库表。 这是我们要创建的表的架构:
场地 | 类型 | 空值 | 钥匙 | 默认 |
---|---|---|---|---|
post_id | int(10) 无符号 | 不 | PRI | 空值 |
标题 | 文本 | 不 | 空值 | |
内容 | 文本 | 不 | 空值 | |
created_at | 时间戳 | 不 | CURRENT_TIMESTAMP |
我们通过创建InstallSchema
类来实现这一点,该类负责管理我们的模式迁移的安装。 该文件位于app/code/Toptal/Blog/Setup/InstallSchema.php
并具有以下内容:
<?php namespace Toptal\Blog\Setup; use \Magento\Framework\Setup\InstallSchemaInterface; use \Magento\Framework\Setup\ModuleContextInterface; use \Magento\Framework\Setup\SchemaSetupInterface; use \Magento\Framework\DB\Ddl\Table; /** * Class InstallSchema * * @package Toptal\Blog\Setup */ class InstallSchema implements InstallSchemaInterface { /** * Install Blog Posts table * * @param SchemaSetupInterface $setup * @param ModuleContextInterface $context */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); $tableName = $setup->getTable('toptal_blog_post'); if ($setup->getConnection()->isTableExists($tableName) != true) { $table = $setup->getConnection() ->newTable($tableName) ->addColumn( 'post_id', Table::TYPE_INTEGER, null, [ 'identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true ], 'ID' ) ->addColumn( 'title', Table::TYPE_TEXT, null, ['nullable' => false], 'Title' ) ->addColumn( 'content', Table::TYPE_TEXT, null, ['nullable' => false], 'Content' ) ->addColumn( 'created_at', Table::TYPE_TIMESTAMP, null, ['nullable' => false, 'default' => Table::TIMESTAMP_INIT], 'Created At' ) ->setComment('Toptal Blog - Posts'); $setup->getConnection()->createTable($table); } $setup->endSetup(); } }
如果您分析install
方法,您会注意到它只是创建了我们的表并一一添加了它的列。
为了确定何时运行模式迁移,Magento 保留一个表,其中包含每个模块的所有当前设置版本,并且每当模块版本更改时,它的迁移类都会被初始化。 该表是setup_module
,如果您查看该表的内容,您会发现到目前为止还没有对我们模块的引用。 所以,让我们改变它。 从终端,触发以下命令:
./bin/magento setup:upgrade
这将向您显示所有已执行的模块及其迁移脚本的列表,包括我们的:
现在,从您偏好的 MySQL 客户端,您可以检查表是否已真正创建:
在setup_module
表中,现在有对我们模块、其架构和数据版本的引用:
好的,那么架构升级呢? 让我们通过升级向该表添加一些帖子,以向您展示如何做到这一点。 首先,在我们的etc/module.xml
setup_version
中设置 setup_version:
现在我们创建我们的app/code/Toptal/Blog/Setup/UpgradeData.php
文件,该文件负责数据(不是模式)迁移:
<?php namespace Toptal\Blog\Setup; use \Magento\Framework\Setup\UpgradeDataInterface; use \Magento\Framework\Setup\ModuleContextInterface; use \Magento\Framework\Setup\ModuleDataSetupInterface; /** * Class UpgradeData * * @package Toptal\Blog\Setup */ class UpgradeData implements UpgradeDataInterface { /** * Creates sample blog posts * * @param ModuleDataSetupInterface $setup * @param ModuleContextInterface $context * @return void */ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); if ($context->getVersion() && version_compare($context->getVersion(), '0.1.1') < 0 ) { $tableName = $setup->getTable('toptal_blog_post'); $data = [ [ 'title' => 'Post 1 Title', 'content' => 'Content of the first post.', ], [ 'title' => 'Post 2 Title', 'content' => 'Content of the second post.', ], ]; $setup ->getConnection() ->insertMultiple($tableName, $data); } $setup->endSetup(); } }
您可以看到它与我们的 Install 类非常相似。 唯一的区别是它实现了一个UpgradeDataInterface
而不是InstallSchemaInterface
,并且主要方法称为upgrade
。 使用这种方法,您可以检查当前模块的安装版本,当比您的版本小时,启动您需要完成的更改。 在我们的示例中,我们使用version_compare
函数在以下行中检查当前版本是否小于 0.1.1:

if ($context->getVersion() && version_compare($context->getVersion(), '0.1.1') < 0 ) {
首次调用setup:upgrade
CLI 命令时, $context->getVersion()
调用将返回 0.1.0。 然后样本数据被加载到数据库中,我们的版本被撞到了 0.1.1。 要让它运行,请继续执行setup:upgrade
:
./bin/magento setup:upgrade
然后检查帖子表中的结果:
在setup_module
表中:
请注意,即使我们使用迁移过程将数据添加到表中,也可以更改架构。 过程相同; 您只会使用UpgradeSchemaInterface
而不是UpgradeDataInterface
。
定义帖子模型
继续前进,如果您还记得我们的架构概述,我们的下一个构建块将是博客文章 ResourceModel。 资源模型非常简单,简单地说明模型将“连接”到的表,以及它的主键是什么。 我们将在app/code/Toptal/Blog/Model/ResourceModel/Post.php
使用以下内容创建我们的 ResourceModel:
<?php namespace Toptal\Blog\Model\ResourceModel; use \Magento\Framework\Model\ResourceModel\Db\AbstractDb; class Post extends AbstractDb { /** * Post Abstract Resource Constructor * @return void */ protected function _construct() { $this->_init('toptal_blog_post', 'post_id'); } }
所有 ResourceModel 操作,除非您需要与通常的 CRUD 操作不同的东西,都由AbstractDb
父类处理。
我们还需要另一个 ResourceModel,一个 Collection。 Collection 将负责使用我们的 ResourceModel 查询数据库中的多个帖子,并返回一系列实例化并填充了信息的模型。 我们使用以下内容创建文件app/code/Toptal/Blog/Model/ResourceModel/Post/Collection.php
:
<?php namespace Toptal\Blog\Model\ResourceModel\Post; use \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; class Collection extends AbstractCollection { /** * Remittance File Collection Constructor * @return void */ protected function _construct() { $this->_init('Toptal\Blog\Model\Post', 'Toptal\Blog\Model\ResourceModel\Post'); } }
请注意,在构造函数中,我们只提到了 Model,它将在整个代码中表示 post 实体,以及 ResourceModel,它将在数据库中获取信息。
这一层缺少的部分是 Post Model 本身。 该模型应该包含我们在模式中定义的所有属性,以及您可能需要的任何业务逻辑。 按照 Magento 2 的模式,我们需要创建一个数据接口,我们的模型将从中扩展。 我们将接口放在app/code/Toptal/Blog/Api/Data/PostInterface.php
,它应该包含表的字段名称,以及访问它们的方法:
<?php namespace Toptal\Blog\Api\Data; interface PostInterface { /**#@+ * Constants for keys of data array. Identical to the name of the getter in snake case */ const POST_; const TITLE = 'title'; const CONTENT = 'content'; const CREATED_AT = 'created_at'; /**#@-*/ /** * Get Title * * @return string|null */ public function getTitle(); /** * Get Content * * @return string|null */ public function getContent(); /** * Get Created At * * @return string|null */ public function getCreatedAt(); /** * Get ID * * @return int|null */ public function getId(); /** * Set Title * * @param string $title * @return $this */ public function setTitle($title); /** * Set Content * * @param string $content * @return $this */ public function setContent($content); /** * Set Crated At * * @param int $createdAt * @return $this */ public function setCreatedAt($createdAt); /** * Set ID * * @param int $id * @return $this */ public function setId($id); }
现在到模型的实现,在app/code/Toptal/Blog/Model/Post.php
。 我们将创建在接口上定义的方法。 我们还将通过CACHE_TAG
常量指定一个缓存标记,并且在构造函数中,我们将指定负责我们模型的数据库访问的 ResourceModel。
<?php namespace Toptal\Blog\Model; use \Magento\Framework\Model\AbstractModel; use \Magento\Framework\DataObject\IdentityInterface; use \Toptal\Blog\Api\Data\PostInterface; /** * Class File * @package Toptal\Blog\Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Post extends AbstractModel implements PostInterface, IdentityInterface { /** * Cache tag */ const CACHE_TAG = 'toptal_blog_post'; /** * Post Initialization * @return void */ protected function _construct() { $this->_init('Toptal\Blog\Model\ResourceModel\Post'); } /** * Get Title * * @return string|null */ public function getTitle() { return $this->getData(self::TITLE); } /** * Get Content * * @return string|null */ public function getContent() { return $this->getData(self::CONTENT); } /** * Get Created At * * @return string|null */ public function getCreatedAt() { return $this->getData(self::CREATED_AT); } /** * Get ID * * @return int|null */ public function getId() { return $this->getData(self::POST_ID); } /** * Return identities * @return string[] */ public function getIdentities() { return [self::CACHE_TAG . '_' . $this->getId()]; } /** * Set Title * * @param string $title * @return $this */ public function setTitle($title) { return $this->setData(self::TITLE, $title); } /** * Set Content * * @param string $content * @return $this */ public function setContent($content) { return $this->setData(self::CONTENT, $content); } /** * Set Created At * * @param string $createdAt * @return $this */ public function setCreatedAt($createdAt) { return $this->setData(self::CREATED_AT, $createdAt); } /** * Set ID * * @param int $id * @return $this */ public function setId($id) { return $this->setData(self::POST_ID, $id); } }
创建视图
Now we are moving one layer up, and will start the implementation of our ViewModel and Controller. To define a route in the front-end (shopping cart) application, we need to create the file app/code/Toptal/Blog/etc/frontend/routes.xml
with the following contents:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> <router> <route frontName="blog"> <module name="Toptal_Blog"/> </route> </router> </config>
List of Posts at the Index Page
Here, we are basically telling Magento that our module, Toptal_Blog
, will be responsible for responding to routes under http://magento2.dev/blog (notice the frontName attribute of the route). Next up is the action, at app/code/Toptal/Blog/Controller/Index/Index.php
:
<?php namespace Toptal\Blog\Controller\Index; use \Magento\Framework\App\Action\Action; use \Magento\Framework\View\Result\PageFactory; use \Magento\Framework\View\Result\Page; use \Magento\Framework\App\Action\Context; use \Magento\Framework\Exception\LocalizedException; class Index extends Action { /** * @var PageFactory */ protected $resultPageFactory; /** * @param Context $context * @param PageFactory $resultPageFactory * * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, PageFactory $resultPageFactory ) { parent::__construct( $context ); $this->resultPageFactory = $resultPageFactory; } /** * Prints the blog from informed order id * @return Page * @throws LocalizedException */ public function execute() { $resultPage = $this->resultPageFactory->create(); return $resultPage; } }
Our action is defining two methods. Let us take a closer look at them:
The constructor method simply sends the
$context
parameter to its parent method, and sets the$resultPageFactory
parameter to an attribute for later use. At this point it is useful to know the Dependency Injection design pattern, as that is what is happening here. In Magento 2's case we have automatic dependency injection. This means that whenever a class instantiation occurs, Magento will automatically try to instantiate all of the class constructor parameters (dependencies) and inject it for you as constructor parameters. It identifies which classes to instantiate for each parameter by inspecting the type hints, in this caseContext
andPageFactory
.The
execute
method is responsible for the action execution itself. In our case, we are simply telling Magento to render its layout by returning aMagento\Framework\View\Result\Page
object. This will trigger the layout rendering process, which we will create in a bit.
Now you should see a blank page at the url http://magento2.dev/blog/index/index. We still need to define the layout structure for that route, and its corresponding Block (our ViewModel) and the template file which will present the data to our user.
The layout structure for the front-end application is defined under view/frontend/layout
, and the file name must reflect our route. As our route is blog/index/index
, the layout file for that route will be app/code/Toptal/Blog/view/frontend/layout/blog_index_index.xml
:
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> <block class="Toptal\Blog\Block\Posts" name="posts.list" template="Toptal_Blog::post/list.phtml" /> </referenceContainer> </body> </page>
Here, we must define three very important structures in the Magento layout structure: Blocks, Containers, and Templates.
Blocks are the ViewModel part of our MVVM architecture, which was explained in earlier sections. They are the building blocks of our template structure.
Containers contain and output Blocks. They hold blocks together in nice hierarchical structures, and help in making things make sense when the layout for a page is being processed.
Templates are PHMTL (mixed HTML and PHP) files used by a special type of block in Magento. You can make calls to methods of a
$block
variable from within a template. The variable is always defined in the template context. You will be invoking your Block's methods by doing so, and thus allowing you to pull information from the ViewModel layer to the actual presentation.
With that extra information at hand, we can analyze the XML layout structure above. This layout structure is basically telling Magento that, when a request is made to the blog/index/index
route, a Block of the type Toptal\Blog\Block\Posts
is to be added to the content
container, and the template which will be used to render it is Toptal_blog::post/list.phtml
.
This leads us to the creation of our two remaining files. Our Block, located at app/code/Toptal/Blog/Block/Posts.php
:
<?php namespace Toptal\Blog\Block; use \Magento\Framework\View\Element\Template; use \Magento\Framework\View\Element\Template\Context; use \Toptal\Blog\Model\ResourceModel\Post\Collection as PostCollection; use \Toptal\Blog\Model\ResourceModel\Post\CollectionFactory as PostCollectionFactory; use \Toptal\Blog\Model\Post; class Posts extends Template { /** * CollectionFactory * @var null|CollectionFactory */ protected $_postCollectionFactory = null; /** * Constructor * * @param Context $context * @param PostCollectionFactory $postCollectionFactory * @param array $data */ public function __construct( Context $context, PostCollectionFactory $postCollectionFactory, array $data = [] ) { $this->_postCollectionFactory = $postCollectionFactory; parent::__construct($context, $data); } /** * @return Post[] */ public function getPosts() { /** @var PostCollection $postCollection */ $postCollection = $this->_postCollectionFactory->create(); $postCollection->addFieldToSelect('*')->load(); return $postCollection->getItems(); } /** * For a given post, returns its url * @param Post $post * @return string */ public function getPostUrl( Post $post ) { return '/blog/post/view/id/' . $post->getId(); } }
这个类相当简单,它的目的仅仅是加载要显示的帖子,并为模板提供一个getPostUrl
方法。 不过有一些事情需要注意。
如果您还记得,我们还没有定义Toptal\Blog\Model\ResourceModel\Post\CollectionFactory
类。 我们只定义了Toptal\Blog\Model\ResourceModel\Post\Collection
。 那么这甚至是如何工作的呢? 对于您在模块中定义的每个类,Magento 2 会自动为您创建一个工厂。 工厂有两种方法: create
,每次调用都会返回一个新实例; get
,每次调用时总是返回相同的实例——用于实现单例模式。
Block 的第三个参数$data
是一个可选数组。 由于它是可选的并且没有类型提示,因此不会被自动注入系统注入。 重要的是要注意可选的构造函数参数必须始终位于参数的最后。 例如,我们的父类Magento\Framework\View\Element\Template
的构造函数具有以下参数:
public function __construct( Template\Context $context, array $data = [] ) { ...
由于我们想在扩展 Template 类之后将CollectionFactory
添加到构造函数参数中,因此我们必须在可选参数之前执行此操作,否则注入将不起作用:
public function __construct( Context $context, PostCollectionFactory $postCollectionFactory, array $data = [] ) { ...
在稍后将由我们的模板访问的getPosts
方法中,我们只需从PostCollectionFactory
调用create
方法,它将返回一个新的PostCollection
并允许我们从数据库中获取我们的帖子并将其发送到我们的响应中。
为了完成这条路线的布局,这是我们的 PHTML 模板app/code/Toptal/Blog/view/frontend/templates/post/list.phtml
:
<?php /** @var Toptal\Blog\Block\Posts $block */ ?> <h1>Toptal Posts</h1> <?php foreach($block->getPosts() as $post): ?> <?php /** @var Toptal\Blog\Model\Post */ ?> <h2><a href="<?php echo $block->getPostUrl($post);?>"><?php echo $post->getTitle(); ?></a></h2> <p><?php echo $post->getContent(); ?></p> <?php endforeach; ?>
请注意,在这里我们可以看到 View 层访问我们的 ModelView ( $block->getPosts()
),后者又使用 ResourceModel (集合) 从数据库中获取我们的模型 ( Toptal\Blog\Model\Post
)。 在每个模板中,只要您想访问其块的方法,都会定义一个$block
变量并等待您的调用。
现在您应该能够通过再次点击我们的路线来查看帖子列表。
查看个别帖子
现在,如果您单击帖子标题,您将收到 404,所以让我们修复它。 有了我们所有的结构,这变得非常简单。 我们只需要创建以下内容:
- 一个新的 action,负责处理对
blog/post/view
路由的请求 - 一个块来呈现帖子
- 一个 PHTML 模板,负责视图本身
- blog/post/view 路由的布局文件,将这些最后的部分放在一起。
我们的新动作非常简单。 它会简单地从请求中接收参数id
并将其注册到 Magento 核心注册表,这是一个中央存储库,用于在整个单个请求周期中可用的信息。 通过这样做,我们将在稍后将 ID 提供给块。 该文件应该位于app/code/Toptal/Blog/Controller/Post/View.php
并且这些是它的内容:
<?php namespace Toptal\Blog\Controller\Post; use \Magento\Framework\App\Action\Action; use \Magento\Framework\View\Result\PageFactory; use \Magento\Framework\View\Result\Page; use \Magento\Framework\App\Action\Context; use \Magento\Framework\Exception\LocalizedException; use \Magento\Framework\Registry; class View extends Action { const REGISTRY_KEY_POST_; /** * Core registry * @var Registry */ protected $_coreRegistry; /** * @var PageFactory */ protected $_resultPageFactory; /** * @param Context $context * @param Registry $coreRegistry * @param PageFactory $resultPageFactory * * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, Registry $coreRegistry, PageFactory $resultPageFactory ) { parent::__construct( $context ); $this->_coreRegistry = $coreRegistry; $this->_resultPageFactory = $resultPageFactory; } /** * Saves the blog id to the register and renders the page * @return Page * @throws LocalizedException */ public function execute() { $this->_coreRegistry->register(self::REGISTRY_KEY_POST_ID, (int) $this->_request->getParam('id')); $resultPage = $this->_resultPageFactory->create(); return $resultPage; } }
请注意,我们已将$coreRegistry
参数添加到__construct
中,并将其保存为属性以供以后使用。 在execute
方法中,我们从请求中检索id
参数,并注册它。 我们还使用类常量self::REGISTRY_KEY_POST_ID
作为注册表的键,我们将在块中使用相同的常量来引用注册表中的 id。
让我们在app/code/Toptal/Blog/Block/View.php
使用以下内容创建块:
<?php namespace Toptal\Blog\Block; use \Magento\Framework\Exception\LocalizedException; use \Magento\Framework\View\Element\Template; use \Magento\Framework\View\Element\Template\Context; use \Magento\Framework\Registry; use \Toptal\Blog\Model\Post; use \Toptal\Blog\Model\PostFactory; use \Toptal\Blog\Controller\Post\View as ViewAction; class View extends Template { /** * Core registry * @var Registry */ protected $_coreRegistry; /** * Post * @var null|Post */ protected $_post = null; /** * PostFactory * @var null|PostFactory */ protected $_postFactory = null; /** * Constructor * @param Context $context * @param Registry $coreRegistry * @param PostFactory $postCollectionFactory * @param array $data */ public function __construct( Context $context, Registry $coreRegistry, PostFactory $postFactory, array $data = [] ) { $this->_postFactory = $postFactory; $this->_coreRegistry = $coreRegistry; parent::__construct($context, $data); } /** * Lazy loads the requested post * @return Post * @throws LocalizedException */ public function getPost() { if ($this->_post === null) { /** @var Post $post */ $post = $this->_postFactory->create(); $post->load($this->_getPostId()); if (!$post->getId()) { throw new LocalizedException(__('Post not found')); } $this->_post = $post; } return $this->_post; } /** * Retrieves the post id from the registry * @return int */ protected function _getPostId() { return (int) $this->_coreRegistry->registry( ViewAction::REGISTRY_KEY_POST_ID ); } }
在视图块中,我们定义了一个受保护的方法_getPostId
,它将简单地从核心注册表中检索帖子 ID。 公共getPost
方法将依次延迟加载帖子并在帖子不存在时抛出异常。 在此处抛出异常将使 Magento 显示其默认错误屏幕,在这种情况下这可能不是最佳解决方案,但为了简单起见,我们将保持这种方式。
转到我们的 PHTML 模板。 添加app/code/Toptal/Blog/view/frontend/templates/post/view.phtml
,内容如下:
<?php /** @var Toptal\Blog\Block\View $block */ ?> <h1><?php echo $block->getPost()->getTitle(); ?></h1> <p><?php echo $block->getPost()->getContent(); ?></p>
很好很简单,只需访问我们之前创建的 View 块getPost
方法。
而且,总而言之,我们在app/code/Toptal/Blog/view/frontend/layout/blog_post_view.xml
为我们的新路由创建一个布局文件,内容如下:
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> <block class="Toptal\Blog\Block\View" name="post.view" template="Toptal_Blog::post/view.phtml" /> </referenceContainer> </body> </page>
这和我们之前做的一样。 它只是将Toptal\Blog\Block\View
添加到content
容器中,并将Toptal_Blog::post/view.phtml
作为关联模板。
要查看它的实际效果,只需将浏览器指向 http://magento2.dev/blog/post/view/id/1 即可成功加载帖子。 您应该会看到如下所示的屏幕:
正如你所看到的,在创建了我们的初始结构之后,向平台添加功能真的很简单,而且我们的大部分初始代码都在这个过程中被重用。
如果您想快速测试模块,这里是我们工作的总结果。
从这往哪儿走
如果你一直跟着我到这里,恭喜! 我很肯定你非常接近成为 Magento 2 开发人员。 我们开发了一个非常先进的 Magento 2 自定义模块,尽管它的功能很简单,但已经涵盖了很多领域。
为了简单起见,本文省略了一些内容。 仅举几例:
- 管理员编辑表单和网格以管理我们的博客内容
- 博客类别、标签和评论
- 我们可以建立的存储库和一些服务合同
- 将模块打包为 Magento 2 扩展
无论如何,这里有一些有用的链接,您可以进一步加深您的知识:
- Alan Storm 关于 Magento 2 的博客 — Alan Storm 在学习 Magento 方面可能拥有最具教育意义的内容。
- 艾伦肯特的博客
- Magento 文档:Magento 2 开发文档
我为您提供了有关如何在 Magento 2 中创建模块的所有相关方面的全面介绍,以及您需要的一些额外资源。 现在由您来编写代码,或者如果您想参与其中,请前往评论区。