Laravel 发送重置密码邮件的原理分析

Laravel默认发送邮件的模板是这样的:

file

全英文界面,我做的项目都是面向国内用户的,肯定得改成中文啊。于是,我看了下 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 方法 > 父类: Authenticatabletrait 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

好了,分析就到这里结束了,感谢看完!

开源教育系统https://meedu.vip
本帖已被设为精华帖!
本帖由系统于 4年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 4

看了贼九没看到在那

然后之间跳到末尾了 嘿嘿

6年前 评论

首先这个文章对想要了解背后机制的同学很有意义。但我个人想问一下,学Laravel需要对每个功能都这么分析一遍吗?我的看法,如果每个功能我都要这样费劲去分析,我还不如写我自己的逻辑,我就不用这个框架了。写自己的代码比看别人的代码要省事得多。Laravel存在的意义就在于我们可以直接使用它提供的功能,所以省事。不用管背后是什么机制,就直接用就行。如果每个功能都要费时费力地分析一遍,是不是这个框架的意义就大打折扣了呢?不知道我理解得对不对?

4年前 评论
小滕 (楼主) 4年前

再分析一下找回密码的吧。 :kissing_heart:

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!