个性化阅读
专注于IT技术分析

Magento 2教程:如何构建完整的模块

本文概述

Magento当前是世界上最大的开源电子商务平台。由于其功能丰富且可扩展的代码库, 全世界范围内大小经营的商人已将其用于各种项目。

Magento 1已经存在了八年, 其继任者Magento 2于2015年底发布, 改善了早期版本的弱点, 例如:

  • 性能提升
  • 官方自动化测试套件
  • 更好的后端用户界面
  • 新的, 更现代的前端代码库
  • 一种更模块化的模块开发方式, 文件包含在Magento代码内, 而不是分散在各处
  • 减少尝试自定义相同功能的模块之间的冲突数量
程式化的Magento 2徽标

尽管还没有完全解决所提到的所有问题, 但仍需要一年多一点的时间, 才能看到明显的改善。现在可以肯定地说, 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是模型视图ViewModel(MVVM)系统。 MVVM体系结构与其同级模型视图控制器(MVC)紧密相关, 但它在模型层和视图层之间提供了更可靠的分隔。以下是MVVM系统各层的说明:

  • 该模型保留了应用程序的业务逻辑, 并依赖于关联的类ResourceModel进行数据库访问。模型依赖服务合同将其功能公开给应用程序的其他层。
  • 视图是用户在屏幕上看到的内容的结构和布局-实际的HTML。这是在随模块分发的PHTML文件中实现的。 PHTML文件与Layout XML文件中的每个ViewModel关联, 在MVVM方言中将其称为活页夹。布局文件可能还会分配要在最终页面中使用的JavaScript文件。
  • ViewModel与Model层进行交互, 仅向View层公开必要的信息。在Magento 2中, 这由模块的Block类处理。请注意, 这通常是MVC系统的Controller角色的一部分。在MVVM上, 控制器仅负责处理用户流, 这意味着它接收请求, 并告诉系统渲染视图或将用户重定向到另一条路线。

Magento 2模块由上述架构的一些(如果不是全部)元素组成。总体架构如下所述(源):

完整的Magento 2架构图

Magento 2模块可以通过使用PHP的依赖性管理器Composer来定义外部依赖性。在上图中, 你可以看到Magento 2核心模块取决于Zend Framework, Symfony以及其他第三方库。

下面是Magento / Cms的结构, 这是一个Magento 2核心模块, 负责处理页面和静态块的创建。

Magento / Cms模块的目录布局

每个文件夹包含体系结构的一部分, 如下所示:

  • Api:服务合同, 定义服务接口和数据接口
  • 块:MVVM体系结构的ViewModels
  • 控制器:控制器, 负责在与系统交互时处理用户流程
  • 等等:配置XML文件-模块在此​​文件夹中定义自身及其部分(路径, 模型, 块, 观察器和cron作业)。非核心模块也可以使用etc文件来覆盖核心模块的功能。
  • 助手:持有多个应用程序层中使用的代码的助手类。例如, 在Cms模块中, 帮助程序类负责准备HTML以呈现给浏览器。
  • i18n:保存国际化CSV文件, 用于翻译
  • 模型:用于模型和资源模型
  • 观察者:持有”观察”系统事件的观察者或模型。通常, 在触发此类事件时, 观察者会实例化模型以处理此类事件的必要业务逻辑。
  • 设置:迁移类, 负责架构和数据创建
  • 测试:单元测试
  • Ui:管理应用程序中使用的UI元素, 例如网格和表单
  • 视图:前端和管理应用程序的布局(XML)文件和模板(PHTML)文件

有趣的是, 实际上, Magento 2的所有内部构造都生活在一个模块中。在上图中, 你可以看到例如Magento_Checkout和Magento_Catalog, 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

好的, 现在我们将一个域指向我们的回送地址。在任何编辑器中打开hosts文件, 但请确保你具有超级用户权限。使用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;

设置此变量后, 我们定义虚拟主机根路径。请注意, 我们在$ MAGE_ROOT变量后缀了/ pub文件夹, 从而使我们的商店中只有一部分可供网络使用。

  root $MAGE_ROOT/pub;

然后, 我们将索引文件定义为index.php(当请求的文件不存在时将加载nginx文件)。此脚本$ MAGE_ROOT / pub / index.php是客户访问购物车和管理应用程序的主要切入点。无论请求的URL是什么, 都会加载index.php并开始路由器分派过程。

  index index.php;

接下来, 我们关闭一些Nginx功能。首先, 我们关闭自动索引功能, 当你请求文件夹但未指定文件且没有索引时, 该功能会显示文件列表。其次, 我们关闭字符集, 这将允许Nginx自动将字符集头添加到响应中。

  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服务器指向你的Web文件夹。对于这些用户, 当根目录定义为$ MAGE_ROOT并带有以下代码段时, Magento团队已为该情况准备了该文件:

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):

Magento 2下载页面

选择.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全新安装:

默认Luma主题的主页

如果你转到http://magento2.dev/admin, 则应该看到Admin应用程序登录页面:

管理员应用程序登录页面

然后使用以下凭据登录:

用户:admin密码:admin123

我们终于可以开始编写代码了!

创建我们的第一个Magento 2模块

为了完成我们的模块, 我们将必须创建以下文件, 我将指导你完成整个过程。我们会需要:

  • 一些样板注册文件, 以使Magento了解我们的博客模块
  • 一个接口文件, 用于定义我们的邮政数据合同
  • 一个Post模型, 代表整个代码中的一个Post, 实现Post数据接口
  • 后期资源模型, 用于将后期模型链接到数据库
  • 帖子集合, 借助资源模型一次从数据库中检索多个帖子
  • 两个迁移类, 用于设置我们的表架构和内容
  • 两项操作:一项列出所有帖子, 另一项单独显示每个帖子
  • 块, 视图和布局文件各两个:列表操作各一个, 视图各一个

首先, 让我们快速看一下核心源代码文件夹结构, 以便我们可以定义将代码放置在何处。我们的安装方式包含所有Magento 2核心代码及其所有依赖项, 它们位于作曲家的vendor文件夹中。

Magento 2核心代码的目录布局

注册我们的模块

我们将代码保存在单独的文件夹app / code中。每个模块的名称都采用Namespace_ModuleName的形式, 并且在文件系统上的位置必须反映该名称, 在本示例中为app / code / Namespace / ModuleName。按照这种模式, 我们将命名模块srcmini_Blog并将文件放置在app / code / srcmini / Blog下。继续创建该文件夹结构。

srcmini_Blog模块的目录布局

现在, 我们需要创建一些样板文件, 以便将模块注册到Magento。首先, 创建app / code / srcmini / Blog / composer.json:

{}

每次运行该文件时, 都会由Composer加载。即使我们实际上并未在模块中使用Composer, 也必须创建它以使Composer满意。

现在, 我们将在Magento中注册我们的模块。继续创建app / code / srcmini / Blog / registration.php:

<?php

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

在这里, 我们正在调用ComponentRegistrar类的register方法, 并发送两个参数:字符串” module”(这是我们正在注册的组件的类型)和模块的名称” srcmini_Blog”。有了这些信息, Magento的自动加载器将知道我们的名称空间, 并知道在哪里查找我们的类和XML文件。

这里要注意的一件有趣的事情是, 我们已经将组件的类型(MODULE)作为参数发送给\ Magento \ Framework \ Component \ ComponentRegistrar :: register函数。我们不仅可以注册模块, 还可以注册其他类型的组件。例如, 主题, 外部库和语言包也使用相同的方法注册。

继续, 让我们创建最后一个注册文件, app / code / srcmini / 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="srcmini_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

最后一个的结果应类似于:

status命令的输出,显示srcmini_Blog模块被禁用

我们的模块在那里, 但是如输出所示, 它仍然被禁用。要启用它, 请运行:

./bin/magento module:enable srcmini_Blog

那应该做的。可以肯定的是, 你可以再次调用module:status, 然后在启用的列表中查找我们模块的名称:

状态命令的输出,显示srcmini_Blog模块已启用

处理数据存储

启用模块后, 我们需要创建数据库表来保存我们的博客文章。这是我们要创建的表的架构:

领域 类型 null default
post_id int(10) unsigned NO PRI null
标题 文本 NO null
内容 文本 NO null
created_at 时间戳记 NO CURRENT_TIMESTAMP

我们通过创建InstallSchema类来实现此目的, 该类负责管理架构迁移的安装。该文件位于app / code / srcmini / Blog / Setup / InstallSchema.php中, 具有以下内容:

<?php

namespace srcmini\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 srcmini\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('srcmini_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('srcmini Blog - Posts');
            $setup->getConnection()->createTable($table);
        }

        $setup->endSetup();
    }
}

如果你分析安装方法, 你会发现它只是创建我们的表并一一添加列。

为了确定何时运行模式迁移, Magento保留了一个表, 其中包含每个模块的所有当前安装版本, 并且每当模块版本更改时, 都会初始化其迁移类。该表是setup_module, 如果你查看该表的内容, 你会发现到目前为止没有引用我们的模块。所以, 让我们改变它。在终端上, 发出以下命令:

./bin/magento setup:upgrade

这将为你显示所有模块及其执行的迁移脚本的列表, 包括我们的:

升级命令的输出,显示我们的迁移正在执行

现在, 从你的MySQL首选客户端中, 你可以检查该表是否已真正创建:

在MySQL客户端中演示我们的表

现在在setup_module表中, 有对我们的模块, 其架构和数据版本的引用:

setup_module表的内容

好的, 架构升级如何?让我们通过升级将一些帖子添加到该表中, 向你展示如何执行此操作。首先, 将setup_version撞到我们的etc / module.xml文件中:

在我们的module.xml文件中突出显示更改的值

现在, 我们创建app / code / srcmini / Blog / Setup / UpgradeData.php文件, 该文件负责数据(而非架构)的迁移:

<?php

namespace srcmini\Blog\Setup;

use \Magento\Framework\Setup\UpgradeDataInterface;
use \Magento\Framework\Setup\ModuleContextInterface;
use \Magento\Framework\Setup\ModuleDataSetupInterface;

/**
 * Class UpgradeData
 *
 * @package srcmini\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('srcmini_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。要运行此程序, 请继续执行设置:升级:

./bin/magento setup:upgrade

然后在posts表中检查结果:

表的内容

在setup_module表中:

setup_module表的更新内容

注意, 即使我们使用迁移过程将数据添加到表中, 也可以更改模式。过程是相同的;你将只使用UpgradeSchemaInterface而不是UpgradeDataInterface。

定义职位模型

继续, 如果你还记得我们的架构概述, 那么我们的下一个构建块将是博客文章ResourceModel。资源模型非常简单, 只需声明模型将与其”连接”的表及其主键。我们将在app / code / srcmini / Blog / Model / ResourceModel / Post.php中创建具有以下内容的ResourceModel:

<?php

namespace srcmini\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('srcmini_blog_post', 'post_id');
    }
}

除非你需要与通常的CRUD操作不同的其他资源, 否则所有ResourceModel操作均由AbstractDb父类处理。

我们还将需要另一个ResourceModel, 即Collection。该集合将负责使用我们的ResourceModel在数据库中查询多个帖子, 并返回实例化并填充了信息的一系列Model。我们使用以下内容创建文件app / code / srcmini / Blog / Model / ResourceModel / Post / Collection.php:

<?php
namespace srcmini\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('srcmini\Blog\Model\Post', 'srcmini\Blog\Model\ResourceModel\Post');
    }
}

注意, 在构造函数中, 我们只提到了Model(将代表整个代码中的post实体)和ResourceModel(将在数据库中获取信息)。

该层缺少的部分是邮政模型本身。该模型应包含我们在架构中定义的所有属性以及你可能需要的任何业务逻辑。按照Magento 2的模式, 我们需要创建一个数据接口, 我们的模型将从该数据接口扩展。我们将界面放置在app / code / srcmini / Blog / Api / Data / PostInterface.php中, 该界面应包含表的字段名称以及访问它们的方法:

<?php

namespace srcmini\Blog\Api\Data;

interface PostInterface
{
    /**#@+
     * Constants for keys of data array. Identical to the name of the getter in snake case
     */
    const POST_ID               = 'post_id';
    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 / srcmini / Blog / Model / Post.php。我们将创建在接口处定义的方法。我们还将通过CACHE_TAG常量指定一个缓存标签, 在构造函数中, 我们将指定ResourceModel, 该模型将负责对模型的数据库访问。

<?php

namespace srcmini\Blog\Model;

use \Magento\Framework\Model\AbstractModel;
use \Magento\Framework\DataObject\IdentityInterface;
use \srcmini\Blog\Api\Data\PostInterface;

/**
 * Class File
 * @package srcmini\Blog\Model
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class Post extends AbstractModel implements PostInterface, IdentityInterface
{
    /**
     * Cache tag
     */
    const CACHE_TAG = 'srcmini_blog_post';

    /**
     * Post Initialization
     * @return void
     */
    protected function _construct()
    {
        $this->_init('srcmini\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);
    }
}

创建视图

现在我们向上移动一层, 并将开始执行ViewModel和Controller。要在前端(购物车)应用程序中定义一条路线, 我们需要创建文件app / code / srcmini / Blog / etc / frontend / routes.xml, 其内容如下:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="blog" frontName="blog">
            <module name="srcmini_Blog"/>
        </route>
    </router>
</config>

索引页面上的帖子列表

在这里, 我们基本上是在告诉Magento, 我们的模块srcmini_Blog将负责响应http://magento2.dev/blog下的路由(注意路由的frontName属性)。接下来是操作, 位于app / code / srcmini / Blog / Controller / Index / Index.php:

<?php

namespace srcmini\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;
    }
}

我们的行动是定义两种方法。让我们仔细看看它们:

  • 构造函数方法只是将$ context参数发送到其父方法, 并将$ resultPageFactory参数设置为一个属性, 以供以后使用。此时, 了解依赖注入设计模式很有用, 因为这就是这里发生的情况。在Magento 2的情况下, 我们具有自动依赖项注入功能。这意味着无论何时发生类实例化, Magento都会自动尝试实例化所有类构造函数参数(依赖项)并将其作为构造函数参数注入。它通过检查类型提示(在本例中为Context和PageFactory)来标识要为每个参数实例化的类。

  • execute方法负责执行动作本身。在我们的例子中, 我们只是通过返回Magento \ Framework \ View \ Result \ Page对象来告诉Magento渲染其布局。这将触发我们稍后创建的布局渲染过程。

现在, 你应该在URL http://magento2.dev/blog/index/index处看到一个空白页。我们仍然需要定义该路线的布局结构及其相应的Block(我们的ViewModel)和模板文件, 以将数据呈现给我们的用户。

前端应用程序的布局结构在view / frontend / layout下定义, 并且文件名必须反映我们的路线。由于我们的路线是blog / index / index, 因此该路线的布局文件将是app / code / srcmini / 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="srcmini\Blog\Block\Posts"
                   name="posts.list"
                   template="srcmini_Blog::post/list.phtml" />
        </referenceContainer>
    </body>
</page>

在这里, 我们必须在Magento布局结构中定义三个非常重要的结构:块, 容器和模板。

  • 块是我们的MVVM体系结构的ViewModel部分, 这已在前面的部分中进行了说明。它们是我们模板结构的基础。

  • 容器包含和输出块。它们将块以良好的层次结构组合在一起, 并在处理页面布局时帮助使事情变得有意义。

  • 模板是Magento中特殊类型的块所使用的PHMTL(HTML和PHP混合)文件。你可以从模板中调用$ block变量的方法。该变量始终在模板上下文中定义。你将通过这样做来调用Block的方法, 从而使你可以将信息从ViewModel层提取到实际的演示文稿中。

有了这些额外的信息, 我们可以分析上面的XML布局结构。这种布局结构基本上告诉Magento, 当对博客/索引/索引路由进行请求时, 会将类型为srcmini \ Blog \ Block \ Posts的Block添加到内容容器中, 并将模板添加为用于渲染它的是srcmini_blog :: post / list.phtml。

这导致我们创建了剩下的两个文件。我们的区块位于app / code / srcmini / Blog / Block / Posts.php:

<?php

namespace srcmini\Blog\Block;

use \Magento\Framework\View\Element\Template;
use \Magento\Framework\View\Element\Template\Context;
use \srcmini\Blog\Model\ResourceModel\Post\Collection as PostCollection;
use \srcmini\Blog\Model\ResourceModel\Post\CollectionFactory as PostCollectionFactory;
use \srcmini\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方法。虽然有些事情要注意。

如果你还记得, 我们还没有定义srcmini \ Blog \ Model \ ResourceModel \ Post \ CollectionFactory类。我们仅定义了srcmini \ Blog \ Model \ ResourceModel \ Post \ Collection。那么, 这怎么工作?对于你在模块中定义的每个类, Magento 2都会自动为你创建一个工厂。工厂有两种方法:create(将为每个调用返回一个新实例)和get(将在每次调用时始终返回同一个实例), 用于实现Singleton模式。

我们块的第三个参数$ 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 / srcmini / Blog / view / frontend / templates / post / list.phtml:

<?php /** @var srcmini\Blog\Block\Posts $block */ ?>
<h1>srcmini Posts</h1>
<?php foreach($block->getPosts() as $post): ?>
    <?php /** @var srcmini\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(集合)从数据库中获取我们的模型(srcmini \ Blog \ Model \ Post)。在每个模板中, 每当你要访问其块的方法时, 都会定义一个$ block变量并等待调用。

现在, 只需再次点击我们的路线, 你就可以看到帖子列表。

我们的索引页面,显示帖子列表

查看个人帖子

现在, 如果你单击帖子标题, 则会得到一个404, 因此, 请对其进行修复。有了我们所有的结构, 这变得非常简单。我们只需要创建以下内容:

  • 一项新操作, 负责处理对博客/帖子/查看路线的请求
  • 渲染帖子的方块
  • 一个PHTML模板, 负责视图本身
  • 博客/帖子/查看路线的布局文件, 将最后几部分放在一起。

我们的新动作非常简单。它将仅从请求中接收参数id并在Magento核心注册表中进行注册, Magento核心注册表是一个中央存储库, 用于存储整个单个请求周期中可用的信息。通过这样做, 我们将在以后使该ID对块可用。该文件应位于app / code / srcmini / Blog / Controller / Post / View.php中, 这些是其内容:

<?php

namespace srcmini\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_ID = 'srcmini_blog_post_id';

    /**
     * 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 / srcmini / Blog / Block / View.php中创建具有以下内容的块:

<?php

namespace srcmini\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 \srcmini\Blog\Model\Post;
use \srcmini\Blog\Model\PostFactory;
use \srcmini\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 / srcmini / Blog / view / frontend / templates / post / view.phtml:

<?php /** @var srcmini\Blog\Block\View $block */ ?>
<h1><?php echo $block->getPost()->getTitle(); ?></h1>
<p><?php echo $block->getPost()->getContent(); ?></p>

简单又好用, 只需访问我们之前创建的View块getPost方法。

并且, 总而言之, 我们在app / code / srcmini / 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="srcmini\Blog\Block\View"
                   name="post.view"
                   template="srcmini_Blog::post/view.phtml" />
        </referenceContainer>
    </body>
</page>

这和我们之前做的一样。只需将srcmini \ Blog \ Block \ View添加到内容容器, 并使用srcmini_Blog :: post / view.phtml作为关联模板。

要查看实际效果, 只需将浏览器定向到http://magento2.dev/blog/post/view/id/1即可成功加载帖子。你应该看到如下屏幕:

用于显示单个帖子的页面

正如你所看到的, 在创建我们的初始结构之后, 向平台添加功能确实非常简单, 并且我们的大多数初始代码在该过程中都得到了重用。

如果你想快速测试模块, 这是我们工作的总结果。

从这往哪儿走

如果你一直关注我, 直到恭喜你!我很肯定你即将成为Magento 2开发人员。我们已经开发了一个非常高级的Magento 2定制模块, 尽管它的功能很简单, 但已经涵盖了很多基础。

为了简单起见, 本文省略了一些内容。仅举几例:

  • 管理员编辑表单和表格以管理我们的博客内容
  • 博客类别, 标签和评论
  • 我们可以建立的存储库和一些服务合同
  • 将模块打包为Magento 2扩展

无论如何, 这里有一些有用的链接, 你可以在这些链接上加深了解:

  • Magento 2上的Alan Storm博客-关于学习Magento, Alan Storm可能是最具说教意义的内容。
  • Alan Kent撰写的博客
  • Magento文档:Magento 2开发文档

我为你提供了有关如何在Magento 2中创建模块的所有相关方面的全面介绍, 并在需要时提供了一些其他资源。现在, 你可以自己进行编码, 或者如果想要考虑的话, 直接去评论。

赞(0)
未经允许不得转载:srcmini » Magento 2教程:如何构建完整的模块

评论 抢沙发

评论前必须登录!