Byte Ebi's Logo

Byte Ebi 🍤

每天一小口,蝦米變鯨魚

開發屬於自己的 Laravel 套件

介紹如何在本機進行 Laravel 套件開發及撰寫測試,並在本機專案透過 composer 安裝自行開發的套件

Ray

目前的 Laravel 生態中有許多好用的套件包可以使用
但要是找不到別人寫好的套件包,又或是必須建置公司共用的套件包怎麼辦呢?
本篇就來在本機進行套件包開發,並且會在本機專案中使用 composer 進行安裝並測試

有些文章會教你在既有的 Laravel 專案中建立 packages 資料夾,並在裡面進行開發
但這不是一個好方法,試想你萬一有天把專案刪掉了,套件包的專案也被刪掉了
並且這種做法不能測試使用 composer 安裝是否正常,所以本篇使用的是另一種方式

1. 初始化套件專案

首先我們開一個資料夾,方便起見我們用 larapeko 作為範例

mkdir larapeko

接著進到剛剛的 larapeko 資料夾中,執行指令初始化套件

cd larapeko
composer init

會經過一連串的問答協助你設定套件的基本資訊
larapeko composer init

接著就會在資料夾內看到剛剛初始化的套件 composer.json 檔案

{
    "name": "ray247k/larapeko",
    "description": "A package for demo peko",
    "license": "MIT",
    "authors": [
        {
            "name": "Ray",
            "email": "[email protected]"
        }
    ],
    "minimum-stability": "dev",
    "require": {}
}

2. 撰寫套件程式

在 larapeko 資料夾中新建 src 資料夾,我們的套件程式碼主要都放在這
建立一個名為 LaraPeko.php 的檔案,並設定所使用的 namespace
為了後續示範,我先建立一個簡單的 function

<?php
# LaraPeko.php

namespace Ray247k\LaraPeko;

class LaraPeko
{
    public function sayPeko()
    {
        echo "好油喔 peko\n";
    }
}

3. ServiceProvider

在 src 目錄內建立剛剛檔案的 ServiceProvider,為了好識別,我們就命名為 LaraPekoServiceProvider.php
透過 register 中的 singleton,會註冊一個叫做LaraPeko的類別,並回傳剛剛建立的 LaraPeko 物件

<?php
# LaraPekoServiceProvider.php

namespace Ray247k\LaraPeko;

use Illuminate\Support\ServiceProvider;

class LaraPekoServiceProvider extends ServiceProvider
{
    public function boot()
    {

    }

    // 註冊套件函式
    public function register()
    {
        $this->app->singleton('LaraPeko', function ($app) {
            return new LaraPeko();
        });
    }
}

4. Facade

Facade 提供一個靜態的介面讓我們直接呼叫註冊過的類別

在 src 目錄內建立剛剛檔案的 Facade,命名為 LaraPekoFacade.php 吧
使用剛剛上面 ServiceProvider 註冊的LaraPeko類別作為 Laravel 的 Facade 物件

<?php
# LaraPekoFacade.php

namespace Ray247k\LaraPeko;

use Illuminate\Support\Facades\Facade;

class LaraPekoFacade extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'LaraPeko';
    }
}

5. 預設 config 以及使用方法

在使用套件的時候,有時候會想讓使用者可以透過設定檔設定參數
這時候可以在套件目錄下建立config資料夾,並且在裡面加入 config 設定檔 lara_peko.php

<?php
# lara_peko.php

return [
    'best_girl' => 'Yagoo',
];

建立好了之後,我們要編輯LaraPekoServiceProvider.php成底下內容

<?php
# LaraPekoServiceProvider.php

namespace Ray247k\LaraPeko;

use Illuminate\Support\ServiceProvider;

class LaraPekoServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $source = realpath($raw = __DIR__.'/../config/lara_peko.php') ?: $raw;
        $this->publishes([
            $source => config_path('lara_peko.php'),
        ]);
    }

    // 註冊套件函式
    public function register()
    {
        $configPath = __DIR__ . '/../config/lara_peko.php';
        $this->mergeConfigFrom($configPath, 'lara_peko');

        $this->app->singleton('LaraPeko', function ($app) {
            return new LaraPeko();
        });
    }
}

boot()中使用publishes方法後,在引用套件的時候可以在專案內使用指令

php artisan vendor:publish

並根據跳出的互動視窗選擇套件對應數字輸入
就可以的在專案目錄的 config 路徑下產生套件包的 config 範本對應的檔案

如果如果你很確定自己要建立的是哪一個套件的 config 檔,也可以直接加上 provider 標籤
指定要呼叫的是哪一個命名空間內的 provider 的 publishes 發布方法

php artisan vendor:publish --provider="Ray247k\LaraPeko\LaraPekoServiceProvider"

register()中則是將預設的 config 設定檔和新建立的設定檔合併
如此一來,若使用者對專案設定檔做修改,會覆蓋掉預設的設定檔,達到客製化的目的

清除 config 的快取

當 config 有變更,則需要使用指令清除專案內的設定檔快取

php artisan config:clear   # 清除設定檔快取
php artisan cache:clear    # 清除一般快取

使用 config 參數

若是要在程式中使用 config 設定,則可以使用 config() 方法呼叫
回到LaraPeko.php,新建個方法getBestGirl取得設定檔中的best_girl參數內容

<?php
# LaraPeko.php

namespace Ray247k\LaraPeko;

class LaraPeko
{
    public function sayPeko()
    {
        echo "好油喔 peko\n";
    }

    public function getBestGirl()
    {
        echo config('lara_peko.best_girl');
    }
}

6. 套件包自動發現

Laravel 5.5 新增的功能,可以借由套件包的設定減少使用者安裝時候需要的步驟
目的是在執行 composer install 之後,不需要手動編輯config/app.phpprovidersaliases陣列
關鍵字:Package Auto-discovery、Laravel 擴充套件包自動發現

作法:
在套件包的 composer.json 裡面加入 autoloadextra 的內容
因為整串貼有點太長,就只貼有新增的部分

{
  "autoload": {
    "psr-4": {
      "Ray\\LaraPeko\\": "src"
    }
  },
  "extra": {
    "laravel": {
      "providers": [
        "Ray\\LaraPeko\\LaraPekoServiceProvider"
      ],
      "aliases": {
        "LaraPeko":  "Ray\\LaraPeko\\LaraPekoFacade"
      }
    }
  }
}

藉由 extra 中的 laravel 定義,會在陣列中的 providers 和 aliases 內各加入一筆資料進行相關註冊
而 autoload 會使用 psr-4 的規則將指定命名空間指向我們套件程式所在的資料夾src

7. 套件包相依套件

如果你的套件包相依於某個套件,例如常用的Guzzle
那麼就在套件包的 composer.json 中加入 require 的項目如下

{
    "name": "ray247k/larapeko",
    "description": "A package for demo peko",
    "license": "MIT",
    "authors": [
        {
            "name": "Ray",
            "email": "[email protected]"
        }
    ],

    "minimum-stability": "dev",
    "require": {
        "php": ">=7.3",
        "laravel/framework": "5.5.*||^6.0|^7.0|^8.0",
        "guzzlehttp/guzzle": "^6.3"
    }
}

我們除了 require guzzle 外,還 require 了 php 必須大於指定版本以及 Laravel

8. 套件包測試

建立測試用專案

經過上面流程,我們的套件包差不多就設定好了
接著就要進入測試流程,因為我們是要在 Laravel 專案中使用的
所以就建立一個新的 Laravel 專案來進行測試,叫做 lara-ahoy

laravel new lara-ahoy

new laravel

可以藉由指令查看安裝的 Laravel 版本

cd <專案目錄>
php artisan -V
# Laravel Framework 6.20.30

專案中引用套件

加入套件項目

在專案中使用指令,在 composer.json 中加入套件的版本儲存位置
這邊因為是在本機測試,所以使用 path 方法指定本機套件包的相對或絕對路徑
關於路徑的設定可以參考官方說明:Composer Documentation #Path

composer config repositories.ray247k path ../larapeko

從相對路徑看得出來我把套件包(larapeko) 和測試用的專案(lara-ahoy) 放在同一個資料夾裡面

指令執行之後可以打開 lara-ahoy 專案的 composer.json 檔案
會發現最下面多了一段

{
    "repositories": {
        "ray247k": {
            "type": "path", 
            "url": "../larapeko"
        }
    }   
}

如果不想用指令加入套件位址,也可以手動加入這段

如果套件是在某個 repository 的話 可以把引用 type 換成 vcs,然後加上遠端版本庫的 url

composer config repositories.ray247k vcs https://github.com/ray247k/larapeko

或是手動調整 composer.json 裡面的 repositories 設定

{
    "repositories": {
        "ray247k": {
            "type": "vcs", 
            "url": "https://github.com/ray247k/larapeko"
        }
    }   
}

遠端的 url 可以使用 https 或是 ssh 方法
但如果該套件的 repository 是私有的,那就必須用 ssh:[email protected]:ray247k/larapeko.git

添加依賴

可以使用指令

composer require ray247k/larapeko @dev

或是直接打開 cmoposer.json 找到require段落加入底下內容

{
    "require": {
        "ray247k/larapeko": "@dev"
    },
}

若是在使用指令的時候發生某個套件的相依套件無法安裝而造成錯誤的話

Problem 1
    - <某個套件> is locked to version 3.1.2 and an update of this package was not requested.
    - <某個套件> 3.1.2 requires ext-rdkafka >=1.0 -> it is missing from your system. Install or enable PHP's rdkafka extension.

可以藉由加上 --ignore-platform-reqs 忽略平台

composer require ray247k/larapeko --ignore-platform-reqs

在添加本機依賴之後,預設每次被使用都會自動去抓取本機套件最新的程式碼
如果沒自動抓取,或是不想要自動抓取,而是每次都想要 composer require 來更新的話
可以在測試專案中的 composer.json 透過 repositories 區塊內的 options.symlink 設定來調整

{
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package",
            "options": {
                "symlink": false
            }
        }
    ]
}

安裝套件

使用套件安裝指令

composer install

composer install

在上圖中可以看到剛剛的套件已經成功被發現,並且安裝

Discovered Package: ray247k/larapeko
Package manifest generated successfully.

如果跟 require 的時候一樣發生相依套件的版本錯誤,一樣可以加入參數忽略平台

composer install --ignore-platform-reqs

打開 lara-ahoy 中的 composer.lock 檔案,可以看到剛剛成功安裝的套件資料

若是先前已經執行過專案,那必須清除原有 autoload 的快取

composer dump-autoload

測試套件

上一步驟中我們在測試用專案 lara-ahoy 中安裝好了本機開發的套件 larapeko
接著會分別用兩種方式測試套件是否有正確被引入,分別是直接呼叫測試和單元測試
在開發套件時候如果就有撰寫單元測試,那這時候就會方便許多
本篇文章也會帶著你建立基本的單元測試並執行,就繼續看下去吧!

方法一、檔案測試

直接在測試用的 Laravel 專案跟目錄資料夾下建立 test-autoload.php 檔案
在檔案中載入 autoload 之後呼叫套件方法

<?php
# test-autoload.php

use Ray247k\LaraPeko\LaraPeko;

require_once './vendor/autoload.php';
LaraPeko::sayPeko();

在 lara-ahoy 專案目錄下執行測試檔案

php test-autoload.php

如果成功載入應該會出現套件中sayPeko()的執行結果
file test

雖然跳出了 PHP Deprecated,但是還是有成功印出我們的預設字串內容
代表剛剛在 ServiceProvider 註冊的 Facade aliases 有成功被呼叫了!

如果真的很不想看到那個錯誤訊息
只要在 test-autoload.php 中加入error_reporting(0);關閉錯誤訊息提示

<?php

error_reporting(0);

use Ray247k\LaraPeko\LaraPeko;

require_once './vendor/autoload.php';

LaraPeko::sayPeko();

如此就不會看到 PHP Deprecated 的提示了
peko test

方法二、使用單元測試

接著就是使用單元測試來測試套件,參考文件:Laravel Package Development #Testing

先試試看 PHPUnit 是否可以執行,在專案目錄下使用指令

php vendor/bin/phpunit

如果出現底下錯誤訊息

/usr/bin/php declares an invalid value for PHP_VERSION.
This breaks fundamental functionality such as version_compare().
Please use a different PHP interpreter.

那看來是蘋果的鍋:PHPUnit does not work when I run the tests on the Laravel framework

PHPUnit refuses to be run with a PHP interpreter where the value of the PHP_VERSION constant contains an invalid value due to modification made by vendors that ship (binary) distributions of PHP.

7.3.24-(to be removed in future macOS) is such an invalid value. The -(to be removed in future macOS) suffix was added by Apple, who is the vendor of the PHP interpreter binary you use.

TL;DR: Do not use the PHP interpreter that is shipped by Apple with macOS. Use Homebrew, or similar, instead.

這時候有兩個解法,一個是用 Homebrew 安裝 PHP,記得要修改 bash/zsh 的設定檔
另一個是把測試專案搬到 docker 環境裡面 PHP mount 的資料夾內
直接進到 php 容器裡面執行 php vendor/bin/phpunit 指令

如果 php vendor/bin/phpunit 可以執行,接著就要來建置單元測試檔案

先在 LaraPeko.php 新增一個測試用的 function

    public function getAhoy()
    {
        return "ahoy";
    }

接著在套件包專案內建立 tests 資料夾,並建立測試檔案 LaraPekoTest.php

<?php

namespace Ray247k\LaraPeko;

use PHPUnit\Framework\TestCase;

class LaraPekoTest extends TestCase
{
    /**
     * @test
     *
     * @return void
     */
    public function testClassInstance()
    {
        $this->assertInstanceOf(LaraPekoTest::class, new LaraPekoTest);
    }

    public function testGetAhoy()
    {
        $larapeko = new LaraPeko();
        $this->assertEquals('ahoy', $larapeko->getAhoy());
    }
}

可以看到我們呼叫 LaraPeko 物件,然後調用了 getAhoy() 方法
因為我們上面有建立了這個方法,會回傳「ahoy」
使用 assertEquals 的測試斷言式判斷回傳值是不是等於「ahoy」

建立好 test file 之後進到容器中的 Laravel 專案的根目錄,使用指令執行指定路徑下的測試

php vendor/bin/phpunit vendor/ray247k/larapeko/tests

phpunit result

補充
假設現在 LaraPeko 套件裡有個 private function 叫做 testMethod

private function testMethod()
{
    return 'Haha';
}

如果想測試 private function 可以使用閉包中的 bindTo() 方法
產生物件之後手動注入一個物件,替換掉 closure 物件中的 $this
如此一來就像是直接使用 LaraPeko 物件去呼叫 testMethod 方法

/**
 * @test
 *
 * @return void
 */
public function testMethodTest()
{
    $contentSegment = new LaraPeko();
    $closure = function () {
        return $this->testMethod();
    };

    $closure_bind = $closure->bindTo($contentSegment, $contentSegment);

    $this->assertEquals('Haha', $closure_bind());
}

這樣去執行單元測試就可以對 private 方法進行測試了!


以上就是這次開發屬於自己的 Laravel 套件的筆記內容

最新文章

Category

Tag