跳至内容

控制器

介绍

与其将所有请求处理逻辑都定义成路由文件中的闭包,不如使用“控制器”类来组织这些行为。控制器可以将相关的请求处理逻辑分组到一个类中。例如,一个UserController类可以处理所有与用户相关的传入请求,包括显示、创建、更新和删除用户。默认情况下,控制器存储在app/Http/Controllers目录中。

编写控制器

基本控制器

要快速生成新的控制器,你可以运行make:controllerArtisan 命令。默认情况下,应用程序的所有控制器都存储在以下app/Http/Controllers目录中:

1php artisan make:controller UserController

我们来看一个基本控制器的例子。一个控制器可以包含任意数量的公共方法,用于响应传入的 HTTP 请求:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Models\User;
6use Illuminate\View\View;
7 
8class UserController extends Controller
9{
10 /**
11 * Show the profile for a given user.
12 */
13 public function show(string $id): View
14 {
15 return view('user.profile', [
16 'user' => User::findOrFail($id)
17 ]);
18 }
19}

一旦编写了控制器类和方法,就可以像这样定义到控制器方法的路由:

1use App\Http\Controllers\UserController;
2 
3Route::get('/user/{id}', [UserController::class, 'show']);

当传入请求与指定的路由 URI 匹配时,将调用类show上的方法App\Http\Controllers\UserController,并将路由参数传递给该方法。

控制器不需要扩展基类。但是,有时扩展一个包含应在所有控制器之间共享的方法的控制器基类会很方便。

单动作控制器

如果控制器操作特别复杂,你可能会发现将整个控制器类专用于该单个操作会很方便。为此,你可以__invoke在控制器中定义一个方法:

1<?php
2 
3namespace App\Http\Controllers;
4 
5class ProvisionServer extends Controller
6{
7 /**
8 * Provision a new web server.
9 */
10 public function __invoke()
11 {
12 // ...
13 }
14}

当为单动作控制器注册路由时,你不需要指定控制器方法。相反,你可以简单地将控制器的名称传递给路由器:

1use App\Http\Controllers\ProvisionServer;
2 
3Route::post('/server', ProvisionServer::class);

--invokable您可以使用 Artisan 命令的选项生成可调用的控制器make:controller

1php artisan make:controller ProvisionServer --invokable

可以使用存根发布来定制控制器存根。

控制器中间件

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

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

或者,你可能会发现在控制器类中指定中间件更方便。为此,你的控制器应该实现HasMiddleware接口,该接口规定控制器应该有一个静态middleware方法。从这个方法中,你可以返回一个中间件数组,这些中间件应该应用于控制器的操作:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Routing\Controllers\HasMiddleware;
6use Illuminate\Routing\Controllers\Middleware;
7 
8class UserController extends Controller implements HasMiddleware
9{
10 /**
11 * Get the middleware that should be assigned to the controller.
12 */
13 public static function middleware(): array
14 {
15 return [
16 'auth',
17 new Middleware('log', only: ['index']),
18 new Middleware('subscribed', except: ['store']),
19 ];
20 }
21 
22 // ...
23}

您还可以将控制器中间件定义为闭包,这提供了一种定义内联中间件的便捷方法,而无需编写整个中间件类:

1use Closure;
2use Illuminate\Http\Request;
3 
4/**
5 * Get the middleware that should be assigned to the controller.
6 */
7public static function middleware(): array
8{
9 return [
10 function (Request $request, Closure $next) {
11 return $next($request);
12 },
13 ];
14}

控制器实现Illuminate\Routing\Controllers\HasMiddleware不应该扩展Illuminate\Routing\Controller

资源控制器

如果您将应用程序中的每个 Eloquent 模型视为一个“资源”,那么通常会对应用程序中的每个资源执行相同的操作。例如,假设您的应用程序包含一个Photo模型和一个Movie模型。用户很可能可以创建、读取、更新或删除这些资源。

由于这种常见的用例,Laravel 资源路由仅用一行代码就将典型的创建、读取、更新和删除(“CRUD”)路由分配给控制器。首先,我们可以使用make:controllerArtisan 命令的--resource选项快速创建一个控制器来处理这些操作:

1php artisan make:controller PhotoController --resource

此命令将在 处生成一个控制器app/Http/Controllers/PhotoController.php。该控制器将包含每个可用资源操作的方法。接下来,你可以注册一个指向该控制器的资源路由:

1use App\Http\Controllers\PhotoController;
2 
3Route::resource('photos', PhotoController::class);

这一个路由声明会创建多个路由来处理资源上的各种操作。生成的控制器已经为每个操作创建了相应的方法。记住,你随时可以通过运行 Artisan 命令来快速概览应用程序的路由route:list

您甚至可以通过向方法传递数组来一次注册多个资源控制器resources

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

资源控制器处理的操作

动词 URI 行动 路线名称
得到 /photos 指数 照片索引
得到 /photos/create 创造 照片.创建
邮政 /photos 店铺 照片商店
得到 /photos/{photo} 展示 照片展示
得到 /photos/{photo}/edit 编辑 照片.编辑
放置/修补 /photos/{photo} 更新 照片.更新
删除 /photos/{photo} 破坏 照片.销毁

自定义缺失模型行为

通常,如果未找到隐式绑定的资源模型,则会生成 404 HTTP 响应。但是,你可以missing在定义资源路由时调用该方法来自定义此行为。该missing方法接受一个闭包,当任何资源路由都找不到隐式绑定的模型时,将调用该闭包:

1use App\Http\Controllers\PhotoController;
2use Illuminate\Http\Request;
3use Illuminate\Support\Facades\Redirect;
4 
5Route::resource('photos', PhotoController::class)
6 ->missing(function (Request $request) {
7 return Redirect::route('photos.index');
8 });

软删除模型

通常,隐式模型绑定不会检索已被软删除的模型,而是返回 404 HTTP 响应。但是,你可以withTrashed在定义资源路由时调用该方法,指示框架允许软删除模型:

1use App\Http\Controllers\PhotoController;
2 
3Route::resource('photos', PhotoController::class)->withTrashed();

不带参数调用将允许软删除和资源路由withTrashed的模型。您可以通过将数组传递给该方法来指定这些路由的子集:showeditupdatewithTrashed

1Route::resource('photos', PhotoController::class)->withTrashed(['show']);

指定资源模型

如果您使用路由模型绑定,并希望资源控制器的方法对模型实例进行类型Prompts,则可以--model在生成控制器时使用该选项:

1php artisan make:controller PhotoController --model=Photo --resource

生成表单请求

您可以--requests在生成资源控制器时提供选项,以指示 Artisan为控制器的存储和更新方法生成表单请求类:

1php artisan make:controller PhotoController --model=Photo --resource --requests

部分资源路由

声明资源路由时,您可以指定控制器应处理的操作子集,而不是完整的默认操作集:

1use App\Http\Controllers\PhotoController;
2 
3Route::resource('photos', PhotoController::class)->only([
4 'index', 'show'
5]);
6 
7Route::resource('photos', PhotoController::class)->except([
8 'create', 'store', 'update', 'destroy'
9]);

API 资源路由

在声明将被 API 使用的资源路由时,通常需要排除包含 HTML 模板(例如create和 )的路由edit。为了方便起见,您可以使用apiResource方法来自动排除以下两个路由:

1use App\Http\Controllers\PhotoController;
2 
3Route::apiResource('photos', PhotoController::class);

您可以通过向方法传递数组来一次注册多个 API 资源控制器apiResources

1use App\Http\Controllers\PhotoController;
2use App\Http\Controllers\PostController;
3 
4Route::apiResources([
5 'photos' => PhotoController::class,
6 'posts' => PostController::class,
7]);

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

1php artisan make:controller PhotoController --api

嵌套资源

有时你可能需要定义指向嵌套资源的路由。例如,一个照片资源可能包含多个评论,这些评论可能附加到该照片。要嵌套资源控制器,你可以在路由声明中使用“点”符号:

1use App\Http\Controllers\PhotoCommentController;
2 
3Route::resource('photos.comments', PhotoCommentController::class);

此路由将注册一个嵌套资源,可以使用如下 URI 进行访问:

1/photos/{photo}/comments/{comment}

嵌套资源的作用域

Laravel 的隐式模型绑定功能可以自动限定嵌套绑定的范围,从而确认解析后的子模型属于父模型。通过scoped在定义嵌套资源时使用该方法,您可以启用自动限定范围,并指示 Laravel 应通过哪个字段来检索子资源。有关如何实现此功能的更多信息,请参阅关于限定资源路由范围的文档。

浅嵌套

通常,URI 中没有必要同时包含父 ID 和子 ID,因为子 ID 本身就具有唯一性。当使用唯一标识符(例如自增主键)在 URI 段中标识模型时,可以选择使用“浅嵌套”:

1use App\Http\Controllers\CommentController;
2 
3Route::resource('photos.comments', CommentController::class)->shallow();

该路线定义将定义以下路线:

动词 URI 行动 路线名称
得到 /photos/{photo}/comments 指数 照片.评论.索引
得到 /photos/{photo}/comments/create 创造 照片.评论.创建
邮政 /photos/{photo}/comments 店铺 照片.评论.商店
得到 /comments/{comment} 展示 评论.显示
得到 /comments/{comment}/edit 编辑 评论.编辑
放置/修补 /comments/{comment} 更新 评论.更新
删除 /comments/{comment} 破坏 评论.销毁

命名资源路由

默认情况下,所有资源控制器操作都有一个路由名称;但是,您可以通过传递names包含所需路由名称的数组来覆盖这些名称:

1use App\Http\Controllers\PhotoController;
2 
3Route::resource('photos', PhotoController::class)->names([
4 'create' => 'photos.build'
5]);

命名资源路由参数

默认情况下,Route::resource将根据资源名称的“单数化”版本为资源路由创建路由参数。您可以使用该parameters方法轻松地为每个资源覆盖此方法。传递给该方法的数组parameters应该是资源名称和参数名称的关联数组:

1use App\Http\Controllers\AdminUserController;
2 
3Route::resource('users', AdminUserController::class)->parameters([
4 'users' => 'admin_user'
5]);

上面的示例为资源的show路由生成以下 URI:

1/users/{admin_user}

确定资源路由的范围

Laravel 的作用域隐式模型绑定功能可以自动确定嵌套绑定的作用域,从而确认解析后的子模型属于父模型。通过scoped在定义嵌套资源时使用该方法,您可以启用自动作用域,并指示 Laravel 应通过哪个字段检索子资源:

1use App\Http\Controllers\PhotoCommentController;
2 
3Route::resource('photos.comments', PhotoCommentController::class)->scoped([
4 'comment' => 'slug',
5]);

此路由将注册一个范围内的嵌套资源,可以使用如下 URI 进行访问:

1/photos/{photo}/comments/{comment:slug}

当使用自定义键值隐式绑定作为嵌套路由参数时,Laravel 会自动将查询范围限定为通过其父级检索嵌套模型,并使用约定猜测父级上的关系名称。在这种情况下,将假定该Photo模型具有一个名为(路由参数名称的复数)的关系comments,该关系可用于检索Comment模型。

本地化资源 URI

默认情况下,Route::resource将使用英语动词和复数规则创建资源 URI。如果您需要本地化createedit动作动词,可以使用方法。这可以在应用程序的 中方法Route::resourceVerbs的开头完成bootApp\Providers\AppServiceProvider

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 Route::resourceVerbs([
7 'create' => 'crear',
8 'edit' => 'editar',
9 ]);
10}

Laravel 的复数器支持多种不同的语言,您可以根据需要进行配置。一旦动词和复数语言配置完成,注册如下资源路由Route::resource('publicacion', PublicacionController::class)将生成以下 URI:

1/publicacion/crear
2
3/publicacion/{publicaciones}/editar

补充资源控制器

如果您需要在默认资源路由集之外向资源控制器添加其他路由,则应在调用该Route::resource方法之前定义这些路由;否则,该方法定义的路由resource可能会无意中优先于您的补充路由:

1use App\Http\Controller\PhotoController;
2 
3Route::get('/photos/popular', [PhotoController::class, 'popular']);
4Route::resource('photos', PhotoController::class);

记住要保持控制器的专注。如果你发现自己经常需要一些常规资源操作之外的方法,可以考虑将控制器拆分成两个更小的控制器。

单例资源控制器

有时,你的应用程序会有一些资源可能只有一个实例。例如,用户的“个人资料”可以被编辑或更新,但一个用户可能只有一个“个人资料”。同样,一张图片可能只有一个“缩略图”。这些资源被称为“单例资源”,这意味着该资源可能存在且只有一个实例。在这些场景中,你可以注册一个“单例”资源控制器:

1use App\Http\Controllers\ProfileController;
2use Illuminate\Support\Facades\Route;
3 
4Route::singleton('profile', ProfileController::class);

上面的单例资源定义将注册以下路由。如您所见,单例资源未注册“创建”路由,并且已注册的路由不接受标识符,因为该资源可能只有一个实例:

动词 URI 行动 路线名称
得到 /profile 展示 个人资料显示
得到 /profile/edit 编辑 个人资料.编辑
放置/修补 /profile 更新 个人资料.更新

单例资源也可以嵌套在标准资源中:

1Route::singleton('photos.thumbnail', ThumbnailController::class);

在此示例中,photos资源将接收所有标准资源路由;但是,该thumbnail资源将是具有以下路由的单例资源:

动词 URI 行动 路线名称
得到 /photos/{photo}/thumbnail 展示 照片.缩略图.显示
得到 /photos/{photo}/thumbnail/edit 编辑 照片.缩略图.编辑
放置/修补 /photos/{photo}/thumbnail 更新 照片.缩略图.更新

可创建的单例资源

有时,你可能需要为单例资源定义创建和存储路由。为此,你可以creatable在注册单例资源路由时调用该方法:

1Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();

在此示例中,将注册以下路由。如您所见,DELETE还将为可创建的单例资源注册一个路由:

动词 URI 行动 路线名称
得到 /photos/{photo}/thumbnail/create 创造 照片.缩略图.创建
邮政 /photos/{photo}/thumbnail 店铺 照片缩略图商店
得到 /photos/{photo}/thumbnail 展示 照片.缩略图.显示
得到 /photos/{photo}/thumbnail/edit 编辑 照片.缩略图.编辑
放置/修补 /photos/{photo}/thumbnail 更新 照片.缩略图.更新
删除 /photos/{photo}/thumbnail 破坏 照片.缩略图.销毁

如果您希望 Laravel 注册DELETE单例资源的路由但不注册创建或存储路由,您可以使用该destroyable方法:

1Route::singleton(...)->destroyable();

API 单例资源

apiSingleton方法可用于注册将通过 API 进行操作的单例资源,从而无需使用createedit路由:

1Route::apiSingleton('profile', ProfileController::class);

当然,API单例资源也可能是creatable,它将为资源注册store和路由:destroy

1Route::apiSingleton('photos.thumbnail', ProfileController::class)->creatable();

依赖注入和控制器

构造函数注入

Laravel服务容器用于解析所有 Laravel 控制器。因此,你可以在控制器的构造函数中为它所需的任何依赖项添加类型Prompts。声明的依赖项将自动解析并注入到控制器实例中:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Repositories\UserRepository;
6 
7class UserController extends Controller
8{
9 /**
10 * Create a new controller instance.
11 */
12 public function __construct(
13 protected UserRepository $users,
14 ) {}
15}

方法注入

除了构造函数注入之外,你还可以在控制器的方法上进行类型Prompts依赖关系。方法注入的一个常见用例是将Illuminate\Http\Request实例注入到控制器方法中:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Http\RedirectResponse;
6use Illuminate\Http\Request;
7 
8class UserController extends Controller
9{
10 /**
11 * Store a new user.
12 */
13 public function store(Request $request): RedirectResponse
14 {
15 $name = $request->name;
16 
17 // Store the user...
18 
19 return redirect('/users');
20 }
21}

如果你的控制器方法也需要路由参数的输入,请在其他依赖项之后列出路由参数。例如,如果你的路由定义如下:

1use App\Http\Controllers\UserController;
2 
3Route::put('/user/{id}', [UserController::class, 'update']);

您仍然可以通过定义控制器方法来输入PromptsIlluminate\Http\Request并访问您的参数,如下所示:id

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Http\RedirectResponse;
6use Illuminate\Http\Request;
7 
8class UserController extends Controller
9{
10 /**
11 * Update the given user.
12 */
13 public function update(Request $request, string $id): RedirectResponse
14 {
15 // Update the user...
16 
17 return redirect('/users');
18 }
19}