Magento 2 教程:如何構建一個完整的模塊

已發表: 2022-03-11

Magento 目前是世界上最大的開源電子商務平台。 由於其功能豐富且可擴展的代碼庫,世界各地的大小型商家已將其用於各種項目。

Magento 1 已經問世八年,它的繼任者 Magento 2 於 2015 年底發布,改善了早期版本的弱點,例如:

  • 提高性能
  • 官方自動化測試套件
  • 更好的後端 UI
  • 新的、更現代的前端代碼庫
  • 一種更加模塊化的模塊開發方式,文件包含在 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 是一個模型視圖視圖模型(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 完整架構圖

Magento 2 模塊可以通過使用 PHP 的依賴管理器 Composer 來定義外部依賴。 在上圖中,您可以看到 Magento 2 的核心模塊依賴於 Zend Framework、Symfony 以及其他第三方庫。

下面是 Magento/Cms 的結構,這是一個 Magento 2 核心模塊,負責處理頁面和靜態塊的創建。

Magento/Cms 模塊的目錄佈局

每個文件夾包含架構的一部分,如下所示:

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

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,實現 Post 數據接口
  • Post 資源模型,用於將 Post 模型鏈接到數據庫
  • 一個帖子集合,在資源模型的幫助下一次從數據庫中檢索多個帖子
  • 兩個遷移類,用於設置我們的表模式和內容
  • 兩個操作:一個列出所有帖子,另一個單獨顯示每個帖子
  • 塊、視圖和佈局文件各兩個:列表操作各一個,視圖各一個

首先,讓我們快速瀏覽一下核心源代碼文件夾結構,這樣我們就可以定義放置代碼的位置。 我們安裝的方式包含 Magento 2 的所有核心代碼,連同它的所有依賴項,都位於 composer 的vendor文件夾中。

Magento 2核心代碼的目錄佈局

註冊我們的模塊

我們會將代碼保存在單獨的文件夾app/code中。 每個模塊的名稱都採用Namespace_ModuleName的形式,並且它在文件系統上的位置必須反映該名稱,本示例中為app/code/Namespace/ModuleName 。 按照該模式,我們將模塊命名為Toptal_Blog並將我們的文件放在app/code/Toptal/Blog下。 繼續創建該文件夾結構。

我們的 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_DirectoryMagento_Config

現在,我們有了一個 Magento 2 應該可以識別的模塊。讓我們使用 Magento 2 CLI 來檢查它。

首先,我們需要禁用 Magento 的緩存。 Magento 的緩存機制值得專門寫一篇文章。 目前,由於我們正在開發一個模塊並希望 Magento 立即識別我們的更改而無需始終清除緩存,我們將簡單地禁用它。 從命令行運行:

 ./bin/magento cache:disable

然後讓我們通過查看模塊的狀態來看看 Magento 是否已經知道我們的修改。 只需運行以下命令:

 ./bin/magento module:status

最後一個的結果應該類似於:

status 命令的輸出,顯示 Toptal_Blog 模塊被禁用

我們的模塊在那裡,但正如輸出所示,它仍然被禁用。 要啟用它,請運行:

 ./bin/magento module:enable Toptal_Blog

那應該做到了。 可以肯定的是,您可以再次調用module:status並在啟用列表中查找我們的模塊名稱:

status 命令的輸出,顯示 Toptal_Blog 模塊已啟用

處理數據存儲

現在我們已經啟用了我們的模塊,我們需要創建保存我們博客文章的數據庫表。 這是我們要創建的表的架構:

場地類型空值鑰匙默認
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 客戶端,您可以檢查表是否已真正創建:

在 MySQL 客戶端中演示我們的表

setup_module表中,現在有對我們模塊、其架構和數據版本的引用:

setup_module 表的內容

好的,那麼架構升級呢? 讓我們通過升級向該表添加一些帖子,以向您展示如何做到這一點。 首先,在我們的etc/module.xml setup_version中設置 setup_version:

在我們的 module.xml 文件中突出顯示更改的值

現在我們創建我們的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表中:

更新了 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 case Context and PageFactory .

  • The execute method is responsible for the action execution itself. In our case, we are simply telling Magento to render its layout by returning a Magento\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 中創建模塊的所有相關方面的全面介紹,以及您需要的一些額外資源。 現在由您來編寫代碼,或者如果您想參與其中,請前往評論區。