laravel 消息通知源码阅读笔记 | laravel china 社区-金年会app官方网
为理解消息通知功能,特别是todatabase
和tomail
方法,为什么要那样写?通读了相关源码。
l02_6.3 《消息通知》
在创建了一个通知类\app\notifications\topicreplied
后,代码填充如下:
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 [
进入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' => '有看看
',
'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 协议》,转载必须注明作者和本文链接
写得很仔细,我看完明白个大概也大约要1.5小时。
从
dispatcher::class
解析到channelmanager::class
,我想可能会用到laravel的,因为dispatcher::class
是一个接口。从实际上来看,是用类的别名来映射。对于异步调试,我一般是加return $data;返回数据,在谷歌浏览器审查元素里面看network对应请求的返回值,不知道这里队列的异步跟ajax是不是同一个概念。
laravel的实现过程太复杂了,看哪天能不能根据思路写个简单版的。
原文里面的异步和ajax不是一个东西。
直接使用绑定接口到实现可能不行,因为manager的构造函数中已经写死了传入参数为
$app
,而不是appliction $app
。我来文章中已经详细分析了一下,你可以看一下。ajax异步调试我也是可以打断点的。队列的异步可能不太一样,因为队列的流程一般都是:
有一个地方存放队列信息,一个php进程在运行时将任务写入,另外一个php守护进程轮询队列信息,将达到执行要求的任务执行并删除
。如果使用redis队列来debug时,经过一番debug,最后到了
\predis\connection\streamconnection::write
$written = @fwrite($socket, $buffer);
执行完这句后,数据就已经写到数据库的notifications
表了,而我关心的todatabase
方法根本就没有执行到。这就奇怪了。这里的
$socket
是resource
,而$buffer
是一个字符串:具体这个
$buffer
字符串在上面这个$socket
资源中进行了什么处理,就是看不到了。。所以我只能暂时理解为异步执行了。。问一下。你看源码有没有使用什么助手工具。
就是要打断点然后去步进
sublime text中,鼠标放到函数或者类名上,会出现一个列表,可以选择跳转到可能是其所在定义的地方。这样找文件比较方便。
@hustnzj 感谢你的辛苦研究!
谢谢认可😃
关键有时候看到一半 phpstorm 跟踪不下去了。比如源码里的事件对应的监听器,我找老长时间都没找到。还有那些从 容器里直接取出的,也不能跟踪,脑袋疼。不过还好有手册查查。
都可以找到的,listeners在
\illuminate\events\dispatcher
对象中有你知道这几个监听器在哪吗?
监听器要注册了才有,比如register
你的意思是这是框架留给我们自己写监听器的?
对啊,不过框架自己默认也有。一般以illuminate开头的就是。你可以参考文档:
(:з」∠)
dd