• TOC
    {:toc}

6.2 应用目录结构

6.2.1 简介

Laravel 应用默认的目录结构试图为不管是大型应用还是小型应用提供一个好的起点,当然,你可以自己按照喜好重新组织应用目录结构,Laravel 对类在何处被加载没有任何限制——只要 Composer 可以自动载入它们即可。


6.2.2 根目录

新安装的 Laravel 应用包含许多文件夹:

目录名称 目录说
app 目录包含了应用的核心代码
bootstrap 目录包含了少许文件用于框架的启动和自动载入配置
bootstrap/cache 用于包含框架生成的启动文件以提高性能
config 目录包含了应用所有的配置文件
database 目录包含了数据迁移及填充文件,如果你喜欢的话还可以将其作为 SQLite 数据库存放目录
public 目录包含了前端控制器和资源文件(图片、JavaScript、CSS等)
resources 目录包含了视图文件及原生资源文件(LESS、SASS、CoffeeScript),以及本地化文件
storage 目录包含了编译过的Blade模板、基于文件的session、文件缓存,以及其它由框架生成的文件
storage/app 目录用于存放应用要使用的文件
storage/framework 目录用于存放框架生成的文件和缓存
storage/logs 目录包含应用的日志文件
tests 目录包含自动化测试,其中已经提供了一个开箱即用的PHPUnit示例
vendor 目录包含Composer依赖

6.2.3 App目录

默认情况下,在项目的App文件夹,并且被 Composer 通过 PSR-4自动载入标准 自动加载。可以通过Artisan命令app:name来修改该命名空间。

App目录下有多个子目录。

ConsoleHttp目录提供了进入应用核心的API。

它们是两个向应用发布命令的方式。

Console目录包含了所有的Artisan命令。

Http目录包含了控制器,中间件请求等等。

Jobs目录是放置列队任务的地方,应用中的任务可以被队列化,也可以在当前请求生命周期内同步执行。

Events目录是放置事件类的地方,事件可以同于通知应用其他部分给定的动作已经发生,并提供灵活的解耦的处理。

Listeners目录包含事件的处理器类,处理器接收一个事件并提供对该事件发生后的响应逻辑。

例如,UserRegstered事件可以被SendWelcomeEmail监听器处理。

Exceptions目录包含应用的异常处理器,同时还是处理应用抛出的异常的好地方。

注意:app目录中的很多类都可以通过Artisan命令生成,要查看所有命令,使用命令:php artisan list make


  • TOC
    {:toc}

6.3 服务提交者

6.3.1 简介

服务提供者是所有laravel应用启动的中心,你自己的应用以及所有Laravel的核心服务都是通过服务提供者启动。

但是,我们所谓的启动指的是什么?通常,这意味着注册事物,包括注册服务容器绑定,事件监听器,中间件甚至路由。服务提供者是应用配置的中心。

如果你打开Laravel自带的config/app.php文件,将会看到一个providers数组,这里就是应用所要加载的所有服务提供者类,当然,其中很多是延迟加载的,也就是说不是每次请求都会被加载,只有真的用到它们的时候才会加载。

本章里你将会学习如何编写自己的服务提供者并在Laravel应用中注册它们。


6.3.2 编写服务提供者

所以得服务提供者继承自Illuminate\Support\ServiceProvider类。继承该抽象类要求至少在服务提供者中定义一个方法:register。在register方法内,你唯一要做的事情就是绑事物到服务容器,不要尝试在其中注册任何时间监听器,路由或者任何其它功能。

通过Artisan命令make:provider可以简单生成一个新的提供者:

php artisan make:provider RiakServiceProvider

文件被创建到App/Providers/RiakServiceProvider.php

6.3.2.1 register方法

正如前面所提到的,在register方法中只绑定事物到服务容器,而不要做其它事情,否则,一不小心就会用到一个尚未被加载的服务提供者提供的服务。

现在来看看一个基本的服务提供者是什么样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Providers;

use Riak\Connection;
use Illuminate\Support\ServiceProvider;

class RiakServiceProvider extends ServiceProvider{
/**
* 在容器中注册绑定.
*
* @return void
*/
public function register()
{
$this->app->singleton('Riak\Contracts\Connection', function ($app) {
return new Connection(config('riak'));
});
}
}

编辑的文件为App/Providers/RiakServiceProvider.php

这个服务提供者只定义了一个register方法,并在服务容器中使用此方法定义了一份Riak/connection的实例。如果不清楚它是如何运作的,查看下一章节服务容器

6.3.2.2 boot方法

如果我们想要在服务提供者中注册视图composer该怎么做?这就要用到boot方法,该方法在所有服务提供者被注册以后才会被调用,这就是说我们可以在其中访问框架已注册的所有其他服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class EventServiceProvider extends ServiceProvider{
/**
* Perform post-registration booting of services.
*
* @return void
*/
public function boot()
{
view()->composer('view', function () {
//
});
}

/**
* 在容器中注册绑定.
*
* @return void
*/
public function register()
{
//
}
}
  • boot方法的依赖注入

我们可以在boot方法中类型提升依赖,服务容器会自动注册你需要的依赖:

1
2
3
4
5
6
7
use Illuminate\Contracts\Routing\ResponseFactory;

public function boot(ResponseFactory $factory){
$factory->macro('caps', function ($value) {
//
});
}

6.3.3 注册服务提供者

所有服务提供者都是通过配置文件config/app.php中进行注册,该文件包含了一个列出所有服务提供者名字的providers数组,默认情况下,其中列出了所有核心服务提供者,这些服务提供者启动Laravel核心组件,比如邮件、列队、缓存等等。

要注册你自己的服务提供者,只需要将其追加到该数组中:

1
2
3
4
'providers' => [
// 其它服务提供者
App\Providers\AppServiceProvider::class,
],

6.3.4 延迟加载服务提供者

如果你的提供者仅仅只是在服务容器中注册绑定,你可以选择在延迟加载该绑,定直到注册绑定真的需要时再加载,延迟加载这样的一个提供者将会提升应用的性能,因为它不会在每次请求时都从文件系统加载。

想要延迟加载一个提供者,设置defer属性为true并定义一个provides方法,该方法返回该提供者注册的服务容器绑定:

想要延迟加载一个提供者,设置defer属性为true并定义一个provides方法,该方法返回该提供者注册的服务容器绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace App\Providers;

use Riak\Connection;
use Illuminate\Support\ServiceProvider;

class RiakServiceProvider extends ServiceProvider{
/**
* 服务提供者加是否延迟加载.
*
* @var bool
*/
protected $defer = true;

/**
* 注册服务提供者
*
* @return void
*/
public function register()
{
$this->app->singleton('Riak\Contracts\Connection', function ($app) {
return new Connection($app['config']['riak']);
});
}

/**
* 获取由提供者提供的服务.
*
* @return array
*/
public function provides()
{
return ['Riak\Contracts\Connection'];
}

}

Laravel编译并保存所有延迟服务者提供的服务以及服务提供者的类名。

然后,只有当你尝试解析其中某个服务时Laravel才会加载其服务提交者。


  • TOC
    {:toc}

6 laravel框架

6.1 一次请求的生命周期

6.1.1 简介

当我们使用现实世界中的任何工具时,如果理解了该工具的工作原理,那么用起来就会得心应手,应用开发也是如此。当你理解了开发工具如何工作,用起来就会更加游刃有余。

本文档的目标就是从一个更好、更高层面向你阐述 Laravel 框架的工作原理。通过对框架更全面的了解,一切都不再那么神秘,你将会更加自信的构建应用。

如果你不能马上理解所有这些条款,不要失去信心!先试着掌握一些基本的东西,你的知识将会随着对本文档的探索而不断提高。


6.1.2 生命周期预览

6.1.2.1 第一件事

Laravel应用的所有请求入口都是public/index.php文件,所有请求都会被web服务器(Apache/Nginx)导向这个文件。index.php文件包含的代码并不多,但是,是加载框架其他部分的起点。

index.php文件载入Composer生成的自动加载设置,然后从bootstrap/app.php脚本获取Laravel应用实例,Laravel的第一个动作就是创建服务容器实例。

6.1.2.2 HTTP/Console内核

接下来,请求被发送到 HTTP 内核或 Console 内核,这取决于进入应用的请求类型。这两个内核是所有请求都要经过的中央处理器,现在,就让我们聚焦在位于 app/Http/Kernel.php 的 HTTP 内核。

HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类,该类定义了一个 bootstrappers 数组,这个数组中的类在请求被执行前运行,这些 bootstrappers 配置了错误处理、日志、检测应用环境以及其它在请求被处理前需要执行的任务。

HTTP 内核还定义了一系列所有请求在处理前需要经过的 HTTP 中间件,这些中间件处理 HTTP 会话的读写、判断应用是否处于维护模式、验证 CSRF 令牌等等。

HTTP 内核的标志性方法 handle 处理的逻辑相当简单:获取一个 Request,返回一个 Response,把该内核想象作一个代表整个应用的大黑盒子,输入 HTTP 请求,返回 HTTP 响应。

  • 服务提供者

    内核启动过程中最重要的动作之一就是为应用载入服务提供者,应用的所有服务提供者都被配置在 config/app.php 配置文件的 providers 数组中。首先,所有提供者的 register 方法被调用,然后,所有提供者被注册之后,boot 方法被调用。

    服务提供者负责启动框架的所有各种各样的组件,比如数据库、队列、验证器,以及路由组件等,正是因为他们启动并配置了框架提供的所有特性,服务提供者是整个 Laravel 启动过程中最重要的部分。

  • 分发请求

    一旦应用被启动并且所有的服务提供者被注册,Request 将会被交给路由器进行分发,路由器将会分发请求到路由或控制器,同时运行所有路由指定的中间件。


6.1.3 聚集服务提交者

服务提供者是启动 Laravel 应用中最关键的部分,应用实例被创建后,服务提供者被注册,请求被交给启动后的应用进行处理,整个过程就是这么简单!

对 Laravel 应用如何通过服务提供者构建和启动有一个牢固的掌握非常有价值,当然,应用默认的服务提供者存放在 app/Providers 目录下。

默认情况下,AppServiceProvider 是空的,这里是添加自定义启动和服务容器绑定的最佳位置,当然,对大型应用,你可能希望创建多个服务提供者,每一个都有着更加细粒度的启动。

阅读感言

上面说了那么多,实际上可以理解为下方的简单流程。

  1. public/index.phplaravel访问的起点。创建服务容器。

  2. 请求进入内核(HTTP或Console)。

  3. HTTP内核启动,laravel最重要的应用——服务提供者。

  4. 服务提供者启动各种各样的组件。

  5. 当所有的服务提供者被注册,路由开始工作。

默认服务提供者存放在app/Providers


  • TOC
    {:toc}

5.7 Blade 模板

5.7.1 简介

Blade 是 Laravel 提供的一个非常简单但很强大的模板引擎,不同于其他流行的PHP模板引擎,Blade在视图中并不约束你使用PHP原生代码。所有的Blade视图都会被编译成原生PHP代码并缓存起来直到被修改,这意味着对应用的性能而言Blade基本上是零开销。Blade视图文件使用.blade.php文件扩展并存放在resources/views目录下。


5.7.2 模板继承

5.7.2.1 定义页面布局

使用Blade的两个最大的优点是模板继承和切片,开始之前先看一个例子。

首先,检测一个主页面布局,由于大多数web应用在不同页面中使用同一个布局,可以很方便的将这个布局定义为一个单独的Blade页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 存放在 resources/views/layouts/master.blade.php -->

<html>
<head>
<title>App Name - @yield('title')</title>
</head>
<body>
@section('sidebar')
This is the master sidebar.
@show

<div class="container">
@yield('content')
</div>
</body>
</html>

正如所看到的,该文件包含典型的HTML标记,然而,注意@section@yield指令,前者正如其名字所暗示的,定义了一个内容的片段,而后者用于显示给定片段的内容。

现在我们已经为应用定义了一个布局,接下来让我们定义继承该布局的子页面。

5.7.2.2 扩展页面布局

定义子页面的时候,可以使用Blade的@extends指令来指定子页面所继承的布局,继承一个Blade布局的视图将会使用@section指令注入内容到布局的片段中,记住,如上面例子所示,这些片段的内容将会显示在布局中使用@yield的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 存放在 resources/views/child.blade.php -->

@extends('layouts.master')

@section('title', 'Page Title')

@section('sidebar')
@parent

<p>This is appended to the master sidebar.</p>
@endsection

@section('content')
<p>This is my body content.</p>
@endsection

在本例中,sidebar片段使用@parent指令来追加内容到布局中sidebar,@parent指令在视图渲染时将会被布局中的内容替换。

当然,和原始PHP视图一样,Blade视图可以通过view方法直接从路由中返回:

1
2
3
Route::get('blade', function () {
return view('child');
});

5.7.3 数据显示

可以通过两个花括号包裹变量来显示传递到视图的数据,比如,如果给出如下路由:

1
2
3
Route::get('greeting', function () {
return view('welcome', ['name' => 'Samantha']);
});

那么可以通过如下方式显示name变量的内容:

Hello, {{ $name }}.

当然,不限制显示到视图中的变量内容,你还可以输出任何PHP函数,实际上,可以将任何PHP代码放到Blade模板语句中:

The current UNIX timestamp is {{ time() }}.

*注意:Blade的{{}}`语句已经经过PHP的`htmlentities`函数处理以避免XSS攻击。* #### 5.7.3.1 Blade & JavaScript 框架 由于很多Javascript框架也是用花括号表示,要显示在浏览器中的表达式,可以使用`@`告诉Blade渲染引擎该表达式应该保持原生格式不作改动。例如:

1
2
3
<h1>Laravel</h1>

Hello, @{{ name }}.
这里`@`符合会被Blade移除。而且,Blade引擎会保留`{{name}}表单式,如此一来可让其它Javascript框架使用。

5.7.3.2 输出存在的数据

有时候想要输出一个变量,但是不确定该变量是否被设置,我们可以通过如下PHP代码:

{{ isset($name) ? $name : 'Default' }}

处理上方的方法,Blade还提供了更简单的方式:

{{ $name or 'Default' }}

在这里,如果$name变量存在,将会显示,否则将显示Default

5.7.3.3 显示原生数据

默认情况下,Blade的{{ }}语法已经通过PHP的htmlentities函数处理以避免XSS攻击,如果不想要数据被处理,可以使用如下语法:

Hello, {!! $name !!}.

注意:对于用户输入的内容要当心,要使用双花括号以避免直接输出HTML代码。


5.7.4 流程控制

除了模板继承和数据显示之外,Blade还为常用的PHP流程提供了便利操作,比如条件语句和循环,这些快捷操作提供了一个干净,简单的方式来处理PHP的流程控制,同时保持和PHP相应语句相似。

5.7.4.1 If 语句

可以使用@if,@elseif,@else,@endif,来构造if语句,这些指令函数和PHP的相同:

1
2
3
4
5
6
7
@if (count($records) === 1)
I have one record!
@elseif (count($records) > 1)
I have multiple records!
@else
I don't have any records!
@endif

为了方便,Blade还提供了@unless指令:

1
2
3
@unless (Auth::check())
You are not signed in.
@endunless

5.7.4.2 循环

除了条件语句,Blade还提供了简单指令处理PHP支持的循环结构,同样,这些指令函数和PHP一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@for ($i = 0; $i < 10; $i++)
The current value is {{ $i }}
@endfor

@foreach ($users as $user)
<p>This is user {{ $user->id }}</p>
@endforeach

@forelse ($users as $user)
<li>{{ $user->name }}</li>
@empty
<p>No users</p>
@endforelse

@while (true)
<p>I'm looping forever.</p>
@endwhile

5.7.4.3 包含子视图

Blade的@include指令允许你很简单的在一个视图中包含另一个Blade视图,所有父级视图中变量在被包含的子视图中依然有效:

1
2
3
4
5
6
7
<div>
@include('shared.errors')

<form>
<!-- Form Contents -->
</form>
</div>

尽管被包含的视图继承所有父视图中的数据,还可以传递额外参数到被包含的视图:

@include('view.name', ['some' => 'data'])

注意:不要再Blade视图中使用__DIR____FIFL__常量,因为他们会指向缓存视图的路径。

5.7.4.4 为集合渲染视图

使用Blade的@each指令将循环或引入 压缩成一行:

@each('view.name', $jobs, 'job')

第一个参数为:对数组或集合的每个元素渲染的局部视图。

第二个参数为:要迭代的数组或集合。

第三个参数为:迭代时被分配到视图中的变数名称。

举例来说:如果迭代一个jobs数组,通常希望在局部视图中透过job变数存取每一个job。

传递第四个参数至@each指令。此参数为:当给定的数组为空时,将会被渲染的视图。

@each('view.name', $jobs, 'job', 'view.empty')

5.7.4.5 注释

Blade还允许在视图中定义注释,然而,不同于HTML注释,Blade注释并不会包含到HTML中被返回:

{{-- This comment will not be present in the rendered HTML --}}

5.7.5 服务注入

@inject指令可以用于从服务容器中获取服务,传递给@inject的第一个参数是服务将要被放置的变量名,第二个参数是要解析的服务类名或接口名:

1
2
3
4
5
@inject('metrics', 'App\Services\MetricsService')

<div>
Monthly Revenue: {{ $metrics->monthlyRevenue() }}.
</div>

5.7.6 扩展 Blade

Blade还允许你自定义指令,可以使用directive方法来注册一个指令。当Blade编辑器遇到该指令,将会传入参数并调用提供的回调。

下面的例子创建了一个@datetime($var)指令,格式化给定的$var:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace App\Providers;

use Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* @return void
*/
public function boot()
{
Blade::directive('datetime', function($expression) {
return "<?php echo with{$expression}->format('m/d/Y H:i'); ?>";
});
}

/**
* 在容器中注册绑定.
*
* @return void
*/
public function register()
{
//
}
}

laravel 辅助函数with被用在该指令中,with方法返回给定的对象或值,允许方法链。最终该指令生成的PHP代码如下:

1
<?php echo with($var)->format('m/d/Y H:i'); ?>

  • TOC
    {:toc}

5.6 视图

5.6.1 基本使用

视图包含的HTML代码并将控制器逻辑和表面逻辑进行分离。视图文件存放在resources/views目录。

下面是一个简单视图:

1
2
3
4
5
6
7
<!-- 该视图存放 resources/views/greeting.php -->

<html>
<body>
<h1>Hello, <?php echo $name; ?></h1>
</body>
</html>

由于这个视图存放在resources/views/greeting.php,可以在全局的辅助函数view中这样返回它:

1
2
3
Route::get('/', function () {
return view('greeting', ['name' => 'James']);
});

view的第一个参是resources/views目录下相应的视图文件的名字,第二个参数是一个数组,该数组包含了在视图中的有效数据。

例子中,传递的是一个name变量,在视图中通过执行echo将其显示出来。

当然,视图还可以嵌套在resources/views的子目录中,用.来引用嵌套视图。

例如,视图存放路径是resources/views/admin/profile.php,那可以这样应用它:

return view('admin.profile', $data);

5.6.1.1 判断视图是否存在

如果需要判断视图是否存在,可调用不带参数的view之后,使用exists方法,如果视图存在则返回ture:

1
2
3
if (view()->exists('emails.customer')) {
//
}

调用不带参数的view时,将会返回一个Illuminate\Contracts\View\Factory实例,从而调用该类上所有方法。


5.6.2 视图数据

5.6.2.1 传递数据到视图

在上述例子中可以看到,可以简单通过数组方式将数据传递到视图:

1
return view('greetings', ['name' => 'Victoria']);

以这种方式传递数据,$data应该是一个键值对应该数组,在视图中,就可以使用响应的键来访问数组值。

例如<?php echo $key; ?>。还可以通过with方法添加独立的数据片段到视图:

1
$view = view('greeting')->with('name', 'Victoria');

5.6.2.2 在视图间共享数据

有时候需要在所有视图之间共享数据片段,这时可以使用视图类的share方法,通常,需要在服务提供者的boot方法中调用share方法,你可以将其添加到AppServiceProvider或生成独立的服务提供者来存放它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
/**
* 启动所有应用服务
*
* @return void
*/
public function boot()
{
view()->share('key', 'value');
}

/**
* 注册服务提供者
*
* @return void
*/
public function register()
{
//
}
}

5.6.3 视图Composer

视图Composer就是在视图被渲染前,会呼叫的闭包或类别方法。

如果想在每次渲染都绑定一些数据,可以使用视图Composer将这种的程序逻辑放到同一个地方。

首先在服务提供者中注册视图Composer。

之后将使用view访问Illuminate\Contracts\View\Factory 的底层实现。

laravel不会包含默认的视图Composer目录,可以随意设置路径。

例如,创建一个App\Http\ViewComposers目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class ComposerServiceProvider extends ServiceProvider
{
/**
* 在容器中注册绑定.
*
* @return void
* @author http://laravelacademy.org
*/
public function boot()
{
// 使用基于类的composers...
view()->composer(
'profile', 'App\Http\ViewComposers\ProfileComposer'
);

// 使用基于闭包的composers...
view()->composer('dashboard', function ($view) {
});
}

/**
* 注册服务提供者.
*
* @return void
*/
public function register()
{
//
}
}

如果创建一个新的服务提供者包含视图Composer注册,需要添加该服务提供者到配置文件config/app.phpproviders数组中。

现在我们已经注册了Composer,每次profile视图被渲染时都会执行PrefileComposer@compose,接下来定义Composer类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace App\Http\ViewComposers;

use Illuminate\Contracts\View\View;
use Illuminate\Users\Repository as UserRepository;

class ProfileComposer
{
/**
* 用户仓库实现.
*
* @var UserRepository
*/
protected $users;

/**
* 创建一个新的属性composer.
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
// Dependencies automatically resolved by service container...
$this->users = $users;
}

/**
* 绑定数据到视图.
*
* @param View $view
* @return void
*/
public function compose(View $view)
{
$view->with('count', $this->users->count());
}
}

视图被渲染前,Composer类的composer方法被调用,同时Illuminate\Contracts\View\View被注入该方法,从而可以使用with方法来绑定数据到视图。

注意:所有视图Composer都通过服务容器被解析,所以可以在Composer类的构造函数中声明任何需要的依赖。

5.6.3.1 添加 Composer 到多个视图

可以传递视图数组作为composer方法的第一个参数,来一次将视图Composer添加到多个视图:

1
2
3
4
view()->composer(
['profile', 'dashboard'],
'App\Http\ViewComposers\MyViewComposer'
);

composer方法接受*通配符,从而允许将一个Composer添加到所有视图:

1
2
3
view()->composer('*', function ($view) {
//
});

5.6.4 视图创建器

视图创造器和视图Composer非常类似。不同在于,前者在视图实例化之后立即失效而不是等到视图即将渲染。

使用create方法可注册一个视图创建器:

1
view()->creator('profile', 'App\Http\ViewCreators\ProfileCreator');

  • TOC
    {:toc}

5.5 响应

5.5.1 基本响应

所有路由和控制器都会返回某种被发送到用户浏览器的响应,laravel提供多种不同的方式来返回响应,最基本的响应就是从路由或控制器返回一个简单的字符串:

1
2
3
Route::get('/', function () {
return 'Hello World';
});

给定的字符串会被框架自动转化为HTTP响应。

5.5.1.1 Response对象

大多数路由和控制器动作都会返回一个完整的Illuminate\Http\Response实例或视图,返回一个完整的Response实例允许你自定义响应的HTTP状态码和头信息。Responses实例继承自Symfony\Component\HttpFoundation\Response类,该类提供了一系列方法用于创建HTTP响应:

1
2
3
4
5
6
use Illuminate\Http\Response;

Route::get('home', function () {
return (new Response($content, $status))
->header('Content-Type', $value);
});

为了方便,还可以使用辅助函数response:

1
2
3
4
Route::get('home', function () {
return response($content, $status)
->header('Content-Type', $value);
});

注意:要查看完整的Response方法列表,看可以查看API文档Symfony API 文档

5.5.1.2 添加响应头

大部分响应方法可以用方法链形式调用,从而使得可以建立平滑的响应。

例如,你可以在响应送出给使用者之前,使用header方法增加一系列的响应头到响应:

1
2
3
4
return response($content)
->header('Content-Type', $type)
->header('X-Header-One', 'Header Value')
->header('X-Header-Two', 'Header Value');

或者,可以使用withHeaders方法指定要增加至回应的响应头数组:

1
2
3
4
5
6
return response($content)
->withHeaders([
'Content-Type' => $type,
'X-Header-One' => 'Header Value',
'X-Header-Two' => 'Header Value',
]);

5.5.1.3 添加Cookie到响应

透过响应实例的函数cookie可以轻松添加Cookie到响应:

1
2
return response($content)->header('Content-Type', $type)
->cookie('name', 'value');

cookie方法接收额外的可选参数,使你可以进一步自定cookies的属性:

1
->cookie($name, $value, $minutes, $path, $domain, $secure, $httpOnly)

默认情况下,laravel框架生成的Cookie经过了加密和签名,以免在客户端被纂改。

如果想要让特定的Cookie子集在生成时取消加密,可以使用中间件App\Http\Middleware\EncryptCookies$except属性来排除这些Cookie:

1
2
3
4
5
6
7
8
/**
* 不需要被加密的cookies名称
*
* @var array
*/
protected $except = [
'cookie_name',
];

5.5.2 其他响应类型

辅助函数response可以很方便地用来生成其他类型的响应实例,当无参数调用response时会返回Illuminate\Contracts\Routing\ResponseFactory的一个实现类实例契约,该契约提供了一些有用的方法来生成响应。

5.5.2.1 视图

如果需要控制响应状态和响应头,并还需要返回一个视图作为响应内容,可以使用view方法:

return response()->view('hello', $data)->header('Content-Type', $type);

当然,如果不需要传递自定义的HTTP状态码和头信息,只需要简单使用全局函数view即可。

5.5.2.2 JSON

json方法会自动将Content-Type头设置为application/json,并使用PHP函数json_encode方法将给定数组转化为JSON:

return response()->json(['name' => 'Abigail', 'state' => 'CA']);

如果要创建一个JSONP响应,可以在json方法之后调用setCallback方法:

return response()->json(['name' => 'Abigail', 'state' => 'CA'])
    ->setCallback($request->input('callback'));

5.5.2.3 文件下载

download方法用于生成强制用户浏览器下载给定路由文件的响应。download文件接受文件名作为第二个参数,该参数决定用户下载文件的显示名称,可以将HTTP头信息作为第三个参数传递到该方法:

return response()->download($pathToFile);
return response()->download($pathToFile, $name, $headers);

注意:管理文件下载的 Symfony HttpFoundation 类要求被下载文件有一个ASCII文件名。


5.5.3 重定向

重定向响应是Illuminate\Http\RedirectResponse类的实例,其中包含了必须的头信息将用户重定向到另一个 URL,有很多方式来生成 RedirectResponse 实例,最简单的方法就是使用全局辅助函数 redirect

1
2
3
Route::get('dashboard', function () {
return redirect('home/dashboard');
});

有时候想要将用户重定向到上一个请求的位置。

例如,表单提交后,验证不通过,就可以使用辅助函数back返回到前一个URL(使用该方法之前确保路由使用了web中间件组或都使用了session 中间件):

1
2
3
4
Route::post('user/profile', function () {
// 验证请求...
return back()->withInput();
});

5.5.3.1 重定向到命名路由

如果调用不带参数的redirect方法,会返回一个Illuminate\Routing\Redirector实例,然后可以调用该实例上的任何方法。

例如:为了生成一个RedirectResponse到命令路由,可以使用route方法:

return redirect()->route('login');

如果路由中有参数,可以将其作为第二个参数传递到route方法:

// For a route with the following URI: profile/{id}
return redirect()->route('profile', [1]);

如果要重定向到带ID参数的路由(Eloquent 模型绑定),可以传递模型本身,ID会被自动解析出来:

return redirect()->route('profile', [$user]);

5.5.3.2 重定向到控制器动作

还可以生成重定向到控制器动作,只需传递控制器和动作名到action方法即可。记住,不需要指定控制器的完整命名。laravel的RouteServiceProvider会自动设置默认的控制器命名:

return redirect()->action('HomeController@index');

当然,如果控制器路由要求参数,可以将参数作为第二个参数传递给action方法:

return redirect()->action('UserController@profile', [1]);

5.5.3.3 带一次性 Session 数据的重定向

重定向到一个新的URL并将数据存储到一次性Session中通常是同时完成的,为了方便,可以创建一个RedirectResponse实例然后在同一方法上将数据存储到Session,这种方式在action之后存储状态信息特别方便:

1
2
3
4
Route::post('user/profile', function () {
// 更新用户属性...
return redirect('dashboard')->with('status', 'Profile updated!');
});

当然,用户重定向到新页面之后,你可以从Session中取出并显示这些一次性信息。

例如,使用Blade语法实现:

1
2
3
4
5
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif

5.5.4 响应宏

如果想要定义一个自定义的响应并且在多个路由和控制器中复用,可以使用Illuminate\Contracts\Routing\ResponseFactory类,或者Response函数中的macro方法。

例如:在某个服务提供者的boot方法中编写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Providers;

use Response;
use Illuminate\Support\ServiceProvider;

class ResponseMacroServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* @return void
*/
public function boot()
{
Response::macro('caps', function ($value) {
return Response::make(strtoupper($value));
});
}
}

macro方法接收响应名称作为第一个参数,闭包函数作为第二个参数,macro的闭包在ResponseFactory类或Response函数中的macro方法时执行:

return response()->caps('foo');

  • TOC
    {:toc}

5.4 请求

5.4.1 获取请求

通过依赖注入获取当前HTTP请求实例,应该在控制器的构造函数或方法中对Illuminate\Http\Request类进行类型提示,当前请求实例会被服务容器自动注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class UserController extends Controller
{
/**
* 存储新用户
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$name=$request->input('name');

//
}
}

如果你的控制器方法还期望获取路由参数输入,只需要将路由参数置于其它依赖之后即可,例如,如果你的路由定义如下:

Route::put('user/{id}','UserController@update');

依然可以对Illuminate\Http\Request进行类型提示并通过如下方式定义控制器方法来访问路由参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

classUser Controller extends Controller
{
/**
* 更新指定用户
*
* @param Request $request
* @param int $id
* @return Response
*/
public function update(Request $request,$id)
{
//
}
}

5.4.1.1 基本请求信息

Illuminate\Http\Request实例提交了多个方法来检查应用的HTTP请求,laravel的Illuminate\Http\Request继承自Symfony\Component\HttpFoundation\Request类,下面演示了一些有用的方法:

  • 获取请求URI

    path方法将会返回请求的URI(返回的是域名之后的部分),如果进入的请求路径是http://domain.com/foo/bar,则path方法将会返回foo/bar:

      $uri=$request->path();
    

    is方法允许你验证进入的请求是否与给定模式匹配。使用该方法可以使用*通用符:

    1
    2
    3
    if($request->is('admin/*')){
    //
    }

    想要获取完整的URL,而不仅仅是路径信息,可以使用请求实例中的urlfullUrl方法:

    1
    2
    $url=$request->url();
    $url = $request->fullUrl();
  • 获取请求方法

    method方法将会返回请求的HTTP请求方式。你可以使用isMethod方法来验证HTTP请求方式是否匹配给定字符串:

    1
    2
    3
    4
    $method=$request->method();
    if($request->isMethod('post')){
    //
    }

5.4.1.2 PSR-7 请求

PSR-7标准指定了HTTP信息接口,包括请求和响应。如果想要获取PSR-7请求实例,首先需要安装一些库,laravel使用Symfony HTTP Message Bridge组件,将原的laravel请求和响应转化为PSR-7兼容的请求:

1
2
composer require symfony/psr-http-message-bridge
composer require zendframework/zend-diactoros

安装完这些库之后,只需要在路由或控制器中通过,对请求类型进行类型提示就可以获取RSR-7请求:

1
2
3
4
5
use Psr\Http\Message\ServerRequestInterface;

Route::get('/', function (ServerRequestInterface $request) {
//
});

如果从路由或控制器返回的是PSR-7响应实例,则其将会自动转化为laravel响应实例并显示出来。


5.4.2 获取输入信息

  • 获取特定输入值

    通过Illuminate\Http\Request 实例,使用简单的方法就可以取得所有的使用者输入资料。

    不需要担心发出请求时使用的HTTP动词,获取输入资料的方式都是相同的。

      $name = $request->input('name');
    

    此外,可以使用Illuminate\Http\Request 的动态属性存取使用者输入。

    例如,如果你的应用程序的表单含有一个name字段,可以使用下面的方式提交:

      $name = $request->name;
    

    还可以传统一个默认值作为第二个参数给input方法,如果请求输入值在当前请求未出现时该值将会返回:

      $name = $request->input('name', 'Sally');
    

    处理表单数组输入时,可以使用.来访问数组输入:

      $input = $request->input('products.0.name');
      $names = $request->input('products.*.name');
    
  • 判断输入值是否出现

    判断值是否在请求中出现,可以使用has方法,如果值出现且不为空,has方法返回true:

    1
    2
    3
    if ($request->has('name')) {
    //
    }
  • 获取所有输入数据

    还可以通过all方法获取所有输入数据:

      $input = $request->all();
    
  • 获取部分输入数据

    如何需要取出输入数据的子集,可以使用onlyexcept方法,这两个方法都接收一个数组或动态列表作为唯一参数:

      $input = $request->only(['username', 'password']);
      $input = $request->only('username', 'password');
      
      $input = $request->except(['credit_card']);
      $input = $request->except('credit_card');
    

5.4.2.1 上一次请求输入

laravel 允许你在两次请求之间保存输入数据,这个特性在检测校验数据失败后需要重新填充表单数据时很有用。

但如果使用的是laravel内置的验证服务,则不需要手动使用这些方法,因为一些laravel内置的校验会自动调用它们。

  • 将输入存储到一次性Session

    Illuminate\Http\Request实例的flash方法会将当前输入存放到一次性Session中,下次使用者发出请求至应用程序时就可以使用它们:

      $request->flash();
    

    还可以使用flashOnlyflashExcept方法将输入数据子集存储到Session中:

      $request->flashOnly('username', 'email');
      $request->flashExcept('password');
    
  • 将输入存储到一次性Session然后重定向

    可能常常需要使用Session中的数据,并重新定向到前一页。可以简单使用withInput方法来将输入数据链接到redirect后面:

      return redirect('form')->withInput();
      return redirect('form')->withInput($request->except('password'));
    
  • 获取上次输入数据

    要从Session中取出上次请求的输入数据,可以使用Request实例的old方法。old方法提供了便利方式从Session中取出数据:

      $username = $request->old('username');
    

    laravel还提供了一个全局的辅助函数old,如果在Blade模板中显示上次输入数据,使用辅助函数old更方便,如果给定参数没有对应输入,返回null:

      {{ old('username') }}
    

5.4.2.2 Cookies

  • 从请求中取出Cookies

    laravel 框架创建的所有cookies都经过加密并使用一个认证码进行签名,这意味着如果客户端修改了它们则需要对其进行有效性验证。使用Illuminate\Http\Request类中的 cookie方法从请求中获取cookie的值:

      $value = $request->cookie('name');
    
  • 新增Cookies到回应

    laravel 提供一个全局辅助函数cookie作为一个简单工厂来生成新的Symfony\Component\HttpFoundation\Cookie实例,新增的cookie通过withCookie方法被附加到Illuminate\Http\Response实例:

      $response = new Illuminate\Http\Response('Hello World');
      $response->withCookie(cookie('name', 'value', $minutes));
      return $response;
    

    想要创建一个长期有效的cookie,可以使用cookie工厂的forever方法:

      $response->withCookie(cookie()->forever('name', 'value'));
    

5.4.2.3 文件上传

  • 获取上传的文件

    使用Illuminate\Http\Request实例的file方法来访问上传文件,该方法返回的对象是Symfony\Component\HttpFoundation\File\UploadedFile类的一个实例,该类继承自PHP标准库中,与文件交互的SplFileInfo类:

      $file = $request->file('photo');
    

    可以使用hasFile方法,判断上传文件是否存在:

      if ($request->hasFile('photo')) {
          //
      }
    
  • 验证文件是否上传成功

    使用isValid方法判断文件在上传过程中是否出错:

      if ($request->file('photo')->isValid()){
          //
      }
      
    
  • 保存上传的文件

    如果要移动文件到新位置,必须使用move方法。该方法会将文件从暂存位置(根据机器PHP设定来确定)移动到指定的永久保存位置:

    1
    2
    3
    $request->file('photo')->move($destinationPath);

    $request->file('photo')->move($destinationPath, $fileName);
  • 其它文件方法

    UploadedFile实例中有很多可用的方法。可以到该类的API了解。

  • TOC
    {:toc}

5.3 控制器

5.3.1 简介

将所有的请求处理逻辑都放在单个routes.php中显示是不合理的,你也许还希望使用控制器类组织管理这些行为。

控制器可以将相关的HTTP请求封装到一个类中进行处理。

通常控制器存放在app/Http/Controllers目录中。


5.3.2 基本控制器

下面是一个基本控制器类的例子。所有的laravel控制器应该继承,laravel自带的控制器基类Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Http\Controllers;

use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
/**
* 为指定用户显示详情
*
* @param int $id
* @return Response
*/
public function showProfile($id)
{
return view('user.profile', ['user' => User::findOrFail($id)]);
}
}

我们可以像这样定义指向控制器动作的路由:

1
Route::get('user/{id}', 'UserController@showProfile');

现在,如果一个请求匹配上面的路由URL,UserControllershowProfile方法就会被执行。

当然,路由参数也会被传递给这个方法。

5.3.2.1 控制器 & 命名空间

在命名时,没有使用完整的控制器命名,而只定义了App\Http\Controllers之后的部分。

默认RouteServiceProvider会将一个路由群组载入routes.php文件,并且该路由群组指定了,群组中路由控制器所在的命名空间。

如果在App\Http\Controllers目录下选择使用PHP命名空间 或者 组织控制器,只需要使用相对于App\Http\Controllers命令空间的指定类名即可。因此,如果完整控制器类是App\Http\Controllers\Photos\AdminController,可以像这样注册路由:

1
Route::get('foo', 'Photos\AdminController@method');

5.3.2.2 命名控制器路由

和闭包路由一样,可以指定控制器路由的名称:

1
Route::get('foo', ['uses' => 'FooController@method', 'as' => 'name']);

你可以使用辅助函数route来为己命名的控制器路由生成对应的URL:

1
$url = route('name');

5.3.3 控制器中间件

中间件可以分配给控制器路由:

1
2
3
4
Route::get('profile', [
'middleware' => 'auth',
'uses' => 'UserController@showProfile'
]);

但是,将中间件放在控制器构造函数中更方便,在控制器的构建函数中使用middleware方法可以很轻松的分配中间件给控制器。

甚至可以限定中间件应用到控制器的指定方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UserController extends Controller
{
/**
* 实例化一个新的 UserController 实例
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('log', ['only' => ['fooAction', 'barAction']]);
$this->middleware('subscribed', ['except' => ['fooAction', 'barAction']]);
}
}

5.3.4 RESTful 资源控制器

laravel的资源控制器使得构建围绕资源的RESTful控制器变得毫无痛苦。

例如,想要在应用中创建一个控制器,用于处理关于图片存储的HTTP请求,使用Artisan命令make:controller,可以快速创建这样的控制器:

php artisan make:controller PhotoController --resource

这个Artisan命令将会生成一个控制器文件app/Http/Controllers/PhotoController.php,这个控制器包含了每一个资源操作的方法。

接下来,可以为该控制器注册一个资源路由:

Route::resource('photo', 'PhotoController');

这个理由声明包含了处理图片资源RESTful 动作的多个路由,相应地,Artisan 生成的控制器也已经为这些动作设置了对应的处理方法。

5.3.4.1 资源控制器处理的动作

方法 路径 动作 路由名称
GET /photo index photo.index
GET /photo/create create photo.create
POST /photo store photo.store
GET /photo/{photo} show photo.show
GET /photo/{photo}/edit edit photo.edit
PUT/PATCH /photo/{photo} update photo.update
DELETE /photo/{photo} destroy photo.destroy

注意:HTTP表单不支持PUT、PATCH、DELETE请求,需要添加一个隐藏的_method文件来仿照这些动词。

1
<input type="hidden" name="_method" value="PUT">

5.3.4.2 只定义部分资源路由

声明资源路由时可以指定该路由处理的动作子集:

1
2
3
4
5
Route::resource('photo', 'PhotoController',
['only' => ['index', 'show']]);

Route::resource('photo', 'PhotoController',
['except' => ['create', 'store', 'update', 'destroy']]);

5.3.4.3 命名资源路由

默认情况下,所有资源控制器动作都有一个路由名称,然而,我们可以通过传入names数组来覆盖这些默认的名字:

1
2
Route::resource('photo', 'PhotoController',
['names' => ['create' => 'photo.build']]);

5.3.4.4 补充资源控制器

如果有必要在默认资源路由之外添加额外的路由到资源控制器,应该在调用Route::resource之前定义这些路由,否则,通过resource方法定义的路由可能无意中优于补充的额外路由:

1
2
Route::get('photos/popular', 'PhotoController@method');
Route::resource('photos', 'PhotoController');

5.3.5 依赖注入 & 控制器

5.3.5.1 构造函数注入

laravel使用服务容器解析所有的Laravel控制器,因此,可以在控制器的构造函数中声明任何依赖,这些依赖会被自动解析并注入到控制器实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controller;
use App\Repositories\UserRepository;

class UserController extends Controller
{
/**
* The user repository instance.
*/
protected $users;

/**
* 创建新的控制器实例
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}

当然,还可以对任何的laravel contact 使用类别提示。若容器能解析它,就可以使用型别提示。

5.3.5.2 方法注入

除了构造函数注入外,还可以在控制器的动作方法中进行依赖的类型提示。

例如,可以对Illuminate\Http\Request 实例,其中的一个方法使用类别提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
/**
* 儲存一個新的使用者。
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$name = $request->input('name');

//
}
}

如果控制器方法期望输入路由参数,只需要将路由参数放到其他依赖之后,例如,如果你的路由定义如下:

1
Route::put('user/{id}', 'UserController@update');

你需要通过定义控制器方法如下所示来类型提示 Illuminate\Http\Request 并访问路由参数 id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class UserController extends Controller
{
/**
* 更新指定用户
*
* @param Request $request
* @param int $id
* @return Response
* @translator http://laravelacademy.org
*/
public function update(Request $request, $id)
{
//
}
}

5.3.6 路由缓存

使用控制器路由的重要性

注意:路由缓存不会作用于基于闭包的路由。要使用路由缓存,必须将闭包路由转化为控制器路由。

如果你的应用完全基于控制器路由,可以使用 Laravel 的路由缓存,使用路由缓存将会极大减少注册所有应用路由所花费的时间开销,在某些案例中,路由注册速度甚至能提高100倍!想要生成路由缓存,只需执行 Artisan 命令·route:cache:

php artisan route:cache

就这么简单!你的缓存路由文件现在取代 app/Http/routes.php 文件被使用,记住,如果你添加新的路由需要重新生成路由缓存。因此,只有在项目部署阶段才需要运行 route:cache 命令。

想要移除缓存路由文件,使用 route:clear 命令即可:

php artisan route:clear

  • TOC
    {:toc}

5.2 中间件

5.2.1 简介

laravel中间件提供一个方便的机制来过滤进入web应用的HTTP请求。

例如,laravel内置一个中间件来验证用户是否经过授权,如果用户没有经过授权,中间件会将用户重定向到登陆页面,否则如果用户经过授权,中间件就会允许请求继续往前进行下一步操作。

当然,除了认证之外,中间件还可以用来处理更多任务。

例如:CORS中间件可以为离开站点的响应添加合适的头,日记中间件可以记录所有进入站点的请求。

laravel框架自带了一些中间件,包括维护模式、认证、CSRF等等。所有的中间件都位于App/Http/Middleware目录中。


5.2.2 定义中间件

可以使用Artisan命令make:middleware创建一个新的中间件

php artisan make:middleware OldMiddleware

其中的OldMiddleware可以为任意名称。

命令会下app/Http/Middleware目录下创建一个新的中间件OldMiddlware,在这个中间件中,设定只允许age大于200的才能访问路由,否则,将用户重新定向到主页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Middleware;

use Closure;

class OldMiddleware
{
/**
* 返回请求过滤器
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->input('age') <= 200) {
return redirect('home');
}

return $next($request);
}

}

如果age<=200,中间件会返回一个HTTP重定向到客户端;否则,请求会被传统下去。将请求往下传递可以通过调用回调函数$next并传入$request

理解中间件最好的方式是将中间件看做HTTP请求,到达目标动作之前必须经过的,每一层都会检查请求并且可以完全拒绝它。

5.2.2.1 中间件之前/之后

一个中间件,在请求前执行还是请求后执行取决于中间件本身。

比如,下方中间件会在请求处理前执行一些任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Middleware;

use Closure;

class OldMiddleware
{
/**
* 返回请求过滤器
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->input('age') <= 200) {
return redirect('home');
}

return $next($request);
}

}

下方中间件会在请求处理后执行任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Http\Middleware;

use Closure;

class AfterMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);

// 执行动作

return $response;
}
}

5.2.3 注册中间件

5.2.3.1 全局中间件

如果想要中间件放在每个HTTP请求期间被执行,只需要将相应的中间件类,设置到app/Http/Kernel.php$middleware属性中。

5.2.3.2 为路由指派中间件

如果想要分配中间件到指定路由,先要在app/Http/Kernel.php文件中分配给中间件一个简写的key,默认情况下,该类的$routeMiddleware属性包含了laravel内置的入口中间件,添加自己的中间件只需要将其追加到后面并为其分配一个key:

1
2
3
4
5
6
7
// 在 App\Http\Kernel 里中
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

中间件在 HTTP Kernel 中被定义后,可以在路由选项数组中使用 $middleware 键来指定该中间件:

1
2
3
4
Route::get('admin/profile', ['middleware' => 'auth', function () {
//
}]);

使用数组分配多个中间件到路由,使用数组方式排列:

1
2
3
Route::get('/', ['middleware' => ['first', 'second'], function () {
//
}]);

处理使用数组外,还可以使用middleware方法链方式定义路由:

1
2
3
Route::get('/', function () {
//
})->middleware(['first', 'second']);

5.2.3.3 中间件群组

有时你可能想将多个中间件组成单一的键,让它可以简单的指派给路由。

这可以通过使用HTTP的$middlewareGroups实现。

laravel自带了webapi两个中间件组,包含可以应用到webUI和API路由的通用中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 应用的路由中间件组
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
],

'api' => [
'throttle:60,1',
'auth:api',
],
];

中间件组可以被分配给路由和控制器动作,使用方法与单个中间件相同。

注意:中间件组只是让指派多个中间件变得简单

1
2
3
Route::group(['middleware' => ['web']], function () {
//
});

5.2.4 中间件参数

中间件还可以接收额外的自定义参数。

例如,应用要先检查使用者是否有权限,后执行特定的操作。

可以建立RoleMiddleware来接收使用者名称作为额外的参数。

额外的中间件参数会在$next参数之后传入中间件:

注意:下方代价中指 Closure $next

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\Http\Middleware;

use Closure;

class RoleMiddleware
{
/**
* 运行请求过滤器
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role
* @return mixed
* translator http://laravelacademy.org
*/
public function handle($request, Closure $next, $role)
{
if (! $request->user()->hasRole($role)) {
// Redirect...
}

return $next($request);
}

}

中间件参数可以在定义路由时通过:分隔中间件名和参数名来指定,多个中间件参数可以通过,分隔:

1
2
3
Route::put('post/{id}', ['middleware' => 'role:editor', function ($id) {
//
}]);

5.2.5 可终止的中间件

有时中间件需要在HTTP响应发送到浏览器后做一些工作。

例如,让Session中间件在响应发送到浏览器后,将Session中的数据写到存储器中。

为了实现这个,定义一个终止中间件,并在其中使用terminable方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace Illuminate\Session\Middleware;

use Closure;

class StartSession
{
public function handle($request, Closure $next)
{
return $next($request);
}

public function terminate($request, $response)
{
// Store the session data...
}
}

这个terminate方法要接受$request$response。一旦使用终止中间,应将其添加到Http Kernel作为全局变量使用。

当调用中间件中的terminate方法时,laravel将从服务器容器中取出这个中间件的新实例。

如果想在调用handleterminate使用同一中间件实例,需要使用container(容器)singleton方法将中间注入到容器。

这里实例可以理解为数据包。

  • TOC
    {:toc}

5 基本功能

5.1 路由

5.1.1 基本路由

laravel所有的路由都可以定义在App/Http/routes.php文件,它会被App/Providers/RouteServiceProvider类载入。

下方是一个最基本的getpostget接收URL,post为一个闭包。

1
2
3
4
5
6
7
Route::get('foo', function () {
return 'Hello World';
});

Route::post('foo', function () {
//
});

默认情况下,routes.php文件包含单个路由和一个套用web中间件的路由组。

这个中间件路由组提供了session状态以及CSRF保护。

一般情况下,会将全部路由放置在这个路由组。

5.1.1.1 有效的路由方法

注册路由来响应任何HTTP请求:

下方是HTTP请求方法:

1
2
3
4
5
6
Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

有时候还需要注册路由响应多个HTTP请求,这时可以使用match方法来实现。甚至可以使用any方法注册一个路由来响应所有HTTP请求:

1
2
3
4
5
6
7
Route::match(['get', 'post'], '/', function () {
//
});

Route::any('foo', function () {
//
});

当路由过多时,使用控制器是个好方法。


5.1.2 路由参数

5.1.2.1 基础路由参数

有时可能需要在路由中捕获URL片段。比如,要从URL中捕获用户ID,需要通过下方代码定义路由参数:

1
2
3
Route::get('user/{id}', function ($id) {
return 'User '.$id;
});

可以按照需要在路由中定义多个路由参数:

1
2
3
Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
//
});

路由参数总是通过{ }进行包裹,这些参数在路由被执行时会被传递到路由的闭包。

注意:路由参数不能包含-,要使用_替换。

5.1.2.2 选择性路由参数

有时候可能需要指定可选的路由参数,这可以通过在参数后加?标记来实现,这种情况下需要指定变量的默认值:

下方代码先设定name可以为空,再设定默认name为John

1
2
3
4
5
6
7
Route::get('user/{name?}', function ($name = null) {
return $name;
});

Route::get('user/{name?}', function ($name = 'John') {
return $name;
});

5.1.2.3 正则约束

路由设置中可以使用where方法来约束路由参数。where方法接收参数名和一个正则表达式来定义参数如何被约束:

1
2
3
4
5
6
7
8
9
10
11
Route::get('user/{name}', function ($name) {
//
})->where('name', '[A-Za-z]+');

Route::get('user/{id}', function ($id) {
//
})->where('id', '[0-9]+');

Route::get('user/{id}/{name}', function ($id, $name) {
//
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

5.1.2.4 全局约束

如果想要全局约束,可以使用pattern方法。在RouteServiceProvider类的boot方式中定义约束:

1
2
3
4
5
6
7
8
9
10
11
/**
* 定义路由模型绑定,模式过滤器等
*
* @param \Illuminate\Routing\Router $router
* @return void
* @translator http://laravelacademy.org
*/
public function boot(Router $router){
$router->pattern('id', '[0-9]+');
parent::boot($router);
}

一旦模式被定义,将会自动应用到所有包含参数名的路由中:

1
2
3
Route::get('user/{id}', function ($id) {
// 只有当 {id} 是数字时才会被调用
});

上方例子中限定id只能是数字。


5.1.3 命令路由

命令路由为生成URL或重新定向提供方便。在定义路由是使用as指定路由名称:

1
2
3
Route::get('user/profile', ['as' => 'profile', function () {
//
}]);

此外,还可以为控制器动作指定路由名称:

1
2
3
Route::get('user/profile', [
'as' => 'profile', 'uses' => 'UserController@showProfile'
]);

还有另一种,路由命名的方法,比如使用name方式来实现:

1
2
3
Route::get('user/profile',function () {
//
})->name('profile');

5.1.3.1 路由群组 & 命名路由

如果你在使用路由群组,可以通过在路由群组的属性数组中指定as关键字来为群组的路由设置一个共用的路由名前缀:

1
2
3
4
5
Route::group(['as' => 'admin::'], function () {
Route::get('dashboard', ['as' => 'dashboard', function () {
// 路由被命名为 "admin::dashboard"
}]);
});

5.1.3.2 为命名路由生成URL

如果为给定路由进行了命名,就可以通过全局route函数为该命名路由生成对应URL,或者重新定向:

1
2
$url = route('profile');		//产生URL
$redirect = redirect()->route('profile'); //重新定向

如果命名路由定义了参数,可以将该参数作为第二个参数穿传递给route函数。路由参数会自动插入URL中。

1
2
3
4
Route::get('user/{id}/profile', ['as' => 'profile', function ($id) {
//
}]);
$url = route('profile', ['id' => 1]);

5.1.4 路由群组

路由组允许共享路由属性,比如中间件与命名空间等。这样利用路由群组套用这些属性到多个路由,而不需要在每个路由都设定一次。

共享属性被要求为数组格式。共享属性作为第一个参数被传递给Route::group方法。

5.1.4.1 中间件

要给路由组中所有路由分配中间件,可以在群组属性中使用middleware。中间件将会按照数组中定义的顺序依次执行:

1
2
3
4
5
6
7
8
9
Route::group(['middleware' => 'auth'], function () {
Route::get('/', function () {
// 使用 Auth 中间件
});

Route::get('user/profile', function () {
// 使用 Auth 中间件
});
});

5.1.4.2 命名空间

另一个例子,指定相同的PHP命名空间下多个控制器,可以在分组属性数组中使用namespace来指定群组中所有控制器的公共命名空间:

1
2
3
4
5
6
7
Route::group(['namespace' => 'Admin'], function(){
// 控制器在 "App\Http\Controllers\Admin" 命名空间下

Route::group(['namespace' => 'User'], function(){
// 控制器在 "App\Http\Controllers\Admin\User" 命名空间下
});
});

默认情况下,RouteServiceProvider会在命名空间群组内导入routes.php文件,让你不用指定完整App/Http/Controllers命令空间,就能注册控制器。所有,只需要指定在App/Http/Controllers之后的命名。

5.1.4.3 子域名路由

子域名可以像URL一样被分配给路由参数,从而允许捕获子域名的部分用于路由或者控制器,子域名可以通过群组属性中的domani来指定:

1
2
3
4
5
Route::group(['domain' => '{account}.myapp.com'], function () {
Route::get('user/{id}', function ($account, $id) {
//
});
});

注意:其中的{account}可以随意修改的。{id}也是可以随意更改的。

5.1.4.4 路由前缀

prefix群组属性,用来为群组中每个路由添加一个给定URL前缀。

例如,你可以为所有路由URL添加admin前缀:

1
2
3
4
5
Route::group(['prefix' => 'admin'], function () {
Route::get('users', function () {
// 匹配 "/admin/users" URL
});
});

还可以使用prefix参数为路由组指定公共路由参数:

1
2
3
4
5
Route::group(['prefix' => 'accounts/{account_id}'], function () {
Route::get('detail', function ($account_id) {
// 匹配 accounts/{account_id}/detail URL
});
});

5.1.5 CSRF保护

5.1.5.1 简介

CSRF指跨网站请求伪造。跨网站请求伪伪造是一种恶意攻击,透过经身份验证的使用者身份执行未经授权的命令。

laravel会自动产生一个CSRF令牌给每个被应用管理的有效的用户,该令牌用于验证授权用户和发起请求者是否为同一个人。

想要生成包含CSRF令牌的隐藏输入字段,可以使用csrf_field函数来实现:

1
<?php echo csrf_field(); ?>

csrf_field函数会生成如下HTML:

1
<input type="hidden" name="_token" value="<?php echo csrf_token(); ?>">

当然推荐使用Blade引擎模板提供的方式:

1
{!! csrf_field() !!}

不需要自己编写代码去验证POST、PUT或者DELETE请求的CSRF令牌,因为Laravel自带的HTTP中间件VerfyCsrfToken会完成这些工作。

只要将请求中的输入token和Session中存储的token作对比来进行验证。

5.1.5.2 不受 CSRF 保护的 URIs

有时需要从CSRF保护中排除一些URL。比如,如果使用Stripe来处理支付并用到他们的webhook系统,这时就需要从laravel的CSRF保护中排除webhook处理路由。

要实现这一目的,需要在VerifyCsrfToken中间件中将要排除的URL添加到$except属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;

class VerifyCsrfToken extends BaseVerifier
{
/**
*从CSRF验证中排除的URL
*
* @var array
*/
protected $except = [
'stripe/*',
];
}

5.1.5.3 X-CSRF-Token

除了检查当前POST参数的CDSRF令牌外,在laravel的VerifyCsrfToken中介层也会确认请求头部中的X-CSRF-Token

例如,可以将其存储在meta标签中:

1
<meta name="csrf-token" content="{{ csrf_token() }}">

一旦建立了meta标签,就可以使用jQuery之类的函数库,将令牌加入到所有请求头部。

这为基于AJAX的应用提供了简单、方便的方式来避免CSRF攻击。

1
2
3
4
5
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});

5.1.5.4 X-XSRF-Token

laravel还会将CSRF令牌保存到名为XSRF-TOKEN的Cookie中,可以使用该Cookie值来设置X-XSRF-TOKEN请求头。

一些javascript框架,比如Angular,会为你自动进行设置,基本上不太需要手动设置这个值。


5.1.6 路由模型绑定

laravel路由模型绑定提供了一个方便的方式来注入类至路由中。

例如,可以将匹配到ID的整个User类注入到路由中,而不是直接注入用户ID。

5.1.6.1 隐式绑定

laravel会自动解析定义在路由或控制器动作(变量名匹配路由片段)中的Eloquent模型类型声明,例如:

1
2
3
Route::get('api/users/{user}', function (App\User $user) {
return $user->email;
});

这个例子中,路由URL的user符合$user实例的ELoquent模型,所以laravel会自动注入与请求URL的ID对应的模型实例。

如果找不到对应的模型实例,会自动生成HTTP404响应。

  • 自定义键名

    如果想要隐式模型绑定,使用数据表的其他字段,可以重写Eloquent模型类的getRouteKeyName方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * Get the route key for the model.
    *
    * @return string
    */
    public function getRouteKeyName()
    {
    return 'slug';
    }

    现在就可以使用slug作为键值了,可以用来模糊关键字

5.1.6.2 显式绑定

要注册显式绑定,需要使用路由的model方法来为给定参数指定绑定类。

必须在RouteServiceProvider::boot方法中定义模型绑定。

  • 绑定参数到模型

    1
    2
    3
    4
    5
    public function boot(Router $router)
    {
    parent::boot($router);
    $router->model('user', 'App\User');
    }

    接下来,定义一个包含user参数的路由:

    1
    2
    3
    $router->get('profile/{user}', function(App\User $user) {
    //
    });

    由于已经绑定{user}参数到App\User模型,User实例会被注入到该路由。因此,如果请求URL是profile/1,就会注入一个用户ID为1的User实例。

    如果匹配的模型实例在数据库不存在,会自动生成并返回HTTP404响应。

  • 自定义解析逻辑

    如果想要使用自定义的解析逻辑需要使用Route::bind方法,传递到bind方法的闭关会获取到URL请求的参数中的值,并返回你想要在该路由中注入的类实例:

    1
    2
    3
    $router->bind('user', function($value) {
    return App\User::where('name', $value)->first();
    });
  • 自定义”Not Found”

    如果想要指定自己的Not Found行为,将封装该行为的闭包作为第三个参数传递给model方法

    1
    2
    3
    $router->model('user', 'App\User', function() {
    throw new NotFoundHttpException;
    });

5.1.7 表单方法伪造

HTML表单不支持PUT、PATCH或者DELETE请求方法。当使用这些路由时,需要添加一个隐藏的_method字段到表单中,其值被用作该表单的HTTP请求方法:

1
2
3
4
<form action="/foo/bar" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>

还可以使用辅助函数method_field来实现这一目的:

1
<?php echo method_field('PUT'); ?>

当然,也支持Blade模板引擎:

{method——field('PUT')}  

0%