说明

Laravel 内置了发送邮件重置密码的功能,本文分析其发送请求重置密码邮件的功能,了解其执行流程。首先,假设我们已经有一个大概长这样的User模型:

.
.
.
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
    use Notifiable;
	.
	.
	.
}

一个必要特征是继承Illuminate\Foundation\Auth\User类,并且引入Notifiable这个 trait。

流程分析

从路由:

Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email')

定位到控制器:app\Http\Controllers\Auth\ForgotPasswordController.php,发起重置密码请求的操作方法在其引入的SendsPasswordResetEmails trait 中,具体代码:

public function sendResetLinkEmail(Request $request)
{
    //验证输入的参数
	$this->validateEmail($request);
	
	$response = $this->broker()->sendResetLink(
		$this->credentials($request)
	);

	return $response == Password::RESET_LINK_SENT
				? $this->sendResetLinkResponse($request, $response)
				: $this->sendResetLinkFailedResponse($request, $response);
}

主要的逻辑都在中间的一句。

$this->broker() 分析

首先,逻辑从$this->broker()开始。broker方法:

public function broker()
{
    // Password 是 Illuminate\Support\Facades\Password.php类
	// 该类是Facade类
	// dd(get_class(resolve('auth.password')))打印出其对应的实现类是:
	// Illuminate\Auth\Passwords\PasswordBrokerManager
	return Password::broker();
}

真正的实现在 Illuminate\Auth\Passwords\PasswordBrokerManager类的broker方法:

public function broker($name = null)
{
	$name = $name ?: $this->getDefaultDriver();
	return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name));
}

由于前面的$name没有传值,所以会先执行$this->getDefaultDriver()获取$name:

public function getDefaultDriver()
{
	// app['config']['auth.defaults.passwords'] == 'user'
	return $this->app['config']['auth.defaults.passwords'];
}

因此默认情况下,$name的值为users。 接下来,由于$this->brokers[$name]还没有值,所以调用后面的resolve方法,其代码如下:

protected function resolve($name)
{
	// A
	$config = $this->getConfig($name);

	if (is_null($config)) {
		throw new InvalidArgumentException("Password resetter [{$name}] is not defined.");
	}
	// B
	return new PasswordBroker(
		$this->createTokenRepository($config),
		$this->app['auth']->createUserProvider($config['provider'] ?? null)
	);
}

该方法主要做个两个操作,其一是获取配置:

protected function getConfig($name)
{
	return $this->app['config']["auth.passwords.{$name}"];
}

可以看出,得到的配置是 auth.php文件中,auth.passwords.users 键的值(各值的作用见注释):

 [
		'provider' => 'users',  // 数据提供者
		'table' => 'password_resets', //保存token的表
		'expire' => 60, //token过期时间
]

其二是实例化一个Illuminate\Auth\Passwords\PasswordBroker类的实例。注意到实例化的时候传入的两个参数,这两个参数分别是:

  • token 仓库类

    打印一下第一个参数,$this->createTokenRepository($config) 的值,得到: 源码分析:重置密码发送邮件原理 其中,$table 属性的值为password_resets,即存放token的数据表名称,该值来自我们的 auth.php 配置文件,因此,我们可以根据实际需要修改存放token的表名,同理,也可以配置token的过期时间。

  • 数据提供类

    第二个参数,$this->app['auth']->createUserProvider($config['provider'] ?? null),默认情况下是一个EloquentUserProvider类:

    源码分析:重置密码发送邮件原理 语句中的 createUserProvider 方法位于 AuthManager 类引入的CreatesUserProviders trait 中,其主要逻辑是:读取auth.php文件中的provider.users的值,然后根据获取到的驱动去创建驱动实例,一般有database和eloquent驱动,默认是使用eloquent驱动。

最终 $this->broker() 得到的值为:

源码分析:重置密码发送邮件原理 一个PasswordBroker类的实例。

得到 PasswordBroker 类的实例后,程序接着调用其旗下的 sendResetLink 方法:

public function sendResetLink(array $credentials)
{
	# A
	$user = $this->getUser($credentials);
	if (is_null($user)) {
		return static::INVALID_USER;
	}
	# B
	$user->sendPasswordResetNotification(
		$this->tokens->create($user)
	);
	return static::RESET_LINK_SENT;
}

A)根据传入的参数$credentials查找对应用户并创建模型

再看一下开头的代码片段:

$response = $this->broker()->sendResetLink(
	$this->credentials($request)
);

sendResetLink 传入的参数值为:$this->credentials($request),其credentials方法的返回值为:$request->only(’email’), 然后该值传递给getUser方法,从而获得 user 模型。由此可知,默认是使用邮箱查找用户。如果我们想要使用的是用户名查找,可以将$this->credentials($request)替换为$request->only('username')

B)sendPasswordResetNotification 方法

我们的邮件是如何发出去的,都要在这里展开来分析。首先,先来分析传入的参数。它接收的参数为$this->tokens->create($user)$this->tokens为前面获取到的token仓库类DatabaseTokenRepository,该类旗下的 create 方法:

public function create(CanResetPasswordContract $user)
{
    # 获取用户的email
	$email = $user->getEmailForPasswordReset();
	# 删除password_resets表中的对应记录
	$this->deleteExisting($user);
	# 创建新的token:hash_hmac('sha256', Str::random(40), $this->hashKey)
	# 传入的 $this->hashKey 是来自 .env 文件的 APP_KEY
	$token = $this->createNewToken();
	# 将token数据保存到password_resets表
	$this->getTable()->insert($this->getPayload($email, $token));
	# 最后将创建的token返回
	return $token;
}

由此以上代码可知,在获得token的过程中,程序还带做了另外几件事:1. 删除password_resets表中的对应记录(如果有的话);2.创建token并保存到password_resets表。

得到 token 后,我们就可以着手分析sendPasswordResetNotification 方法了。该方法位于Illuminate\Foundation\Auth\User类引入的CanResetPassword trait 中(从最开头的代码片段可以看出,User模型继承了Illuminate\Foundation\Auth\User类,所以拥有该方法的),该方法具体实现:

public function sendPasswordResetNotification($token)
{
	$this->notify(new ResetPasswordNotification($token));
}

首先,我们先看传入的参数,是一个ResetPassword类的实例,该类继承了Notification类,打印下传入的参数,是这样子的:

Laravel 重置密码发送邮件分析

接着,我们来分析notify方法,它位于我们创建的User模型引入的Notifiable trait 中,而实际又是在Notifiable trait 引入的RoutesNotifications trait 中:

public function notify($instance)
{
	# Dispatcher::class 对应 Illuminate\Contracts\Notifications\Dispatcher接口
	app(Dispatcher::class)->send($this, $instance);
}

Dispatcher::class 只是一个接口类,那么它的具体实现是哪个类呢?由Illuminate\Notifications\RoutesNotifications 类定位到其所在的文件夹Notifications,在这个文件夹中,有一个服务提供者NotificationServiceProvider,正是在这里定义了Dispatcher::class由哪个类来实现,定义代码如下:

public function register()
{
	//B ChannelManager::class的实现绑定为 ChannelManager 类的实例
	$this->app->singleton(ChannelManager::class, function ($app) {
		return new ChannelManager($app);
	});
	// A 将Dispatcher接口类别名设为ChannelManager::class
	$this->app->alias(
		ChannelManager::class, DispatcherContract::class
	);

	$this->app->alias(
		ChannelManager::class, FactoryContract::class
	);
}

这里倒过来看,先看A语句设置了别名,在看B语句绑定接口到一个实例。所以,app(Dispatcher::class)->send($this, $instance);中的send方法是属于ChannelManager::class类的,其实现如下:

public function send($notifiables, $notification)
{
	return (new NotificationSender(
		$this, $this->container->make(Bus::class), $this->container->make(Dispatcher::class), $this->locale)
	)->send($notifiables, $notification);
}

该方法接收了两个参数,第一个是被通知的对象,比如这里是传入User模型,第二个是消息实例(通知的内容),从前面分析可知,这里是传入了一个ResetPassword类的实例。该方法实例化了NotificationSender类并调用其send方法,让我们跳转到这个send方法:

public function send($notifiables, $notification)
{
	// 将被通知对象格式化成模型集合
	$notifiables = $this->formatNotifiables($notifiables);
	// 如果使用队列
	if ($notification instanceof ShouldQueue) {
		return $this->queueNotification($notifiables, $notification);
	}

	return $this->sendNow($notifiables, $notification);
}

该方法主要是格式化了一遍传入的被通知对象,然后调用sendNow方法:

public function sendNow($notifiables, $notification, array $channels = null)
{
	// 将被通知对象格式化成模型集合
	$notifiables = $this->formatNotifiables($notifiables);
	// 克隆通知消息实例
	$original = clone $notification;
	// 检查被通知对象是否有channel,比如database,mail
	// 没有的话就略过,不对其作通知
	// A
	foreach ($notifiables as $notifiable) {
		if (empty($viaChannels = $channels ?: $notification->via($notifiable))) {
			continue;
		}
		// B 设置使用的语言
		$this->withLocale($this->preferredLocale($notifiable, $notification), function () use ($viaChannels, $notifiable, $original) {
			$notificationId = Str::uuid()->toString();
			// C 发送通知到每一个频道
			foreach ((array) $viaChannels as $channel) {
				$this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
			}
		});
	}
}

(A) 分析$notification->via($notifiable)。从前面可知,$notificationResetPassword类的实例,所以via方法是其旗下的方法,代码如下:

public function via($notifiable)
{
	return ['mail'];
}

由此可以看出,默认是使用发送邮件的方式。 (B) 分析 preferredLocale 方法。

protected function preferredLocale($notifiable, $notification)
{
	return $notification->locale ?? $this->locale ?? value(function () use ($notifiable) {
		// 如果被通知对象实现了HasLocalePreference接口
		if ($notifiable instanceof HasLocalePreference) {
			return $notifiable->preferredLocale();
		}
	});
}

由以上代码可知,被通知对象可以实现HasLocalePreference接口,从而通过实现preferredLocale方法指定使用的语言。 (C) 分析 sendToNotifiable 方法。

protected function sendToNotifiable($notifiable, $id, $notification, $channel)
{
	if (! $notification->id) {
		$notification->id = $id;
	}
	// 触发通知将要发送的事件,如果返货false,通知将不会被发送
	if (! $this->shouldSendNotification($notifiable, $notification, $channel)) {
		return;
	}
	// $this->manager->driver($channel)将根据传入的$channel创建对应channel类的实例
	// 比如,mail channel,将创建MailChannel类的实例
	// C-1 
	$response = $this->manager->driver($channel)->send($notifiable, $notification);
	// 触发消息已发送的事件
	$this->events->dispatch(
		new Events\NotificationSent($notifiable, $notification, $channel, $response)
	);
}

(C-1) $this->manager->driver($channel)得到的是MailChannel类的实例,程序接着调用它的send方法:

public function send($notifiable, Notification $notification)
{
    // $notification是Illuminate\Auth\Notifications\ResetPassword类的实例
	// C-1-1
	$message = $notification->toMail($notifiable);

	if (! $notifiable->routeNotificationFor('mail', $notification) &&
		! $message instanceof Mailable) {
		return;
	}
	// 如果实现了Mailable接口
	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)
	);
}

(C-1-1) Illuminate\Auth\Notifications\ResetPassword类的toMail方法:

public function toMail($notifiable)
{
	if (static::$toMailCallback) {
		return call_user_func(static::$toMailCallback, $notifiable, $this->token);
	}

	return (new MailMessage)
		->subject(Lang::get('Reset Password Notification'))
		->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
		->action(Lang::get('Reset Password'), url(config('app.url').route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false)))
		->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
		->line(Lang::get('If you did not request a password reset, no further action is required.'));
}

发送邮件通知的内容都在这里设置。$message = $notification->toMail($notifiable);最终得到的值$message如下:

Laravel 重置密码发送邮件分析 最后,根据$message实例的各种属性设置发送邮件,限于篇幅,具体细节就不再分析了。

发送完邮件消息之后,Illuminate\Auth\Passwords\PasswordBroker类的方法返回static::RESET_LINK_SENT。回到开头的sendResetLinkEmail方法:

public function sendResetLinkEmail(Request $request)
{
	$this->validateEmail($request);

	// We will send the password reset link to this user. Once we have attempted
	// to send the link, we will examine the response then see the message we
	// need to show to the user. Finally, we'll send out a proper response.

	$response = $this->broker()->sendResetLink(
		$this->credentials($request)
	);

	return $response == Password::RESET_LINK_SENT
				? $this->sendResetLinkResponse($request, $response)
				: $this->sendResetLinkFailedResponse($request, $response);
}

由于$response == Password::RESET_LINK_SENT为true,所以执行$this->sendResetLinkResponse($request, $response)

protected function sendResetLinkResponse(Request $request, $response)
{
	return back()->with('status', trans($response));
}

所以发送消息成功后,页面后退,同时带上消息发送成功的信息。