From 1347bf62709ed2295f24fb84c0f8af58beea560b Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 27 Oct 2025 08:58:20 -0500 Subject: [PATCH] feat: make mail command --- src/Mail/Console/MakeMail.php | 122 ++++++++++++ src/Mail/MailServiceProvider.php | 8 + src/stubs/mail-view.stub | 14 ++ src/stubs/mailable.stub | 16 ++ .../Unit/Mail/Console/MakeMailCommandTest.php | 186 ++++++++++++++++++ 5 files changed, 346 insertions(+) create mode 100644 src/Mail/Console/MakeMail.php create mode 100644 src/stubs/mail-view.stub create mode 100644 src/stubs/mailable.stub create mode 100644 tests/Unit/Mail/Console/MakeMailCommandTest.php diff --git a/src/Mail/Console/MakeMail.php b/src/Mail/Console/MakeMail.php new file mode 100644 index 00000000..901bdf63 --- /dev/null +++ b/src/Mail/Console/MakeMail.php @@ -0,0 +1,122 @@ +addArgument('name', InputArgument::REQUIRED, 'The name of the mailable class'); + + $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create mailable'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->input = $input; + + $name = $this->input->getArgument('name'); + $force = $this->input->getOption('force'); + + $namespace = explode(DIRECTORY_SEPARATOR, $name); + $className = array_pop($namespace); + $fileName = $this->getCustomFileName() ?? $className; + + $filePath = $this->preparePath($namespace) . DIRECTORY_SEPARATOR . "{$fileName}.php"; + $namespaceString = $this->prepareNamespace($namespace); + + if (File::exists($filePath) && ! $force) { + $output->writeln(["{$this->commonName()} already exists!", self::EMPTY_LINE]); + + return Command::SUCCESS; + } + + $viewName = Str::snake($className); + $viewDotPath = empty($namespace) + ? $viewName + : implode('.', array_map('strtolower', $namespace)) . ".{$viewName}"; + + $stub = $this->getStubContent(); + $stub = str_replace(['{namespace}', '{name}', '{view}'], [$namespaceString, $className, $viewDotPath], $stub); + + File::put($filePath, $stub); + + $outputPath = str_replace(base_path(), '', $filePath); + + $output->writeln(["{$this->commonName()} [{$outputPath}] successfully generated!", self::EMPTY_LINE]); + + $this->createView($input, $output, $namespace, $viewName); + + return Command::SUCCESS; + } + + protected function outputDirectory(): string + { + return 'app' . DIRECTORY_SEPARATOR . 'Mail'; + } + + protected function commonName(): string + { + return 'Mailable'; + } + + protected function stub(): string + { + return 'mailable.stub'; + } + + protected function createView(InputInterface $input, OutputInterface $output, array $namespace, string $viewName): void + { + $force = $input->getOption('force'); + + $viewPath = base_path('resources' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'emails'); + $this->checkDirectory($viewPath); + + foreach ($namespace as $directory) { + $viewPath .= DIRECTORY_SEPARATOR . strtolower($directory); + $this->checkDirectory($viewPath); + } + + $viewFilePath = $viewPath . DIRECTORY_SEPARATOR . "{$viewName}.php"; + + if (File::exists($viewFilePath) && ! $force) { + $output->writeln(["View already exists!", self::EMPTY_LINE]); + + return; + } + + $viewStub = $this->getViewStubContent(); + $viewStub = str_replace('{title}', ucwords(str_replace('_', ' ', $viewName)), $viewStub); + + File::put($viewFilePath, $viewStub); + + $outputPath = str_replace(base_path(), '', $viewFilePath); + + $output->writeln(["View [{$outputPath}] successfully generated!", self::EMPTY_LINE]); + } + + protected function getViewStubContent(): string + { + $path = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR . 'stubs' + . DIRECTORY_SEPARATOR . 'mail-view.stub'; + + return File::get($path); + } +} diff --git a/src/Mail/MailServiceProvider.php b/src/Mail/MailServiceProvider.php index a95e9f78..2081415f 100644 --- a/src/Mail/MailServiceProvider.php +++ b/src/Mail/MailServiceProvider.php @@ -4,6 +4,7 @@ namespace Phenix\Mail; +use Phenix\Mail\Console\MakeMail; use Phenix\Providers\ServiceProvider; class MailServiceProvider extends ServiceProvider @@ -19,4 +20,11 @@ public function register(): void { $this->bind(MailManager::class)->setShared(true); } + + public function boot(): void + { + $this->commands([ + MakeMail::class, + ]); + } } diff --git a/src/stubs/mail-view.stub b/src/stubs/mail-view.stub new file mode 100644 index 00000000..b329a7ba --- /dev/null +++ b/src/stubs/mail-view.stub @@ -0,0 +1,14 @@ + + + + + {title} + + + + +

{title}

+

Your email content goes here.

+ + + diff --git a/src/stubs/mailable.stub b/src/stubs/mailable.stub new file mode 100644 index 00000000..af7bcda5 --- /dev/null +++ b/src/stubs/mailable.stub @@ -0,0 +1,16 @@ +view('emails.{view}') + ->subject('Subject here'); + } +} diff --git a/tests/Unit/Mail/Console/MakeMailCommandTest.php b/tests/Unit/Mail/Console/MakeMailCommandTest.php new file mode 100644 index 00000000..313e437f --- /dev/null +++ b/tests/Unit/Mail/Console/MakeMailCommandTest.php @@ -0,0 +1,186 @@ +expect( + exists: fn (string $path) => false, + get: function (string $path) { + if (str_contains($path, 'mailable.stub')) { + return "view('emails.{view}')\n ->subject('Subject here');\n }\n}\n"; + } + if (str_contains($path, 'mail-view.stub')) { + return "\n\n{title}\n

{title}

\n\n"; + } + + return ''; + }, + put: function (string $path, string $content) { + if (str_contains($path, 'app/Mail')) { + expect($path)->toContain('app' . DIRECTORY_SEPARATOR . 'Mail' . DIRECTORY_SEPARATOR . 'WelcomeMail.php'); + expect($content)->toContain('namespace App\Mail'); + expect($content)->toContain('class WelcomeMail extends Mailable'); + expect($content)->toContain("->view('emails.welcome_mail')"); + } + if (str_contains($path, 'resources/views/emails')) { + expect($path)->toContain('resources' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'emails' . DIRECTORY_SEPARATOR . 'welcome_mail.php'); + expect($content)->toContain('Welcome Mail'); + } + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:mail', [ + 'name' => 'WelcomeMail', + ]); + + $command->assertCommandIsSuccessful(); + + $display = $command->getDisplay(); + expect($display)->toContain('Mailable [app/Mail/WelcomeMail.php] successfully generated!'); + expect($display)->toContain('View [resources/views/emails/welcome_mail.php] successfully generated!'); +}); + +it('does not create the mailable because it already exists', function (): void { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => str_contains($path, 'app/Mail'), + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:mail', [ + 'name' => 'WelcomeMail', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Mailable already exists!'); +}); + +it('creates mailable with force option when it already exists', function (): void { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => str_contains($path, 'app/Mail'), + get: function (string $path) { + if (str_contains($path, 'mailable.stub')) { + return "view('emails.{view}')\n ->subject('Subject here');\n }\n}\n"; + } + if (str_contains($path, 'mail-view.stub')) { + return "\n\n{title}\n

{title}

\n\n"; + } + + return ''; + }, + put: fn (string $path, string $content) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:mail', [ + 'name' => 'WelcomeMail', + '--force' => true, + ]); + + $command->assertCommandIsSuccessful(); + + $display = $command->getDisplay(); + expect($display)->toContain('Mailable [app/Mail/WelcomeMail.php] successfully generated!'); + expect($display)->toContain('View [resources/views/emails/welcome_mail.php] successfully generated!'); +}); + +it('creates mailable successfully in nested namespace', function (): void { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: function (string $path) { + if (str_contains($path, 'mailable.stub')) { + return "view('emails.{view}')\n ->subject('Subject here');\n }\n}\n"; + } + if (str_contains($path, 'mail-view.stub')) { + return "\n\n{title}\n

{title}

\n\n"; + } + + return ''; + }, + put: function (string $path, string $content) { + if (str_contains($path, 'app/Mail')) { + expect($path)->toContain('app' . DIRECTORY_SEPARATOR . 'Mail' . DIRECTORY_SEPARATOR . 'Auth' . DIRECTORY_SEPARATOR . 'PasswordReset.php'); + expect($content)->toContain('namespace App\Mail\Auth'); + expect($content)->toContain('class PasswordReset extends Mailable'); + expect($content)->toContain("->view('emails.auth.password_reset')"); + } + if (str_contains($path, 'resources/views/emails')) { + expect($path)->toContain('resources' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'emails' . DIRECTORY_SEPARATOR . 'auth' . DIRECTORY_SEPARATOR . 'password_reset.php'); + } + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:mail', [ + 'name' => 'Auth/PasswordReset', + ]); + + $command->assertCommandIsSuccessful(); + + $display = $command->getDisplay(); + expect($display)->toContain('Mailable [app/Mail/Auth/PasswordReset.php] successfully generated!'); + expect($display)->toContain('View [resources/views/emails/auth/password_reset.php] successfully generated!'); +}); + +it('does not create view when it already exists but creates mailable', function (): void { + $mock = Mock::of(File::class)->expect( + exists: function (string $path) { + return str_contains($path, 'resources/views/emails'); + }, + get: function (string $path) { + if (str_contains($path, 'mailable.stub')) { + return "view('emails.{view}')\n ->subject('Subject here');\n }\n}\n"; + } + + return ''; + }, + put: function (string $path, string $content) { + if (str_contains($path, 'app/Mail')) { + return true; + } + + return false; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:mail', [ + 'name' => 'WelcomeMail', + ]); + + $command->assertCommandIsSuccessful(); + + $display = $command->getDisplay(); + expect($display)->toContain('Mailable [app/Mail/WelcomeMail.php] successfully generated!'); + expect($display)->toContain('View already exists!'); +});