Laravel 消息通知源码阅读笔记
前言
为理解消息通知功能,特别是toDatabase
和toMail
方法,为什么要那样写?通读了相关源码。
来源
L02_6.3 《消息通知》
问题
在创建了一个通知类\App\Notifications\TopicReplied
后,代码填充如下:
<?php
namespace App\Notifications;
use App\Models\Reply;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Log;
//class TopicReplied extends Notification implements ShouldQueue
class TopicReplied extends Notification
{
use Queueable;
public $reply;
public function __construct(Reply $reply)
{
$this->reply = $reply;
}
public function via($notifiable)
{
// 开启通知的频道
return ['database', 'mail'];
}
public function toDatabase($notifiable)
{
$topic = $this->reply->topic;
$link = $topic->link(['#reply' . $this->reply->id]);
// 存入数据库里的数据
return [
'reply_id' => $this->reply->id,
'reply_content' => $this->reply->content,
'user_id' => $this->reply->user->id,
'user_name' => $this->reply->user->name,
'user_avatar' => $this->reply->user->avatar,
'topic_link' => $link,
'topic_id' => $topic->id,
'topic_title' => $topic->title,
];
}
public function toMail($notifiable)
{
$url = $this->reply->topic->link(['#reply' . $this->reply->id]);
return (new MailMessage)
->line('尊敬的'.$notifiable->name.',您的话题有新回复!')
->action('查看回复', $url);
}
}
现在的问题是:为什么这样写了之后,就可以通过$user->notify(new TopicReplied($reply))
这样的代码发送通知了?
下面将简单的分析一下源码。
具体代码分析
背景介绍
首先,是在创建话题的回复后,就发送通知。这个使用的是ReplyObserver的created事件来监控的。代码如下:
public function created(Reply $reply)
{
$reply->topic->increment('reply_count', 1);
// 通知作者话题被回复了
$reply->topic->user->topicNotify(new TopicReplied($reply));
}
user模型中的topicNotify代码:
public function topicNotify($instance)
{
// 如果要通知的人是当前用户,就不必通知了!
if ($this->id == Auth::id()) {
return;
}
$this->increment('notification_count');
$this->notify($instance);
}
解析出ChannelManager
从$this->notify($instance)
开始,由于user模型使用了Notifiable这个trait,而Notifiable又使用了RoutesNotifications这个trait,因此,调用的是其中的notify:
public function notify($instance)
{
app(Dispatcher::class)->send($this, $instance);
}
这里的app(Dispatcher::class)
解析出ChannelManager。
在\Illuminate\Notifications\RoutesNotifications::notify
中,有这么一句:
app(Dispatcher::class)->send($this, $instance);
//Illuminate\Contracts\Notifications\Dispatcher
因为在:\Illuminate\Notifications\NotificationServiceProvider::register
中有如下代码:
public function register()
{
//注册`\Illuminate\Notifications\ChannelManager`
$this->app->singleton(ChannelManager::class, function ($app) {
return new ChannelManager($app);
});
//对`\Illuminate\Notifications\ChannelManager`起别名为`Illuminate\Contracts\Notifications\Dispatcher`
$this->app->alias(
ChannelManager::class, DispatcherContract::class
);
...
}
所以,这里的app(Dispatcher::class)
解析出来是一个\Illuminate\Notifications\ChannelManager
对象。
在解析这个ChannelManager
对象时,有朋友指出,可以使用绑定接口到实现的功能来解析。也就是说为什么不直接使用这个功能去解析,反而要绕个圈子去起别名,然后再去绑定单例?
的确,在bootstrap/app.php
中,我们就可以看到如下绑定接口到实现
的例子:
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
于是,我修改了框架源码,\Illuminate\Notifications\NotificationServiceProvider::register
:
public function register()
{
// $this->app->singleton(ChannelManager::class, function ($app) {
// return new ChannelManager($app);
// });
//TODO: just for testing!
$this->app->singleton(DispatcherContract::class, ChannelManager::class);
$this->app->alias(
ChannelManager::class, DispatcherContract::class
);
...
}
然后运行,回复&发送通知,此时,会提示:"Unresolvable dependency resolving [Parameter #0 [ <required> $app ]] in class Illuminate\\Support\\Manager"
进入Illuminate\\Support\\Manager
:
public function __construct($app)
{
$this->app = $app;
}
发现其构造函数并没有声明参数的类型,因此,在使用反射
解析ChannelManager
对象时,无法根据参数类型使用依赖注入,所以就无法解析依赖关系了。
而在源码自己的单例绑定中,是将此实现类绑定到了一个回调函数上,
$this->app->singleton(ChannelManager::class, function ($app) {
return new ChannelManager($app);
});
在解析ChannelManager::class
字符串时,会去运行这个回调函数,并自动传入app
对象。
\Illuminate\Container\Container::build
:
public function build($concrete)
{
如果绑定的是闭包,那么这里默认都会传入`app`对象
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
...
这样,ChannelManager的构造函数中就可以传入app
对象了,也就用不着使用反射
再去推敲构造函数中的参数类型了。
回到bootstrap/app.php
中:
App\Http\Kernel
和App\Console\Kernel
都是\Illuminate\Foundation\Http\Kernel
的子类,都看\Illuminate\Foundation\Http\Kernel
的构造函数:
public function __construct(Application $app, Router $router)
{
...
}
可以看到,都是声明了参数类型的。
App\Exceptions\Handler
的基类为\Illuminate\Foundation\Exceptions\Handler
,其构造函数:
public function __construct(Container $container)
{
$this->container = $container;
}
也是声明了参数类型的。
所以,这二者差别不大,区别在于实现类的构造函数是否需要参数以及是否使用了类型依赖注入。
ChannelManner来发送消息
ChannelManner来发送消息实际上是使用了\Illuminate\Notifications\NotificationSender
对象的send方法。
public function send($notifiables, $notification)
{
return (new NotificationSender(
$this, $this->app->make(Bus::class), $this->app->make(Dispatcher::class), $this->locale)
)->send($notifiables, $notification);
}
实例化IlluminateNotificationSender
对象
传入几个参数:
\Illuminate\Bus\Dispatcher
对象\Illuminate\Events\Dispatcher
对象\Illuminate\Notifications\ChannelManager
对象
调用IlluminateNotificationSender
对象的send方法
public function send($notifiables, $notification)
{
//Format the notifiables into a Collection / array if necessary.
$notifiables = $this->formatNotifiables($notifiables);
if ($notification instanceof ShouldQueue) {
return $this->queueNotification($notifiables, $notification);
}
return $this->sendNow($notifiables, $notification);
}
$notifiables = $this->formatNotifiables($notifiables);
将待通知的实体转为集合。
由于$notification
没有实现ShouldQueue接口,就直接到了$this->sendNow($notifiables, $notification)
。
调用IlluminateNotificationSender
对象的sendNow方法
public function sendNow($notifiables, $notification, array $channels = null)
{
$notifiables = $this->formatNotifiables($notifiables);
$original = clone $notification;
foreach ($notifiables as $notifiable) {
if (empty($viaChannels = $channels ?: $notification->via($notifiable))) {
continue;
}
$this->withLocale($this->preferredLocale($notifiable, $notification), function () use ($viaChannels, $notifiable, $original) {
$notificationId = Str::uuid()->toString();
foreach ((array) $viaChannels as $channel) {
$this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
}
});
}
}
$notification->via($notifiable)
这里的$notification
就是我们要发送的通知对象\App\Notifications\TopicReplied
,因此调用的代码如下。
public function via($notifiable)
{
// 开启通知的频道
return ['database', 'mail'];
}
很明显,返回一个频道数组供后面来遍历处理。
foreach ((array) $viaChannels as $channel) {
$this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
}
$this->sendToNotifiable(...)
这个就是发送通知到目的地了。
protected function sendToNotifiable($notifiable, $id, $notification, $channel)
{
if (! $notification->id) {
$notification->id = $id;
}
if (! $this->shouldSendNotification($notifiable, $notification, $channel)) {
return;
}
$response = $this->manager->driver($channel)->send($notifiable, $notification);
$this->events->dispatch(
new Events\NotificationSent($notifiable, $notification, $channel, $response)
);
}
$this->manager->driver($channel)->send($notifiable, $notification);
$this->manager
就是channelManager,调用其driver方法,在其中获取或者创建一个driver:
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
'Unable to resolve NULL driver for [%s].', static::class
));
}
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
DatabaseChannel
该driver方法返回的是一个channel,比如DatabaseChannel
public function send($notifiable, Notification $notification)
{
return $notifiable->routeNotificationFor('database', $notification)->create(
$this->buildPayload($notifiable, $notification)
);
}
这里的$notifiable->routeNotificationFor('database', $notification)
返回的是一个MorphMany
(多态一对多)的关系。
$this->buildPayload($notifiable, $notification)
返回的是一个数组:
protected function buildPayload($notifiable, Notification $notification)
{
return [
'id' => $notification->id,
'type' => get_class($notification),
'data' => $this->getData($notifiable, $notification),
'read_at' => null,
];
}
这里的$this->getData
代码如下:
protected function getData($notifiable, Notification $notification)
{
if (method_exists($notification, 'toDatabase')) {
return is_array($data = $notification->toDatabase($notifiable))
? $data : $data->data;
}
if (method_exists($notification, 'toArray')) {
return $notification->toArray($notifiable);
}
throw new RuntimeException('Notification is missing toDatabase / toArray method.');
}
可以看到,这里就是去调用$notification->toDatabase($notifiable)
方法!也就是:
public function toDatabase($notifiable)
{
//Log::info($notifiable);
$topic = $this->reply->topic;
$link = $topic->link(['#reply' . $this->reply->id]);
// 存入数据库里的数据
return [
'reply_id' => $this->reply->id,
'reply_content' => $this->reply->content,
'user_id' => $this->reply->user->id,
'user_name' => $this->reply->user->name,
'user_avatar' => $this->reply->user->avatar,
'topic_link' => $link,
'topic_id' => $topic->id,
'topic_title' => $topic->title,
];
}
由于MorphMany
对象没有create方法,因此会去调用其父类的方法,在\Illuminate\Database\Eloquent\Relations\HasOneOrMany::create
:
public function create(array $attributes = [])
{
return tap($this->related->newInstance($attributes), function ($instance) {
$this->setForeignAttributesForCreate($instance);
$instance->save();
});
}
这里的$this->related
是\Illuminate\Notifications\DatabaseNotification
,因此,$this->related->newInstance($attributes)
:
public function newInstance($attributes = [], $exists = false)
{
// This method just provides a convenient way for us to generate fresh model
// instances of this current model. It is particularly useful during the
// hydration of new objects via the Eloquent query builder instances.
$model = new static((array) $attributes);
$model->exists = $exists;
$model->setConnection(
$this->getConnectionName()
);
return $model;
}
这里的$attributes
就是我们前面toDatabase
方法返回的数据:
array (
'id' => 'b61035ff-339c-4017-bf10-315bfe302f10',
'type' => 'App\\Notifications\\TopicReplied',
'data' =>
array (
'reply_id' => 1039,
'reply_content' => '<p>有看看</p>',
'user_id' => 8,
'user_name' => '马娟',
'user_avatar' => 'https://fsdhubcdn.phphub.org/uploads/images/201710/14/1/LOnMrqbHJn.png?imageView2/1/w/200/h/200',
'topic_link' => 'http://olarabbs.test/topics/68?#reply1039',
'topic_id' => 68,
'topic_title' => 'Nisi blanditiis et ut delectus distinctio.',
),
'read_at' => NULL,
)
new static((array) $attributes)
就是创建一个新的model,返回后,在\Illuminate\Database\Eloquent\Relations\HasOneOrMany::create
中,
$this->setForeignAttributesForCreate($instance);
这个是设置外键属性。
$instance->save()
这样,就把数据写到默认的notifications
表中了!
最后数据库notifications
表如下图:
MailChannel
如果channel是mail,那么代码稍有不同:$response = $this->manager->driver($channel)->send($notifiable, $notification);
这一句,使用的driver就是mail关键字去创建的MailChannel
了。\Illuminate\Notifications\Channels\MailChannel::send
:
public function send($notifiable, Notification $notification)
{
$message = $notification->toMail($notifiable);
if (! $notifiable->routeNotificationFor('mail', $notification) &&
! $message instanceof Mailable) {
return;
}
if ($message instanceof Mailable) {
return $message->send($this->mailer);
}
$this->mailer->send(
$this->buildView($message),
array_merge($message->data(), $this->additionalMessageData($notification)),
$this->messageBuilder($notifiable, $notification, $message)
);
}
$message = $notification->toMail($notifiable);
返回一个message对象:
public function toMail($notifiable)
{
$url = $this->reply->topic->link(['#reply' . $this->reply->id]);
return (new MailMessage)
->line('尊敬的'.$notifiable->name.',您的话题有新回复!')
->action('查看回复', $url);
}
$this->mailer->send
进行邮件的发送!
小结
- 在创建的消息类中,如果实现了
ShouldQueue
接口,那么将会把此消息放入队列中,不在本文考虑范围内。 - 如果要研究队列,则.env文件中的
QUEUE_CONNECTION
不要选择redis
,而选择sync
,否则异步起来执行也无法打断点,我就是在这里郁闷了好久。。。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: