53、新手进阶指南 —— 带用户功能的任务管理系统

1、简介

本进阶指南提供了对Laravel框架更深入的介绍,包括数据库迁移、Eloquent ORM、路由认证授权依赖注入验证视图以及Blade模板。如果你对Laravel框架或其他PHP框架已经有了基本的认识,本章节将是你新的起点,如果你完全还是新手,请从新手入门指南开始。

本节的示例仍然是构建一个任务系统,但是在上一节基础上,本任务系统将允许用户注册登录,同样完整的代码已经放到GitHub上:https://github.com/laravel/quickstart-intermediate

2、安装

安装Laravel

首先你需要安装一个新的Laravel应用。你可以使用Homestead虚拟机或者本地PHP开发环境来运行应用。开发环境准备完毕后,可以使用Composer来安装应用:

composer create-project laravel/laravel quickstart --prefer-dist
安装Quickstart项目

你可以继续往下阅读,也可以选择去GitHub下载项目源码并在本地运行:

git clone https://github.com/laravel/quickstart-intermediate quickstart
cd quickstart
composer install
php artisan migrate

关于构建本地开发环境的详细文档可查看Homestead安装文档。

3、准备好数据库

3.1 数据库迁移

首先,我们使用迁移来定于处理所有任务的数据库。Laravel的数据库迁移使用平滑、优雅的PHP代码来提供一个简单的方式定义和修改数据表结构。团队成员们无需在本地数据库手动添加或删除列,只需要简单运行你提交到源码控制系统中的迁移即可。

users表

由于我们允许用户注册,所以需要一张用来存储用户的表。幸运的是 Laravel已经自带了这个迁移用于创建基本的users表,我们不需要手动生成。该迁移文件默认位于database/migrations目录下。

tasks表

接下来,让我们来创建用于处理所有任务的数据表tasks。我们使用Artisan命令make:migration来为tasks生成一个新的数据库迁移:

php artisan make:migration create_tasks_table --create=tasks

生成的新迁移文件位于database/migrations目录下。你可能已经注意到了,make:migration命令已经在迁移文件中添加了自增ID和时间戳。我们将编辑该文件添加更多的字段到任务表tasks

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTasksTable extends Migration{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->index();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    { 
        Schema::drop('tasks');
    }
}

其中,user_id用于建立tasks表与users表之间的关联。

要运行迁移,可以使用migrate命令。如果你使用的是Homestead,需要在虚拟机中运行该命令,因为你的主机不能直接访问Homestead上的数据库:

php artisan migrate

该命令将会创建迁移中定义的尚未创建的所有数据表。如果你使用MySQL客户端(如Navicat For MySQL)查看数据表,你将会看到新的users表和tasks表。下一步,我们将要定义Eloquent ORM模型

3.2 Eloquent模型

Eloquent是Laravel默认的ORM,Eloquent使用“模型”这一概念使得从数据库存取数据变得轻松。通常,每个Eloquent模型都对应一张数据表。

User模型

首先,我们一个与users表相对应的模型User。Laravel已经自带了这一模型app/User,所以我们不需要重复创建了。

Task模型

接下来,我们来定义与tasks表相对应的模型Task。同样,我们使用Artisan命令来生成模型类,在本例中,我们使用make:model命令:

php artisan make:model Task

该模型位于应用的app目录下,默认情况下,该模型类是空的。我们不需要明确告诉Eloquent模型对应哪张表,Laravel底层会有一个映射规则,这一点在之前Eloquent文档已有说明,按照规则,这里Task模型默认对应tasks表。

接下来,让我们在Task模型类中加一些代码。首先,我们声明模型上的name属性支持“批量赋值”(关于批量赋值说明可查看这篇文章):

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

我们将在后续添加路由到应用中学习更多如何使用Eloquent模型。当然,你也可以先去查看完整的Eloquent文档了解更多。

3.3 Eloquent关联关系

现在,模型已经定义好了,我们需要将它们关联起来。例如,一个User实例对应多个Task实例,而一个Task实例从属于某个User。定义关联关系后将允许我们更方便的获取关联模型:

$user = App\User::find(1);

foreach ($user->tasks as $task) {
    echo $task->name;
}
tasks关联关系

首先,我们在User模型中定义tasks关联关系。Eloquent关联关系被定义成模型的方法,并且支持多种不同的关联关系类型(查看完整的Eloquent关联关系文档了解更多)。在本例中,我们将会在User模型中定义tasks方法并在其中调用Eloquent提供的hasMany方法:

<?php

namespace App;
// Namespace Imports...
class User extends Model implements AuthenticatableContract,
AuthorizableContract,CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword;

    // Other Eloquent Properties...

    /**
     * Get all of the tasks for the user.
     */
    public function tasks()
    {
        return $this->hasMany(Task::class);
    }
}
user关联关系

接下来,我们会在Task模型中定义user关联关系。同样,我们将其定义为模型的方法。在本例中,我们使用Eloquent提供的belongsTo方法来定义该关联关系:

<?php

namespace App;

use App\User;
use Illuminate\Database\Eloquent\Model;

class Task extends Model{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];

    /**
     * Get the user that owns the task.
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

好极了!现在我们已经定义好了关联关系,接下来可以正式开始创建控制器了!

4、路由

新手入门指南创建的任务管理系统中,我们在routes.php中使用闭包定义所有的业务逻辑。而实际上,大部分应用都会使用控制器来组织路由。

4.1 显示视图

我们还保留一个路由使用闭包:/路由,该路由是用于展示给游客的引导页,我们将在该路由中渲染欢迎页面。

在Laravel中,所有的HTML模板都位于resources/views目录,并且我们使用view函数从路由中返回其中一个模板:

Route::get('/', function () {
    return view('welcome');
});

当然,我们需要创建这个视图,稍后就会。

4.2 用户认证

此外,我们还要让用户注册并登录到应用。通常,在web应用中构建整个登录认证层是一件相当冗长乏味的工作,然而,由于它是一个如此通用的需求,Laravel试图将这一过程变得简单而轻松。

首先,注意到新安装的Laravel应用中已经包含了app/Http/Controllers/AuthController这个控制器,该控制器中使用了一个特殊的AuthenticatesAndRegistersUsers trait,而这个trait中包含了用户注册登录的所必须的相关逻辑。

认证路由&视图

那么接下来我们该怎么做呢?我们仍然需要创建注册和登录模板并定义指向认证控制器AuthController的路由。我们可以通过Artisan命令make:auth来完成所有工作:

php artisan make:auth --views

注:如果你想要查看这些视图的完整示例,可以去下载相应的GitHub项目:https://github.com/laravel/quickstart-intermediate

接下来,我们还要添加认证路由到路由文件,我们可以通过使用Route门面上的auth方法来实现这一目的,该方法会注册我们所需的所有认证路由,包括注册、登录和密码重置:
// Authentication Routes...
Route::auth();

4.3 任务控制器

由于我们需要获取和保存任务,所以还需要使用Artisan命令创建一个TaskController,生成的控制器位于app/Http/Controllers目录:

php artisan make:controller TaskController

现在这个控制器已经生成了,让我们去app/Http/routes.php中定义一些指向该控制器的路由吧:

Route::get('/tasks', 'TaskController@index');
Route::post('/task', 'TaskController@store');
Route::delete('/task/{task}', 'TaskController@destroy');
设置所有任务路由需要登录才能访问

对本应用而言,我们想要所有任务需要登录用户才能访问,换句话说,用户必须登录到系统才能创建新任务。所以,我们需要限制访问任务路由的用户为登录用户。Laravel使用中间件来处理这种限制。

如果要限制登录用户才能访问该控制器的所有动作,可以在控制器的构造函数中添加对middleware方法的调用。所有有效的路由中间件都定义在app/Http/Kernel.php文件中。在本例中,我们想要定义一个auth中间件到TaskController上的所有动作:

<?php

namespace App\Http\Controllers;

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

class TaskController extends Controller{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }
}

5、创建布局&视图

本应用仍然只有一个视图,该视图包含了用于添加新任务的表单和显示已存在任务的列表。为了让你更直观的查看该视图,我们将已完成的应用截图如下:

basic-overview

5.1 定义布局

几乎所有的web应用都会在不同界面共享同一布局,例如,本应用顶部的导航条将会在每个页面显示。Laravel使用Blade让不同页面共享这些公共特性变得简单。

正如我们之前讨论的,所有Laravel视图都存放在resources/views中,因此,我们在resources/views/layouts/app.blade.php中定义一个新的布局视图,.blade.php扩展表明框架使用Blade模板引擎来渲染视图,当然,你可以使用原生的PHP模板,然而,Blade提供了的标签语法可以帮助我们编写更加清爽、简短的模板。

编辑app.blade.php内容如下:

// resources/views/layouts/app.blade.php
<!DOCTYPE html><html lang="en">
    <head>
        <title>Laravel Quickstart - Advanced</title>

        <!-- CSS And JavaScript -->
    </head>

    <body>
        <div class="container">
            <nav class="navbar navbar-default">
                <!-- Navbar Contents -->
            </nav>
        </div>

        @yield('content')
    </body>
</html>

注意布局中的@yield('content')部分,这是一个Blade指令,用于指定继承布局的子页面在这里可以注入自己的内容。接下来,我们来定义使用该布局的子视图来提供主体内容。

5.2 定义子视图

好了,我们已经创建了应用的布局视图,下面我们需要定义一个包含创建新任务的表单和已存在任务列表的视图,该视图文件存放在resources/views/tasks.blade.php,对应TaskController中的index方法。

我们将跳过Bootstrap CSS的样板文件而只专注在我们所关注的事情上,不要忘了,你可以从GitHub下载本应用的所有资源:

// resources/views/tasks.blade.php

@extends('layouts.app')

@section('content')

    <!-- Bootstrap Boilerplate... -->

    <div class="panel-body">
        <!-- Display Validation Errors -->
        @include('common.errors')

        <!-- New Task Form -->
        <form action="/task" method="POST" class="form-horizontal">
            {{ csrf_field() }}

            <!-- Task Name -->
            <div class="form-group">
                <label for="task" class="col-sm-3 control-label">Task</label>

                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>

            <!-- Add Task Button -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> Add Task
                    </button>
                </div>
            </div>
        </form>
    </div>

    <!-- TODO: Current Tasks -->
@endsection
一些需要解释的地方

在继续往下之前,让我们简单谈谈这个模板。首先,我们使用@extends指令告诉Blade我们要使用定义在resources/views/layouts/app.blade.php的布局,所有@section('content')@endsection之间的内容将会被注入到app.blade.php布局的@yield('contents')指令位置。

@include(‘common.errors’)指令会加载 resources/views/common/errors.blade.php模板,我们后续会定义这个模板。

现在,我们已经为应用定义了基本的布局和视图,然后我们回到TaskControllerindex方法:

/**
 * Display a list of all of the user's task.
 *
 * @param Request $request
 * @return Response
 */
public function index(Request $request){
    return view('tasks.index');
}

接下来,让我们继续添加代码到POST /task路由的控制器方法来处理表单输入并添加新任务到数据库。

6、添加任务

6.1 验证表单输入

现在我们已经在视图中定义了表单,接下来需要编写代码到TaskController@store方法来处理表单请求并创建一个新任务。

对这个表单而言,我们将name字段设置为必填项,而且长度不能超过255个字符。如果表单验证失败,将会跳转到/tasks页面,并且将错误信息存放到一次性session中:

/**
 * Create a new task.
 *
 * @param Request $request
 * @return Response
 */
public function store(Request $request){
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);

    // Create The Task...
}

如果你已经看过新手入门教程,那么你可能会注意到这里的验证代码与之前大不相同,这是因为我们现在在控制器中,可以方便地调用ValidatesRequests trait(包含在Laravel控制器基类中)提供的validate方法。

我们甚至不需要手动判读是否验证失败然后重定向。如果验证失败,用户会自动被重定向到来源页面,而且错误信息也会被存放到一次性Session中。简直太棒了,有木有!

$errors变量

我们在视图中使用了@include('common.errors')指令来渲染表单验证错误信息,common.errors允许我们在所有页面以统一格式显示错误信息。我们定义common.errors内容如下:

// resources/views/common/errors.blade.php

@if (count($errors) > 0)
    <!-- Form Error List -->
    <div class="alert alert-danger">
        <strong>Whoops! Something went wrong!</strong>

        <br><br>

        <ul>
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
        </ul>
    </div>
@endif

注:$errors变量在每个Laravel视图中都可以访问,如果没有错误信息的话它就是一个空的ViewErrorBag实例。

6.2 创建任务

现在输入验证已经做好了,接下来正式开始创建一个新任务。一旦新任务创建成功,页面会跳转到/tasks。要创建任务,可以借助Eloquent模型的关联关系。

大部分Laravel的关联关系提供了save方法,该方法接收一个关联模型实例并且会在保存到数据库之前自动设置外键值到关联模型上。在本例中,save方法会自动将当前用户登录认证用户的ID赋给给给定任务的user_id属性。我们通过$request->user()获取当前登录用户实例:

/**
 * Create a new task.
 *
 * @param Request $request
 * @return Response
 */
public function store(Request $request){
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);

    $request->user()->tasks()->create([
        'name' => $request->name,
    ]);

    return redirect('/tasks');
}

很好,到了这里,我们已经可以成功创建任务,接下来,我们继续添加代码到视图来显示所有任务列表。

7、显示已存在的任务

首先,我们需要编辑TaskController@index传递所有已存在任务到视图。view函数接收一个数组作为第二个参数,我们可以将数据通过该数组传递到视图中:

/**
 * Display a list of all of the user's task.
 *
 * @param Request $request
 * @return Response
 */
public function index(Request $request){
    $tasks = Task::where('user_id', $request->user()->id)->get();

    return view('tasks.index', [
        'tasks' => $tasks,
    ]);
}

这里,我们还要讲讲Laravel的依赖注入,这里我们将TaskRepository注入到TaskController,以方便对Task模型所有数据的访问和使用。

7.1 依赖注入

Laravel的服务容器是整个框架中最重要的特性,在看完快速入门教程后,建议去研习下服务容器的文档。

创建Repository

正如我们之前提到的,我们想要定义一个TaskRepository来处理所有对Task模型的数据访问,随着应用的增长当你需要在应用中共享一些Eloquent查询时这就变得特别有用。

因此,我们创建一个app/Repositories目录并在其中创建一个TaskRepository类。记住,Laravel项目的app文件夹下的所有目录都使用 PSR-4 自动加载标准被自动加载,所以你可以在其中随心所欲地创建需要的目录:

<?php

namespace App\Repositories;

use App\User;
use App\Task;

class TaskRepository{
    /**
     * Get all of the tasks for a given user.
     *
     * @param User $user
     * @return Collection
     */
    public function forUser(User $user)
    {
        return Task::where('user_id', $user->id)
            ->orderBy('created_at', 'asc')
            ->get();
    }
}
注入Repository

Repository创建好了之后,可以简单通过在TaskController的构造函数中以类型提示的方式注入该Repository,然后就可以在index方法中使用 —— 由于Laravel使用容器来解析所有控制器,所以我们的依赖会被自动注入到控制器实例:

<?php

namespace App\Http\Controllers;

use App\Task;use App\Http\Requests;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Repositories\TaskRepository;

class TaskController extends Controller{
    /**
     * The task repository instance.
     * 
     * @var TaskRepository
     */  
    protected $tasks;

    /**
     * Create a new controller instance.
     *
     * @param TaskRepository $tasks 
     * @return void
     */
    public function __construct(TaskRepository $tasks)
    {
        $this->middleware('auth');
        $this->tasks = $tasks;
    }

    /**
     * Display a list of all of the user's task.
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request)
    {
        return view('tasks.index', [
            'tasks' => $this->tasks->forUser($request->user()),
        ]);
    }
}

7.2 显示任务

数据被传递到视图后,我们可以在tasks/index.blade.php中以表格形式显示所有任务。Blade中使用@foreach处理循环数据:

@extends('layouts.app')

@section('content')
     <!-- Create Task Form... -->

    <!-- Current Tasks -->
    @if (count($tasks) > 0)
        <div class="panel panel-default">
            <div class="panel-heading">
                Current Tasks
            </div>

            <div class="panel-body">
                <table class="table table-striped task-table">

                <!-- Table Headings -->
                <thead>
                    <th>Task</th>
                    <th>&nbsp;</th>
                </thead>

                <!-- Table Body -->
                <tbody>
                @foreach ($tasks as $task)
                    <tr>
                        <!-- Task Name -->
                        <td class="table-text">
                            <div>{{ $task->name }}</div>
                        </td>

                        <td>
                            <!-- TODO: Delete Button -->
                        </td>
                    </tr>
                @endforeach
                </tbody>
                </table>
            </div>
        </div>
    @endif
@endsection

至此,本应用基本完成。但是,当任务完成时我们还没有途径删除该任务,接下来我们就来处理这件事。

8、删除任务

8.1 添加删除按钮

我们在tasks/index.blade.php视图中留了一个“TODO”注释用于放置删除按钮。当删除按钮被点击时,DELETE /task请求被发送到应用后台并触发TaskController@destroy方法:

<tr>
    <!-- Task Name -->
    <td class="table-text">
        <div>{{ $task->name }}</div>
    </td>

    <!-- Delete Button -->
    <td>
        <form action="/task/{{ $task->id }}" method="POST">
            {{ csrf_field() }}
            {{ method_field('DELETE') }}

            <button>Delete Task</button>
        </form>
    </td>
</tr>
关于方法伪造

尽管我们使用的路由是Route::delete,但我们在删除按钮表单中使用的请求方法为POST,HTML表单只支持GET和POST两种请求方式,因此我们需要使用某种方式来伪造DELETE请求。

我们可以在表单中通过输出method_field('DELETE')来伪造DELETE请求,该函数生成一个隐藏的表单输入框,然后Laravel识别出该输入并使用其值覆盖实际的HTTP请求方法。生成的输入框如下:

<input type="hidden" name="_method" value="DELETE">

8.2 路由模型绑定

现在,我们准备在TaskController中定义destroy方法,但是,在此之前,让我们回顾下路由中对删除任务的定义:

Route::delete('/task/{task}', 'TaskController@destroy');

对应控制器TaskController中删除方法destroy定义如下:


/**
 * Destroy the given task.
 *
 * @param  Request  $request
 * @param  Task  $task
 * @return Response
 */
public function destroy(Request $request, Task $task){
    //
}

由于路由中的{task}变量与控制器方法中的$task变量相匹配,Laravel的隐式模型绑定特性将会自动注入相应的Task模型实例到destroy方法中。

8.3 用户授权

现在,我们已经将Task实例注入到destroy方法;然而,我们并不能保证当前登录认证用户是给定任务的实际拥有人。例如,一些恶意请求可能尝试通过传递随机任务ID到/tasks/{task}链接删除另一个用户的任务。因此,我们需要使用Laravel的授权功能来确保当前登录用户拥有对注入到路由中的Task实例进行删除的权限。

创建Policy

Laravel使用“策略”来将授权逻辑组织到单个类中,通常,每个策略都对应一个模型,因此,让我们使用Artisan命令创建一个TaskPolicy,生成的文件位于app/Policies/TaskPolicy.php

php artisan make:policy TaskPolicy

接下来,让我们添加destroy方法到策略中,该方法会获取一个User实例和一个Task实例。该方法简单检查用户ID和任务的user_id值是否相等。实际上,所有的策略方法都会返回truefalse

<?php

namespace App\Policies;

use App\User;
use App\Task;
use Illuminate\Auth\Access\HandlesAuthorization;

class TaskPolicy{
    use HandlesAuthorization;

    /**
     * Determine if the given user can delete the given task.
     *
     * @param User $user
     * @param Task $task
     * @return bool
     */
    public function destroy(User $user, Task $task)
    {
        return $user->id === $task->user_id;
    }
}

最后,我们需要关联Task模型和TaskPolicy,这可以通过在app/Providers/AuthServiceProvider.php文件的policies属性中添加注册来实现,注册后会告知Laravel无论何时我们尝试授权动作到Task实例时该使用哪个策略类进行判断:

/**
 * The policy mappings for the application.
 *
 * @var array
 */
protected $policies = [
    'App\Task' => 'App\Policies\TaskPolicy',
];
授权动作

现在我们编写好了策略,让我们在destroy方法中使用它。所有的Laravel控制器中都可以调用authorize方法,该方法由AuthorizesRequest trait提供:

/**
 * Destroy the given task.
 *
 * @param Request $request
 * @param Task $task
 * @return Response
 */
public function destroy(Request $request, Task $task){
    $this->authorize('destroy', $task);
    // Delete The Task...
}

我们可以检查下该方法调用:传递给authorize方法的第一个参数是我们想要调用的策略方法名,第二个参数是当前操作的模型实例。由于我们在之前告诉过Laravel,Task模型对应的策略类是TaskPolicy,所以框架知道触发哪个策略类上的destroy方法。当前用户实例会被自动传递到策略方法,所以我们不需要手动传递。

如果授权成功,代码会继续执行。如果授权失败,会抛出一个403异常并显示一个错误页面给用户。

注:除此之外,Laravel还提供了其它授权服务实现方式,可以查看授权文档了解更多。

8.4 删除任务

最后,让我们添加业务逻辑到路由中执行删除操作,我们可以使用Eloquent提供的delete方法从数据库中删除给定的模型实例。记录被删除后,跳转到/tasks页面:

/**
 * Destroy the given task.
 *
 * @param Request $request
 * @param Task $task
 * @return Response
 */
public function destroy(Request $request, Task $task){
    $this->authorize('destroy', $task);
    $task->delete();
    return redirect('/tasks');
}