diff --git a/docs/en/components/mail.md b/docs/en/components/mail.md index c66b552c6..3eb507783 100644 --- a/docs/en/components/mail.md +++ b/docs/en/components/mail.md @@ -161,8 +161,13 @@ return [ ```shell php bin/hyperf.php gen:mail TestMail +php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown` (or `-m`) is a flag and takes no value. It generates a mailable referencing +`mail.test-mail` for `TestMail`; create that Blade template separately in your configured view +directory. Without the flag, the generated class uses a regular view. + ```php // app/Mail/TestMail.php @@ -217,6 +222,22 @@ class TestMail extends Mailable } ``` +For pre-rendered HTML, return `Content(htmlString: ...)` from your mailable. The HTML is used +directly without rendering a Blade view: + +```php +use FriendsOfHyperf\Mail\Mailable; +use FriendsOfHyperf\Mail\Mailable\Content; + +class HtmlMail extends Mailable +{ + public function content(): Content + { + return new Content(htmlString: '
Mail body
'); + } +} +``` + ### Defining the Controller or Service ```php diff --git a/docs/zh-cn/components/mail.md b/docs/zh-cn/components/mail.md index cc7b0cdf4..68daa4539 100644 --- a/docs/zh-cn/components/mail.md +++ b/docs/zh-cn/components/mail.md @@ -162,8 +162,12 @@ return [ ```shell php bin/hyperf.php gen:mail TestMail +php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是无需传值的开关。对于 `TestMail`,生成的邮件类引用 `mail.test-mail`; +请在配置的视图目录中另行创建对应的 Blade 模板。不使用该开关时,生成的类使用普通视图。 + ```php // app/Mail/TestMail.php @@ -218,6 +222,22 @@ class TestMail extends Mailable } ``` +对于已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`,直接作为邮件正文, +无需渲染 Blade 视图: + +```php +use FriendsOfHyperf\Mail\Mailable; +use FriendsOfHyperf\Mail\Mailable\Content; + +class HtmlMail extends Mailable +{ + public function content(): Content + { + return new Content(htmlString: 'Mail body
'); + } +} +``` + ### 定义控制器或 Service ```php diff --git a/docs/zh-hk/components/mail.md b/docs/zh-hk/components/mail.md index c435a93ee..842b10034 100644 --- a/docs/zh-hk/components/mail.md +++ b/docs/zh-hk/components/mail.md @@ -162,8 +162,12 @@ return [ ```shell php bin/hyperf.php gen:mail TestMail +php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是無需傳值的開關。對於 `TestMail`,生成的郵件類引用 `mail.test-mail`; +請在配置的視圖目錄中另行創建對應的 Blade 模板。不使用該開關時,生成的類使用普通視圖。 + ```php // app/Mail/TestMail.php @@ -218,6 +222,22 @@ class TestMail extends Mailable } ``` +對於已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`,直接作為郵件正文, +無需渲染 Blade 視圖: + +```php +use FriendsOfHyperf\Mail\Mailable; +use FriendsOfHyperf\Mail\Mailable\Content; + +class HtmlMail extends Mailable +{ + public function content(): Content + { + return new Content(htmlString: 'Mail body
'); + } +} +``` + ### 定義控制器或 Service ```php diff --git a/docs/zh-tw/components/mail.md b/docs/zh-tw/components/mail.md index 064ec641b..9c166a6f3 100644 --- a/docs/zh-tw/components/mail.md +++ b/docs/zh-tw/components/mail.md @@ -162,8 +162,12 @@ return [ ```shell php bin/hyperf.php gen:mail TestMail +php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是無需傳值的開關。對於 `TestMail`,生成的郵件類引用 `mail.test-mail`; +請在配置的檢視目錄中另行建立對應的 Blade 模板。不使用該開關時,生成的類使用普通檢視。 + ```php // app/Mail/TestMail.php @@ -218,6 +222,22 @@ class TestMail extends Mailable } ``` +對於已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`,直接作為郵件正文, +無需渲染 Blade 檢視: + +```php +use FriendsOfHyperf\Mail\Mailable; +use FriendsOfHyperf\Mail\Mailable\Content; + +class HtmlMail extends Mailable +{ + public function content(): Content + { + return new Content(htmlString: 'Mail body
'); + } +} +``` + ### 定義控制器或 Service ```php diff --git a/src/mail/README.md b/src/mail/README.md index 1b67962f5..4e836d306 100644 --- a/src/mail/README.md +++ b/src/mail/README.md @@ -168,8 +168,13 @@ return [ ```shell php bin/hyperf.php gen:mail TestMail +php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown` (or `-m`) is a flag and takes no value. It generates a mailable referencing +`mail.test-mail` for `TestMail`; create that Blade template separately in your configured view +directory. Without the flag, the generated class uses a regular view. + ```php // app/Mail/TestMail.php @@ -224,6 +229,22 @@ class TestMail extends Mailable } ``` +For pre-rendered HTML, return `Content(htmlString: ...)` from your mailable. The HTML is used +directly without rendering a Blade view: + +```php +use FriendsOfHyperf\Mail\Mailable; +use FriendsOfHyperf\Mail\Mailable\Content; + +class HtmlMail extends Mailable +{ + public function content(): Content + { + return new Content(htmlString: 'Mail body
'); + } +} +``` + ### Your Controller or Service ```php diff --git a/src/mail/src/Command/MailCommand.php b/src/mail/src/Command/MailCommand.php index 3fe18ca21..b81492149 100644 --- a/src/mail/src/Command/MailCommand.php +++ b/src/mail/src/Command/MailCommand.php @@ -32,7 +32,7 @@ public function __construct( protected function getStub(): string { - return $this->getConfig()['stub'] ?? ($this->input->getOption('markdown') !== false ? __DIR__ . '/stubs/mail.stub' : __DIR__ . '/stubs/markdown-mail.stub'); + return $this->getConfig()['stub'] ?? ($this->input->getOption('markdown') ? __DIR__ . '/stubs/markdown-mail.stub' : __DIR__ . '/stubs/mail.stub'); } protected function getDefaultNamespace(): string @@ -81,7 +81,7 @@ protected function getView(): string { $view = $this->input->getOption('markdown'); - if (! $view) { + if (! is_string($view) || $view === '') { $name = str_replace('\\', '/', $this->input->getArgument('name')); $view = 'mail.' . collect(explode('/', $name)) @@ -103,7 +103,7 @@ protected function buildClass(string $name): string parent::buildClass($name) ); - if ($this->input->getOption('markdown') !== false) { + if ($this->input->getOption('markdown')) { $class = str_replace(['DummyView', '{{ view }}'], $this->getView(), $class); } diff --git a/src/mail/src/Mailer.php b/src/mail/src/Mailer.php index b39087f84..7579cee9a 100644 --- a/src/mail/src/Mailer.php +++ b/src/mail/src/Mailer.php @@ -285,9 +285,9 @@ protected function sendMailable(MailableContract $mailable): ?SentMessage /** * Parse the given view name or array. */ - protected function parseView(null|Closure|array|string $view): array + protected function parseView(null|Closure|Htmlable|HtmlString|array|string $view): array { - if (is_string($view) || $view instanceof Closure) { + if (is_string($view) || $view instanceof Closure || $view instanceof Htmlable || $view instanceof HtmlString) { return [$view, null, null]; } @@ -315,7 +315,7 @@ protected function parseView(null|Closure|array|string $view): array /** * Add the content to a given message. */ - protected function addContent(Message $message, null|Closure|string $view, null|Closure|string $plain, ?string $raw, array $data = []): void + protected function addContent(Message $message, null|Closure|Htmlable|HtmlString|string $view, null|Closure|Htmlable|HtmlString|string $plain, ?string $raw, array $data = []): void { if (isset($view)) { $message->html($this->renderView($view, $data) ?: ' '); @@ -333,7 +333,7 @@ protected function addContent(Message $message, null|Closure|string $view, null| /** * Render the given view. */ - protected function renderView(Closure|string $view, array $data): string + protected function renderView(Closure|Htmlable|HtmlString|string $view, array $data): string { $view = value($view, $data); if ($view instanceof Htmlable) { diff --git a/tests/Mail/MailCommandTest.php b/tests/Mail/MailCommandTest.php new file mode 100644 index 000000000..e9146b8dd --- /dev/null +++ b/tests/Mail/MailCommandTest.php @@ -0,0 +1,62 @@ +config = new Config([]); + $this->swap(ConfigInterface::class, $this->config); + $this->files = new Filesystem(); + $this->directory = tempnam(sys_get_temp_dir(), 'mail-command-'); + unlink($this->directory); + $this->files->makeDirectory($this->directory); +}); + +afterEach(function () { + $this->files->deleteDirectory($this->directory); +}); + +it('generates a mailable with the correct content definition', function (string $name, array $options, string $namespace, string $content) { + $tester = new CommandTester(new MailCommand($this->files, $this->config)); + + $tester->execute(['name' => $name, '--path' => $this->directory] + $options); + + $tester->assertCommandIsSuccessful(); + $class = $this->files->get($this->directory . '/WelcomeMail.php'); + expect($class) + ->toContain('namespace ' . $namespace . ';') + ->toContain('class WelcomeMail extends Mailable') + ->toContain("subject: 'Welcome Mail'") + ->toContain($content) + ->not->toContain('{{'); +})->with([ + 'view mailable' => ['WelcomeMail', [], 'App\Mail', "view: 'view.name'"], + 'markdown flag' => ['WelcomeMail', ['--markdown' => true], 'App\Mail', "markdown: 'mail.welcome-mail'"], + 'short markdown flag' => ['WelcomeMail', ['-m' => true], 'App\Mail', "markdown: 'mail.welcome-mail'"], + 'slash namespace' => ['Orders/WelcomeMail', ['--markdown' => true], 'App\Mail\Orders', "markdown: 'mail.orders.welcome-mail'"], + 'backslash namespace' => ['Orders\WelcomeMail', ['--markdown' => true], 'App\Mail\Orders', "markdown: 'mail.orders.welcome-mail'"], +]); + +it('respects the configured mail stub', function () { + $stub = $this->directory . '/custom.stub'; + $this->files->put($stub, 'config->set('devtool.generator.mail.stub', $stub); + $tester = new CommandTester(new MailCommand($this->files, $this->config)); + + $tester->execute(['name' => 'WelcomeMail', '--path' => $this->directory, '--markdown' => true]); + + $tester->assertCommandIsSuccessful(); + expect($this->files->get($this->directory . '/WelcomeMail.php')) + ->toBe('shouldNotReceive('make'); + $mailer = new Mailer('array', $view, new ArrayTransport()); + + $sentMessage = $mailer->send( + ['html' => new HtmlString('Hello Hyperf
'), 'text' => new HtmlString('Hello World')], + [], + function (Message $message) { + $message->to('recipient@example.com')->from('sender@example.com'); + } + ); + + $this->assertSame('Hello Hyperf
', $sentMessage->getOriginalMessage()->getHtmlBody()); + $this->assertSame('Hello World', $sentMessage->getOriginalMessage()->getTextBody()); +}); + +it('sends Htmlable content without rendering a view', function () { + $view = m::mock(Factory::class); + $view->shouldNotReceive('make'); + $mailer = new Mailer('array', $view, new ArrayTransport()); + $html = m::mock(Htmlable::class); + $html->expects('toHtml')->andReturn('Hello Hyperf
'); + $text = m::mock(Htmlable::class); + $text->expects('toHtml')->andReturn('Hello World'); + + $sentMessage = $mailer->send(['html' => $html, 'text' => $text], [], function (Message $message) { + $message->to('recipient@example.com')->from('sender@example.com'); + }); + + $this->assertSame('Hello Hyperf
', $sentMessage->getOriginalMessage()->getHtmlBody()); + $this->assertSame('Hello World', $sentMessage->getOriginalMessage()->getTextBody()); +}); + +it('renders HTML and text string content', function () { + $view = m::mock(Factory::class); + $view->shouldNotReceive('make'); + $mailer = new Mailer('array', $view, new ArrayTransport()); + + $this->assertSame('Hello Hyperf
', $mailer->render(['html' => new HtmlString('Hello Hyperf
')])); + $this->assertSame('Hello World', $mailer->render(['text' => new HtmlString('Hello World')])); +}); + +it('sends a mailable with pre-rendered HTML content', function () { + $view = m::mock(Factory::class); + $view->shouldNotReceive('make'); + $transport = new ArrayTransport(); + $mailer = new Mailer('array', $view, $transport); + $mailable = new class extends Mailable { + public function content(): Content + { + return new Content(htmlString: 'Mail body
'); + } + }; + $mailable->from('sender@example.com')->to('recipient@example.com')->subject('Greeting'); + + $sentMessage = $mailer->send($mailable); + + $this->assertCount(1, $transport->messages()); + $this->assertSame('Mail body
', $sentMessage->getOriginalMessage()->getHtmlBody()); +});