詳解 packagit 用了什麼黑魔法,並可完全替換 artisan 命令

扣丁禪師發表於2022-06-01

packagit 為啥誕生

我早期搞過一個 zencodex/package-make,實現上參考了 nWidart/laravel-modules,這個包目前有4.2k stars,還真沒想到還能這麼受歡迎。

packagit 本質和這個包的作用一樣,都是把大點的專案切割成小的模組,並且模組可以獨立維護,利用 git submodules 也可以很方便在很多專案之間複用模組。

nWidart/laravel-modules 有些缺點:

1、擴充命令太多了,幾乎1:1 每個artisan make:xxx的命令都有一個針對module的對等命令,比如 artisan make:command 對應artisan module:make-command,命令輸出就比原來多了一倍,太亂了。

2、釋出的時候還要依賴這個包,用於 provider 的載入,但我 zencodex/package-make 的實現解決這個問題了,只需要 require-dev 開發階段用。

3、這個實現維護代價太高,比如官方針對 laravel 5.4-9.0 都要維護個版本與官方對應,所有資源都有個 stubs 模板,也都需要維護。

4、命令用起來繁瑣,所有 module:make-xxx 命令後面都會多一個指定是哪個module的引數。

為了解決上面這些不爽,我就測試了能不能複用 laravel artisan 已有的功能,針對 module 需要處理的是生成資源的路徑,和裡面 package namespace 的規則。測試後覺得完全可行,當然也遇到了一些超預期的坑,後面會講到這些。

packagit 怎麼使用

packagit 名字主要是為了滿足 composer packagist 官網的命名規則,好名字基本都被佔用了,所以想了一個 packagit 屬於自己造的詞。雖然 packagit 拼寫不復雜,但為了敲命令省事,我還是實現一個 p 這個縮寫替代。

packagit 自身只實現了一個 p new ModuleName 的命令,其餘所有 artisan 的命令,都是複用,如下表:

artisan packagit
php artisan tinker p tinker
php artisan make:controller p make:controller
php artisan make:migration p make:migration
php artisan make:job p make:job
php artisan test p test
php artisan … p …

相比 artisan,p 這個命令,可以在任何目錄下執行,不像 artisan 只能在工程目錄裡。如果你之前配置過 .zshrc/.bashrc,用過 a 縮寫,也可以修改下 a 定義指向 packagit。不清楚怎麼回事的,就不用管了,可以直接用 p就好,使用上沒什麼區別。

詳解 packagit 工作原理

程式碼參考位置:
github.com/packagit/packagit/blob/...

1、先透過判斷 artisan 和 composer.json 的檔案路徑,來獲取專案根路徑,和module的路徑。

// find laravel project directory
$rootDir = $workDir = getcwd();
while (1) {
    if (file_exists($rootDir . DIRECTORY_SEPARATOR . 'artisan')) {
        break;
    }

    $pos = strrpos($rootDir, DIRECTORY_SEPARATOR);
    if ($pos === false) {
        echo "Can't find laravel project in current path" . PHP_EOL;
        echo "You should run 'packagit' under a laravel project" . PHP_EOL;
        return -1;
    }

    $rootDir = substr($rootDir, 0, strrpos($rootDir, DIRECTORY_SEPARATOR));
}

$startPos = strpos($workDir, DIRECTORY_SEPARATOR . 'modules' . DIRECTORY_SEPARATOR);
if ($startPos === false) {
    $workDir = $rootDir;
}

while ($startPos != false) {
    if (file_exists($workDir . DIRECTORY_SEPARATOR . 'composer.json')) {
        break;
    }

    $pos = strrpos($workDir, DIRECTORY_SEPARATOR);
    if ($pos === false) {
        $workDir = $rootDir;
        return -2;
    }

    $workDir = substr($workDir, 0, strrpos($workDir, DIRECTORY_SEPARATOR));
}

2、確定 autoload.php 路徑,並把 packagit 裡的放後面,覆蓋掉原有的定義,注意看 Workaround 部分,都是我填坑用的,因為 laravel 裡的namespace 是寫死的,沒法透過注入去修改了。

還有透過分析 laravel 載入過程,使用 useAppPath,useDatabasePath 更改 app/* 和 databases/* 目錄的位置。但千萬不能動 basePath,因為laravel 裡太多地方基於這個,改變就影響太大了,引起很多問題。

require __DIR__ . '/../vendor/autoload.php';
require $rootDir . '/vendor/autoload.php';
$app = require_once $rootDir . '/bootstrap/app.php';

$asModule = $workDir !== $rootDir;
$input = new Symfony\Component\Console\Input\ArgvInput;
$grabCommand = $input->getFirstArgument();

if (!in_array($grabCommand, $usableCommands)) {
    $asModule = false;
}

// change path for module
if ($asModule) {
    $moduleName = substr($workDir, strrpos($workDir, '/') + 1);
    echo "Work Module: " . substr($workDir, $startPos) . PHP_EOL;

    $app->useAppPath($workDir . '/src');
    $app->useDatabasePath($workDir . '/database');
    $app->packagitModuleName = $moduleName;
    require __DIR__ . '/../src/Workaround/TestMakeCommand.php';
    require __DIR__ . '/../src/Workaround/FactoryMakeCommand.php';
    require __DIR__ . '/../src/Workaround/SeedCommand.php';
    require __DIR__ . '/../src/Workaround/SeederMakeCommand.php';
    require __DIR__ . '/../src/Workaround/TestCommand.php';
}

3、反射注入,只注入了一個 MakeCommand,實現不復雜,主要是分析過程複雜,我是完全基於 xdebug 動態除錯,我有篇招聘文章提到過 xdebug 除錯的重要性,如果不是跟蹤程式碼執行過程,我是沒辦法實現這個,我也不明白一些靠 echo log 輸出的大神是咋排雷的,有機會大神們可以分享下。

// inject namespace
if ($asModule) {
    $reflection = new ReflectionClass($app);
    $property = $reflection->getProperty('namespace');
    $property->setAccessible(true);
    $property->setValue($app, "Packagit\\{$moduleName}\\");
    $property->setAccessible(false);
}

// inject MakeCommand
$reflection = new ReflectionClass($kernel);
$property = $reflection->getProperty('commands');
$property->setAccessible(true);
$commands = $property->getValue($kernel);

$commands[] = \Packagit\Commands\MakeCommand::class;
$property->setValue($kernel, $commands);
$property->setAccessible(false);

4、最後是針對 make:controller 命令的一個 workaround,主要是針對 module 替換 對應的 namespace。實現簡單,就不多說了,有興趣的同學可以去看程式碼。

專案程式碼

github.com/packagit/packagit

創作不易,都是業餘時間的點滴積累,喜歡的可以幫忙給個 star, thanks.

本作品採用《CC 協議》,轉載必須註明作者和本文連結
尊道貴德 / 多行佈施

相關文章