详解 packagit 用了什么黑魔法,并可完全替换 artisan 命令
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 协议》,转载必须注明作者和本文链接
看完都不知道怎么用...能不能举个例子