详解 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。实现简单,就不多说了,有兴趣的同学可以去看代码。

项目代码

github.com/packagit/packagit

创作不易,都是业余时间的点滴积累,喜欢的可以帮忙给个 star, thanks.

本作品采用《CC 协议》,转载必须注明作者和本文链接
尊道贵德 / 多行布施
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3

看完都不知道怎么用...能不能举个例子

4个月前 评论
扣丁禅师 (楼主) 4个月前
Chenhappy (作者) 4个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!