From 92b15272270c893752f9ac56e5e8f1692f977732 Mon Sep 17 00:00:00 2001 From: Ninja Date: Thu, 18 Jul 2024 15:50:23 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Context=20Men?= =?UTF-8?q?us=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/discord.php | 15 + src/Commands/AbstractCommand.php | 31 +- src/Commands/ApplicationCommand.php | 42 +++ src/Commands/Command.php | 15 + src/Commands/ContextMenu.php | 80 +++++ src/Commands/Contracts/ContextMenu.php | 13 + src/Commands/SlashCommand.php | 31 +- src/Console/Commands/MakeMenuCommand.php | 98 ++++++ src/Console/Commands/stubs/context-menu.stub | 63 ++++ src/Console/Commands/stubs/slash-command.stub | 2 +- src/Laracord.php | 287 ++++++++++++------ src/LaracordServiceProvider.php | 1 + 12 files changed, 563 insertions(+), 115 deletions(-) create mode 100644 src/Commands/ApplicationCommand.php create mode 100644 src/Commands/ContextMenu.php create mode 100644 src/Commands/Contracts/ContextMenu.php create mode 100644 src/Console/Commands/MakeMenuCommand.php create mode 100644 src/Console/Commands/stubs/context-menu.stub diff --git a/config/discord.php b/config/discord.php index ac202e1..9f7277a 100644 --- a/config/discord.php +++ b/config/discord.php @@ -137,6 +137,21 @@ Laracord\Commands\HelpCommand::class, ], + /* + |-------------------------------------------------------------------------- + | Additional Context Menus + |-------------------------------------------------------------------------- + | + | Here you may specify any additional context menus for the Discord bot. + | These context menus will be loaded in addition to the context menus + | automatically loaded in your project. + | + */ + + 'menus' => [ + // + ], + /* |-------------------------------------------------------------------------- | Additional Services diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index c28ab0f..eb967c0 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -3,6 +3,7 @@ namespace Laracord\Commands; use Discord\Parts\Guild\Guild; +use Discord\Parts\Interactions\Command\Command; use Discord\Parts\User\User; use Illuminate\Support\Str; use Laracord\Discord\Concerns\HasModal; @@ -54,6 +55,11 @@ abstract class AbstractCommand */ protected $description; + /** + * The command type. + */ + protected string|int $type = 'chat'; + /** * The guild the command belongs to. * @@ -61,6 +67,11 @@ abstract class AbstractCommand */ protected $guild; + /** + * Determine whether the command can be used in a direct message. + */ + protected bool $directMessage = true; + /** * Determines whether the command requires admin permissions. * @@ -140,6 +151,14 @@ public function isAdmin($user) return $this->getUser($user)->is_admin; } + /** + * Determine if the command can be used in a direct message. + */ + public function canDirectMessage(): bool + { + return $this->directMessage; + } + /** * Resolve a Discord user. */ @@ -189,7 +208,7 @@ public function getName() */ public function getSignature() { - return Str::start($this->getName(), $this->bot()->getPrefix()); + return $this->getName(); } /** @@ -218,6 +237,16 @@ public function getDescription() return $this->description; } + /** + * Get the command type. + */ + public function getType(): int + { + return match ($this->type) { + default => Command::CHAT_INPUT, + }; + } + /** * Retrieve the command guild. */ diff --git a/src/Commands/ApplicationCommand.php b/src/Commands/ApplicationCommand.php new file mode 100644 index 0000000..ca50709 --- /dev/null +++ b/src/Commands/ApplicationCommand.php @@ -0,0 +1,42 @@ +permissions) { + return null; + } + + $permissions = collect($this->permissions) + ->mapWithKeys(fn ($permission) => [$permission => true]) + ->all(); + + return (new RolePermission($this->discord(), $permissions))->__toString(); + } + + /** + * Determine if the command is not safe for work. + */ + public function isNsfw(): bool + { + return $this->nsfw; + } +} diff --git a/src/Commands/Command.php b/src/Commands/Command.php index dfcf9a6..3803687 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -2,6 +2,7 @@ namespace Laracord\Commands; +use Illuminate\Support\Str; use Laracord\Commands\Contracts\Command as CommandContract; abstract class Command extends AbstractCommand implements CommandContract @@ -43,6 +44,10 @@ abstract class Command extends AbstractCommand implements CommandContract */ public function maybeHandle($message, $args) { + if (! $this->canDirectMessage() && ! $message->guild_id) { + return; + } + if ($this->getGuild() && $message->guild_id !== $this->getGuild()) { return; } @@ -89,6 +94,16 @@ public function getCooldownMessage() return $this->cooldownMessage; } + /** + * Retrieve the command signature. + * + * @return string + */ + public function getSignature() + { + return Str::start($this->getName(), $this->bot()->getPrefix()); + } + /** * Retrieve the command usage. * diff --git a/src/Commands/ContextMenu.php b/src/Commands/ContextMenu.php new file mode 100644 index 0000000..016e3a9 --- /dev/null +++ b/src/Commands/ContextMenu.php @@ -0,0 +1,80 @@ + $this->getName(), + 'type' => $this->getType(), + 'guild_id' => $this->getGuild(), + 'default_member_permissions' => $this->getPermissions(), + 'default_permission' => true, + 'dm_permission' => $this->canDirectMessage(), + 'nsfw' => $this->isNsfw(), + ])->reject(fn ($value) => blank($value)); + + return new DiscordCommand($this->discord(), $menu->all()); + } + + /** + * Handle the context menu interaction. + * + * @param \Discord\Parts\Interactions\Interaction $interaction + * @return void + */ + abstract public function handle($interaction); + + /** + * Maybe handle the context menu interaction. + * + * @param \Discord\Parts\Interactions\Interaction $interaction + * @return void + */ + public function maybeHandle($interaction) + { + if (! $this->isAdminCommand()) { + $this->handle($interaction); + + return; + } + + if ($this->isAdminCommand() && ! $this->isAdmin($interaction->member->user)) { + return $interaction->respondWithMessage( + $this + ->message('You do not have permission to run this command.') + ->title('Permission Denied') + ->error() + ->build(), + ephemeral: true + ); + } + + $this->handle($interaction); + } + + /** + * Get the context menu type. + */ + public function getType(): int + { + return match ($this->type) { + 'user' => DiscordCommand::USER, + DiscordCommand::USER => DiscordCommand::USER, + default => DiscordCommand::MESSAGE, + }; + } +} diff --git a/src/Commands/Contracts/ContextMenu.php b/src/Commands/Contracts/ContextMenu.php new file mode 100644 index 0000000..f00db11 --- /dev/null +++ b/src/Commands/Contracts/ContextMenu.php @@ -0,0 +1,13 @@ +setName($this->getName()) - ->setDescription($this->getDescription()); + ->setDescription($this->getDescription()) + ->setType($this->getType()) + ->setDmPermission($this->canDirectMessage()) + ->setNsfw($this->isNsfw()); if ($permissions = $this->getPermissions()) { $command = $command->setDefaultMemberPermissions($permissions); @@ -243,22 +238,6 @@ public function getSignature() return Str::start($this->getName(), '/'); } - /** - * Retrieve the slash command bitwise permission. - */ - public function getPermissions(): ?string - { - if (! $this->permissions) { - return null; - } - - $permissions = collect($this->permissions) - ->mapWithKeys(fn ($permission) => [$permission => true]) - ->all(); - - return (new RolePermission($this->discord(), $permissions))->__toString(); - } - /** * Retrieve the slash command options. */ diff --git a/src/Console/Commands/MakeMenuCommand.php b/src/Console/Commands/MakeMenuCommand.php new file mode 100644 index 0000000..5562212 --- /dev/null +++ b/src/Console/Commands/MakeMenuCommand.php @@ -0,0 +1,98 @@ +option('command') ?: Str::of($name)->classBasename()->kebab()->value(); + + return str_replace(['dummy:command', '{{ command }}'], $command, $stub); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + $relativePath = '/stubs/context-menu.stub'; + + return file_exists($customPath = $this->laravel->basePath(trim($relativePath, '/'))) + ? $customPath + : __DIR__.$relativePath; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Menus'; + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['name', InputArgument::REQUIRED, 'The name of the context menu'], + ]; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the Discord context menu already exists'], + ['command', null, InputOption::VALUE_OPTIONAL, 'The Discord context menu that will be used to invoke the class'], + ]; + } +} diff --git a/src/Console/Commands/stubs/context-menu.stub b/src/Console/Commands/stubs/context-menu.stub new file mode 100644 index 0000000..15ee39e --- /dev/null +++ b/src/Console/Commands/stubs/context-menu.stub @@ -0,0 +1,63 @@ +respondWithMessage( + $this + ->message() + ->title('{{ class }}') + ->content('Hello world!') + ->button('👋', route: 'wave') + ->build() + ); + } + + /** + * The context menu interaction routes. + */ + public function interactions(): array + { + return [ + 'wave' => fn (Interaction $interaction) => $this->message('👋')->reply($interaction), + ]; + } +} diff --git a/src/Console/Commands/stubs/slash-command.stub b/src/Console/Commands/stubs/slash-command.stub index bb58c70..f2aeaf6 100644 --- a/src/Console/Commands/stubs/slash-command.stub +++ b/src/Console/Commands/stubs/slash-command.stub @@ -73,7 +73,7 @@ class {{ class }} extends SlashCommand public function interactions(): array { return [ - 'wave' => fn (Interaction $interaction) => $this->message('👋')->reply($interaction), + 'wave' => fn (Interaction $interaction) => $this->message('👋')->reply($interaction), ]; } } diff --git a/src/Laracord.php b/src/Laracord.php index 01042d1..bddd94d 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -15,7 +15,9 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; +use Laracord\Commands\ApplicationCommand; use Laracord\Commands\Command; +use Laracord\Commands\ContextMenu; use Laracord\Commands\SlashCommand; use Laracord\Concerns\CanAsync; use Laracord\Console\Commands\Command as ConsoleCommand; @@ -31,7 +33,6 @@ use ReflectionClass; use Throwable; -use function React\Async\async; use function React\Async\await; use function React\Promise\all; @@ -110,6 +111,11 @@ class Laracord */ protected array $slashCommands = []; + /** + * The Discord bot context menus. + */ + protected array $contextMenus = []; + /** * The Discord events. */ @@ -120,11 +126,6 @@ class Laracord */ protected array $services = []; - /** - * The bot interaction routes. - */ - protected array $interactions = []; - /** * The console input stream. * @@ -158,6 +159,11 @@ class Laracord */ protected array $registeredCommands = []; + /** + * The registered context menus. + */ + protected array $registeredContextMenus = []; + /** * The registered Discord events. */ @@ -168,6 +174,11 @@ class Laracord */ protected array $registeredServices = []; + /** + * The registered bot interaction routes. + */ + protected array $registeredInteractions = []; + /** * Determine whether to show the commands on boot. */ @@ -214,7 +225,7 @@ public function boot(): void ->registerEvents() ->bootServices() ->bootHttpServer() - ->registerSlashCommands() + ->registerApplicationCommands() ->handleInteractions(); $this->afterBoot(); @@ -334,6 +345,7 @@ public function restart(): void $this->discord = null; $this->registeredCommands = []; + $this->registeredContextMenus = []; $this->registeredEvents = []; $this->registeredServices = []; @@ -415,48 +427,73 @@ protected function registerCommands(): self } /** - * Handle the bot slash commands. + * Register the bot application commands. */ - protected function registerSlashCommands(): self + protected function registerApplicationCommands(): self { - $existing = cache()->get('laracord.slash-commands'); + $normalize = function ($data) use (&$normalize) { + if (is_object($data)) { + $data = (array) $data; + } - if (! $existing) { - $existing = []; + if (is_array($data)) { + ksort($data); + + return array_map($normalize, $data); + } + + return $data; + }; + + $existing = cache()->get('laracord.application-commands', []); - $existing[] = async(fn () => await($this->discord->application->commands->freshen()))(); + if (! $existing) { + $existing[] = $this->discord->application->commands->freshen(); foreach ($this->discord->guilds as $guild) { - $existing[] = async(fn () => await($guild->commands->freshen()))(); + $existing[] = $guild->commands->freshen(); } - $existing = all($existing)->then(function ($commands) { - return collect($commands) - ->flatMap(fn ($command) => $command->toArray()) - ->map(fn ($command) => collect($command->getUpdatableAttributes())->prepend($command->id, 'id')->filter()->all()) - ->map(fn ($command) => array_merge($command, [ - 'guild_id' => $command['guild_id'] ?? null, - 'dm_permission' => $command['dm_permission'] ?? null, - 'default_permission' => $command['default_permission'] ?? true, - 'options' => collect($command['options'] ?? [])->map(fn ($option) => collect($option)->sortKeys()->all())->all(), - ])) - ->keyBy('name'); - })->then(fn ($commands) => cache()->rememberForever('laracord.slash-commands', fn () => $commands)); + $existing = all($existing)->then(fn ($commands) => collect($commands) + ->flatMap(fn ($command) => $command->toArray()) + ->map(fn ($command) => collect($command->getCreatableAttributes()) + ->merge([ + 'id' => $command->id, + 'guild_id' => $command->guild_id ?? null, + 'dm_permission' => $command->guild_id ? null : ($command->dm_permission ?? false), + 'default_permission' => $command->default_permission ?? true, + ]) + ->all() + ) + ->map(fn ($command) => array_merge($command, [ + 'options' => json_decode(json_encode($command['options'] ?? []), true), + ])) + ->filter(fn ($command) => ! blank($command)) + ->keyBy('name') + ); + + $existing = await($existing); + + cache()->forever('laracord.application-commands', $existing); } - $existing = $existing instanceof Collection ? $existing : collect(); + $existing = collect($existing); $registered = collect($this->getSlashCommands()) + ->merge($this->getContextMenus()) ->map(fn ($command) => $command::make($this)) ->filter(fn ($command) => $command->isEnabled()) ->mapWithKeys(function ($command) { - $attributes = $command->create()->getUpdatableAttributes(); - - $attributes = array_merge($attributes, [ - 'type' => $attributes['type'] ?? 1, - 'dm_permission' => $attributes['dm_permission'] ?? null, - 'guild_id' => $attributes['guild_id'] ?? false, - ]); + $attributes = $command->create()->getCreatableAttributes(); + + $attributes = collect($attributes) + ->merge([ + 'guild_id' => $command->getGuild() ?? null, + 'dm_permission' => ! $command->getGuild() ? $command->canDirectMessage() : null, + 'nsfw' => $command->isNsfw(), + ]) + ->sortKeys() + ->all(); return [$command->getName() => [ 'state' => $command, @@ -469,42 +506,36 @@ protected function registerSlashCommands(): self $updated = $registered ->map(function ($command) { - $options = collect($command['attributes']['options'] ?? []) - ->filter() - ->all(); - - $attributes = collect($command['attributes']); - - $attributes = $attributes - ->put('options', collect($options)->map(fn ($option) => collect($option)->filter()->all())->all()) - ->forget('guild_id') - ->filter() - ->prepend($command['state']->getGuild(), 'guild_id') + $attributes = collect($command['attributes']) + ->reject(fn ($value) => blank($value)) ->all(); return array_merge($command, ['attributes' => $attributes]); }) - ->filter(function ($command, $name) use ($existing) { + ->filter(function ($command, $name) use ($existing, $normalize) { if (! $existing->has($name)) { return false; } - $current = collect($existing->get($name))->forget('id'); + $current = collect($existing->get($name)) + ->forget('id') + ->reject(fn ($value) => blank($value)); - foreach ($command['attributes'] as $key => $value) { - $attributes = $current->get($key); + $attributes = collect($command['attributes']) + ->reject(fn ($value) => blank($value)); - if (is_array($attributes) && is_array($value)) { - $attributes = collect($attributes) - ->map(fn ($attribute) => collect($attribute)->sortKeys()->all()) - ->toJson(); + $keys = collect($current->keys()) + ->merge($attributes->keys()) + ->unique(); - $value = collect($value) - ->map(fn ($attribute) => collect($attribute)->sortKeys()->all()) - ->toJson(); - } + foreach ($keys as $key) { + $attribute = $current->get($key); + $value = $attributes->get($key); - if ($attributes === $value) { + $attribute = $normalize($attribute); + $value = $normalize($value); + + if ($attribute === $value) { continue; } @@ -512,34 +543,42 @@ protected function registerSlashCommands(): self } return false; - })->each(function ($command) use ($existing) { + }) + ->each(function ($command) use ($existing) { $state = $existing->get($command['state']->getName()); - if (Arr::get($command, 'attributes.guild_id') && ! Arr::get($state, 'guild_id')) { - $this->unregisterSlashCommand($state['id']); + $current = Arr::get($command, 'attributes.guild_id'); + $existing = Arr::get($state, 'guild_id'); + + if ($current && ! $existing) { + $this->unregisterApplicationCommand($state['id']); } - if (! Arr::get($command, 'attributes.guild_id') && $guild = Arr::get($state, 'guild_id')) { - $this->unregisterSlashCommand($state['id'], $guild); + if ((! $current && $existing) || $current !== $existing) { + $this->unregisterApplicationCommand($state['id'], $existing); } }); if ($updated->isNotEmpty()) { - $this->console()->warn("Updating {$updated->count()} slash command(s)."); + $this->console()->warn("Updating {$updated->count()} application command(s)."); - $updated->each(fn ($command) => $this->registerSlashCommand($command['state'])); + $updated->each(function ($command) { + $state = $command['state']; + + $this->registerApplicationCommand($state); + }); } if ($deleted->isNotEmpty()) { - $this->console()->warn("Deleting {$deleted->count()} slash command(s)."); + $this->console()->warn("Deleting {$deleted->count()} application command(s)."); - $deleted->each(fn ($command) => $this->unregisterSlashCommand($command['id'], $command['guild_id'] ?? null)); + $deleted->each(fn ($command) => $this->unregisterApplicationCommand($command['id'], $command['guild_id'] ?? null)); } if ($created->isNotEmpty()) { - $this->console()->log("Creating {$created->count()} new slash command(s)."); + $this->console()->log("Creating {$created->count()} new application command(s)."); - $created->each(fn ($command) => $this->registerSlashCommand($command['state'])); + $created->each(fn ($command) => $this->registerApplicationCommand($command['state'])); } if ($registered->isEmpty()) { @@ -549,6 +588,17 @@ protected function registerSlashCommands(): self $registered->each(function ($command, $name) { $this->registerInteractions($name, $command['state']->interactions()); + if ($command['state'] instanceof ContextMenu) { + $this->discord()->listenCommand( + $name, + fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandle($interaction)) + ); + + $this->registeredContextMenus[] = $command['state']; + + return; + } + $subcommands = collect($command['state']->getRegisteredOptions()) ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND) ->map(fn (Option $subcommand) => [$name, $subcommand->name]); @@ -581,17 +631,20 @@ protected function registerSlashCommands(): self ); }); - $this->registeredCommands = array_merge($this->registeredCommands, $registered->pluck('state')->all()); + $this->registeredCommands = array_merge( + $this->registeredCommands, + $registered->pluck('state')->reject(fn ($command) => $command instanceof ContextMenu)->all() + ); return $this; } /** - * Register the specified slash command. + * Register the specified application command. */ - public function registerSlashCommand(SlashCommand $command): void + public function registerApplicationCommand(ApplicationCommand $command): void { - cache()->forget('laracord.slash-commands'); + cache()->forget('laracord.application-commands'); if ($command->getGuild()) { $guild = $this->discord()->guilds->get('id', $command->getGuild()); @@ -611,11 +664,11 @@ public function registerSlashCommand(SlashCommand $command): void } /** - * Unregister the specified slash command. + * Unregister the specified application command. */ - public function unregisterSlashCommand(string $id, ?string $guildId = null): void + public function unregisterApplicationCommand(string $id, ?string $guildId = null): void { - cache()->forget('laracord.slash-commands'); + cache()->forget('laracord.application-commands'); if ($guildId) { $guild = $this->discord()->guilds->get('id', $guildId); @@ -647,7 +700,7 @@ protected function registerInteractions(string $name, array $routes = []): void return; } - $this->interactions = array_merge($this->interactions, $routes); + $this->registeredInteractions = array_merge($this->registeredInteractions, $routes); } /** @@ -702,7 +755,7 @@ protected function handleInteractions(): self $this->discord()->on(DiscordEvent::INTERACTION_CREATE, function (Interaction $interaction) { $id = $interaction->data->custom_id; - $handlers = collect($this->getInteractions()) + $handlers = collect($this->getRegisteredInteractions()) ->partition(fn ($route, $name) => ! Str::contains($name, '{')); $static = $handlers[0]; @@ -785,7 +838,7 @@ public function showCommands(): self $this->console()->table( ['Command', 'Description'], - collect($this->registeredCommands)->map(fn ($command) => [ + collect($this->getRegisteredCommands())->map(fn ($command) => [ $command->getSignature(), $command->getDescription(), ])->toArray() @@ -1005,11 +1058,21 @@ public function getSlashCommands(): array } /** - * Get the bot interaction routes. + * Get the bot context menus. */ - public function getInteractions(): array + public function getContextMenus(): array { - return $this->interactions; + if ($this->contextMenus) { + return $this->contextMenus; + } + + $contextMenus = $this->extractClasses($this->getContextMenuPath()) + ->merge(config('discord.menus', [])) + ->unique() + ->filter(fn ($contextMenu) => $this->handleSafe($contextMenu, fn () => is_subclass_of($contextMenu, ContextMenu::class) && ! (new ReflectionClass($contextMenu))->isAbstract())) + ->all(); + + return $this->contextMenus = $contextMenus; } /** @@ -1054,15 +1117,56 @@ public function getRegisteredCommands(): array return $this->registeredCommands; } + /** + * Get the registered context menus. + */ + public function getRegisteredContextMenus(): array + { + return $this->registeredContextMenus; + } + + /** + * Get the registered events. + */ + public function getRegisteredEvents(): array + { + return $this->registeredEvents; + } + + /** + * Get the registered services. + */ + public function getRegisteredServices(): array + { + return $this->registeredServices; + } + + /** + * Get the registered interactions. + */ + public function getRegisteredInteractions(): array + { + return $this->registeredInteractions; + } + /** * Get a registered command by name. */ public function getCommand(string $name): Command|SlashCommand|null { - return collect($this->registeredCommands) + return collect($this->getRegisteredCommands()) ->first(fn ($command) => $command->getName() === $name); } + /** + * Get a registered context menu by name. + */ + public function getContextMenu(string $name): ?ContextMenu + { + return collect($this->getRegisteredContextMenus()) + ->first(fn ($contextMenu) => $contextMenu->getName() === $name); + } + /** * Get the path to the Discord commands. */ @@ -1079,6 +1183,14 @@ public function getSlashCommandPath(): string return app_path('SlashCommands'); } + /** + * Get the path to the Discord context menus. + */ + public function getContextMenuPath(): string + { + return app_path('Menus'); + } + /** * Get the path to the Discord events. */ @@ -1173,10 +1285,11 @@ public function getApplication(): Application public function getStatus(): Collection { return collect([ - 'command' => count($this->registeredCommands), - 'event' => count($this->registeredEvents), - 'service' => count($this->registeredServices), - 'interaction' => count($this->interactions), + 'command' => count($this->getRegisteredCommands()), + 'menu' => count($this->getRegisteredContextMenus()), + 'event' => count($this->getRegisteredEvents()), + 'service' => count($this->getRegisteredServices()), + 'interaction' => count($this->getRegisteredInteractions()), 'route' => count(Route::getRoutes()->getRoutes()), ])->filter()->mapWithKeys(fn ($count, $type) => [Str::plural($type, $count) => $count]); } diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 21dd1cb..2cd6401 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -69,6 +69,7 @@ public function boot() Console\Commands\KeyGenerateCommand::class, Console\Commands\MakeCommand::class, Console\Commands\MakeSlashCommand::class, + Console\Commands\MakeMenuCommand::class, Console\Commands\ModelMakeCommand::class, Console\Commands\ServiceMakeCommand::class, Console\Commands\TokenMakeCommand::class,