Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/Mail/Console/MakeMail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Phenix\Mail\Console;

use Phenix\Console\Maker;
use Phenix\Facades\File;
use Phenix\Util\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
name: 'make:mail',
description: 'Create a new mailable class'
)]
class MakeMail extends Maker
{
protected function configure(): void
{
$this->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(["<comment>{$this->commonName()} already exists!</comment>", 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(["<info>{$this->commonName()} [{$outputPath}] successfully generated!</info>", 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(["<comment>View already exists!</comment>", 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(["<info>View [{$outputPath}] successfully generated!</info>", self::EMPTY_LINE]);
}

protected function getViewStubContent(): string
{
$path = dirname(__DIR__, 2)
. DIRECTORY_SEPARATOR . 'stubs'
. DIRECTORY_SEPARATOR . 'mail-view.stub';

return File::get($path);
}
}
8 changes: 8 additions & 0 deletions src/Mail/MailServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Phenix\Mail;

use Phenix\Mail\Console\MakeMail;
use Phenix\Providers\ServiceProvider;

class MailServiceProvider extends ServiceProvider
Expand All @@ -19,4 +20,11 @@ public function register(): void
{
$this->bind(MailManager::class)->setShared(true);
}

public function boot(): void
{
$this->commands([
MakeMail::class,
]);
}
}
14 changes: 14 additions & 0 deletions src/stubs/mail-view.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>{title}</title>
<meta charset="utf-8">
</head>

<body>
<h1>{title}</h1>
<p>Your email content goes here.</p>
</body>

</html>
16 changes: 16 additions & 0 deletions src/stubs/mailable.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace {namespace};

use Phenix\Mail\Mailable;

class {name} extends Mailable
{
public function build(): self
{
return $this->view('emails.{view}')
->subject('Subject here');
}
}
186 changes: 186 additions & 0 deletions tests/Unit/Mail/Console/MakeMailCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

use Phenix\Filesystem\Contracts\File;
use Phenix\Testing\Mock;

it('creates mailable and view successfully', function (): void {
$mock = Mock::of(File::class)->expect(
exists: fn (string $path) => false,
get: function (string $path) {
if (str_contains($path, 'mailable.stub')) {
return "<?php\n\nnamespace {namespace};\n\nuse Phenix\Mail\Mailable;\n\nclass {name} extends Mailable\n{\n public function build(): self\n {\n return \$this->view('emails.{view}')\n ->subject('Subject here');\n }\n}\n";
}
if (str_contains($path, 'mail-view.stub')) {
return "<!DOCTYPE html>\n<html>\n<head><title>{title}</title></head>\n<body><h1>{title}</h1></body>\n</html>\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('<title>Welcome Mail</title>');
}

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 "<?php\n\nnamespace {namespace};\n\nuse Phenix\Mail\Mailable;\n\nclass {name} extends Mailable\n{\n public function build(): self\n {\n return \$this->view('emails.{view}')\n ->subject('Subject here');\n }\n}\n";
}
if (str_contains($path, 'mail-view.stub')) {
return "<!DOCTYPE html>\n<html>\n<head><title>{title}</title></head>\n<body><h1>{title}</h1></body>\n</html>\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 "<?php\n\nnamespace {namespace};\n\nuse Phenix\Mail\Mailable;\n\nclass {name} extends Mailable\n{\n public function build(): self\n {\n return \$this->view('emails.{view}')\n ->subject('Subject here');\n }\n}\n";
}
if (str_contains($path, 'mail-view.stub')) {
return "<!DOCTYPE html>\n<html>\n<head><title>{title}</title></head>\n<body><h1>{title}</h1></body>\n</html>\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 "<?php\n\nnamespace {namespace};\n\nuse Phenix\Mail\Mailable;\n\nclass {name} extends Mailable\n{\n public function build(): self\n {\n return \$this->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!');
});