本进阶指南提供了对Laravel框架更深入的介绍,包括数据库迁移、Eloquent ORM、路由、认证、授权、依赖注入、验证、视图以及Blade模板。如果你对Laravel框架或其他PHP框架已经有了基本的认识,本章节将是你新的起点,如果你完全还是新手,请从新手入门指南开始。
本节的示例仍然是构建一个任务系统,但是在上一节基础上,本任务系统将允许用户注册登录,同样完整的代码已经放到GitHub上:https://github.com/laravel/quickstart-intermediate。
首先你需要安装一个新的Laravel应用。你可以使用Homestead虚拟机或者本地PHP开发环境来运行应用。开发环境准备完毕后,可以使用Composer来安装应用:
composer create-project laravel/laravel quickstart --prefer-dist
你可以继续往下阅读,也可以选择去GitHub下载项目源码并在本地运行:
git clone https://github.com/laravel/quickstart-intermediate quickstart cd quickstart composer install php artisan migrate
关于构建本地开发环境的详细文档可查看Homestead和安装文档。
首先,我们使用迁移来定于处理所有任务的数据库。Laravel的数据库迁移使用平滑、优雅的PHP代码来提供一个简单的方式定义和修改数据表结构。团队成员们无需在本地数据库手动添加或删除列,只需要简单运行你提交到源码控制系统中的迁移即可。
由于我们允许用户注册,所以需要一张用来存储用户的表。幸运的是 Laravel已经自带了这个迁移用于创建基本的users表,我们不需要手动生成。该迁移文件默认位于database/migrations
目录下。
接下来,让我们来创建用于处理所有任务的数据表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模型。
Eloquent是Laravel默认的ORM,Eloquent使用“模型”这一概念使得从数据库存取数据变得轻松。通常,每个Eloquent模型都对应一张数据表。
首先,我们一个与users
表相对应的模型User
。Laravel已经自带了这一模型app/User
,所以我们不需要重复创建了。
接下来,我们来定义与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文档了解更多。
现在,模型已经定义好了,我们需要将它们关联起来。例如,一个User
实例对应多个Task
实例,而一个Task
实例从属于某个User
。定义关联关系后将允许我们更方便的获取关联模型:
$user = App\User::find(1); foreach ($user->tasks as $task) { echo $task->name; }
首先,我们在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); } }
接下来,我们会在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); } }
好极了!现在我们已经定义好了关联关系,接下来可以正式开始创建控制器了!
在新手入门指南创建的任务管理系统中,我们在routes.php
中使用闭包定义所有的业务逻辑。而实际上,大部分应用都会使用控制器来组织路由。
我们还保留一个路由使用闭包:/路由,该路由是用于展示给游客的引导页,我们将在该路由中渲染欢迎页面。
在Laravel中,所有的HTML模板都位于resources/views
目录,并且我们使用view
函数从路由中返回其中一个模板:
Route::get('/', function () { return view('welcome'); });
当然,我们需要创建这个视图,稍后就会。
此外,我们还要让用户注册并登录到应用。通常,在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();
由于我们需要获取和保存任务,所以还需要使用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'); } }
本应用仍然只有一个视图,该视图包含了用于添加新任务的表单和显示已存在任务的列表。为了让你更直观的查看该视图,我们将已完成的应用截图如下:
几乎所有的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指令,用于指定继承布局的子页面在这里可以注入自己的内容。接下来,我们来定义使用该布局的子视图来提供主体内容。
好了,我们已经创建了应用的布局视图,下面我们需要定义一个包含创建新任务的表单和已存在任务列表的视图,该视图文件存放在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模板,我们后续会定义这个模板。
现在,我们已经为应用定义了基本的布局和视图,然后我们回到TaskController
的index
方法:
/** * 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
路由的控制器方法来处理表单输入并添加新任务到数据库。
现在我们已经在视图中定义了表单,接下来需要编写代码到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中。简直太棒了,有木有!
我们在视图中使用了@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
实例。
现在输入验证已经做好了,接下来正式开始创建一个新任务。一旦新任务创建成功,页面会跳转到/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'); }
很好,到了这里,我们已经可以成功创建任务,接下来,我们继续添加代码到视图来显示所有任务列表。
首先,我们需要编辑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
模型所有数据的访问和使用。
Laravel的服务容器是整个框架中最重要的特性,在看完快速入门教程后,建议去研习下服务容器的文档。
正如我们之前提到的,我们想要定义一个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创建好了之后,可以简单通过在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()), ]); } }
数据被传递到视图后,我们可以在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> </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
至此,本应用基本完成。但是,当任务完成时我们还没有途径删除该任务,接下来我们就来处理这件事。
我们在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">
现在,我们准备在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
方法中。
现在,我们已经将Task
实例注入到destroy
方法;然而,我们并不能保证当前登录认证用户是给定任务的实际拥有人。例如,一些恶意请求可能尝试通过传递随机任务ID到/tasks/{task}
链接删除另一个用户的任务。因此,我们需要使用Laravel的授权功能来确保当前登录用户拥有对注入到路由中的Task
实例进行删除的权限。
Laravel使用“策略”来将授权逻辑组织到单个类中,通常,每个策略都对应一个模型,因此,让我们使用Artisan命令创建一个TaskPolicy
,生成的文件位于app/Policies/TaskPolicy.php
:
php artisan make:policy TaskPolicy
接下来,让我们添加destroy
方法到策略中,该方法会获取一个User
实例和一个Task
实例。该方法简单检查用户ID和任务的user_id
值是否相等。实际上,所有的策略方法都会返回true
或false
:
<?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还提供了其它授权服务实现方式,可以查看授权文档了解更多。
最后,让我们添加业务逻辑到路由中执行删除操作,我们可以使用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'); }