---
# System prepended metadata

title: LaravelConf 2023
tags: [教材, Laravel]

---

成為功能管理大師：應用持續整合精神 與 Laravel Pennant 實戰工作坊

教學大綱：

* 介紹主幹開發與持續整合
* 介紹功能標誌與它的類型
* 工作坊實際感受功能標誌的威力
* 分享使用功能標誌的注意事項

## 前置技能

- Git / GitHub
  - 熟悉 GitHub Fork 操作，需要能夠處理多個 Remote
  - 使用 Actions 確認功能正常
  - 熟悉 Merge、Rebase、Branch、Reset 等操作
- PHP 8.1 / 8.2
  - 主要使用 PHP 8.1，PHP 8.2 測過可用
- Composer 2
- Laravel 10 + Laravel Pennant
- MySQL（SQLite）
- Node v16.20+
  - 建置前端資源時需要

## 環境建置

先到 GitHub 上 Fork 此公開的版本庫：https://github.com/MilesChou/Mart

下載回來後，先安裝 Composer 依賴套件

```
composer install
```

複製 `.env` 並產生 key

```
cp .env.example .env
php artisan key:generate
```

設定資料庫，已有設定好 Docker Compose，可以直接透過指令啟動：

```
docker-compose up -d
```

替代方案是使用 SQLite，先產生空檔案：

```
# 清除資料庫也可以用一樣的指令
echo > database/database.sqlite
```

接著再到 `.env` 檔裡調整設定，主要是改 `DB_CONNECTION=sqlite` 以及註解 `#DB_DATABASE=laravel`，其他 DB 設定不變

```
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
#DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=pass
```

DB 設定完成後，即可執行 Migration

```
php artisan migrate:fresh --seed
```

執行 PHP built-in 伺服器

```
php artisan serve
```

打開 http://localhost:8000 即可看到首頁。打開 http://localhost:8000/admin/login 可登入後台 

預設帳號如下，密碼統一為 `password`

```
# 管理員
admin@2023.laravelconf.tw

# 物流帳號
miles@2023.laravelconf.tw
nathan@2023.laravelconf.tw

# 普通帳號
ban@2023.laravelconf.tw
```

建立帳號指令如下：

```
Usage:
  app:user:create [options]

Options:
      --name[=NAME]          
      --email[=EMAIL]        
      --password[=PASSWORD]  
      --role[=ROLE]           [default: "user"]
```

可以透過 Artisan 指令建帳號：

```
php artisan app:user:create --name=Miles --email=miles@tester.com --password=test
```

前端相關的資源已提交進版本庫。不更新前端資源的前提下，執行本工作坊可能會有一點影響，但不會到非常嚴重。

建置前端資源的方法如下：

```
# 使用 .nvmrc 所標記的 Node 版本 v16.20.0
nvm use

# 安裝 Node 套件
npm install

# 建置前端資源
npm run dev
```

## 工作坊

本工作坊會介紹 Laravel Pennant 套件，與適用它的實作內容

### Laravel Pennant 能做什麼事？

參考 [Laravel Pennant](https://laravel.com/docs/10.x/pennant) 官網內容，裡面說明寫得很詳細，工作坊的第一個內容是來介紹這個套件的功能：

類別圖

```mermaid
classDiagram
  class FeatureManager
  class PendingScopedFeatureInteraction
  class Driver
  <<Interface>> Driver
  class ArrayDriver
  class DatabaseDriver
  class Decorator
  Driver <|-- ArrayDriver
  Driver <|-- DatabaseDriver
  Driver <|-- Decorator
  Decorator o-- Driver
  FeatureManager *-- Driver
  Decorator --> PendingScopedFeatureInteraction
  PendingScopedFeatureInteraction o-- Driver
```

管理 Feature 的主要物件是 `FeatureManager`，而產生（resolve 方法）`Driver` 的時候會使用 `Decorator` 包裝：

```php=
return new Decorator(
    $name,
    $driver,
    $this->defaultScopeResolver($name),
    $this->container,
    new Collection
);
```

Laravel 設計 Manager 的習慣，是它把當作 Aggregate Root 在做的，因此所有存取背後的 Entity 都必須要經過它。

在呼叫的時候可以透過 `__call()` 直接 Proxy 到預設的 Driver：

```php=
public function __call($method, $parameters)
{
    return $this->store()->$method(...$parameters);
}
```

最後就是 `PendingScopedFeatureInteraction`，Decorator 把所有的呼叫都轉給它處理。它的用途是讓 Decorator 整合 Scope 參數的邏輯整理在這個 class，而原本的 Decorator 則是專注在實作 Driver，是一種職責分離的處理方法。（[Container](https://mileschou.me/ironman/2019/analyze-laravel/day03/) 也有一樣的設計）

而對廣義的程式設計來說，這是一種 Mixin 的手法：

```
FeatureManager mixin Decorator
Decorator mixin PendingScopedFeatureInteraction
```

這個結果也就讓 FeatureManager 可以同時擁有 Decorator 與 PendingScopedFeatureInteraction 所提供的方法。

但還是要回到前面提到的，Laravel 設計 Manager 的概念，就是把 Manager 當作是 Aggregate Root。


https://github.com/MilesChou/Mart/pull/7

1. Driver 選擇
	1. 介紹內建的 Database 與 Array
	2. 實務上會需要的 Driver 可能為何
2. 定義 Feature
	1. Feature 的兩個重要元素：名稱與初始化
	2. 定義 Feature 的兩種方法，以及如何發現（discover）Feature
	3. Class 定義的注意事項
	    1. 名稱
3. 檢查 Feature 狀態
    1. 透過注入物件
    2. 透過 Facade（官方文件上說明的主要方法）
    3. 透過 Trait 加入特性
    4. 透過 Blade Directive
    5. 透過 Middleware
    6. In-Memory Cache 會引發的問題（確認中）
    7. Eager Loading
4. 控制 Feature 狀態
	1. 單一更新
	2. 批量更新
	3. 清除狀態
5. Feature 作用範圍
6. 超越開與關的 Feature
    1. 存入更多內容 
7. 測試
8. 事件
	1. `RetrievingKnownFeature`
	2. `RetrievingUnknownFeature`
	3. `DynamicallyDefiningFeature`

以上說明應該是完整的，但缺少使用情境，比較不容易快速理解。接著要正式開始依使用情境開發，先回顧 Flags 的四個種類如下：

![](https://martinfowler.com/articles/feature-toggles/chart-4.png)

- Release Toggles
- Experiment Toggles
- Ops Toggles
- Permission Toggles

接著我們把這四種開關區分成兩類：

- 常常使用的開關，或是「開關就是功能」
- 暫時使用的開關

這兩類開關設計的差異關鍵在於，常常使用的開關就是一種「功能」，因此在一開始在寫程式的時候，就能夠導入開關的概念；相反地，短暫使用的開關因為最終會移除，因此在寫程式的時候，是能夠將「分支」當作是開關在使用，切換分支即可切換關開，而合併分支則是移除開關。

依這個概念即可推測，Ops Toggles、Permission Toggles、Experiment Toggles，會是一開始就把開關就做好，而 Release Toggles 則是一開始沒有開關，而是可以後續補上。

### 熱身練習 - Experiment Toggles

實驗性質的功能：*Didn’t Find Your Match*，這是一個讓顧客在找不到商品的時候，可以知道還有什麼選擇可以用的 UX 設計。

這個功能可能很棒，但並不確定是否對我們的使用者有用，因此會是個實驗性質的功能。

我們將會把這個功能做成開關，並決定要對哪些使用者開放功能，此功能的 PR：[Feature: Didn't Find Your Match by MilesChou · Pull Request #4 · MilesChou/Mart · GitHub](https://github.com/MilesChou/Mart/pull/4)

根據 Laravel Pennant 提供的方法，我們知道可以用 Blade 語法來達成這個目的：

```
@feature('experiment')

...

@endfeature
```

一個開關除了程式判斷外，還要定義它的初始化方法，以及強制的開關方法，也將會在練習中說明。


初始化的方法，可以使用 Class，先用 `php artisan pennant:feature Experiment` 建立 class：

```
class Experiment
{
    public string $name = 'experiment';

    public function resolve(mixed $scope): mixed
    {
        return false;
    }
}
```

這裡有一些方案可以參考，例如：

1. 隨機給予開或關，像 1% 的打開機率可以這樣寫：
    * `return Lottery::odds(1/100);`
2. 特定使用者打開，這時的 `$scope` 就必須要限制是 User 型態，讓 Feature 有辦法判斷與選擇，像 id 是 2 的倍數可以這樣寫：
    * `return ($scope->getAuthIdentifier() % 2) === 0;`
3. 後台決定特定的使用者開或關，這時可以讓它 `return false` 就可以了，我們可以額外寫指令來啟用開或關。
    * 檢查狀態 `php artisan app:experiment:status`
    * 啟用服務 `php artisan app:experiment:activate`
    * 停用服務 `php artisan app:experiment:deactivate`

接著再來看看如何測試：

> （預計會加測試練習項目）

### 簡單應用 - Ops Toggles

[Ops Toggles](https://github.com/MilesChou/Mart/pull/5)

遇到問題可以緊急關閉營運的功能：Ops Flags

另一個類似的功能是 `php artisan down`，但我們這裡使用 Laravel Pennant 來達成這個目的，但不用讓整個服務下線。

首先先定義使用下面這三個指令來控制開關：

```
# 檢查狀態
php artisan app:operation:status

# 啟用服務
php artisan app:operation:activate

# 停用服務
php artisan app:operation:deactivate
```

只要停用，註冊、登入、購物功能就會被隱藏起來（表面上），以讓系統能夠先把手頭上的任務完成後，再重新打開服務。

> （預計會加測試練習項目）

Q: 是否可以開一個 DB 欄位存，就能達到一樣的需求。
A: 如果有 Scope 概念的話（如 login 與 order 要區分）那 Laravel Pennant 就是個不錯的選擇。

### 進階應用 - Permission Toggles

此為 VIP 會員專屬的額外打折功能。

https://github.com/MilesChou/Mart/pull/6

非 VIP 會員會是原價售出，而身為 VIP 會員，可以無條件打九折。

這跟 Ops Toggles 很像，通常會由使用者介入觸發設定的開與關（也有可能是自動的），差別在於 Ops Toggles 在意的全站功能調

### 部署應用 - Release Toggles

這是壓軸，也是最難完成的工作坊。

首先先論三個功能同時開發的可能性：

- A：使用者評論商品與賣家回覆功能 [CustomerQuestion](https://github.com/MilesChou/Mart/pull/1)
- B：首頁輪播功能 [Carousel](https://github.com/MilesChou/Mart/pull/2)
- C：快速購買功能 [FlashSale](https://github.com/MilesChou/Mart/pull/3)

主幹開發有提到一個概念，稱之為抽象分支（Branch by Abstraction），這是使用 Feature Flags 的概念來達到「用程式做分支」的結果，這同時也是「在飛行中的飛機更換引擎」的技術。

我們可以先試著在本機合併這些分支，它們有極高的機率會發生衝突，這正是主幹開發所提到的 distance。

此工作坊的執行步驟：

1. 將三個分支拆 Commit，需使用 rebase 指令處理。例如：
  - A1 → A2 → A3
  - B1 → B2 → B3
  - C1 → C2 → C3
2. 將新功能關鍵流程加上 Laravel Pennant，並設定預設關閉。例如：
  - A1 → A2（Flags） → A3
  - B1 → B2 → B3（Flags）
  - C1（Flags） → C2 → C3
3. 模擬現實開發過程，Commit 的合併時機點是不固定的，但每個功能開發是依照順序的。例如：
  - A1 → B1 → B2 → C1（Flags） → A2（Flags） → B3（Flags） → C2 → C3 → A3
4. 每個分支最後一個 Commit 完成時，可以透過啟用功能的方法來測試該流程是否正常。例如：
  - 單元測試寫法
  - 第一個完成的功能為 B3，因此可以在 B3 完成的時候，使用指令啟用特定使用者的功能做測試。
5. 最後在全部完成時，決定要正式啟用哪項功能，這時即可做線上測試
  - 線上內部使用者測試
  - 線上外部使用者測試，透過規則或是使用者自行啟用等
6. 功能測試完成後，即可正式開放
  - 調整 Feature 預設開關
  - 清除過去使用者的記錄 `php artisan pennant:purge`
