支持Laravel.io的持续开发 →

利用域感知Artisan命令简化Laravel开发

2023年11月28日 6分钟阅读 205次查看

欢迎来到利用自定义命令在特定目录中有效生成文件的简明指南。这个自定义命令是为了针对特定的领域进行工作,使得生成基于领域的文件变得更加容易。让我们直接进入领域感知 Artisan 命令的世界!

介绍

采取这种方法的理由是,我需要类似领域驱动设计的东西,但又不希望陷入维护这种架构的麻烦。我想保护升级 Laravel 版本的能力、维护的便利性和快速让团队成员上手。

然后我想起了我曾经使用过一个 Spatie 包(我们大家都这么做),具体是多租户包,它可以运行针对每个租户的 Artisan 命令,所以我想到可以很好地使用他们的一部分方法来调整我想要的 artisan 文件生成的位置。

命令类

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use function Laravel\Prompts\select;
use Illuminate\Support\Facades\Artisan;

class DomainArtisanCommand extends Command
{
    public const DOMAINS = [
        self::APP_DOMAIN => [],
        self::TENANT_DOMAIN => [
            'api',
            'dashboard',
        ],
        self::STAFF_DOMAIN => [],
        self::CLIENT_DOMAIN => [],
    ];
    public const APP_DOMAIN = 'app';
    public const CLIENT_DOMAIN = 'client';
    public const STAFF_DOMAIN = 'staff';
    public const TENANT_DOMAIN = 'tenant';

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:artisan
                            {artisanCommand : The Artisan command to run within "quotes"}
                            {domain? : The domain to run the command for (optional)}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Run Artisan commands within a specific domain context.';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle() : int
    {
        $domain = $this->retrieveDomain();
        $artisanCommand = $this->argument('artisanCommand') ?: $this->askArtisanCommand($domain);

        if ($this->isDomainSpecificCommand($artisanCommand) &&
            $domain !== self::APP_DOMAIN
        ) {
            $artisanCommand = $this->prefixCommandWithPath($artisanCommand, $domain);
        }

        info("Running artisan command: $artisanCommand");

        Artisan::call($artisanCommand, [], $this->getOutput());

        info('Artisan command completed successfully!');

        return 0;
    }

    protected function isDomainSpecificCommand($artisanCommand) : bool
    {
        return in_array(
            explode(' ', $artisanCommand)[0],
            $this->getCommandsThatGenerateFilesSpecificToDomain()
        );
    }

    protected function prefixCommandWithPath($artisanCommand, $domain) : string
    {
        $module = $this->retrieveModule($domain);
        $modulePathSegment = $module ? Str::studly($module) . '/' : '';

        $commandName = explode(' ', $artisanCommand)[0];
        $fileFromCommand = explode(' ', $artisanCommand)[1] ?? '';
        $path = $this->getDomainPath($domain) . '/' . $modulePathSegment . $this->getPathByArtisanCommand($commandName);

        return "{$commandName} {$path}/{$fileFromCommand}";
    }

    protected function getDomainPath($domain) : string
    {
        return strtolower($domain) === self::APP_DOMAIN ?
            "App" :
            "App/Domains/{$domain}";
    }

    private function retrieveModule(string $domain): string
    {
        $modules = $this->getModulesPerDomain($domain);

        if (empty($modules)) {
            return '';
        }

        return select(
            "Select a module to run this command for within the {$domain} domain:",
            $modules,
            0
        );
    }

    private function getModulesPerDomain(string $domain): array
    {
        return self::DOMAINS[strtolower($domain)];
    }

    protected function askArtisanCommand($domain)
    {
        return $this->ask("Which artisan command do you want to run for the {$domain} Domain?");
    }

    public function getCommandsThatGenerateFilesSpecificToDomain() : array
    {
        return [
            'make:controller',
            'make:command',
            'make:event',
            'make:exception',
            'make:job',
            'make:listener',
            'make:mail',
            'make:middleware',
            'make:model',
            'make:notification',
            'make:observer',
            'make:policy',
            'make:provider',
            'make:request',
            'make:resource',
            'make:rule',
        ];
    }

    private function retrieveDomain(): string
    {
        if (!$this->argument('domain') || !in_array(
            $this->argument('domain'),
            array_keys($this->getDomainsWithPath())
        )) {
            $domain = select(
                'Select a domain to run this command for:',
                array_keys($this->getDomainsWithPath()),
                0,
            );
        } else {
            $domain = $this->argument('domain');
        }



        return Str::studly(Str::camel($domain));
    }

    public function getDomainsWithPath(): array
    {
        return [
            'client' => app_path('Domains/Client'),
            'staff' => app_path('Domains/Staff'),
            'tenant' => app_path('Domains/Tenant'),
        ];
    }

    public function getPathByArtisanCommand($command): string
    {
        return match ($command) {
            'make:controller' => 'Http/Controllers',
            'make:command' => 'Console/Commands',
            'make:event' => 'Events',
            'make:exception' => 'Exceptions',
            'make:job' => 'Jobs',
            'make:listener' => 'Listeners',
            'make:mail' => 'Mail',
            'make:middleware' => 'Http/Middleware',
            'make:model' => 'Models',
            'make:notification' => 'Notifications',
            'make:observer' => 'Observers',
            'make:policy' => 'Policies',
            'make:provider' => 'Providers',
            'make:request' => 'Http/Requests',
            'make:resource' => 'Http/Resources',
            'make:rule' => 'Rules',
            default => app_path(),
        };
    }
}

理解命令

领域感知 Artisan 命令无缝集成到 Laravel 的默认 Artisan 命令中,同时满足我们独特的项目结构。其主要功能是在指定的领域(App、Client、Staff、Tenant 等)内生成文件,确保纷争的清楚分离,同时又尽可能不偏离 Laravel 的常规。

基本语法

php artisan app:domain {artisanCommand} {domain?}

参数

  • {artisanCommand}:用引号括起来所需的 Laravel Artisan 命令。
    • 示例
      • "make:controller SomeController"
      • "make:observer Somewhere/SomeObserver"
  • {域?}(可选):指定您要针对的域:app、client、staff或tenant。如果您跳过此操作,命令将提示您进行交互式选择。

示例

在Staff域内创建控制器

php artisan app:domain "make:controller ClientController" staff

在Tenant域内生成请求

php artisan app:domain "make:request CompanyRequest" tenant

幕后

当运行app:domain命令时,它执行以下任务:

域确定

如果您没有提供域,它将提示您选择一个。目前,我们有3个已确认的域:App(是标准App命名空间,所以我们不计入)、Client、Staff和Tenant。

路径解析

它根据提供的域和Artisan命令的性质确定正确的路径。例如,如果您在Staff域内生成控制器,它将解析为app/Domains/Staff/Http/Controllers

Artisan委派

然后它将命令委派给Laravel的Artisan,确保文件在正确的位置生成,保持我们的域结构完整。

公平起见,“App”域不应包含在此命令中,因为您可以使用Artisan来完成此操作,但我添加它只是为了预防可能的需要。

每个域的模块

app:domain命令旨在与所有域一起使用。然而,一些域有模块,而另一些则没有。

例如

tenant域有模块,因此您可以在特定模块内生成文件

  • API
  • 仪表板

所以如果您想在API模块中生成控制器,您可以通过选择要生成文件的模块来完成。

Select a module to run this command for within the {$domain} domain:
  [0] api
  [1] dashboard

我们正在使用Jess Archer的出色软件包Laravel Prompts来为我们的命令提供良好的格式。

这将在app/Domains/Tenant/Api/Http/Controllers目录中生成控制器。

您可以通过在DomainArtisanCommand类中的DOMAINS常量数组中添加小写模块名称将模块添加到域中(这可能需要更好的处理,只是需要一个快速解决模块问题的方法)。

const DOMAINS = [
    self::APP_DOMAIN => [],
    self::TENANT_DOMAIN => [
        'api',
        'dashboard',
    ],
    self::STAFF_DOMAIN => [],
    self::CLIENT_DOMAIN => [],
];

提示

  • 始终引用您的命令:为了避免潜在的问题,始终用引号包住实际的Laravel Artisan命令。
  • 了解您的域:熟悉您包含的域。这有助于快速确定生成的文件应该位于哪里。

总结

这就是全部!artisan:domain命令是为了使我们的模块化方法更加流畅。所以下次您需要在一个特定的域中快速创建新的控制器、请求或其他文件时,试试artisan:domain。

让我们保持代码库整洁高效。

最后更新于8个月前。

driesvints, mlambertz喜欢这篇文章

2
喜欢这篇文章?让作者知道并给予他们掌声!
bcryp7 (Alberto Rosas) 你好,我是Alberto。作为一名高级软件工程师和顾问。

你可能喜欢的一些其他文章

2024年3月11日

如何使用 Larastan 将 Laravel 应用从 0 到 9 进行优化

在 Laravel 应用还未执行之前就发现 bug 是可能的,多亏了 Larastan...

阅读文章
2024年7月19日

无需特性实现标准化 API 响应

我发现大多数用于 API 响应的库都是使用特性实现的,...

阅读文章
2024年7月17日

在 Laravel 项目中使用 Discord 通知收集反馈

如何在 Laravel 项目中创建反馈模块,并在收到消息时通过 Discord 收...

阅读文章

我们感谢这些 令人难以置信的公司 对我们的支持

您的标志在这里?

Laravel.io

Laravel 门户,用于解决问题、知识分享和社区建设。

© 2024 Laravel.io - 版权所有。