控制器
简介
与其把所有请求处理逻辑写成路由文件里的闭包,不如用"控制器"类来组织。控制器能把相关功能聚在一个类里——比如 UserController 专门处理用户相关的请求:展示、创建、更新、删除。控制器默认放在 app/Http/Controllers 目录。
编写控制器
基础控制器
用 make:controller 命令快速生成一个新控制器:
php artisan make:controller UserController
来看一个最基础的控制器。控制器里写若干个 public 方法,每个方法响应对应的 HTTP 请求:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* 展示指定用户的个人资料。
*/
public function show(string $id): View
{
return view('user.profile', [
'user' => User::findOrFail($id)
]);
}
}
写好控制器类和方法后,在路由文件里把它挂上去:
use App\Http\Controllers\UserController;
Route::get('/user/{id}', [UserController::class, 'show']);
当请求匹配到这条路由时,App\Http\Controllers\UserController 的 show() 方法就会被调用,路由参数也会自动传进去。
信息
控制器不强制继承基类。不过继承一个基础控制器类有时很方便——可以把所有控制器共用的方法放在基类里。
单动作控制器
如果一个控制器动作特别复杂,把整个控制器类只用于这一个动作反而更清爽。做法是:在控制器里只定义一个 __invoke() 方法:
<?php
namespace App\Http\Controllers;
class ProvisionServer extends Controller
{
/**
* 配置一台新的 Web 服务器。
*/
public function __invoke()
{
// ...
}
}
注册单动作控制器的路由时不需要指定方法名,直接把控制器类传给路由就行:
use App\Http\Controllers\ProvisionServer;
Route::post('/server', ProvisionServer::class);
你也可以用 --invokable 选项来生成这种控制器:
php artisan make:controller ProvisionServer --invokable
信息
- 控制器的模板存根可以通过发布存根来定制。
- 个人感觉单动作控制器没多大作用,控制器本身就是轻代码的,都是调用别处的代码
控制器中间件
中间件可以直接在路由文件里挂到控制器路由上:
Route::get('/profile', [UserController::class, 'show'])->middleware('auth');
不过,把中间件声明写在控制器类里面会更直观——一眼就能看出这个控制器有哪些中间件保护。做法是:让控制器实现 HasMiddleware 接口,然后定义一个静态的 middleware() 方法,返回中间件数组:
<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
class UserController implements HasMiddleware
{
/**
* 获取应用于该控制器的中间件。
*/
public static function middleware(): array
{
return [
'auth',
new Middleware('log', only: ['index']),
new Middleware('subscribed', except: ['store']),
];
}
// ...
}
你也可以返回闭包作为中间件——不用单独写一个中间件类,直接在控制器里定义内联逻辑:
use Closure;
use Illuminate\Http\Request;
/**
* 获取应用于该控制器的中间件。
*/
public static function middleware(): array
{
return [
function (Request $request, Closure $next) {
return $next($request);
},
];
}
中间件属性
你还可用 PHP 8 的 Attribute(属性)语法来给控制器挂中间件:
<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
#[Middleware('log', only: ['index'])]
#[Middleware('subscribed', except: ['store'])]
class UserController
{
// ...
}
属性也可以挂在单个方法上。方法上的中间件会和类级别的中间件合并:
<?php
namespace App\Http\Controllers;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class UserController
{
#[Middleware('log')]
#[Middleware('subscribed')]
public function index()
{
// ...
}
#[Middleware(static function (Request $request, Closure $next) {
// ...
return $next($request);
})]
public function store()
{
// ...
}
}
个人意见
不建议把以上案例中的通用中间件放在控制器中,以代码的可维护性来说,把中间件以组的形式放在路由中会更清楚,更方便管理。
授权属性
如果你用策略(Policy)来做控制器授权,可以用 Authorize 属性作为 can 中间件的快捷写法:
<?php
namespace App\Http\Controllers;
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Routing\Attributes\Controllers\Authorize;
class CommentController
{
#[Authorize('create', [Comment::class, 'post'])]
public function store(Post $post)
{
// ...
}
#[Authorize('delete', 'comment')]
public function destroy(Comment $comment)
{
// ...
}
}
第一个参数是要授权的能力名称,第二个参数是传给策略的模型类、路由参数或参数列表。
资源控制器
关于资源控制器
在国内,建议不要使用资源控制器,资源控制器按照RESTful APi 设计的, 但是以国内的环境,请求的方法只用GET和POST会更稳妥,更方便。
如果把每个 Eloquent 模型看成一种"资源",你会发现对每种资源的操作模式基本一样——创建、读取、更新、删除(CRUD)。Laravel 的资源路由让你一行代码就能生成全套 CRUD 路由。
先用 make:controller 的 --resource 选项快速生成一个资源控制器:
php artisan make:controller PhotoController --resource
这条命令会在 app/Http/Controllers/PhotoController.php 生成一个控制器,里面已经为每种资源操作准备了对应的方法。接着注册一条资源路由指向它:
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class);
这一行路由声明等于一次性注册了下面所有的路由:
资源控制器处理的动作
| 动词 | URI | 方法 | 路由名称 |
|---|---|---|---|
| GET | /photos |
index | photos.index |
| GET | /photos/create |
create | photos.create |
| POST | /photos |
store | photos.store |
| GET | /photos/{photo} |
show | photos.show |
| GET | /photos/{photo}/edit |
edit | photos.edit |
| PUT/PATCH | /photos/{photo} |
update | photos.update |
| DELETE | /photos/{photo} |
destroy | photos.destroy |
你也可以用 resources 方法一次注册多个资源控制器:
Route::resources([
'photos' => PhotoController::class,
'posts' => PostController::class,
]);
softDeletableResources 方法则一次注册多个资源控制器,并且每个资源的 show、edit、update 路由都会自动加上 withTrashed:
Route::softDeletableResources([
'photos' => PhotoController::class,
'posts' => PostController::class,
]);
自定义模型未找到时的行为
通常情况下,隐式绑定的资源模型找不到时,框架会返回 404 响应。你可以用 missing() 方法来自定义这个行为——传一个闭包,当任何资源路由的隐式绑定模型找不到时就会调用它:
use App\Http\Controllers\PhotoController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
Route::resource('photos', PhotoController::class)
->missing(function (Request $request) {
return Redirect::route('photos.index');
});
软删除模型
隐式模型绑定默认不会检索已软删除的模型,而是返回 404。如果你想在资源路由中也允许检索软删除的模型,调用 withTrashed() 方法:
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class)->withTrashed();
不传参数时,withTrashed() 会对 show、edit、update 三条路由生效。你也可以传一个数组来指定只对哪些路由生效:
Route::resource('photos', PhotoController::class)->withTrashed(['show']);
指定资源模型
如果你用了路由模型绑定,并希望资源控制器的方法参数类型提示为模型实例,可以在生成控制器时加 --model 选项:
php artisan make:controller PhotoController --model=Photo --resource
生成表单验证类
加 --requests 选项可以在生成资源控制器时,同时为 store 和 update 方法生成表单验证类:
php artisan make:controller PhotoController --model=Photo --resource --requests
局部资源路由
声明资源路由时,你可以用 only() 或 except() 只注册其中一部分动作:
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class)->only([
'index', 'show'
]);
Route::resource('photos', PhotoController::class)->except([
'create', 'store', 'update', 'destroy'
]);
API 资源路由
给 API 用的资源路由一般不需要 create 和 edit 这两个展示 HTML 模板的路由。用 apiResource() 方法,自动排除它们:
use App\Http\Controllers\PhotoController;
Route::apiResource('photos', PhotoController::class);
批量注册 API 资源控制器用 apiResources:
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\PostController;
Route::apiResources([
'photos' => PhotoController::class,
'posts' => PostController::class,
]);
生成 API 资源控制器(不含 create 和 edit 方法)用 --api 选项:
php artisan make:controller PhotoController --api
嵌套资源
有时你需要为嵌套资源定义路由。比如一张照片(photo)下面有多条评论(comment)。在路由里用"点号"表示嵌套关系:
use App\Http\Controllers\PhotoCommentController;
Route::resource('photos.comments', PhotoCommentController::class);
这条路由会注册像下面这样的嵌套 URI:
/photos/{photo}/comments/{comment}
限定嵌套资源的查询范围
Laravel 的隐式模型绑定功能可以自动限定嵌套绑定的查询范围——确保解析出的子模型确实属于父模型。在定义嵌套资源时用 scoped() 方法,既能开启自动范围限定,又能指定子资源用哪个字段检索。详见下面的章节。
浅嵌套
很多时候没必要在 URI 里同时带父级和子级的 ID——因为子级 ID 已经是全局唯一的了。如果你的模型用的是自增主键这种唯一标识,可以用"浅嵌套":
use App\Http\Controllers\CommentController;
Route::resource('photos.comments', CommentController::class)->shallow();
这条路由定义会生成以下路由(注意:show、edit、update、destroy 的 URI 里不再带 {photo}):
| 动词 | URI | 方法 | 路由名称 |
|---|---|---|---|
| GET | /photos/{photo}/comments |
index | photos.comments.index |
| GET | /photos/{photo}/comments/create |
create | photos.comments.create |
| POST | /photos/{photo}/comments |
store | photos.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} |
destroy | comments.destroy |
命名资源路由
默认情况下,所有资源控制器动作都有对应的路由名称。你可以用 names() 方法覆盖它们:
use App\Http\Controllers\PhotoController;
Route::resource('photos', PhotoController::class)->names([
'create' => 'photos.build'
]);
命名资源路由参数
Route::resource 默认会根据资源名的单数形式来生成路由参数名。用 parameters() 方法可以按资源逐个覆盖:
use App\Http\Controllers\AdminUserController;
Route::resource('users', AdminUserController::class)->parameters([
'users' => 'admin_user'
]);
上面的例子中,资源的 show 路由 URI 会变成:
/users/{admin_user}
限定资源路由的查询范围
回到嵌套资源的范围限定——用 scoped() 方法开启自动范围限定,同时指定子资源按什么字段检索:
use App\Http\Controllers\PhotoCommentController;
Route::resource('photos.comments', PhotoCommentController::class)->scoped([
'comment' => 'slug',
]);
这条路由会注册一个限定范围的嵌套资源,URI 类似:
/photos/{photo}/comments/{comment:slug}
当嵌套路由参数使用了自定义键的隐式绑定时,Laravel 会自动按父模型限定查询范围——它通过约定来推测父模型上的关联方法名。在这个例子里,框架认为 Photo 模型上有一个名为 comments 的关联方法(路由参数名的复数),用它来检索 Comment 模型。
本地化资源 URI
Route::resource 默认用英文动词和复数规则来生成 URI。如果你想本地化 create 和 edit 这两个动词,用 Route::resourceVerbs() 方法。一般在 App\Providers\AppServiceProvider 的 boot() 方法开头调用:
/**
* 启动应用的任何服务。
*/
public function boot(): void
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
}
Laravel 的复数化工具支持多种语言,你可以按需配置。定制完动词和复数化语言后,像 Route::resource('publicacion', PublicacionController::class) 这样的资源路由注册会生成以下 URI:
/publicacion/crear
/publicacion/{publicaciones}/editar
给资源控制器补充额外路由
如果想给资源控制器增加默认动作之外的额外路由,把额外路由写在 Route::resource 之前,否则 resource 方法生成的路由可能会意外覆盖你的自定义路由:
use App\Http\Controller\PhotoController;
Route::get('/photos/popular', [PhotoController::class, 'popular']);
Route::resource('photos', PhotoController::class);
信息
保持控制器聚焦。如果你经常需要标准资源动作之外的方法,考虑把一个控制器拆成两个更小的控制器。
单例资源控制器
有些资源在整个应用里只存在唯一一个实例。比如用户的"个人资料"——可以编辑、更新,但一个用户只能有一份。同理,一张图片也只能有一个"缩略图"。这类资源就是"单例资源"。用 singleton() 方法来注册单例资源控制器:
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
Route::singleton('profile', ProfileController::class);
上面的定义会注册以下路由。注意:单例资源没有"创建"类路由,而且路由 URI 里不带标识符——因为只有一个实例,不需要 ID:
| 动词 | URI | 方法 | 路由名称 |
|---|---|---|---|
| GET | /profile |
show | profile.show |
| GET | /profile/edit |
edit | profile.edit |
| PUT/PATCH | /profile |
update | profile.update |
单例资源也可以嵌套在标准资源里:
Route::singleton('photos.thumbnail', ThumbnailController::class);
在这个例子中,photos 资源会收到所有标准资源路由,而 thumbnail 是一个单例资源,路由如下:
| 动词 | URI | 方法 | 路由名称 |
|---|---|---|---|
| GET | /photos/{photo}/thumbnail |
show | photos.thumbnail.show |
| GET | /photos/{photo}/thumbnail/edit |
edit | photos.thumbnail.edit |
| PUT/PATCH | /photos/{photo}/thumbnail |
update | photos.thumbnail.update |
可创建的单例资源
偶尔你可能也需要为单例资源定义"创建"和"存储"路由。在注册时调用 creatable() 方法:
Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();
这样会额外注册以下路由(注意:可创建的单例资源还会出现 DELETE 路由):
| 动词 | URI | 方法 | 路由名称 |
|---|---|---|---|
| GET | /photos/{photo}/thumbnail/create |
create | photos.thumbnail.create |
| POST | /photos/{photo}/thumbnail |
store | photos.thumbnail.store |
| GET | /photos/{photo}/thumbnail |
show | photos.thumbnail.show |
| GET | /photos/{photo}/thumbnail/edit |
edit | photos.thumbnail.edit |
| PUT/PATCH | /photos/{photo}/thumbnail |
update | photos.thumbnail.update |
| DELETE | /photos/{photo}/thumbnail |
destroy | photos.thumbnail.destroy |
如果你只想要 DELETE 路由,不需要创建和存储路由,用 destroyable() 方法:
Route::singleton(...)->destroyable();
API 单例资源 {#api-singleton-resources}
apiSingleton() 方法用于注册供 API 使用的单例资源,同样不需要 create 和 edit 路由:
Route::apiSingleton('profile', ProfileController::class);
API 单例资源也可以加上 creatable(),这会额外注册 store 和 destroy 路由:
Route::apiSingleton('photos.thumbnail', ProfileController::class)->creatable();
中间件与资源控制器
Laravel 支持对资源路由的全部方法或特定方法分配中间件,通过 middleware()、middlewareFor() 和 withoutMiddlewareFor() 三个方法实现精细控制。
对所有方法应用中间件
用 middleware() 方法给资源或单例资源的所有路由统一挂中间件:
Route::resource('users', UserController::class)
->middleware(['auth', 'verified']);
Route::singleton('profile', ProfileController::class)
->middleware('auth');
对特定方法应用中间件
用 middlewareFor() 方法只给一个或多个指定的资源方法挂中间件:
Route::resource('users', UserController::class)
->middlewareFor('show', 'auth');
Route::apiResource('users', UserController::class)
->middlewareFor(['show', 'update'], 'auth');
Route::resource('users', UserController::class)
->middlewareFor('show', 'auth')
->middlewareFor('update', 'auth');
Route::apiResource('users', UserController::class)
->middlewareFor(['show', 'update'], ['auth', 'verified']);
middlewareFor() 同样适用于单例和 API 单例资源控制器:
Route::singleton('profile', ProfileController::class)
->middlewareFor('show', 'auth');
Route::apiSingleton('profile', ProfileController::class)
->middlewareFor(['show', 'update'], 'auth');
对特定方法排除中间件
用 withoutMiddlewareFor() 方法从资源控制器的特定方法中排除中间件:
Route::middleware(['auth', 'verified', 'subscribed'])->group(function () {
Route::resource('users', UserController::class)
->withoutMiddlewareFor('index', ['auth', 'verified'])
->withoutMiddlewareFor(['create', 'store'], 'verified')
->withoutMiddlewareFor('destroy', 'subscribed');
});
依赖注入
构造函数注入
Laravel 的服务容器负责解析所有控制器。因此,你可以在控制器的构造函数里类型提示任何依赖项——声明的依赖会自动被解析并注入到控制器实例中:
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller
{
/**
* 创建一个新的控制器实例。
*/
public function __construct(
protected UserRepository $users,
) {}
}
方法注入
除了构造函数注入,你也可以在控制器方法的参数上做类型提示。最常见的场景就是注入 Illuminate\Http\Request 实例:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* 存储一个新用户。
*/
public function store(Request $request): RedirectResponse
{
$name = $request->name;
// 存储用户...
return redirect('/users');
}
}
如果你的控制器方法还要接收路由参数,把路由参数放在依赖项之后。比如路由是这样定义的:
use App\Http\Controllers\UserController;
Route::put('/user/{id}', [UserController::class, 'update']);
你依然可以类型提示 Illuminate\Http\Request,同时通过把 $id 参数放在后面来获取它:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* 更新指定用户。
*/
public function update(Request $request, string $id): RedirectResponse
{
// 更新用户...
return redirect('/users');
}
}