Laravel 发送重置密码邮件的原理分析
Laravel默认发送邮件的模板是这样的:
全英文界面,我做的项目都是面向国内用户的,肯定得改成中文啊。于是,我看了下 resources/views/vendor/mail
下面的文件,初步估计认为修改这里的文件就可以了,但是事实证明并不是这些文件,那是哪些文件呢?乱找肯定不行,于是有了下文。
从路由开始:password/email
,直接分析对应的 Controller
锁定到:Auth\ForgotPasswordController@sendResetLinkEmail
:
上面不明白的话可以看我之前分析的一篇文章:记Laravel的Auth::routes()方法追踪
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
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(
$request->only('email')
);
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($response)
: $this->sendResetLinkFailedResponse($request, $response);
}
上面就是发送邮件的方法了,锁定 $this->broker()->sendResetLink()
方法。首先 $this->broker()
代码为:
/**
* Get the broker to be used during password reset.
*
* @return \Illuminate\Contracts\Auth\PasswordBroker
*/
public function broker()
{
return Password::broker();
}
调用了 Password
(Facade),所以直接查看 config/app.php
文件:
'Password' => Illuminate\Support\Facades\Password::class,
锁定文件到 Illuminate\Support\Facades\Password::class
:
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'auth.password';
}
返回服务容器绑定的 auth.password
,于是重新回到 config/app.php
文件,但是并没有找到,So,打开文档,在 https://learnku.com/docs/laravel/5.4/facades 中查找到:
Facade | Class | Service Container Binding |
---|---|---|
Password | Illuminate\Auth\Passwords\PasswordBrokerManager |
auth.password |
直接锁定 Illuminate\Auth\Passwords\PasswordBrokerManager
:
/**
* Attempt to get the broker from the local cache.
*
* @param string $name
* @return \Illuminate\Contracts\Auth\PasswordBroker
*/
public function broker($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return isset($this->brokers[$name])
? $this->brokers[$name]
: $this->brokers[$name] = $this->resolve($name);
}
/**
* Resolve the given broker.
*
* @param string $name
* @return \Illuminate\Contracts\Auth\PasswordBroker
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Password resetter [{$name}] is not defined.");
}
// The password broker uses a token repository to validate tokens and send user
// password e-mails, as well as validating that password reset process as an
// aggregate service of sorts providing a convenient interface for resets.
return new PasswordBroker(
$this->createTokenRepository($config),
$this->app['auth']->createUserProvider($config['provider'])
);
}
前面调用了 Password::broker()
方法就是这里的 broker()
方法。可以看到返回一个 broker
,因为无参数传入,所以需要调用 resolve()
方法,返回 PasswordBroker
对象,这里传入了两个参数。那么,直接锁定到 PasswordBroker
这个文件:
/**
* Create a new password broker instance.
*
* @param \Illuminate\Auth\Passwords\TokenRepositoryInterface $tokens
* @param \Illuminate\Contracts\Auth\UserProvider $users
* @return void
*/
public function __construct(TokenRepositoryInterface $tokens,
UserProvider $users)
{
$this->users = $users;
$this->tokens = $tokens;
}
/**
* Send a password reset link to a user.
*
* @param array $credentials
* @return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
return static::RESET_LINK_SENT;
}
//... 此处省略N行
/**
* Get the user for the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\CanResetPassword
*
* @throws \UnexpectedValueException
*/
public function getUser(array $credentials)
{
$credentials = Arr::except($credentials, ['token']);
$user = $this->users->retrieveByCredentials($credentials);
if ($user && ! $user instanceof CanResetPasswordContract) {
throw new UnexpectedValueException('User must implement CanResetPassword interface.');
}
return $user;
}
还记得前面的 $this->broker()->sendResetLink()
的方法调用吗?正主出现了,正是调用了这里的 sendResetLink
方法,分析该方法可以发现发送邮件的操作是下面代码完成的:
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
从注释中我们也可以明显的看出 ^ - ^。追其根源,$user
> $this->getUser($credentials)
> getUser()
> $user = $this->users->retrieveByCredentials($credentials)
> $this->user
> __construct(TokenRepositoryInterface $tokens, UserProvider $users)
得出,是通过第二个参数传递进来的,所以返回上一步中的:
return new PasswordBroker(
$this->createTokenRepository($config),
$this->app['auth']->createUserProvider($config['provider'])
);
$this->app['auth']->createUserProvider($config['provider'])
调用了容器中的 auth
Service,查看文档得到:
Facade | Class | Service Container Binding |
---|---|---|
Auth | Illuminate\Auth\AuthManager |
auth |
锁定到 Illuminate\Auth\AuthManager
文件:
class AuthManager implements FactoryContract
{
use CreatesUserProviders;
// ... 省略
}
指向: CreatesUserProviders
:
<?php
namespace Illuminate\Auth;
use InvalidArgumentException;
trait CreatesUserProviders
{
/**
* The registered custom provider creators.
*
* @var array
*/
protected $customProviderCreators = [];
/**
* Create the user provider implementation for the driver.
*
* @param string $provider
* @return \Illuminate\Contracts\Auth\UserProvider
*
* @throws \InvalidArgumentException
*/
public function createUserProvider($provider)
{
$config = $this->app['config']['auth.providers.'.$provider];
if (isset($this->customProviderCreators[$config['driver']])) {
return call_user_func(
$this->customProviderCreators[$config['driver']], $this->app, $config
);
}
switch ($config['driver']) {
case 'database':
return $this->createDatabaseProvider($config);
case 'eloquent':
return $this->createEloquentProvider($config);
default:
throw new InvalidArgumentException("Authentication user provider [{$config['driver']}] is not defined.");
}
}
/**
* Create an instance of the database user provider.
*
* @param array $config
* @return \Illuminate\Auth\DatabaseUserProvider
*/
protected function createDatabaseProvider($config)
{
$connection = $this->app['db']->connection();
return new DatabaseUserProvider($connection, $this->app['hash'], $config['table']);
}
/**
* Create an instance of the Eloquent user provider.
*
* @param array $config
* @return \Illuminate\Auth\EloquentUserProvider
*/
protected function createEloquentProvider($config)
{
return new EloquentUserProvider($this->app['hash'], $config['model']);
}
}
分析得出,该类主要作用是创建 User
对象,从 config/auth.php
配置中:
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
'table' => 'users',
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
我们使用的驱动是 eloquent
,于是,创建 App\User::class
对象,所以目标转向 app/User.php
文件:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
还记得之前的 $user->sendPasswordResetNotification()
吗?现在我们要找出 sendPasswordResetNotification()
方法。分析过程:User
文件并没有直接的 sendPasswordResetNotification
方法 > 父类: Authenticatable
和 trait
Notifiable
> Illuminate\Foundation\Auth\User
> Illuminate\Auth\Passwords\CanResetPassword
中:
<?php
namespace Illuminate\Auth\Passwords;
use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;
trait CanResetPassword
{
/**
* Get the e-mail address where password reset links are sent.
*
* @return string
*/
public function getEmailForPasswordReset()
{
return $this->email;
}
/**
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
}
锁定到 ResetPasswordNotification
指向 Illuminate\Auth\Notifications\ResetPassword
:
<?php
namespace Illuminate\Auth\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ResetPassword extends Notification
{
/**
* The password reset token.
*
* @var string
*/
public $token;
/**
* Create a notification instance.
*
* @param string $token
* @return void
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('You are receiving this email because we received a password reset request for your account.')
->action('Reset Password', url(config('app.url').route('password.reset', $this->token, false)))
->line('If you did not request a password reset, no further action is required.');
}
}
锁定方法 toMail()
,其返回了 MailMessage()
,锁定到它:
<?php
namespace Illuminate\Notifications\Messages;
class MailMessage extends SimpleMessage
{
/**
* The view to be rendered.
*
* @var array|string
*/
public $view;
/**
* The view data for the message.
*
* @var array
*/
public $viewData = [];
/**
* The Markdown template to render (if applicable).
*
* @var string|null
*/
public $markdown = 'notifications::email';
/**
* The "from" information for the message.
*
* @var array
*/
public $from = [];
/**
* The "reply to" information for the message.
*
* @var array
*/
public $replyTo = [];
/**
* The attachments for the message.
*
* @var array
*/
public $attachments = [];
/**
* The raw attachments for the message.
*
* @var array
*/
public $rawAttachments = [];
/**
* Priority level of the message.
*
* @var int
*/
public $priority;
/**
* Set the view for the mail message.
*
* @param array|string $view
* @param array $data
* @return $this
*/
public function view($view, array $data = [])
{
$this->view = $view;
$this->viewData = $data;
$this->markdown = null;
return $this;
}
// ... 省略
}
观其属性:
/**
* The Markdown template to render (if applicable).
*
* @var string|null
*/
public $markdown = 'notifications::email';
遂知默认重置密码邮件的模板位于:resources/views/vendor/notifications/email.blade.php
。
好了,分析就到这里结束了,感谢看完!
看了贼九没看到在那
然后之间跳到末尾了 嘿嘿
真会找
首先这个文章对想要了解背后机制的同学很有意义。但我个人想问一下,学Laravel需要对每个功能都这么分析一遍吗?我的看法,如果每个功能我都要这样费劲去分析,我还不如写我自己的逻辑,我就不用这个框架了。写自己的代码比看别人的代码要省事得多。Laravel存在的意义就在于我们可以直接使用它提供的功能,所以省事。不用管背后是什么机制,就直接用就行。如果每个功能都要费时费力地分析一遍,是不是这个框架的意义就大打折扣了呢?不知道我理解得对不对?
再分析一下找回密码的吧。 :kissing_heart: