基于Laravel开发博客应用系列 —— 联系我们 & 发送邮件 & 队列使用(基于数据库)

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

1、邮件发送设置

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

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

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

配置 163 邮箱账户

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

首先,编辑 config/mail.php 文件内容如下:

// 将如下这行
 'from' => ['address' => null, 'name' => null],
// 修改成
 'from' => ['address' => env('MAIL_FROM'), 'name' => env('MAIL_NAME')],

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

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

MAIL_DRIVER=smtp
MAIL_HOST=smtp.163.com
MAIL_PORT=25
MAIL_USERNAME=YOUR@EMAIL.COM
MAIL_PASSWORD=YOUR-163-PASSWORD
MAIL_FROM=YOUR@EMAIL.COM
MAIL_NAME=YOUR-NAME

使用 Tinker 测试邮件发送

Laravel 使用邮件视图作为模板发送邮件,所有发送邮件前首先创建一个简单的测试邮件视图模板。在 resources/views/emails 目录下新建一个 test.blade.php 文件,编辑其内容如下:

<p>
    这是一封测试邮件。
</p>
<p>
    变量 <code>$testVar</code> 的值是:
</p>
<ul>
     <li><strong>{{ $testVar }}</strong></li>
</ul>
<hr>
<p>就是这样。</p>

在命令行运行 artisan tinker 命令发送邮件:

使用Artisan Tinker命令发送邮件

我们使用 Mail::send 发送邮件,该方法第一个参数是邮件视图模板,第二个参数是传递到视图的变量数组,第三个参数是对消息进行额外处理的闭包,这里我们只是设置了收件人及邮件主题。你可以在闭包中做更多事情,更多设置可参考Laravel邮件文档

上述 tinker 示例使用 163 邮箱配置,如果邮件发送成功返回1。

去收件箱收取邮件,该邮件内容如下:

使用Laravel Tinker发送邮件收取

2、添加联系我们表单

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

添加链接和路由

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

// 将如下区块内容
{{-- Collect the nav links, forms, and other content for toggling --}}
<div class="collapse navbar-collapse" id="navbar-main">
    <ul class="nav navbar-nav">
        <li>
            <a href="/">Home</a>
        </li>
    </ul>
</div>

// 替换成
{{-- Collect the nav links, forms, and other content for toggling --}}
<div class="collapse navbar-collapse" id="navbar-main">
    <ul class="nav navbar-nav">
        <li>
            <a href="/">Home</a>
        </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
        <li>
            <a href="/contact">Contact</a>
        </li>
    </ul>
</div>

很简单,现在还需要为这个链接定义一个路由:

// 在下面这行路由下面
get('blog/{slug}', 'BlogController@showPost');

// 添加如下两行路由
$router->get('contact', 'ContactController@showForm');
Route::post('contact', 'ContactController@sendContactInfo');

注意我们直接使用了 $router 而不是 get() 函数,然后使用了 Route 门面而不是 post() 函数,这只是为了演示有多种方式来定义路由。通常我使用辅助函数来设置路由,但是有些人倾向于使用 $router,也有些人喜欢使用 Route 门面。

此外 Laravel 5.1 还提供了以下辅助函数来设置路由:

  • delete($uri, $action) 注册一个DELETE路由
  • get($uri, $action) 注册一个GET路由
  • patch($uri, $action) 注册一个PATCH路由
  • post($uri, $action) 注册一个POST路由
  • put($uri, $action) 注册一个PUT路由
  • resource($name, $controller, $options) 注册一个RESTful路由

如果要对路由进行分组,只能使用 Route::group$router->group()

创建表单请求类

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

php artisan make:request ContactMeRequest

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

<?php

namespace App\Http\Requests;

use App\Http\Requests\Request;
class ContactMeRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize()
    {
        return true;
    }

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

创建控制器

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

php artisan make:controller --plain ContactController

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

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

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

class ContactController extends Controller
{
    /**
     * 显示表单
     *
     * @return View
     */
    public function showForm()
    {
        return view('blog.contact');
    }

    /**
     * Email the contact request
     *
     * @param ContactMeRequest $request
     * @return Redirect
     */
    public function sendContactInfo(ContactMeRequest $request)
    {
        $data = $request->only('name', 'email', 'phone');
        $data['messageLines'] = explode("\n", $request->get('message'));

        Mail::send('emails.contact', $data, function ($message) use ($data) {
            $message->subject('Blog Contact Form: '.$data['name'])
              ->to(config('blog.contact_email'))
              ->replyTo($data['email']);
        });

        return back()
            ->withSuccess("Thank you for your message. It has been sent.");
    }
}

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

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

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

注:记得添加 contact_email 到博客配置文件,在控制器中我们使用了 config('blog.contact_email') 来获取该配置值。

创建视图

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

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

@extends('blog.layouts.master', ['meta_description' => 'Contact Form'])

@section('page-header')
  <header class="intro-header"
          style="background-image: url('{{ page_image('contact-bg.jpg') }}')">
    <div class="container">
      <div class="row">
        <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
          <div class="site-heading">
            <h1>Contact Me</h1>
            <hr class="small">
            <h2 class="subheading">
              Have questions? I have answers (maybe).
            </h2>
          </div>
        </div>
      </div>
    </div>
  </header>
@stop

@section('content')
  <div class="container">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        @include('admin.partials.errors')
        @include('admin.partials.success')
        <p>
          Want to get in touch with me? Fill out the form below to send me a
          message and I will try to get back to you within 24 hours!
        </p>
        <form action="/contact" method="post">
          <input type="hidden" name="_token" value="{!! csrf_token() !!}">
          <div class="row control-group">
            <div class="form-group col-xs-12">
              <label for="name">Name</label>
              <input type="text" class="form-control" id="name" name="name"
                     value="{{ old('name') }}">
            </div>
          </div>
          <div class="row control-group">
            <div class="form-group col-xs-12">
              <label for="email">Email Address</label>
              <input type="email" class="form-control" id="email" name="email"
                     value="{{ old('email') }}">
            </div>
          </div>
          <div class="row control-group">
            <div class="form-group col-xs-12 controls">
              <label for="phone">Phone Number</label>
              <input type="tel" class="form-control" id="phone" name="phone"
                     value="{{ old('phone') }}">
            </div>
          </div>
          <div class="row control-group">
            <div class="form-group col-xs-12 controls">
              <label for="message">Message</label>
              <textarea rows="5" class="form-control" id="message"
                        name="message">{{ old('message') }}</textarea>
            </div>
          </div>
          <br>
          <div class="row">
            <div class="form-group col-xs-12">
              <button type="submit" class="btn btn-default">Send</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
@endsection

接下来在 resources/views/emails 目录下创建 contact.blade.php

<p>
  You have received a new message from your website contact form.</p><p>
  Here are the details:
</p>
<ul>
  <li>Name: <strong>{{ $name }}</strong></li>
  <li>Email: <strong>{{ $email }}</strong></li>
  <li>Phone: <strong>{{ $phone }}</strong></li>
</ul>
<hr>
<p>
  @foreach ($messageLines as $messageLine)
    {{ $messageLine }}<br>
  @endforeach
</p>
<hr>

发送邮件

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

Laravel 博客联系我们功能

填写完表单后,点击“Send”提交信息。收到成功信息后,可以去邮箱收邮件,邮件内容如下:

Laravel博客联系我们收件箱邮件内容

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

要减少这个延迟时间,我们可以设置队列,以便在后台异步处理这个耗时任务

3、关于队列

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

如何工作

队列实际上很容易理解:

队列工作原理

整个过程应该是这样:Web 请求到达处理该请求的控制器,在处理过程中,某些任务被推送到队列(比如邮件发送),然后返回响应。队列会在后台异步执行。

队列驱动

Laravel 为队列提供了的不同实现驱动:

  • sync – 当任务被推送到sync队列后会立即执行
  • database数据库队列会将任务存放到数据表中,该表默认是jobs
  • beanstalkd – 要实现beanstalkd队列需要先配置并运行beanstalkd,而且还要使用Composer安装依赖包:composer require "pda/pheanstalk=~3.0"
  • sqs – 该驱动会将任务推送到Amazon SQS队列,同样需要先使用Composer安装依赖包:composer require aws/aws-sdk-php
  • iron – 该驱动会将任务推送到IronMQ,使用前需要安装依赖:composer required "iron-io/iron_mq=~1.5"
  • redis – Redis队列会将任务推送到Redis数据库,使用前需要安装redis依赖:composer require "presdis/presis=~1.0"

使用数据库驱动

这里我们使用数据库驱动实现队列。

登录到 Homestead 虚拟机,运行如下迁移命令创建存放队列任务的 jobs 表:

php artisan queue:table 
php artisan migrate

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

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

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

修改控制器

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

// 找到这一行
Mail::send('emails.contact', $data, function ($message) use ($data) {

// 然后将其修改为:
Mail::queue('emails.contact', $data, function ($message) use ($data) {

就是这么简单!

邮件在哪

再次测试联系我们表单,点击“Send”之后没有任何延迟就会跳转到成功页面。

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

为什么?

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

运行 queue:work

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

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

5、自动处理队列

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

一种方式是将 artisan queue:listen 命令加入到服务器启动脚本中,该命令会在新任务推送到队列时自动调用 artisan queue:work。这种方案的问题是 queue:listen  命令会一直挂在那里,消耗 CPU 资源,而且一旦命令挂掉,新的任务还是无法执行,更好的解决方案是使用 Supervisor 来运行 queue:listen

使用 Supervisor 运行 queue:listen

Supervisor 是 *nix 系统上用于监控和管理进程的工具,我们这里不深入探究如何安装这个工具,如果你使用 Homestead 作为本地开发环境,则该工具已经为我们安装好了。

以 Homestead 上预装的 Supervisor 为例,在 /etc/supervisor/conf.d 目录下创建 blog.conf,并编辑该文件内容如下:

[program:blog-queue-listen]
command=php /home/vagrant/Code/blog/artisan queue:listen
user=vagrant
process_name=%(program_name)s_%(process_num)d
directory=/home/vagrant/Code/blog
stdout_logfile=/home/vagrant/Code/blog/storage/logs/supervisord.log
redirect_stderr=true
numprocs=1

保存该文件后关闭在正在运行的 Supervisor 服务,然后使用如下命令重新启动 Supervisor:

sudo supervisord -c /etc/supervisor/supervisord.conf

使用如下命令可以查看所有正在监听的队列:

sudo supervisorctl status

这样,推送到队列的任务就可以正常被执行了。

使用调度命令

对小的站点而言还有一种方式是使用调度任务每分钟运行一次 queue:work,或者每五分钟,这可以通过使用 Laravel 5.1 的命令行调度器来完成。

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

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

这将会每分钟运行一次 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 并添加如下这行调度任务:

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

注:需要将 /path/to 修改成项目根目录,比如在Homestead上是 /home/vagrant/Code/blog

这将会调用 Artisan 运行任何当前调度任务,并将输出发送到空设备。

6、队列任务

队列的另一个使用场景是异步任务。我们在控制器中使用 $this->dispatch(new JobName) 分发任务,但是这些任务只是被推送到队列,只有当运行 queue:work 或者其它处理队列任务 的操作执行时这些队列任务才会执行。

更多关于队列任务的详情请查看Laravel队列文档

学院君

学院君 has written 550 articles

资深PHP工程师,Laravel学院院长