简介

除了支持发送邮件之外,Laravel 还支持通过多种投递渠道发送通知,包括电子邮件、短信(通过 Vonage,前身为 Nexmo)以及 Slack。此外,还创建了多种社区构建的通知渠道,可用于通过数十种不同渠道发送通知!通知也可以存储在数据库中,以便在 Web 界面中展示。

通常,通知应为简短的信息性消息,用于告知用户应用程序中发生了某些事件。例如,如果你正在编写计费应用程序,你可能会通过电子邮件和短信渠道向用户发送“账单已支付”通知。

生成通知

在 Laravel 中,每条通知由单个类表示,该类通常存储在 app/Notifications 目录中。如果你的应用中未看到该目录,不必担心——当你执行 make:notification Artisan 命令时,系统会自动创建该目录:

php artisan make:notification InvoicePaid

此命令会在你的 app/Notifications 目录中生成一个新的通知类。每个通知类都包含一个 via 方法以及若干消息构建方法(如 toMail 或 toDatabase),用于将通知转换为针对特定渠道定制的消息。

发送通知

使用 Notifiable Trait

通知可以通过两种方式发送:使用 Notifiable trait 的 notify 方法,或使用 Notification 门面(facade)。Notifiable trait 默认包含在应用的 App\Models\User 模型中:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}

此 trait 提供的 notify 方法需要接收一个通知实例:

use App\Notifications\InvoicePaid;

$user->notify(new InvoicePaid($invoice));

请记住,你可以在任意模型中使用 Notifiable trait。你无需仅限于在 User 模型中使用它。

使用 Notification 门面

或者,你也可以通过 Notification 门面(facade)发送通知。当你需要向多个可通知实体(例如用户集合)发送通知时,这种方法非常有用。要使用门面发送通知,需将所有可通知实体和通知实例传递给 send 方法:

use Illuminate\Support\Facades\Notification;

Notification::send($users, new InvoicePaid($invoice));

你还可以使用 sendNow 方法立即发送通知。即使通知实现了 ShouldQueue 接口,该方法也会立即发送通知:

Notification::sendNow($developers, new DeploymentCompleted($deployment));

指定投递渠道

每个通知类都有一个 via 方法,用于确定通知将通过哪些渠道进行投递。通知可以发送到 mail、database、broadcast、vonage 和 slack 渠道。

如果你想使用其他投递渠道(如 Telegram 或 Pusher),请查看社区驱动的Laravel 通知渠道网站。

via 方法接收一个 $notifiable 实例,该实例是接收通知的类的一个实例。你可以使用 $notifiable 来确定通知应通过哪些渠道进行投递:

/**
 * 获取通知的投递渠道。
 *
 * @return array<int, string>
 */
public function via(object $notifiable): array
{
    return $notifiable->prefers_sms ? ['vonage'] : ['mail', 'database'];
}

通知队列

在对通知进行队列化之前,你应该配置队列并启动一个工作进程。

发送通知可能需要时间,尤其是当渠道需要通过外部 API 调用来投递通知时。为了加快应用程序的响应速度,你可以通过在类中添加 ShouldQueue 接口和 Queueable trait,让通知进入队列。使用 make:notification 命令生成的所有通知已默认导入该接口和 trait,因此你可以立即将它们添加到通知类中:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    // ...
}

一旦通知类添加了 ShouldQueue 接口,你就可以像平常一样发送通知。Laravel 会检测类中的 ShouldQueue 接口,并自动将通知的投递操作加入队列:

$user->notify(new InvoicePaid($invoice));

在对通知进行队列化时,系统会为每个收件人与渠道的组合创建一个队列作业。例如,如果你的通知有三个收件人和两个渠道,那么将会向队列中分发六个作业。

延迟通知

如果你希望延迟投递通知,可以在通知实例化时链式调用 delay 方法:

$delay = now()->plus(minutes: 10);

$user->notify((new InvoicePaid($invoice))->delay($delay));

你可以向 delay 方法传入一个数组,以指定特定渠道的延迟时长:

$user->notify((new InvoicePaid($invoice))->delay([
    'mail' => now()->plus(minutes: 5),
    'sms' => now()->plus(minutes: 10),
]));

或者,你也可以在通知类本身中定义 withDelay 方法。withDelay 方法应返回一个包含渠道名称和延迟值的数组:

/**
 * 确定通知的投递延迟。
 *
 * @return array<string, \Illuminate\Support\Carbon>
 */
public function withDelay(object $notifiable): array
{
    return [
        'mail' => now()->plus(minutes: 5),
        'sms' => now()->plus(minutes: 10),
    ];
}

自定义通知队列连接

默认情况下,队列通知将使用应用程序的默认队列连接进行队列化。如果你想为特定通知指定其他连接,可以在通知的构造函数中调用 onConnection 方法:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    /**
     * 创建新的通知实例。
     */
    public function __construct()
    {
        $this->onConnection('redis');
    }
}

或者,如果你想为通知支持的每个通知渠道指定特定的队列连接,可以在通知类中定义 viaConnections 方法。该方法应返回一个渠道名称/队列连接名称的键值对数组:

/**
 * 确定每个通知渠道应使用的连接。
 *
 * @return array<string, string>
 */
public function viaConnections(): array
{
    return [
        'mail' => 'redis',
        'database' => 'sync',
    ];
}

自定义通知渠道队列

如果你想为通知支持的每个通知渠道指定特定的队列,可以在通知类中定义 viaQueues 方法。该方法应返回一个渠道名称/队列名称的键值对数组:

/**
 * 确定每个通知渠道应使用的队列。
 *
 * @return array<string, string>
 */
public function viaQueues(): array
{
    return [
        'mail' => 'mail-queue',
        'slack' => 'slack-queue',
    ];
}

自定义队列通知作业属性

你可以在通知类中定义队列属性,以自定义底层队列作业的行为。这些属性将被用于发送通知的队列作业继承:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\Attributes\FailOnTimeout;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;

#[Tries(5)]
#[Timeout(120)]
#[MaxExceptions(3)]
#[FailOnTimeout]
class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    // ...
}

如果你想通过加密来确保排队通知数据的隐私性与完整性,请将 ShouldBeEncrypted 接口添加到你的通知类中:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue, ShouldBeEncrypted
{
    use Queueable;

    // ...
}

除了直接在通知类上定义这些特性外,你也可以定义 backoff 和 retryUntil 方法,来指定排队通知任务的退避策略和重试超时时间:

use DateTime;

/**
 * 计算重试通知前需要等待的秒数。
 */
public function backoff(): int
{
    return 3;
}

/**
 * 确定通知应在何时超时。
 */
public function retryUntil(): DateTime
{
    return now()->plus(minutes: 5);
}

若要进一步了解这些任务特性和方法,请参阅关于排队任务的文档。

排队通知中间件

排队通知可以像排队任务一样定义中间件。若要开始使用,请在你的通知类中定义一个 middleware 方法。该 middleware 方法会接收 $notifiable 和 $channel 变量,借此你可以根据通知的目的地来自定义返回的中间件:

use Illuminate\Queue\Middleware\RateLimited;

/**
 * 获取通知任务应通过的中间件。
 *
 * @return array<int, object>
 */
public function middleware(object $notifiable, string $channel)
{
    return match ($channel) {
        'mail' => [new RateLimited('postmark')],
        'slack' => [new RateLimited('slack')],
        default => [],
    };
}

排队通知与数据库事务

当排队通知在数据库事务中派发时,可能会在数据库事务提交之前就被队列处理。发生这种情况时,你在数据库事务期间对模型或数据库记录所做的任何更新可能尚未反映到数据库中。此外,在该事务中创建的任何模型或数据库记录在数据库中可能还不存在。如果你的通知依赖这些模型,那么在处理发送该排队通知的任务时就可能出现意外错误。

如果你的队列连接的 after_commit 配置项设置为 false,你依然可以通过在发送通知时调用 afterCommit 方法,指明某个排队通知应当在所有开启的数据库事务都提交后再派发:

use App\Notifications\InvoicePaid;

$user->notify((new InvoicePaid($invoice))->afterCommit());

或者,你也可以在通知的构造函数中调用 afterCommit 方法:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    /**
     * Create a new notification instance.
     */
    public function __construct()
    {
        $this->afterCommit();
    }
}

若要进一步了解如何处理这些问题,请参阅关于排队任务与数据库事务的文档。

判断是否应当发送排队通知

排队通知被派发到队列以进行后台处理后,通常会由队列工作进程接收,并发送给其目标接收者。

不过,如果你想在排队通知被队列工作进程处理之后,再对其是否应当发送做出最终决定,可以在通知类中定义一个 shouldSend 方法。若该方法返回 false,则不会发送该通知:

/**
 * 判断是否应当发送通知。
 */
public function shouldSend(object $notifiable, string $channel): bool
{
    return $this->invoice->isPaid();
}

发送通知之后

如果你想在通知发送完成后执行代码,可以在通知类中定义一个 afterSending 方法。该方法将接收通知实体、通道名称以及来自通道的响应:

/**
 * 处理通知在发送后的后续操作。
 */
public function afterSending(object $notifiable, string $channel, mixed $response): void
{
    // ...
}

按需通知

有时你可能需要向并非应用程序“用户”的人发送通知。借助 Notification 门面的 route 方法,你可以在发送通知前指定临时的通知路由信息:

use Illuminate\Broadcasting\Channel;
use Illuminate\Support\Facades\Notification;

Notification::route('mail', '[email protected]')
    ->route('vonage', '5555555555')
    ->route('slack', '#slack-channel')
    ->route('broadcast', [new Channel('channel-name')])
    ->notify(new InvoicePaid($invoice));

如果你想在将按需通知发送到 mail 通道时提供接收者的姓名,可以传入一个数组,其中以电子邮件地址作为键,并将姓名作为数组第一个元素的值:

Notification::route('mail', [
    '[email protected]' => 'Barrett Blair',
])->notify(new InvoicePaid($invoice));

使用 routes 方法,你可以一次性为多个通知通道提供临时路由信息:

Notification::routes([
    'mail' => ['[email protected]' => 'Barrett Blair'],
    'vonage' => '5555555555',
])->notify(new InvoicePaid($invoice));

邮件通知

格式化邮件内容

如果某个通知支持以电子邮件形式发送,你应当在通知类中定义一个 toMail 方法。该方法会接收一个 $notifiable 实体,并应返回一个 Illuminate\Notifications\Messages\MailMessage 实例。

MailMessage 类包含一些简单方法,可帮助你构建事务性邮件内容。邮件内容可以包含若干文本行以及一个“操作引导”(call to action)。下面来看一个 toMail 方法示例:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    $url = url('/invoice/'.$this->invoice->id);

    return (new MailMessage)
        ->greeting('Hello!')
        ->line('One of your invoices has been paid!')
        ->lineIf($this->amount > 0, "Amount paid: {$this->amount}")
        ->action('View Invoice', $url)
        ->line('Thank you for using our application!');
}

请注意,我们在 toMail 方法中使用了 $this->invoice->id。你可以将生成邮件所需的任意数据,通过通知的构造函数传入通知中。

在此示例中,我们依次添加了问候语、一行文本、一个操作引导,然后又添加了一行文本。MailMessage 对象提供的这些方法,让格式化简短的事务性邮件变得简单而快捷。邮件通道随后会将这些消息组件转换为美观、响应式的 HTML 邮件模板,并提供对应的纯文本版本。下面是 mail 通道生成的一封示例邮件:

通知

在发送邮件通知时,请务必在 config/app.php 配置文件中设置 name 配置选项。该值将用于邮件通知的页眉和页脚。

错误消息

有些通知会向用户告知错误,例如发票支付失败。你可以通过在构建邮件内容时调用 error 方法,来表明该邮件内容属于错误通知。在邮件内容中使用 error 方法时,操作引导按钮会显示为红色,而不是黑色:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->error()
        ->subject('Invoice Payment Failed')
        ->line('...');
}

其他邮件通知格式选项

您可以不用在通知类中定义文本“行”,而使用 view 方法指定用于渲染通知邮件的自定义模板:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)->view(
        'mail.invoice.paid', ['invoice' => $this->invoice]
    );
}

您可以通过将视图名称作为传递给 view 方法的数组的第二个元素,来指定邮件消息的纯文本视图:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)->view(
        ['mail.invoice.paid', 'mail.invoice.paid-text'],
        ['invoice' => $this->invoice]
    );
}

或者,如果您的消息只有纯文本视图,可以使用 text 方法:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)->text(
        'mail.invoice.paid-text', ['invoice' => $this->invoice]
    );
}

自定义发件人

默认情况下,邮件的发件人/发件地址在 config/mail.php 配置文件中定义。但是,您可以使用 from 方法为特定通知指定发件地址:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->from('[email protected]', 'Barrett Blair')
        ->line('...');
}

自定义收件人

当通过 mail 通道发送通知时,通知系统会自动在您的可通知实体上查找 email 属性。您可以通过在可通知实体上定义 routeNotificationForMail 方法,来自定义用于投递通知的邮箱地址:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 为邮件通道路由通知。
     *
     * @return  array<string, string>|string
     */
    public function routeNotificationForMail(Notification $notification): array|string
    {
        // 仅返回邮箱地址...
        return $this->email_address;

        // 返回邮箱地址和姓名...
        return [$this->email_address => $this->name];
    }
}

自定义主题

默认情况下,邮件的主题是将通知的类名格式化为“标题大小写”。因此,如果您的通知类名为 InvoicePaid,邮件的主题将是 Invoice Paid。如果您想为消息指定不同的主题,可以在构建消息时调用 subject 方法:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->subject('Notification Subject')
        ->line('...');
}

自定义邮件程序

默认情况下,邮件通知将使用 config/mail.php 配置文件中定义的默认邮件程序发送。但是,您可以在运行时通过在构建消息时调用 mailer 方法来指定不同的邮件程序:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->mailer('postmark')
        ->line('...');
}

自定义模板

您可以通过发布通知包的资源来修改邮件通知使用的 HTML 和纯文本模板。运行此命令后,邮件通知模板将位于 resources/views/vendor/notifications 目录中:

php artisan vendor:publish --tag=laravel-notifications

附件

要向邮件通知添加附件,可在构建消息时使用 attach 方法。attach 方法的第一个参数接受文件的绝对路径:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->greeting('Hello!')
        ->attach('/path/to/file');
}

通知邮件消息提供的 attach 方法还接受可附加对象。请查阅全面的可附加对象文档以了解详情。

向消息附加文件时,您还可以通过将 array 作为第二个参数传递给 attach 方法,来指定显示名称和/或 MIME 类型:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->greeting('Hello!')
        ->attach('/path/to/file', [
            'as' => 'name.pdf',
            'mime' => 'application/pdf',
        ]);
}

必要时,可以使用 attachMany 方法为消息附加多个文件:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->greeting('Hello!')
        ->attachMany([
            '/path/to/forge.svg',
            '/path/to/vapor.svg' => [
                'as' => 'Logo.svg',
                'mime' => 'image/svg+xml',
            ],
        ]);
}

您可以使用 attachFromStorageDisk 方法来附加特定文件系统磁盘上存在的文件。该方法接受磁盘名称和该磁盘上文件的路径:

use App\Mail\InvoicePaid as InvoicePaidMailable;

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): Mailable
{
    return (new InvoicePaidMailable($this->invoice))
        ->to($notifiable->email)
        ->attachFromStorageDisk('s3', '/path/to/file', 'invoice.pdf', [
            'mime' => 'application/pdf',
        ]);
}

原始数据附件

可以使用 attachData 方法将原始字节字符串作为附件附加。调用 attachData 方法时,您应提供要分配给该附件的文件名:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->greeting('Hello!')
        ->attachData($this->pdf, 'name.pdf', [
            'mime' => 'application/pdf',
        ]);
}

添加标签和元数据

一些第三方电子邮件服务提供商(如 Mailgun 和 Postmark)支持消息“标签”和“元数据”,可用于对您的应用程序发送的邮件进行分组和跟踪。您可以通过 tag 和 metadata 方法为电子邮件消息添加标签和元数据:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->greeting('Comment Upvoted!')
        ->tag('upvote')
        ->metadata('comment_id', $this->comment->id);
}

如果您的应用程序使用的是 Mailgun 驱动程序,您可以查阅 Mailgun 的文档,以获取有关标签和元数据的更多信息。同样,您也可以查阅 Postmark 的文档,了解其对标签和元数据的支持情况。

如果您的应用程序使用 Amazon SES 发送电子邮件,您应该使用 metadata 方法将SES “标签”附加到消息中。

自定义 Symfony 消息

MailMessage 类的 withSymfonyMessage 方法允许您注册一个闭包,该闭包将在发送消息之前使用 Symfony Message 实例进行调用。这让您有机会在消息被投递之前对其进行深度自定义:

use Symfony\Component\Mime\Email;

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->withSymfonyMessage(function (Email $message) {
            $message->getHeaders()->addTextHeader(
                'Custom-Header', 'Header Value'
            );
        });
}

使用 Mailables

如果需要,您可以从通知的 toMail 方法中返回一个完整的mailable 对象。当返回 Mailable 而不是 MailMessage 时,您需要使用 mailable 对象的 to 方法指定消息接收者:

use App\Mail\InvoicePaid as InvoicePaidMailable;
use Illuminate\Mail\Mailable;

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): Mailable
{
    return (new InvoicePaidMailable($this->invoice))
        ->to($notifiable->email);
}

Mailables 与按需通知

如果您要发送按需通知,传递给 toMail 方法的 $notifiable 实例将是 Illuminate\Notifications\AnonymousNotifiable 的实例,该类提供了 routeNotificationFor 方法,可用于获取按需通知应发送到的电子邮件地址:

use App\Mail\InvoicePaid as InvoicePaidMailable;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Mail\Mailable;

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): Mailable
{
    $address = $notifiable instanceof AnonymousNotifiable
        ? $notifiable->routeNotificationFor('mail')
        : $notifiable->email;

    return (new InvoicePaidMailable($this->invoice))
        ->to($address);
}

预览邮件通知

在设计邮件通知模板时,像典型的 Blade 模板一样在浏览器中快速预览渲染后的邮件消息会非常方便。为此,Laravel 允许您直接从路由闭包或控制器中返回邮件通知生成的任何邮件消息。当返回 MailMessage 时,它将被渲染并显示在浏览器中,让您无需发送到实际电子邮件地址即可快速预览其设计:

use App\Models\Invoice;
use App\Notifications\InvoicePaid;

Route::get('/notification', function () {
    $invoice = Invoice::find(1);

    return (new InvoicePaid($invoice))
        ->toMail($invoice->user);
});

Markdown 邮件通知

Markdown 邮件通知让您可以利用邮件通知的预构建模板,同时让您更自由地编写更长的自定义消息。由于消息是用 Markdown 编写的,Laravel 能够为消息渲染美观、响应式的 HTML 模板,同时也会自动生成纯文本版本。

生成消息

要生成带有相应 Markdown 模板的通知,您可以使用 make:notification Artisan 命令的 --markdown 选项:

php artisan make:notification InvoicePaid --markdown=mail.invoice.paid

与所有其他邮件通知一样,使用 Markdown 模板的通知应在其通知类上定义 toMail 方法。不过,不是使用 line 和 action 方法来构建通知,而是使用 markdown 方法指定应使用的 Markdown 模板名称。您希望提供给模板的数据数组可作为该方法的第二个参数传入:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    $url = url('/invoice/'.$this->invoice->id);

    return (new MailMessage)
        ->subject('Invoice Paid')
        ->markdown('mail.invoice.paid', ['url' => $url]);
}

编写消息

Markdown 邮件通知结合了 Blade 组件和 Markdown 语法,让您在利用 Laravel 预先构建的通知组件的同时,轻松构建通知:

<x-mail::message>
# Invoice Paid

您的发票已支付!

<x-mail::button :url="$url">
查看发票
</x-mail::button>

感谢,<br>
{{ config('app.name') }}
</x-mail::message>

编写 Markdown 邮件时不要使用过多的缩进。根据 Markdown 标准,Markdown 解析器会将缩进的内容渲染为代码块。

按钮组件

按钮组件会渲染一个居中的按钮链接。该组件接受两个参数:url 和一个可选的 color。支持的颜色有 primary、green 和 red。您可以在通知中添加任意数量的按钮组件:

<x-mail::button :url="$url" color="green">
查看发票
</x-mail::button>

面板组件

面板组件会在具有与通知其余部分稍有不同的背景色的面板中渲染给定的文本块。这可以让您突出显示某个文本块:

<x-mail::panel>
这是面板内容。
</x-mail::panel>

表格组件

表格组件可将 Markdown 表格转换为 HTML 表格。该组件接受 Markdown 表格作为其内容。支持使用默认的 Markdown 表格对齐语法来设置表格列的对齐方式:

<x-mail::table>
| Laravel       | Table         | Example       |
| ------------- | :-----------: | ------------: |
| Col 2 is      | Centered      | $10           |
| Col 3 is      | Right-Aligned | $20           |
</x-mail::table>

自定义组件

您可以将所有 Markdown 通知组件导出到自己的应用程序中进行自定义。要导出组件,请使用 vendor:publish Artisan 命令发布 laravel-mail 资源标签:

php artisan vendor:publish --tag=laravel-mail

该命令会将 Markdown 邮件组件发布到 resources/views/vendor/mail 目录。mail 目录将包含 html 和 text 目录,每个目录都包含每个可用组件对应的表示形式。您可以随意自定义这些组件。

自定义 CSS

导出组件后,resources/views/vendor/mail/html/themes 目录将包含一个 default.css 文件。您可以自定义此文件中的 CSS,您的样式将自动内联到 Markdown 通知的 HTML 表示形式中。

如果您想为 Laravel 的 Markdown 组件构建全新的主题,可以将 CSS 文件放置在 html/themes 目录中。对 CSS 文件命名并保存后,将 mail 配置文件中的 theme 选项更新为您的新主题名称。

要为单个通知自定义主题,可以在构建通知的邮件消息时调用 theme 方法。theme 方法接受发送通知时应使用的主题名称:

/**
 * 获取通知的邮件表示形式。
 */
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->theme('invoice')
        ->subject('Invoice Paid')
        ->markdown('mail.invoice.paid', ['url' => $url]);
}

数据库通知

前提条件

database 通知通道将通知信息存储在数据库表中。该表将包含诸如通知类型以及描述通知的 JSON 数据结构等信息。

您可以查询该表,以便在应用程序的用户界面中显示通知。但是,在此之前,您需要创建一个数据库表来保存通知。您可以使用 make:notifications-table 命令生成具有正确表结构的迁移文件:

php artisan make:notifications-table

php artisan migrate

如果您的可通知模型使用的是UUID 或 ULID 主键,则在通知表迁移中,您应将 morphs 方法替换为 uuidMorphs 或 ulidMorphs。

格式化数据库通知

如果通知支持存储在数据库表中,你应在通知类中定义 toDatabase 或 toArray 方法。该方法将接收一个 $notifiable 实体,并应返回一个纯 PHP 数组。返回的数组会被编码为 JSON 并存储在 notifications 表的 data 列中。让我们来看一个 toArray 方法的示例:

/**
 * 获取通知的数组表示形式。
 *
 * @return array<string, mixed>
 */
public function toArray(object $notifiable): array
{
    return [
        'invoice_id' => $this->invoice->id,
        'amount' => $this->invoice->amount,
    ];
}

当通知存储在应用程序的数据库中时,type 列默认会设置为通知的类名,而 read_at 列会为 null。不过,你可以通过在通知类中定义 databaseType 和 initialDatabaseReadAtValue 方法来自定义此行为:

use Illuminate\Support\Carbon;

/**
 * 获取通知的数据库类型。
 */
public function databaseType(object $notifiable): string
{
    return 'invoice-paid';
}

/**
 * 获取 “read_at” 列的初始值。
 */
public function initialDatabaseReadAtValue(): ?Carbon
{
    return null;
}

toDatabase 与 toArray

toArray 方法也会被 broadcast 通道用于确定要向由 JavaScript 驱动的前端广播哪些数据。如果你想为 database 和 broadcast 通道设置两种不同的数组表示,应该定义 toDatabase 方法,而非 toArray 方法。

访问通知

通知一旦存储在数据库中,你就需要一种便捷的方式从可通知实体中访问它们。Laravel 默认的 App\Models\User 模型所包含的 Illuminate\Notifications\Notifiable 特性,带有一个 notifications Eloquent 关系,用于返回该实体的通知。要获取通知,你可以像访问其他 Eloquent 关系一样访问该方法。默认情况下,通知会按 created_at 时间戳排序,最新的通知排在集合的开头:

$user = App\Models\User::find(1);

foreach ($user->notifications as $notification) {
    echo $notification->type;
}

如果你想仅检索“未读”通知,可以使用 unreadNotifications 关系。同样,这些通知会按 created_at 时间戳排序,最新的通知排在集合的开头:

$user = App\Models\User::find(1);

foreach ($user->unreadNotifications as $notification) {
    echo $notification->type;
}

如果你想仅检索“已读”通知,可以使用 readNotifications 关系:

$user = App\Models\User::find(1);

foreach ($user->readNotifications as $notification) {
    echo $notification->type;
}

要从 JavaScript 客户端访问通知,应为应用程序定义一个通知控制器,该控制器返回可通知实体(如当前用户)的通知。之后,你可以从 JavaScript 客户端向该控制器的 URL 发起 HTTP 请求。

将通知标记为已读

通常,当用户查看通知时,你会希望将其标记为“已读”。Illuminate\Notifications\Notifiable 特性提供了一个 markAsRead 方法,该方法会更新通知数据库记录中的 read_at 列:

$user = App\Models\User::find(1);

foreach ($user->unreadNotifications as $notification) {
    $notification->markAsRead();
}

不过,你也可以不用逐一遍历每条通知,而是直接在通知集合上使用 markAsRead 方法:

$user->unreadNotifications->markAsRead();

你也可以使用批量更新查询,无需从数据库中检索通知,即可将所有通知标记为已读:

$user = App\Models\User::find(1);

$user->unreadNotifications()->update(['read_at' => now()]);

你可以 delete 通知,将其从表中彻底移除:

$user->notifications()->delete();

广播通知

前提条件

在广播通知之前,你应该配置并熟悉 Laravel 的事件广播服务。事件广播提供了一种方式,让由 JavaScript 驱动的前端能够对服务端的 Laravel 事件做出响应。

格式化广播通知

broadcast 通道使用 Laravel 的事件广播服务广播通知,使你的 JavaScript 前端能够实时捕获通知。如果通知支持广播,你可以在通知类中定义 toBroadcast 方法。该方法将接收一个 $notifiable 实体,并应返回一个 BroadcastMessage 实例。如果 toBroadcast 方法不存在,则会使用 toArray 方法来收集要广播的数据。返回的数据会被编码为 JSON 并广播到你的 JavaScript 前端。让我们来看一个 toBroadcast 方法的示例:

use Illuminate\Notifications\Messages\BroadcastMessage;

/**
 * 获取通知的可广播表示形式。
 */
public function toBroadcast(object $notifiable): BroadcastMessage
{
    return new BroadcastMessage([
        'invoice_id' => $this->invoice->id,
        'amount' => $this->invoice->amount,
    ]);
}

广播队列配置

所有广播通知都会被加入队列以进行广播。如果你想配置用于排队广播操作的队列连接或队列名称,可以使用 BroadcastMessage 的 onConnection 和 onQueue 方法:

return (new BroadcastMessage($data))
    ->onConnection('sqs')
    ->onQueue('broadcasts');

自定义通知类型

除了你指定的数据外,所有广播通知还都有一个 type 字段,其中包含通知的完整类名。如果你想自定义通知的 type,可以在通知类中定义一个 broadcastType 方法:

/**
 * 获取正在广播的通知的类型。
 */
public function broadcastType(): string
{
    return 'broadcast.message';
}

监听通知

通知会按照 {notifiable}.{id} 的约定格式在私有通道上进行广播。因此,如果你向 ID 为 1 的 App\Models\User 实例发送通知,该通知会在 App.Models.User.1 私有通道上广播。使用Laravel Echo 时,你可以使用 notification 方法轻松监听通道上的通知:

Echo.private('App.Models.User.' + userId)
    .notification((notification) => {
        console.log(notification.type);
    });

使用 React、Vue 或 Svelte

Laravel Echo 包含 React、Vue 和 Svelte 钩子,让监听通知变得轻而易举。开始时,调用 useEchoNotification 钩子,该钩子用于监听通知。当使用该钩子的组件卸载时,useEchoNotification 钩子会自动退出通道:

import { useEchoNotification } from "@laravel/echo-react";

useEchoNotification(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.type);
    },
);
<script setup lang="ts">
import { useEchoNotification } from "@laravel/echo-vue";

useEchoNotification(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.type);
    },
);
</script>
<script>
import { useEchoNotification } from "@laravel/echo-svelte";

useEchoNotification(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.type);
    },
);
</script>

默认情况下,该钩子会监听所有通知。若要指定想要监听的通知类型,你可以向 useEchoNotification 提供一个类型字符串或类型数组:

import { useEchoNotification } from "@laravel/echo-react";

useEchoNotification(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.type);
    },
    'App.Notifications.InvoicePaid',
);
<script setup lang="ts">
import { useEchoNotification } from "@laravel/echo-vue";

useEchoNotification(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.type);
    },
    'App.Notifications.InvoicePaid',
);
</script>
<script>
import { useEchoNotification } from "@laravel/echo-svelte";

useEchoNotification(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.type);
    },
    'App.Notifications.InvoicePaid',
);
</script>

您还可以指定通知负载数据的结构,从而提供更强的类型安全性和编辑便利性:

type InvoicePaidNotification = {
    invoice_id: number;
    created_at: string;
};

useEchoNotification<InvoicePaidNotification>(
    `App.Models.User.${userId}`,
    (notification) => {
        console.log(notification.invoice_id);
        console.log(notification.created_at);
        console.log(notification.type);
    },
    'App.Notifications.InvoicePaid',
);

自定义通知通道

如果您希望自定义实体的广播通知所使用的通道,可以在可通知实体上定义一个 receivesBroadcastNotificationsOn 方法:

<?php

namespace App\Models;

use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 用户接收通知广播的通道。
     */
    public function receivesBroadcastNotificationsOn(): string
    {
        return 'users.'.$this->id;
    }
}

短信通知

前提条件

Laravel 中的短信通知发送功能由 Vonage(前身为 Nexmo)提供支持。在能够通过 Vonage 发送通知之前,您需要安装 laravel/vonage-notification-channel 和 guzzlehttp/guzzle 软件包:

composer require laravel/vonage-notification-channel guzzlehttp/guzzle

该软件包包含一个 配置文件。不过,您不必将该配置文件导出到自己的应用程序中。您可以直接使用 VONAGE_KEY 和 VONAGE_SECRET 环境变量来定义您的 Vonage 公钥和密钥。

定义密钥后,您应设置一个 VONAGE_SMS_FROM 环境变量,用于指定默认情况下发送短信时使用的电话号码。您可以在 Vonage 控制面板中生成该电话号码:

VONAGE_SMS_FROM=15556666666

格式化短信通知

如果通知支持以短信形式发送,应在通知类中定义一个 toVonage 方法。该方法将接收一个 $notifiable 实体,并应返回一个 Illuminate\Notifications\Messages\VonageMessage 实例:

use Illuminate\Notifications\Messages\VonageMessage;

/**
 * 获取通知的 Vonage / 短信表示形式。
 */
public function toVonage(object $notifiable): VonageMessage
{
    return (new VonageMessage)
        ->content('Your SMS message content');
}

Unicode 内容

如果您的短信内容包含 Unicode 字符,在构建 VonageMessage 实例时应调用 unicode 方法:

use Illuminate\Notifications\Messages\VonageMessage;

/**
 * 获取通知的 Vonage / 短信表示形式。
 */
public function toVonage(object $notifiable): VonageMessage
{
    return (new VonageMessage)
        ->content('Your unicode message')
        ->unicode();
}

自定义“发件人”号码

如果您希望从与 VONAGE_SMS_FROM 环境变量指定的号码不同的电话号码发送某些通知,可以在 VonageMessage 实例上调用 from 方法:

use Illuminate\Notifications\Messages\VonageMessage;

/**
 * 获取通知的 Vonage / 短信表示形式。
 */
public function toVonage(object $notifiable): VonageMessage
{
    return (new VonageMessage)
        ->content('Your SMS message content')
        ->from('15554443333');
}

添加客户端引用

如果您希望按用户、团队或客户端跟踪费用,可以在通知中添加“客户端引用”。Vonage 允许您使用该客户端引用生成报告,以便更好地了解特定客户的短信使用情况。客户端引用可以是不超过 40 个字符的任意字符串:

use Illuminate\Notifications\Messages\VonageMessage;

/**
 * 获取通知的 Vonage / 短信表示形式。
 */
public function toVonage(object $notifiable): VonageMessage
{
    return (new VonageMessage)
        ->clientReference((string) $notifiable->id)
        ->content('Your SMS message content');
}

路由短信通知

要将 Vonage 通知路由到正确的电话号码,请在可通知实体上定义一个 routeNotificationForVonage 方法:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 为 Vonage 通道路由通知。
     */
    public function routeNotificationForVonage(Notification $notification): string
    {
        return $this->phone_number;
    }
}

Slack 通知

前提条件

在发送 Slack 通知之前,应通过 Composer 安装 Slack 通知通道:

composer require laravel/slack-notification-channel

此外,您必须为 Slack 工作区创建一个 Slack 应用。

如果您只需将通知发送到创建该应用的同一 Slack 工作区,请确保您的应用具有 chat:write、chat:write.public 和 chat:write.customize 权限范围。这些权限范围可在 Slack 中应用管理选项卡下的“OAuth & 权限”中添加。

接下来,复制应用的“机器人用户 OAuth 令牌”,并将其放入应用程序 services.php 配置文件中的 slack 配置数组内。该令牌可在 Slack 中的“OAuth & 权限”选项卡下找到:

'slack' => [
    'notifications' => [
        'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
        'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
    ],
],

应用分发

如果您的应用程序需要向应用程序用户所拥有的外部 Slack 工作区发送通知,您需要通过 Slack“分发”您的应用。应用分发可在 Slack 中应用的“管理分发”选项卡下进行管理。应用分发完成后,您可以使用 Socialite 来代表您的应用程序用户获取 Slack 机器人令牌。

格式化 Slack 通知

如果通知支持以 Slack 消息形式发送,应在通知类中定义一个 toSlack 方法。该方法将接收一个 $notifiable 实体,并应返回一个 Illuminate\Notifications\Slack\SlackMessage 实例。您可以使用 Slack 的 Block Kit API 构建丰富的通知。以下示例可在Slack 的 Block Kit 构建器中预览:

use Illuminate\Notifications\Slack\BlockKit\Blocks\ContextBlock;
use Illuminate\Notifications\Slack\BlockKit\Blocks\SectionBlock;
use Illuminate\Notifications\Slack\SlackMessage;

/**
 * 获取通知的 Slack 表示形式。
 */
public function toSlack(object $notifiable): SlackMessage
{
    return (new SlackMessage)
        ->text('您的其中一张发票已支付!')
        ->headerBlock('发票已支付')
        ->contextBlock(function (ContextBlock $block) {
            $block->text('客户 #1234');
        })
        ->sectionBlock(function (SectionBlock $block) {
            $block->text('一张发票已支付。');
            $block->field("*发票编号:*\n1000")->markdown();
            $block->field("*发票收件人:*\[email protected]")->markdown();
        })
        ->dividerBlock()
        ->sectionBlock(function (SectionBlock $block) {
            $block->text('恭喜!');
        });
}

使用 Slack 的 Block Kit Builder 模板

您可以不用使用流式消息构建方法来构建 Block Kit 消息,而是将 Slack 的 Block Kit Builder 生成的原始 JSON 载荷提供给 usingBlockKitTemplate 方法:

use Illuminate\Notifications\Slack\SlackMessage;
use Illuminate\Support\Str;

/**
 * 获取通知的 Slack 表示形式。
 */
public function toSlack(object $notifiable): SlackMessage
{
    $template = <<<JSON
        {
          "blocks": [
            {
              "type": "header",
              "text": {
                "type": "plain_text",
                "text": "团队公告"
              }
            },
            {
              "type": "section",
              "text": {
                "type": "plain_text",
                "text": "我们正在招聘!"
              }
            }
          ]
        }
    JSON;

    return (new SlackMessage)
        ->usingBlockKitTemplate($template);
}

Slack 交互性

Slack 的 Block Kit 通知系统提供了强大的功能来处理用户交互。要使用这些功能,您的 Slack 应用应启用“交互性”,并配置指向您的应用程序提供服务的 URL 的“请求 URL”。这些设置可以在 Slack 中的“交互性与快捷方式”应用管理选项卡中进行管理。

在下面使用 actionsBlock 方法的示例中,Slack 会向您的“请求 URL”发送一个 POST 请求,其中包含点击按钮的 Slack 用户、被点击按钮的 ID 等信息的载荷。您的应用程序可以根据该载荷确定要执行的操作。您还应该验证请求是由 Slack 发起的:

use Illuminate\Notifications\Slack\BlockKit\Blocks\ActionsBlock;
use Illuminate\Notifications\Slack\BlockKit\Blocks\ContextBlock;
use Illuminate\Notifications\Slack\BlockKit\Blocks\SectionBlock;
use Illuminate\Notifications\Slack\SlackMessage;

/**
 * 获取通知的 Slack 表示形式。
 */
public function toSlack(object $notifiable): SlackMessage
{
    return (new SlackMessage)
        ->text('您的其中一张发票已支付!')
        ->headerBlock('发票已支付')
        ->contextBlock(function (ContextBlock $block) {
            $block->text('客户 #1234');
        })
        ->sectionBlock(function (SectionBlock $block) {
            $block->text('一张发票已支付。');
        })
        ->actionsBlock(function (ActionsBlock $block) {
             // ID 默认为 "button_acknowledge_invoice"...
            $block->button('确认发票')->primary();

            // 手动配置 ID...
            $block->button('拒绝')->danger()->id('deny_invoice');
        });
}

确认模态框

如果您希望用户在执行操作之前必须确认,在定义按钮时可以调用 confirm 方法。confirm 方法接受一条消息和一个接收 ConfirmObject 实例的闭包:

use Illuminate\Notifications\Slack\BlockKit\Blocks\ActionsBlock;
use Illuminate\Notifications\Slack\BlockKit\Blocks\ContextBlock;
use Illuminate\Notifications\Slack\BlockKit\Blocks\SectionBlock;
use Illuminate\Notifications\Slack\BlockKit\Composites\ConfirmObject;
use Illuminate\Notifications\Slack\SlackMessage;

/**
 * 获取通知的 Slack 表示形式。
 */
public function toSlack(object $notifiable): SlackMessage
{
    return (new SlackMessage)
        ->text('您的其中一张发票已支付!')
        ->headerBlock('发票已支付')
        ->contextBlock(function (ContextBlock $block) {
            $block->text('客户 #1234');
        })
        ->sectionBlock(function (SectionBlock $block) {
            $block->text('一张发票已支付。');
        })
        ->actionsBlock(function (ActionsBlock $block) {
            $block->button('确认发票')
                ->primary()
                ->confirm(
                    '确认付款并发送感谢邮件?',
                    function (ConfirmObject $dialog) {
                        $dialog->confirm('是');
                        $dialog->deny('否');
                    }
                );
        });
}

检查 Slack 区块

如果您想快速检查正在构建的区块,可以对 SlackMessage 实例调用 dd 方法。dd 方法将生成并输出一个指向 Slack 的 Block Kit Builder 的 URL,它会在您的浏览器中显示载荷和通知的预览。您可以将 true 传递给 dd 方法以输出原始载荷:

return (new SlackMessage)
    ->text('您的其中一张发票已支付!')
    ->headerBlock('发票已支付')
    ->dd();

路由 Slack 通知

要将 Slack 通知导向合适的 Slack 团队和频道,请在可通知模型上定义 routeNotificationForSlack 方法。该方法可以返回以下三种值之一:

  • null - 将路由推迟到通知本身中配置的频道。您可以在构建 SlackMessage 时使用 to 方法来在通知中配置频道。
  • 指定发送通知的 Slack 频道的字符串,例如 #support-channel。
  • SlackRoute 实例,允许您指定 OAuth 令牌和频道名称,例如 SlackRoute::make($this->slack_channel, $this->slack_token)。该方法应用于向外部工作区发送通知。

例如,从 routeNotificationForSlack 方法返回 #support-channel 会将通知发送到应用程序的 services.php 配置文件中 Bot User OAuth 令牌所关联工作区的 #support-channel 频道:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 为 Slack 频道路由通知。
     */
    public function routeNotificationForSlack(Notification $notification): mixed
    {
        return '#support-channel';
    }
}

通知外部 Slack 工作区

在向外部 Slack 工作区发送通知之前,您的 Slack 应用必须已发布。

当然,您通常希望向应用程序用户所拥有的 Slack 工作区发送通知。为此,您首先需要为用户获取一个 Slack OAuth 令牌。幸运的是,Laravel Socialite 包含一个 Slack 驱动程序,让您可以轻松地让应用程序用户通过 Slack 进行身份验证,并获取机器人令牌。

一旦您获得了机器人令牌并将其存储在应用程序的数据库中,就可以使用 SlackRoute::make 方法将通知路由到用户的工作区。此外,您的应用程序可能需要为用户提供指定应发送通知的频道的机会:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Slack\SlackRoute;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 为 Slack 频道路由通知。
     */
    public function routeNotificationForSlack(Notification $notification): mixed
    {
        return SlackRoute::make($this->slack_channel, $this->slack_token);
    }
}

本地化通知

Laravel 允许您使用 HTTP 请求当前语言环境以外的语言环境发送通知,如果通知被排队,甚至会记住这个语言环境。

为此,Illuminate\Notifications\Notification 类提供了 locale 方法,用于设置所需语言。在评估通知时,应用程序会切换到该语言环境,评估完成后再恢复到之前的语言环境:

$user->notify((new InvoicePaid($invoice))->locale('es'));

也可以通过 Notification 门面实现多个可通知条目的本地化:

Notification::locale('es')->send(
    $users, new InvoicePaid($invoice)
);

用户首选语言环境

有时,应用程序会存储每个用户的首选语言环境。通过在可通知模型上实现 HasLocalePreference 契约,你可以指示 Laravel 在发送通知时使用此存储的语言环境:

use Illuminate\Contracts\Translation\HasLocalePreference;

class User extends Model implements HasLocalePreference
{
    /**
     * 获取用户的首选语言环境。
     */
    public function preferredLocale(): string
    {
        return $this->locale;
    }
}

实现该接口后,Laravel 在向模型发送通知和邮件时会自动使用首选语言环境。因此,使用该接口时无需调用 locale 方法:

$user->notify(new InvoicePaid($invoice));

测试

你可以使用 Notification 门面的 fake 方法来阻止通知被发送。通常情况下,发送通知与你实际正在测试的代码无关。大多数情况下,只需断言 Laravel 被指示发送了某条通知就足够了。

调用 Notification 门面的 fake 方法后,你可以断言已指示向用户发送通知,甚至检查通知接收到的数据:

<?php

use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;

test('orders can be shipped', function () {
    Notification::fake();

    // 执行订单发货...

    // 断言未发送任何通知...
    Notification::assertNothingSent();

    // 断言已向指定用户发送通知...
    Notification::assertSentTo(
        [$user], OrderShipped::class
    );

    // 断言未发送通知...
    Notification::assertNotSentTo(
        [$user], AnotherNotification::class
    );

    // 断言通知已发送两次...
    Notification::assertSentTimes(WeeklyReminder::class, 2);

    // 断言已向用户发送通知,且仅发送一次...
    Notification::assertSentToOnce($user, OrderShipped::class);

    // 断言已发送指定数量的通知...
    Notification::assertCount(3);
});
<?php

namespace Tests\Feature;

use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_orders_can_be_shipped(): void
    {
        Notification::fake();

        // 执行订单发货...

        // 断言未发送任何通知...
        Notification::assertNothingSent();

        // 断言已向指定用户发送通知...
        Notification::assertSentTo(
            [$user], OrderShipped::class
        );

        // 断言未发送通知...
        Notification::assertNotSentTo(
            [$user], AnotherNotification::class
        );

        // 断言通知已发送两次...
        Notification::assertSentTimes(WeeklyReminder::class, 2);

        // 断言已向用户发送通知,且仅发送一次...
        Notification::assertSentToOnce($user, OrderShipped::class);

        // 断言已发送指定数量的通知...
        Notification::assertCount(3);
    }
}

你可以向 assertSentTo 或 assertNotSentTo 方法传入一个闭包,以此断言发送的通知通过了指定的“真实性测试”。如果至少有一条发送的通知通过了给定的真实性测试,则断言成功:

Notification::assertSentTo(
    $user,
    function (OrderShipped $notification, array $channels) use ($order) {
        return $notification->order->id === $order->id;
    }
);

另外,你可以将一个预期属性值数组作为第三个参数传递给 assertSentTo 或 assertNotSentTo:

Notification::assertSentTo($user, OrderShipped::class, ['order' => $order]);

所有指定的属性都必须匹配。值采用严格相等进行比较,而 Eloquent 模型则通过其 is 方法进行比较。你也可以将预期属性值数组作为第二个参数传递给 assertSentOnDemand。

按需通知

如果你正在测试的代码会发送按需通知,可以通过 assertSentOnDemand 方法测试该按需通知是否已发送:

Notification::assertSentOnDemand(OrderShipped::class);
Notification::assertSentOnDemandOnce(OrderShipped::class);

通过将闭包作为第二个参数传递给 assertSentOnDemand 方法,你可以确定按需通知是否发送到了正确的“路由”地址:

Notification::assertSentOnDemand(
    OrderShipped::class,
    function (OrderShipped $notification, array $channels, object $notifiable) use ($user) {
        return $notifiable->routes['mail'] === $user->email;
    }
);

通知事件

通知发送事件

当通知正在发送时,通知系统会触发 Illuminate\Notifications\Events\NotificationSending 事件。该事件包含“可通知”实体和通知实例本身。你可以在应用程序中为此事件创建事件监听器:

use Illuminate\Notifications\Events\NotificationSending;

class CheckNotificationStatus
{
    /**
     * 处理事件。
     */
    public function handle(NotificationSending $event): void
    {
        // ...
    }
}

如果 NotificationSending 事件的事件监听器在其 handle 方法中返回 false,则通知将不会发送:

/**
 * 处理事件。
 */
public function handle(NotificationSending $event): bool
{
    return false;
}

在事件监听器中,你可以访问事件上的 notifiable、notification 和 channel 属性,以了解更多关于通知收件人或通知本身的信息:

/**
 * 处理事件。
 */
public function handle(NotificationSending $event): void
{
    // $event->channel
    // $event->notifiable
    // $event->notification
}

通知已发送事件

当通知已发送时,通知系统会触发 Illuminate\Notifications\Events\NotificationSent 事件。该事件包含“可通知”实体和通知实例本身。你可以在应用程序中为此事件创建事件监听器:

use Illuminate\Notifications\Events\NotificationSent;

class LogNotification
{
    /**
     * 处理事件。
     */
    public function handle(NotificationSent $event): void
    {
        // ...
    }
}

在事件监听器中,你可以访问事件上的 notifiable、notification、channel 和 response 属性,以了解更多关于通知收件人或通知本身的信息:

/**
 * 处理事件。
 */
public function handle(NotificationSent $event): void
{
    // $event->channel
    // $event->notifiable
    // $event->notification
    // $event->response
}

自定义通道

Laravel 自带了一些通知通道,但你可能希望编写自己的驱动,通过其他通道投递通知。Laravel 让这一点变得很简单。首先,定义一个包含 send 方法的类。该方法应接收两个参数:$notifiable 和 $notification。

在 send 方法中,你可以调用通知上的方法,以获取你的通道可理解的消息对象,然后以任何你想要的方式将通知发送给 $notifiable 实例:

<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;

class VoiceChannel
{
    /**
     * 发送给定通知。
     */
    public function send(object $notifiable, Notification $notification): void
    {
        $message = $notification->toVoice($notifiable);

        // 向 $notifiable 实例发送通知...
    }
}

定义好通知渠道类后,你可以在任意通知的 via 方法中返回该类名。在本示例中,通知的 toVoice 方法可以返回任意你用于表示语音消息的对象。例如,你可以自定义一个 VoiceMessage 类来表示这些消息:

<?php

namespace App\Notifications;

use App\Notifications\Messages\VoiceMessage;
use App\Notifications\VoiceChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification
{
    use Queueable;

    /**
     * 获取通知渠道。
     */
    public function via(object $notifiable): string
    {
        return VoiceChannel::class;
    }

    /**
     * 获取通知的语音表示。
     */
    public function toVoice(object $notifiable): VoiceMessage
    {
        // ...
    }
}