基于 Laravel 5.7 开发博客应用系列(八) —— 博客前台联系我们 & 邮件发送功能实现

本节我们将会添加联系我们功能到博客应用,要实现该功能我们需要了解 Laravel 的邮件发送功能以及队列处理机制。

1、邮件发送设置

为了使用 Laravel 的邮件发送功能,首先需要配置邮件发送,配置很简单,打开 .env 文件,查看邮件配置部分:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

正如你所看到的,都是一些简单的配置项。

配置 163 邮箱账户

假设你有一个 163 邮箱账户,下面我们就以 163 邮箱为例演示如何配置邮件发送。

首先,编辑 config/mail.php 文件修改 from 配置项内容如下:

'from' => [
    'address' => env('MAIL_FROM_ADDRESS', 'admin@laravelacademy.org'),
    'name' => env('MAIL_FROM_NAME', '学院君'),
],

这一步设置邮件发送人和发送人姓名。

接下来,编辑 .env,修改邮件配置如下:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.163.com
MAIL_PORT=465
MAIL_USERNAME=YOUR-EMAIL-NAME
MAIL_PASSWORD=YOUR-163-PASSWORD
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=YOUR-EMAIL
MAIL_FROM_NAME=YOUR-NAME

编写可邮寄测试类

在 Laravel 中,应用发送的每一封邮件都抽象为「可邮寄」类,我们通过可邮寄类来实现邮件发送。在开始之前,我们先创建一个可邮寄测试类测试邮件是否可以发送,我们可以通过 make:mail 命令生成可邮寄类:

php artisan make:mail TestMail

该命令会在 app/Mail 目录下创建一个 TestMail.php 文件,修改可邮寄类 TestMailbuild 方法如下:

public function build()
{
    return $this->subject('测试邮件')->view('emails.test');
}

我们主要修改了邮件内容视图,然后在 resources/views 目录下创建一个新的 emails 子目录,并在该子目录下创建邮件视图 test.blade.php,初始化测试邮件视图代码如下:

一封来自Laravel学院的测试邮件!

至此,一个简单的可邮寄类就编写好了,默认的配置会从配置文件 mail.php 中读取。

使用 Tinker 测试邮件发送

接下来,我们在命令行运行 php artisan tinker 测试邮件发送:

上述 Tinker 示例使用 163 邮箱配置,如果邮件发送成功返回 null,否则打印错误信息。

去收件箱收取邮件,可以看到已经收取到刚刚发送的测试邮件(收件箱没有,可以去垃圾邮箱找):

2、添加联系我们表单

现在我们知道 Laravel 邮件发送功能没有问题,接下来我们来创建联系我们表单。

添加链接和路由

联系我们链接应该出现在博客的每一个页面,所以我们将其放到顶部导航条中,编辑 blog.partials.page-nav 视图文件如下:

// 将如下区块内容
<div class="collapse navbar-collapse" id="navbarResponsive">
   <ul class="navbar-nav ml-auto">
       <li class="nav-item">
           <a class="nav-link" href="/">首页</a>
       </li>
   </ul>
</div>

// 替换成
<div class="collapse navbar-collapse" id="navbarResponsive">
  <ul class="navbar-nav">
      <li class="nav-item">
          <a class="nav-link" href="/">首页</a>
      </li>
  </ul>
  <ul class="navbar-nav ml-auto">
      <li class="nav-item">
          <a class="nav-link" href="/contact">联系我们</a>
      </li>
  </ul>
</div>

很简单,现在还需要为这个链接定义一个路由,在 routes/web.php 新增如下路由定义:

Route::get('contact', 'ContactController@showForm');
Route::post('contact', 'ContactController@sendContactInfo');

创建表单请求类

我们知道联系表单包含用户名,邮箱地址,以及消息内容,这里我们和之前后台系统一样使用表单请求类来对表单字段进行验证。使用 Artisan 命令创建该请求类:

php artisan make:request ContactMeRequest

编辑新生成的 ContactMeRequest.php 文件内容如下:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ContactMeRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required',
            'email' => 'required|email',
            'message' => 'required',
        ];
    }
}

创建可邮寄类

在编写控制器逻辑之前,我们先来创建联系我们对应的可邮寄类(需要发送邮件来实现),还是通过 Artisan 命令来创建:

php artisan make:mail ContactMail

编写 ContactMail 类代码如下:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;

class ContactMail extends Mailable
{
    use Queueable, SerializesModels;

    protected $formData;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($formData)
    {
        $this->formData = $formData;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->subject('博客联系我们表单:' . $this->formData['name'])
            ->view('emails.contact', ['data' => $this->formData]);
    }
}

创建控制器

下面我们创建在路由中使用的控制器:

php artisan make:controller ContactController

然后编辑新生成的控制器文件 ContactController.php 内容如下:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ContactMeRequest;
use App\Mail\ContactMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class ContactController extends Controller
{
    public function showForm()
    {
        return view('blog.contact');
    }

    public function sendContactInfo(ContactMeRequest $request)
    {
        $data = $request->only('name', 'email', 'phone');
        $data['messageLines'] = explode("\n", $request->get('message'));

        Mail::to($data['email'])->send(new ContactMail($data));

        return back()
            ->with("success", "消息已发送,感谢您的反馈");
    }
}

sendContactInfo() 方法中,我们使用 ContactMeRequest 验证表单请求,然后使用 $data 填充表单数据,对 message 字段,我们以行为单位将其进行分割并传递给视图。

再然后我们使用 Mail 门面发送邮件,你还可以在 sendContactInfo() 方法中依赖注入 Illuminate\Mail\Mailer 对象的方式实现邮件发送。

消息被发送后我们跳转到联系页面,传递发送成功消息。

创建视图

要完成联系表单功能,我们还需要创建两个视图,一个用于显示表单,一个用于定义被发送邮件视图模板。

首先在 resources/views/blog 目录下创建 contact.blade.php

@extends('blog.layouts.master', ['meta_description' => '联系我们'])

@section('page-header')
    <header class="masthead" style="background-image: url('{{ page_image('contact-bg.jpg') }}')">
        <div class="overlay"></div>
        <div class="container">
            <div class="row">
                <div class="col-lg-8 col-md-10 mx-auto">
                    <div class="page-heading">
                        <h1>联系我们</h1>
                        <span class="subheading">你有问题?我有答案。</span>
                    </div>
                </div>
            </div>
        </div>
    </header>
@stop

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-md-10 mx-auto">
                @include('admin.partials.errors')
                @include('admin.partials.success')
                <p>
                    想与我联系?填写下面的表单给我发消息,我会尽快给你回复!
                </p>
                <form name="sentMessage" action="/contact" method="post" id="contactForm" novalidate>
                    <input type="hidden" name="_token" value="{!! csrf_token() !!}">
                    <div class="control-group">
                        <div class="form-group floating-label-form-group controls">
                            <label>姓名</label>
                            <input type="text" name="name" class="form-control" placeholder="填写你的名字" id="name" value="{{ old('name') }}" required>
                        </div>
                    </div>
                    <div class="control-group">
                        <div class="form-group floating-label-form-group controls">
                            <label>邮箱</label>
                            <input type="email" name="email" class="form-control" placeholder="填写你的邮箱" id="email" value="{{ old('email') }}" required>
                        </div>
                    </div>
                    <div class="control-group">
                        <div class="form-group col-xs-12 floating-label-form-group controls">
                            <label>手机</label>
                            <input type="tel" name="phone" class="form-control" placeholder="填写你的手机号" id="phone" value="{{ old('phone') }}" required>
                        </div>
                    </div>
                    <div class="control-group">
                        <div class="form-group floating-label-form-group controls">
                            <label>消息</label>
                            <textarea rows="5" name="message" class="form-control" placeholder="填写你想发送的消息" id="message" value="{{ old('message') }}" required></textarea>
                        </div>
                    </div>
                    <br>
                    <div class="form-group">
                        <button type="submit" class="btn btn-primary" id="sendMessageButton">发送</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
@endsection

接下来在 resources/views/emails 目录下创建邮件模板视图 contact.blade.php

<p>
    收到来自博客网站联系表单的新消息。
</p>
<p>
    下面是消息明细:
</p>
<ul>
    <li>姓名: <strong>{{ $data['name'] }}</strong></li>
    <li>邮箱: <strong>{{ $data['email'] }}</strong></li>
    <li>手机: <strong>{{ $data['phone'] }}</strong></li>
</ul>
<hr>
<p>
    @foreach ($data['messageLines'] as $messageLine)
        {{ $messageLine }}<br>
    @endforeach
</p>
<hr>

发送邮件

联系表单功能现在已经全部完成,在浏览器地址栏中访问 http://blog57.test/contact 来测试该功能:


填写完表单后,点击「发送」按钮提交信息。收到成功信息后,可以去邮箱收邮件,邮件内容如下:

你可能已经注意到点击发送按钮和收到成功回复之间有一定延迟,这是因为使用了 smtp 邮箱驱动,该操作需要连接到对应 SMTP 服务器实现邮件发送。

要减少这个延迟时间,我们可以设置队列,将邮件发送操作推送到队列,以便在后台异步处理这个耗时任务。

3、关于队列

队列允许我们延迟耗时任务的处理,例如邮件发送,从而使得 Web 请求响应更快。

如何工作

队列实际上很容易理解:

整个过程应该是这样:Web 请求到达处理该请求的控制器,在处理过程中,某些任务被推送到队列(比如邮件发送),推送成功后立即返回响应,队列任务会在后台异步执行,从而提高了响应速度,优化了用户体验。

使用数据库驱动

Laravel 提供了多种队列驱动,这里我们使用数据库驱动实现队列。

进入数据库所在环境,运行如下迁移命令创建存放队列任务的 jobs 表:

php artisan queue:table 
php artisan migrate

然后编辑 .env 文件并修改 QUEUE_CONNECTION 的配置值为 database

4、将邮件发送任务推送到队列

现在队列已经设置好了,接下来我们将邮件发送推送到队列中而不是等待邮件发送完成再返回响应给用户。

修改控制器

要将邮件发送任务推送到队列,只需要在控制器 ContactController 中做如下改动即可:

// 找到这一行
Mail::to($data['email'])->send(new ContactMail($data));

// 然后将其修改为:
Mail::to($data['email'])->queue(new ContactMail($data));

就是这么简单!

邮件在哪

再次测试联系我们表单,点击「发送」按钮之后没有任何延迟就会跳转到成功页面。

你可以通过安装配置 Horizon 查看队列任务,对于本例而言,你还可以直接在数据表 jobs 中查看任务数据。

但是,如果仅仅这样你就以为完事的话那就错了,你将永远无法接收到邮件。

为什么?

因为后台没有运行对队列进行处理的轮询命令,接下来我们就要来做这件事。

运行 queue:work

要处理队列中的任务,需要手动运行 Artisan 命令:

php artisan queue:work

如果队列为空,这个命令什么也不会做。一旦有任务推送到队列中,该命令会捕获任务并执行它:

以上输出表示邮件发送任务已处理,可以去收件箱看看是否已经收到邮件。

5、自动处理队列

使用 Supervisor

queue:work 命令有个缺陷,就是每次有新任务推送到队列后需要手动登录到服务器并运行该命令,任务才会被执行,这显然是不合理的,对此我们可以使用一些自动化解决方案。

一种方式是将 php artisan queue:listen 命令加入到服务器启动脚本中,该命令会在新任务推送到队列时自动调用 php artisan queue:work。这种方案的问题是 queue:listen 命令会一直挂在那里,消耗 CPU 资源,而且一旦命令挂掉,新的任务还是无法执行,更好的解决方案是使用 Supervisor 来管理 queue:listen 进程。关于如何使用 Supervisor 配置队列任务自动执行,可以参考官方文档,这里就不展开了。

使用调度命令

对小的站点而言还有一种方式自动运行队列任务,那是使用调度任务每分钟运行一次 queue:work,或者每五分钟,这可以通过使用 Laravel 的任务调度功能来完成。

编辑 app/Console/Kernel.php 文件如下:

// 修改如下这个方法
/**
 * Define the application's command schedule.
 *
 * @param  Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    $schedule->command('queue:work')->everyMinute();
}

这将会每分钟运行一次 queue:work,你还可以通过如下方式修改运行频率:

// 每5分钟运行一次
$schedule->command('queue:work')->everyFiveMinutes();

// 一天运行一次
$schedule->command('queue:work')->daily();

// 每个星期一早上8:15运行
$schedule->command('queue:work')->weeklyOn(1, '8:15');

要了解更多调度任务运行频率配置可查看 Laravel 任务调度文档

下一步需要编辑服务器的 crontab 开启调度命令。使用 crontab -e 命令编辑 crontab 并添加如下这行调度任务(如果已经存在则跳过):

* * * * * php /path/to/artisan schedule:run 1>> /dev/null 2>&1

注:需要将 /path/to 修改成你的项目根目录,比如我的是 /var/www/blog57

编辑完成后保存退出,通过 crontab -l 命令查看刚刚配置的调度任务是否已经存在。如果已经存在的话,它将会每分钟调用一次 php artisan schedule:run,检查定义在 App\Console\Kernelschedule 方法中的所有调度任务,如果某个任务到了执行时间需要执行,则执行该任务,否则跳过,最后将输出发送到空设备。

重新发送联系表单测试一下,这次,不需要手动执行 queue:work 也能收到邮件了。

学院君 has written 1198 articles

Laravel学院院长,终身学习者

积分:157912 等级:P12 职业:手艺人 城市:杭州

13 条回复

  1. nenemiku39 nenemiku39 says:

    为什么发送的邮件总是发到表单填写的那个邮箱里面去了? 正常情况应该是别人填写好以后那些信息发到自己的邮箱里面去呀?

  2. 樱花树下 樱花树下 says:

    PHP Warning: stream_socket_client(): SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number in D:/testapp/v endor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php on li ne 267 PHP Warning: stream_socket_client(): Failed to enable crypto in D:/testapp/vend or/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php on line 267 PHP Warning: stream_socket_client(): unable to connect to ssl://smtp.mailtrap.i o:465 (Unknown error) in D:/testapp/vendor/swiftmailer/swiftmailer/lib/classes/S wift/Transport/StreamBuffer.php on line 267

  3. JackyBen JackyBen says:

    Mail::to($data['email'])->send(new ContactMail($data));

    这里send方法直接发送能收到邮件,修改为queue方法后,能保存到数据库,在本地调用php artisan queue:work也提示发送成功。但是我并没有在邮箱收到邮件,我找不到是哪个环节出了问题。怀疑是环境的问题,但也不能够确定。

    我用的是Windows 10 + Virtual Box + Homestead,队列是在本地虚拟机里执行的命令。直接发送是在网页端http://blog57.test/contact进行发送的。

  4. LJP88 LJP88 says:

    部署线上之后那邮件发不出去,在数据库里有记录,而且那id还不断自增加1,好神奇

登录后才能进行评论,立即登录?