標籤彙整: laravel

mma

分離式客製化開發:兼顧模組化與客製化的 Laravel 彈性架構

接案的時候,常常很想把一套系統寫完之後,一口氣賣給多個客戶。

偏偏每個客戶都有各自不同的客製化需求,沒辦法真的都拿同一個專案去佈署。

只好把一個專案複製成好幾份,然後分別客製化給不同客戶。

但這樣又會出現維護問題:各專案的差異越變越大。

之後就算更新了核心功能、修理了核心 bug,也很難一口氣替所有客戶升級。

如果各專案的差異是無法避免的,那有沒有辦法至少,讓核心系統與客製化的程式碼分開來放呢?

最好能讓各專案的檔案結構都像這樣:

如此一來,雖然 Customization/ 內的 code 不能重複使用,至少 System/ 內的 code 可以。

可以將 System/ 內的 code 更新進其他專案,也可以拿強化後的核心系統去接未來的新案子。

我把這種檔案結構稱為「分離式客製化架構」,下文用 SCA (Separated Customization Architecture) 代稱。

這篇文章跟大家分享在 Laravel 底下如何使用這種 SCA 架構。

前置需求

使用 SCA 架構非常簡單,只要安裝 laravel-modules 套件就可以了。

laravel-modules 套件非常單純,它只是運用了 Laravel 原生的 Package Development 觀念,幫助你把 app/ 與 resources/ 資料夾底下的程式碼移進另外的 Modules/ 資料夾而已。

Modules/ 內可以有一或多個資料夾,每個資料夾就跟一個獨立 laravel app 一樣,有自己的 controllers, views, entities, routes, migrations… 等等內容。

laravel-modules 套件就只是幫你讀取各個模組的內容而已,沒有其他黑魔法。

如此一來,你的 Laravel 專案的檔案結構會變成這樣:

注意到了嗎?它剛好對應到前面說的檔案結構:

Modules/ 就是 System/,而 app/ 與 resources/ 就是 Customization/。

那麼實際上該如何開發呢?以下直接舉例說明。

情境:你開發了一套 Awesome 通用系統來接案

首先你根據過去的經驗開發了一套 Awesome 電商官網模組,打算用它來接許多案子,賺大錢。

你的 laravel 檔案結構長這樣:

Awesome 模組的 controllers, views, entities, routes, migrations… 等等內容都在 Modules/Awesome/ 裡面,就跟一個獨立 laravel app 一樣。

而 app 與 resources 資料夾內目前只有 laravel 預設檔案,你沒寫半行 code 在裡面。

你拿 Awesome 系統開始接案,接著客戶出現。他們有以下的客製化需求。

情境:客戶想修改 UI

客戶要求對首頁畫面微調。

這個時候請利用 laravel 原生的 Overriding Package Views 機制,你直接新增一個檔案:

就在這個檔案內隨心所欲的重寫或是亂寫吧!它會直接取代原本的這個檔案:

如果不只是畫面微調,這種模板覆蓋不夠用,那就可以視為修改系統的 business logic,請接著往下讀。

情境:客戶想修改系統的 business logic

客戶想修改結帳前的購物車頁面與功能。

此功能原本的 routing 定義在 Modules/Awesome/Http/routes.php 內:

請在原本 laravel 預設的 routes/web.php 內新增:

接著建立 app/Http/Controllers/ShoppingController.php 檔案,去繼承原先在 Modules/Awesome/ 內的 controller:

這樣就會直接取代原本的功能了!

最棒的是,這只是用上了基本的 OOP 類別繼承觀念,以及 laravel 的特性:「重複的話,後定義的 route 會覆蓋先定義的 route」。

有需要的話,domain model 內的類別也可以用同樣手法繼承來擴充。

比如說原本有個 Modules/Awesome/Cart.php 類別:

你可以建立 app/Cart.php 這樣的子類別:

在新建立的 controller 內就可以使用這些擴充後的新 domain model 類別。

用這種手法可以自由新增程式碼!而且全是在 app/ 或 resources/ 資料夾內新增,完全沒碰到 Modules 內的東西。

情境:客戶想開發新功能

開發全新功能就更簡單了,直接在 routes/web.php 內增加 routing 後,在 app/ 與 resources/ 內增加你需要的檔案就行了。

就跟平常開發 laravel 專案一樣!

有需要的話,就在新的檔案裡面引入或是繼承 Modules/ 內的類別就行了。

情境:需要更新資料庫 schema

laravel-modules 有提供指令幫忙產生 migration 檔案。

所以屬於 Awesome 系統的 migration 檔案會在 Modules/Awesome/Database/Migrations/ 底下。

如果客製化時需要更新 schema ,就用 laravel 原生的 migration 指令在 database/migrations/ 內建立 migration file 即可。

連 migration file 都可以完全分離在 System/ 與 Customization/ 裡面!

結論

SCA 的特色在於它不預設你打算把專案模組化或是客製化到哪種程度。

你可以為了客戶在 Customization/ 內先寫完功能。之後有空再完全重構或是部份重構進 System/。

也可以遇到需求就先強化 System/,寫到不適合的地方再開始從 Customization/ 接手客製化。

你可以視情況來來回回,以一種混合的方式寫 code。

你可以邊寫邊決定,事後隨時反悔也沒問題,所以 SCA 非常適合接案類型的軟體開發。

針對 SCA 還有許多進階議題可以討論,本文只談最基本的觀念與技巧,但應該足以解決大部份的問題。

我自己用這種方式接案開發了幾個月,效果非常不錯,分享給各位參考。

有任何問題歡迎留言讓我知道,或是寄信到 howtomakeaturn@hotmail.com 與我討論。

(完)

star

胖胖 Model 的減重方法:Form

Web application常常需要對使用者輸入的參數做驗證。

這些驗證(validation)logic該放哪裡呢?

現代框架通常會提供工具協助這件事,

以Laravel來說,使用內建的Validator可以在controller內這麼做:

同樣的validation logic只出現一次還好,出現在不同controller就會導致這段code duplicate。

該怎麼辦呢?

試試看放進model:

controller內的code就會簡化成這樣:

還算OK的作法…如果你能接受將validation logic視為business logic的話。

這類的code有比controller和model更適合的地方。寫一個form class即可:

controller內直接呼叫即可,看起來readable許多:

如果多個model都有form classs的話,可以將共用的code抽出來成為抽象類別:

這樣原本的form class可以簡化到只剩下validation rules:

甚至能寫多種validation rules給不同controller:

實作上可以參考Laravel官方論壇原始碼

(注意:若您是用Laravel 5,Request的封裝可以取代這件事。)

下次你的application包含大量validation logic時,不妨試試看這個作法。

(Photo via Susanne Nilsson, CC licensed)

star

胖胖 Model 的減重方法:Repository

開發application常常要對資料庫做各種不同條件的查詢。

這類的查詢code很容易到處重複。

以 Laravel Eloquent來說,controller很容易出現這樣的code:

這段「取出受歡迎的女性使用者」的code,在中小型application還可以接受,

一旦規模稍大就會在不同controller到處重複(duplicate)。

利用內建的query scope功能,雖然能夠稍微改善,但還是不夠:

不只如此,還會讓model裡面多出定義query scope的程式碼,導致model變胖:

這種時候,適合寫一個Repository進行封裝:

它會讓controller內的code簡潔、可讀許多:

如果您是 Laravel 使用者,這種搭配Automatic Resolution的寫法一定經常看到:

甚至可以寫一個共用的abstract class減少duplicate code:

透過Repository封裝還有一個好處:

測試controller或是service的時候可以抽換掉Repository,讓測試時不用碰到資料庫!

下次你的application包含大量query logic時,不妨試試看這個作法。

(Photo via Susanne Nilsson, CC licensed)

Laravel-–-a-beautiful-PHP-framework

胖胖 Model 減重的五個方法

隨著軟體專案不斷增加新功能,你是否隱約感到不安?覺得Model越來越胖、開發速度越來越慢?

一元翻譯用Laravel 4開發半年多,我也遇到同樣問題。

上禮拜受PHP 也有 Day的邀請,

跟大家分享了幾個我在工作上找到的改善方法,分別是:

  • Presenter
  • Repository
  • Form
  • Service
  • Package

投影片在此

影片在此

投影片是以Laravel 4當範例,但是概念上通用於任何語言、框架。

歡迎在這邊留言和我討論,有問題想問也可以。謝謝各位。

codeigniter-logo

CodeIgniter不是OOP好老師

寫完框架不應該有「MODELS」資料夾之後,

收到林先生來信如下:

您好, 我剛拜讀完您的「框架不應該有「models」資料夾」
我對文章的內容有許多地方不理解…不…可以說是混亂
我本身較熟悉的框架只有CI, 平時的撰寫模式就是將使用者的輸入放在controller做驗證, 驗證通過之後才會將之丟進model class(或是您稱呼的 entities)做處理, 丟進model class處理時會有一些除了真正該執行的任務外以外的行為(像是註冊新會員時我會需要檢查帳號是否已被使用或是不是黑名單), 根據model class 的回應結果決定controller該輸出什麼結果或view
然後我看到了「抽出行為邏輯變成Service Objects、抽出表單驗證邏輯變成Form Objects、抽出資料庫查詢邏輯變成Query Objects、抽出呈現邏輯變成View Objects…聽起來真棒,也確實很有幫助,不是嗎?」這一段, 您想表達的意思是否是將各種行為(驗證輸入資料、查詢/寫入資料庫或其他的行為邏輯)細分出來放在model(或像您所的依照專案/公司名稱做分類)底下, 而controller則是扮演操作這些entities角色?

我可以理解你的困惑,我可以理解你的混亂。

我有整整一年使用CodeIgniter開發。

當我第一次看到Reddit上頭大家的討論、當我第一次看到Laravel論壇的原始碼

我也是完全一頭霧水:這些人在用的PHP跟我在用的PHP是同一個嗎?

轉進Laravel社群幾個月之後,我終於知道問題出在哪了。請聽我娓娓道來。

CI不鼓勵你打造Entity

…驗證通過之後才會將之丟進model class(或是您稱呼的 entities)…

這句話不正確。CI的Model不是Entity。

…而controller則是扮演操作這些entities角色?

這句話不正確。service object, view object, form object都不是entity。它們是與entity相關的類別。

CI的Model並沒有代表「現實生活中的一種事物」,

它只是負責「把一張資料表的內容直接丟回去」而已。

以Blog Model舉例,假設它在資料表有title跟content欄位,但是沒有summary欄位。

如果我們需要文章簡介summary(由title加上content前100字組合而成),

在Laravel它會是這樣:

在Model之外用到Entity的時候,會是這樣:

你得到了$blog這個entity,它代表著一篇部落格文章。

這篇文章很樂意告訴你它的summary。

但是在CI呢?

從CI的Model拿到的東西,只代表「跟文章相關的一坨資料」。可以是Array或是stdClass。

並不代表一篇部落格文章,也無法具有「行為」。

CI的Model不是Entity。

我提到的Service Objects、Query Objects、View Objects都是圍繞著Entity打轉,而CI本身只幫你拉資料出來,沒幫你做Entity。也難怪會一團混亂了。

怎麼會這樣?

這裡的Entity說穿了就是將資料庫的資料轉換成物件而已。

可以自己寫Class手動將資料庫的value一個一個轉成物件的property。這麼做又麻煩又緩慢。

常見的作法是直接用ORM,可以是Data Mapper Pattern或是更常見的Active Record Pattern。

許多框架都會提供Active Record Pattern的實作,Laravel, Rails都是。

有Entity可以幹嘛?

可以讓程式碼更優雅。

我就直接拿View Objects、Query Objects、Form Objects做舉例了。

View Objects

這個View Object實作為例,

你可以把User Entity中只與「呈現」相關的logic抽出來成這種類別:

使用的時候這樣即可:

Query Objects

這個Query Object實作為例,

你可以把取得文章的多種不同方式logic獨立出來,成為這樣的class:

在使用的時候,只要這樣即可:

Form Objects

Form Objects倒是跟entity比較無關,但我還是舉例給你看它長相。

這個Form Object實作為例,

你可以把建立Article的表單驗證logic抽出來成這種類別:

在使用的時候,只要這樣即可:

以上,你會發現各式各樣的logic都可以拆分成獨立class,讓思維上比較OOP。

controller裡面的code可讀性也高很多。

這些在CI中比較難做到。CodeIgniter不是OOP好老師。

當然了,有些人很討厭OOP,對它一點興趣也沒有。看著辦吧。


Q&A

Q1: 可是CI有Active Record類別啊?

CI的Active Record類別是整個CI社群最悲慘的誤會之一。

CI的Active Record完全是叫爽的。

那根本沒有實作Active Record Pattern,只是個陽春的Query Builder(幫你打造SQL query的助手)而已。

CodeIgniter’s “Active Record” isn’t what the real active record is all about.

You’re also being lead astray by CI’s rather confusing name ActiveRecord, which actually doesn’t implement the ActiveRecord pattern.

CodeIgniter has a query builder it calls “ActiveRecord”, but which does not implement the Active Record pattern.

CI的狗屁Active Record就是一個對使用者有害、混淆觀念、沒人要點破、

大家不求甚解、為了行銷而存在的垃圾名詞。

業界不像學界嚴謹,大家永遠為了行銷在瞎搞,這種狗屁到處都是。

「MVC」就是其中程度最誇張的例子。

Q2: 我整篇文章都看不太懂,我現在更緊張了。

別急,慢慢來。OOP要寫得優雅,本來就非常不容易。

有空去摸摸看Laravel吧,感受一下,也許會有靈感。

有問題可以到Laravel台灣發問唷。


社群看法(2015-4-6)

PHP台灣

double

一堆dependency怎麼測試?把替身叫出來。

我任職的翻譯公司需要線上報價功能。

其中,上傳文件到專案的功能,會同時計算字數、備份檔案到遠端。

程式碼大概是這樣:

由於這是個核心功能,我希望多寫幾個測試確保它的運作正常。

這樣的function該怎麼測試呢?

我是用Laravel + PHPUnit開發。首先,寫個基本測試,確保function沒有打錯變數名字、語法、不會有莫名exception噴出來:

問題來了,這個測試跑得非常慢。因為:

  • 它真的把檔案上傳到資料備份主機
  • 它真的用Reader數字數

我只想確認基本邏輯正常,並不在乎備份、計算字數功能是否正常。(那兩個應該另外測試。)

該怎麼辦呢?

是時候叫替身出來幫忙了!

Test Double

Test Double,我翻譯為「測試替身」,是Gerard Meszaros在2007年提出來的名詞。
它是軟體測試的常見手法之一,會做出類別的替身供測試使用。
又可細分為Fake, Mock, Stub, Dummy… 等等。
我們先不管差別何在,想辦法解決眼前的狗屁就好!

首先,我們得將原本的function改寫,把dependency改用參數傳入:

呼叫這個function的相關code,記得改寫成:

重點來了,測試怎麼寫?怎麼做出測試替身?

我想要一個不管收到什麼,一律回傳5555當作假id的$remote替身;
還要一個不管收到什麼,都說它計算出6666個字數的$reader替身。

PHPUnit本身就支援Test Double,只要這樣即可:

搞定!
成功避開遠端備份與計算字數的dependency!

測試完基本邏輯,接著可以寫各種不同的測試,去檢驗你想確認的部份,
全都不會真的上傳檔案到備份主機與計算字數!

利用test double的手法,你可以避開一大堆的dependency,測試到你原本以為不可能測試的地方。

很神奇吧!


Q&A

Q1: 把物件當dependency傳入真的好帥喔,這招叫什麼?

這招只是dependency injection的一種。

為了讓code能夠被測試,這招很常用到。

Q2: function傳入檔案$file又傳入$remote跟$reader。我怎麼覺得醜到爆啊?

這樣寫確實很醜。

通常物件dependency是先在constructor傳入,再從function內拿來用:

你常常看到的這種pattern就是在幹這件事:

但是我懶得改寫所有相關code,而且Laravel的Eloquent建構式又很難搞定物件dependency。

學到精神就好,寫法隨機應變。

Q3: 我還滿好奇,你提到的翻譯公司網站長怎樣?

http://wordcorp.net/

(Photo via AlmaArte Photography, CC licensed. )