diff --git a/docs/en/components/mail.md b/docs/en/components/mail.md index 649d72b6f..5b167f5d2 100644 --- a/docs/en/components/mail.md +++ b/docs/en/components/mail.md @@ -76,14 +76,17 @@ sending, which is useful in development. ## Creating a Mailable -With `hyperf/devtool` installed, generate a view-based mailable or use `--markdown` to also create a -Markdown template: +With `hyperf/devtool` installed, generate a view-based mailable or use `--markdown` to generate a +mailable that references a Markdown view: ```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. For `TestMail`, the generated class references +`mail.test-mail`; create that Blade template separately in your configured view directory. + `Envelope` defines addresses, subject, tags, metadata, and Symfony message callbacks. `Content` accepts `view` (or its `html` alias), `text`, `markdown`, `htmlString`, and `with`. Public properties declared by your mailable are also exposed to the view. @@ -130,6 +133,18 @@ Attachments can also be created with `Attachment::fromData()`, `fromStorage()`, `fromStorageDisk()`. A reusable attachable object may implement `FriendsOfHyperf\Mail\Contract\Attachable`. +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\Content; + +public function content(): Content +{ + return new Content(htmlString: '
Mail body
'); +} +``` + ## Sending Mail `Mail::mailer()` selects a configured mailer; omitting its argument uses `mail.default`. The diff --git a/docs/zh-cn/components/mail.md b/docs/zh-cn/components/mail.md index 491ec570d..40525d241 100644 --- a/docs/zh-cn/components/mail.md +++ b/docs/zh-cn/components/mail.md @@ -72,13 +72,16 @@ mailer 专属的 `from`、`reply_to`、`to` 或 `return_path` 会覆盖对应全 ## 创建 Mailable -安装 `hyperf/devtool` 后,可生成基于视图的 mailable;使用 `--markdown` 会同时创建 Markdown 模板: +安装 `hyperf/devtool` 后,可生成基于视图的 mailable;使用 `--markdown` 会生成引用 Markdown 视图的邮件类: ```shell php bin/hyperf.php gen:mail TestMail php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是无需传值的开关。对于 `TestMail`,生成的类引用 `mail.test-mail`; +请在配置的视图目录中另行创建对应的 Blade 模板。 + `Envelope` 定义地址、主题、标签、元数据和 Symfony 消息回调。`Content` 接受 `view`(或其 `html` 别名)、`text`、`markdown`、`htmlString` 和 `with`。mailable 中声明的 public 属性也会暴露给视图。 @@ -123,6 +126,18 @@ class TestMail extends Mailable 附件还可通过 `Attachment::fromData()`、`fromStorage()` 或 `fromStorageDisk()` 创建。可复用的附件 对象可以实现 `FriendsOfHyperf\Mail\Contract\Attachable`。 +对于已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`。HTML 将直接作为邮件正文, +无需渲染 Blade 视图: + +```php +use FriendsOfHyperf\Mail\Mailable\Content; + +public function content(): Content +{ + return new Content(htmlString: 'Mail body
'); +} +``` + ## 发送邮件 `Mail::mailer()` 选择已配置的 mailer;省略参数时使用 `mail.default`。`to()`、`cc()` 和 `bcc()` diff --git a/docs/zh-hk/components/mail.md b/docs/zh-hk/components/mail.md index 85d828882..3be5bf0b3 100644 --- a/docs/zh-hk/components/mail.md +++ b/docs/zh-hk/components/mail.md @@ -72,13 +72,16 @@ mailer 專屬的 `from`、`reply_to`、`to` 或 `return_path` 會覆蓋對應全 ## 創建 Mailable -安裝 `hyperf/devtool` 後,可生成基於視圖的 mailable;使用 `--markdown` 會同時創建 Markdown 模板: +安裝 `hyperf/devtool` 後,可生成基於視圖的 mailable;使用 `--markdown` 會生成引用 Markdown 視圖的郵件類: ```shell php bin/hyperf.php gen:mail TestMail php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是無需傳值的開關。對於 `TestMail`,生成的類引用 `mail.test-mail`; +請在配置的視圖目錄中另行創建對應的 Blade 模板。 + `Envelope` 定義地址、主題、標籤、元數據和 Symfony 消息回調。`Content` 接受 `view`(或其 `html` 別名)、`text`、`markdown`、`htmlString` 和 `with`。mailable 中聲明的 public 屬性也會暴露給視圖。 @@ -123,6 +126,18 @@ class TestMail extends Mailable 附件還可通過 `Attachment::fromData()`、`fromStorage()` 或 `fromStorageDisk()` 創建。可複用的附件 對象可以實現 `FriendsOfHyperf\Mail\Contract\Attachable`。 +對於已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`。HTML 將直接作為郵件正文, +無需渲染 Blade 視圖: + +```php +use FriendsOfHyperf\Mail\Mailable\Content; + +public function content(): Content +{ + return new Content(htmlString: 'Mail body
'); +} +``` + ## 發送郵件 `Mail::mailer()` 選擇已配置的 mailer;省略參數時使用 `mail.default`。`to()`、`cc()` 和 `bcc()` diff --git a/docs/zh-tw/components/mail.md b/docs/zh-tw/components/mail.md index 82cc33e29..77ba19ba1 100644 --- a/docs/zh-tw/components/mail.md +++ b/docs/zh-tw/components/mail.md @@ -72,13 +72,16 @@ mailer 專屬的 `from`、`reply_to`、`to` 或 `return_path` 會覆蓋對應全 ## 建立 Mailable -安裝 `hyperf/devtool` 後,可生成基於檢視的 mailable;使用 `--markdown` 會同時建立 Markdown 模板: +安裝 `hyperf/devtool` 後,可生成基於檢視的 mailable;使用 `--markdown` 會生成引用 Markdown 檢視的郵件類: ```shell php bin/hyperf.php gen:mail TestMail php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是無需傳值的開關。對於 `TestMail`,生成的類引用 `mail.test-mail`; +請在配置的檢視目錄中另行建立對應的 Blade 模板。 + `Envelope` 定義地址、主題、標籤、元資料和 Symfony 訊息回撥。`Content` 接受 `view`(或其 `html` 別名)、`text`、`markdown`、`htmlString` 和 `with`。mailable 中宣告的 public 屬性也會暴露給檢視。 @@ -123,6 +126,18 @@ class TestMail extends Mailable 附件還可透過 `Attachment::fromData()`、`fromStorage()` 或 `fromStorageDisk()` 建立。可複用的附件 物件可以實現 `FriendsOfHyperf\Mail\Contract\Attachable`。 +對於已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`。HTML 將直接作為郵件正文, +無需渲染 Blade 檢視: + +```php +use FriendsOfHyperf\Mail\Mailable\Content; + +public function content(): Content +{ + return new Content(htmlString: 'Mail body
'); +} +``` + ## 傳送郵件 `Mail::mailer()` 選擇已配置的 mailer;省略引數時使用 `mail.default`。`to()`、`cc()` 和 `bcc()` diff --git a/src/mail/README.md b/src/mail/README.md index 15bece868..83ef0ef1e 100644 --- a/src/mail/README.md +++ b/src/mail/README.md @@ -78,14 +78,17 @@ sending, which is useful in development. ## Creating a Mailable -With `hyperf/devtool` installed, generate a view-based mailable or use `--markdown` to also create a -Markdown template: +With `hyperf/devtool` installed, generate a view-based mailable or use `--markdown` to generate a +mailable that references a Markdown view: ```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. For `TestMail`, the generated class references +`mail.test-mail`; create that Blade template separately in your configured view directory. + `Envelope` defines addresses, subject, tags, metadata, and Symfony message callbacks. `Content` accepts `view` (or its `html` alias), `text`, `markdown`, `htmlString`, and `with`. Public properties declared by your mailable are also exposed to the view. @@ -132,6 +135,18 @@ Attachments can also be created with `Attachment::fromData()`, `fromStorage()`, `fromStorageDisk()`. A reusable attachable object may implement `FriendsOfHyperf\Mail\Contract\Attachable`. +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\Content; + +public function content(): Content +{ + return new Content(htmlString: 'Mail body
'); +} +``` + ## Sending Mail `Mail::mailer()` selects a configured mailer; omitting its argument uses `mail.default`. The diff --git a/src/mail/README_CN.md b/src/mail/README_CN.md index 97e61a194..ed5f613a2 100644 --- a/src/mail/README_CN.md +++ b/src/mail/README_CN.md @@ -74,13 +74,16 @@ mailer 专属的 `from`、`reply_to`、`to` 或 `return_path` 会覆盖对应全 ## 创建 Mailable -安装 `hyperf/devtool` 后,可生成基于视图的 mailable;使用 `--markdown` 会同时创建 Markdown 模板: +安装 `hyperf/devtool` 后,可生成基于视图的 mailable;使用 `--markdown` 会生成引用 Markdown 视图的邮件类: ```shell php bin/hyperf.php gen:mail TestMail php bin/hyperf.php gen:mail TestMail --markdown ``` +`--markdown`(或 `-m`)是无需传值的开关。对于 `TestMail`,生成的类引用 `mail.test-mail`; +请在配置的视图目录中另行创建对应的 Blade 模板。 + `Envelope` 定义地址、主题、标签、元数据和 Symfony 消息回调。`Content` 接受 `view`(或其 `html` 别名)、`text`、`markdown`、`htmlString` 和 `with`。mailable 中声明的 public 属性也会暴露给视图。 @@ -125,6 +128,18 @@ class TestMail extends Mailable 附件还可通过 `Attachment::fromData()`、`fromStorage()` 或 `fromStorageDisk()` 创建。可复用的附件 对象可以实现 `FriendsOfHyperf\Mail\Contract\Attachable`。 +对于已渲染的 HTML,可在 mailable 中返回 `Content(htmlString: ...)`。HTML 将直接作为邮件正文, +无需渲染 Blade 视图: + +```php +use FriendsOfHyperf\Mail\Mailable\Content; + +public function content(): Content +{ + return new Content(htmlString: 'Mail body
'); +} +``` + ## 发送邮件 `Mail::mailer()` 选择已配置的 mailer;省略参数时使用 `mail.default`。`to()`、`cc()` 和 `bcc()` 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..4c885ac75 --- /dev/null +++ b/tests/Mail/MailCommandTest.php @@ -0,0 +1,99 @@ +refreshContainer(); + $this->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); + } + + protected function tearDown(): void + { + $this->files->deleteDirectory($this->directory); + $this->flushContainer(); + parent::tearDown(); + } + + #[DataProvider('mailOptions')] + public function testGeneratesMailable(string $name, array $options, string $namespace, string $content): void + { + $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'); + $this->assertStringContainsString('namespace ' . $namespace . ';', $class); + $this->assertStringContainsString('class WelcomeMail extends Mailable', $class); + $this->assertStringContainsString("subject: 'Welcome Mail'", $class); + $this->assertStringContainsString($content, $class); + $this->assertStringNotContainsString('{{', $class); + } + + public static function mailOptions(): array + { + return [ + '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'"], + ]; + } + + public function testRespectsConfiguredStub(): void + { + $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(); + $this->assertSame( + 'files->get($this->directory . '/WelcomeMail.php') + ); + } +} diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index e20394bf2..e2d6f0d7c 100644 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -14,11 +14,13 @@ use FriendsOfHyperf\Mail\Event\MessageSending; use FriendsOfHyperf\Mail\Event\MessageSent; use FriendsOfHyperf\Mail\Mailable; +use FriendsOfHyperf\Mail\Mailable\Content; use FriendsOfHyperf\Mail\Mailer; use FriendsOfHyperf\Mail\Message; use FriendsOfHyperf\Mail\Transport\ArrayTransport; use FriendsOfHyperf\Support\HtmlString; use Hyperf\ViewEngine\Contract\FactoryInterface as Factory; +use Hyperf\ViewEngine\Contract\Htmlable; use Hyperf\ViewEngine\Contract\ViewInterface; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -96,6 +98,72 @@ function (Message $message) { $this->assertStringContainsString('Hello World', $sentMessage->toString()); } + public function testMailerSendsHtmlStringsWithoutCallbacks(): void + { + $view = m::mock(Factory::class); + $view->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()); + } + + public function testMailerSendsHtmlableContent(): void + { + $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()); + } + + public function testMailerRendersHtmlStringContent(): void + { + $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')])); + } + + public function testMailerSendsMailableWithHtmlStringContent(): void + { + $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()); + } + public function testMailerSendSendsMessageWithProperViewContentUsingStringCallbacks() { $view = m::mock(Factory::class);