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。實現簡單,就不多說了,有興趣的同學可以去看程式碼。
專案程式碼
創作不易,都是業餘時間的點滴積累,喜歡的可以幫忙給個 star
, thanks.
本作品採用《CC 協議》,轉載必須註明作者和本文連結