控制器


简介

我们之前的演示示例都是将所有的请求处理逻辑放在路由文件的闭包函数中,这显然是不合理的,我们需要使用控制器类组织管理相对复杂的业务逻辑处理。控制器用于将相关的 HTTP 请求封装到一个类中进行处理,这些控制器类存放在 app/Http/Controllers 目录下。

控制器入门

定义控制器

下面是一个基本控制器类的例子。首先我们使用 Artisan 命令快速创建一个控制器:

php artisan make:controller UserController 

所有的 Laravel 控制器应该继承自 Laravel 自带的控制器基类 App\Http\Controllers\Controller(基类控制器提供了很多便捷的方法供子类使用,比如 middleware 等),我们为该控制器添加一个 show 方法:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\View\View;

class UserController extends Controller
{
    /**
     * Show the profile for the given user.
     *
     * @param User $user
     * @return View
     */
    public function show(User $user)
    {
        return view('user.profile', ['user' => $user]);
    }
}

我们可以在 routes/web.php 中像这样定义指向该控制器动作的路由:

use App\Http\Controllers\UserController;

Route::get('user/{user}', [UserController::class, 'show']);

这里我们在路由中使用了隐式模型绑定

现在,如果一个请求匹配上面的路由 URI,UserControllershow 方法就会被执行,当然,路由参数也会被传递给这个方法。此外,这里 show 方法里面还用到了 view 方法,该方法用于将 user 变量渲染到 user/profile 视图中,后面我们讲视图的时候会继续讨论该方法的使用,现在我们只是做简单演示,在 resources/veiws 目录下创建 user 子目录,然后在 user 目录下新建 profile.blade.php 文件,编辑文件内容如下:

{{ $user }}

这样我们在浏览器中访问 http://blog.test/user/1,就会看到打印结果了:

-w655

注:控制器并不是一定要继承自基类,不过,那样的话就不能使用一些基类提供的便利方法了,比如 middlewarevalidatedispatch 等,后面我们慢慢接触和了解到这些方法的使用。

单一动作控制器

如果你想要定义一个只处理一个动作的控制器,可以在这个控制器中定义 __invoke 方法:

<?php
    
namespace App\Http\Controllers;
    
use App\Models\User;
use App\Http\Controllers\Controller;
    
class ShowProfile extends Controller
{
    /**
     * 展示给定用户的个人主页          
     *
     * @param  int  $id
     * @return Response
     */
    public function __invoke($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

当你为这个单动作控制器注册路由的时候,不需要指定方法:

Route::get('user/{user}', \App\Http\Controllers\ShowProfile::class);

这背后的原理是在 PHP 中当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

你可以通过以下 Artisan 命令快速创建单一动作控制器,只需带上 --invokable 选项即可:

 php artisan make:controller ShowProfile --invokable

注:默认控制器模板可以通过桩发布来进行自定义。

控制器中间件

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

Route::get('profile', [UserController::class, 'show'])->middleware('auth');

不过,在控制器的构造函数中设置中间件更方便,你可以使用基类提供的 middleware 方法轻松分配中间件给该控制器的动作,你甚至可以限制中间件只应用到该控制器类的指定方法:

class UserController extends Controller
{
    /**
     * Instantiate a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        // auth 中间件会应用到所有方法
        $this->middleware('auth');
        
        // log 中间件只会应用到 index 方法
        $this->middleware('log')->only('index');

        // subscribed 中间件会应用到 store 之外的所有方法
        $this->middleware('subscribed')->except('store');
    }
}

在控制器中还可以使用闭包注册中间件,这为我们定义只在某个控制器使用的中间件提供了方便(无需定义完整的中间件类):

$this->middleware(function ($request, $next) {
    // ...

    return $next($request);
});

由于闭包函数在真正使用的时候才会执行,所有这个功能可用于在控制器中获取 Session 数据(众所周知,在 Laravel 控制器中不能直接获取 Session 数据)。

注:你还可以将中间件分配给多个控制器动作,不过,这意味着你的控制器会变得越来越臃肿,这种情况下,需要考虑将控制器分割成多个更小的控制器。

资源控制器

Laravel 的资源控制器可以让我们很便捷地构建基于资源的 RESTful 控制器,例如,你可能想要在应用中创建一个控制器,用于处理关于文章存储的 HTTP 请求,使用 Artisan 命令 make:controller,我们可以快速创建这样的控制器:

php artisan make:controller PostController --resource

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

<?php
    
namespace App\Http\Controllers;
    
use Illuminate\Http\Request;
    
class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }
    
    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }
    
    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }
    
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        //
    }
    
    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        //
    }
    
    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        //
    }
    
    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //
    }
}

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

Route::resource('posts', \App\Http\Controllers\PostController::class);

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

你可以通过传递数组到 resources 方法从而一次注册多个资源控制器:

Route::resources([
    'photos' => PhotoController::class,
    'posts' => PostController::class,
]);

资源控制器处理的动作

请求方式 URI路径 控制器方法 路由名称
GET /posts index posts.index
GET /posts/create create posts.create
POST /posts store posts.store
GET /posts/{post} show posts.show
GET /posts/{post}/edit edit posts.edit
PUT/PATCH /posts/{post} update posts.update
DELETE /posts/{post} destroy posts.destroy

指定资源模型

如果你使用了路由模型绑定,并且想要在资源控制器的方法中对模型实例进行依赖注入,可以在生成控制器的使用使用 --model 选项:

php artisan make:controller PostController --resource --model=Post

-w865

可以看到,如果 Post 类不存在的话会询问并自动创建,同时 PostController 类中的方法参数也变成了模型实例:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function show(Post $post)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function edit(Post $post)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Post $post)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function destroy(Post $post)
    {
        //
    }
}

部分资源路由

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

Route::resource('posts', PostController::class, ['only' => 
    ['index', 'show']
]);
    
Route::resource('posts', PostController::class, ['except' => 
    ['create', 'store', 'update', 'destroy']
]);

API 资源路由

声明被 API 消费的资源路由时,你可能需要排除展示 HTML 模板的路由,如 createedit,为了方便起见,Laravel 提供了 apiResource 方法自动排除这两个路由:

Route::apiResource('posts', PostController::class);

同样,你可以传递数组到 apiResources 方法以便一次注册多个 API 资源控制器:

Route::apiResources([
    'photos' => PhotoController::class,
    'posts' => PostController::class,
]);

要想快速生成不包含 createedit 方法的 API 资源控制器,可以在执行 make:controller 命令时使用 --api 开关:

php artisan make:controller API/PostController --api

嵌套资源

有时候你可能需要定义嵌套资源的路由,例如,一个文章资源可能包含多条从属它的评论,要嵌套这个资源控制器,可以在路由定义中使用「.」符号:

Route::resource('posts.comments', PostCommentController::class);

这个路由将会注册一个可以下面这种 URL 访问的嵌套资源:

/posts/{post}/comments/{comment}

嵌套资源作用域

Laravel 的隐式模型绑定功能可以自动为嵌套绑定设置作用域,这样一来解析后的子模型就可以确保归属于对应的父模型。在定义嵌套资源的时候,使用 scoped 方法你可以启用自动作用域并告知 Laravel 通过哪个字段获取子模型资源:

Route::resource('posts.comments', PostCommentController::class)->scoped([
    'comment' => 'slug',
]);

这个路由会注册一个包含作用域的嵌套资源,你可以通过如下 URI 访问嵌套资源:

/posts/{post}/comments/{comment:slug}

浅嵌套

通常,当子模型 ID 已经是唯一标识符的情况下,并不需要同时包含父模型和子模型的 ID,因此,如果在 URL 片段中使用诸如自增主键 ID 作为唯一标识符来确定模型,可以使用「浅嵌套」:

Route::resource('posts.comments', CommentController::class)->shallow();

上述路由定义会注册如下路由:

HTTP 请求方法 URI 控制器方法 路由名称
GET /posts/{post}/comments index posts.comments.index
GET /posts/{post}/comments/create create posts.comments.create
POST /posts/{post}/comments store posts.comments.store
GET /comments/{comment} show comments.show
GET /comments/{comment}/edit edit comments.edit
PUT/PATCH /comments/{comment} update comments.update
DELETE /comments/{comment} delete comments.destroy

命名资源路由

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

Route::resource('posts', PostController::class, ['names' => 
    ['create' => 'posts.build']
]);

命名资源路由参数

默认情况下,Route::resource 将会基于资源名称的单数格式为资源路由创建路由参数,你可以通过在选项数组中传递 parameters 来覆盖这一默认设置。 parameters 是资源名称和参数名称的关联数组:

Route::resource('users', AdminUserController::class, ['parameters' => [
    'users' => 'admin_user'
]]);

上面的示例代码会为资源的 show 路由生成如下 URL:

/user/{admin_user}

资源路由作用域

有时候,在资源路由定义中隐式绑定多个 Eloquent 模型时,你可能希望对第二个 Eloquent 模型设置作用域,这样一来它就必须是第一个 Eloquent 模型的子模型。例如,考虑下面这个为指定用户通过 slug 获取博客文章的场景:

use App\Http\Controllers\PostsController;

Route::resource('users.posts', PostsController::class)->scoped();

你可以通过传递数组到 scoped 方法覆盖默认的路由模型键:

use App\Http\Controllers\PostsController;

Route::resource('users.posts', PostsController::class)->scoped([
    'post' => 'slug',
]);

使用自定义键的隐式模型绑定作为嵌套路由参数时,Laravel 会自动为通过父模型上约定的关联关系名获取嵌套子模型的查询设置作用域。在这个案例中,会假设 User 模型有一个 名为 posts(路由参数名的复数格式)的关联关系用于获取 Post 模型。

本地化资源 URI

默认情况下,Route::resource 创建的资源 URI 是英文风格的,如果你需要本地化 createedit 请求路由,可以使用 Route::resourceVerbs 方法。该功能可以在 AppServiceProviderboot 方法中实现:

use Illuminate\Support\Facades\Route;
    
/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Route::resourceVerbs([
        'create' => 'xinzeng',
        'edit' => 'bianji',
    ]);
}

定制化请求方式完成后,注册资源路由如 Route::resource('wenzhang', PostController::class) 将会生成如下 URI:

/wenzhang/xinzeng
/wenzhang/{wenzhang}/bianji

好吧,你可以看出来,我是用拼音的方式对资源路由进行了本地化设置。

补充资源控制器

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

Route::get('posts/popular', [PostController::class, 'popular']);
Route::resource('posts', PostController::class);

注:注意保持控制器的单一职责,如果你发现指向控制器动作的路由超过默认提供的资源控制器动作集合了,考虑将你的控制器分割成多个更小的控制器。

依赖注入

构造函数注入

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

<?php
    
namespace App\Http\Controllers;
    
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 契约,如果容器可以解析,就可以进行依赖注入。注入依赖到控制器可以让应用更加易于测试,同时也更加方便使用。

方法注入

除了构造函数注入之外,还可以在控制器的动作方法中进行依赖注入,例如,我们可以在某个方法中注入 Illuminate\Http\Request 实例:

<?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->name;
    
        //
    }
}

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

Route::put('user/{id}', [UserController::class, 'update']);

则需要以如下方式定义控制器方法来注入 Illuminate\Http\Request 依赖并访问路由参数 id

<?php
    
namespace App\Http\Controllers;
    
use Illuminate\Http\Request;
    
class UserController extends Controller
{
    /**
     * 更新指定用户
     *
     * @param Request $request
     * @param int $id
     * @return Response
     * @translator http://laravelacademy.org
     */
    public function update(Request $request, $id)
    {
        //
    }
}

路由缓存

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

php artisan route:cache

运行完成后,每次请求都会从缓存中读取路由,所以如果你添加了新的路由需要重新生成路由缓存。因此,只有在项目部署阶段才需要运行 route:cache 命令,本地开发环境完全无此必要。

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

php artisan route:clear

点赞 取消点赞 收藏 取消收藏

<< 上一篇: CSRF 防护

>> 下一篇: 请求