Laravel 编码技巧 - 路由
路由分组
在路由中,你可以在一个组内创建子组,并为 "父" 组中的某些 URL 分配特定的中间件。
Route::group(['prefix' => 'account', 'as' => 'account.'], function() {
Route::get('login', [AccountController::class, 'login']);
Route::get('register', [AccountController::class, 'register']);
Route::group(['middleware' => 'auth'], function() {
Route::get('edit', [AccountController::class, 'edit']);
});
});
在模型中声明一个 resolveRouteBinding 方法
在Laravel中,路由模型绑定非常强大,但有时候我们无法仅仅通过ID让用户轻松访问资源。我们可能需要验证他们拥有资源的所有权。
你可以在你的模型中声明一个 resolveRouteBinding 方法,并在其中添加自定义逻辑。
public function resolveRouteBinding($value, $field = null)
{
$user = request()->user();
return $this->where([
['user_id' => $user->id],
['id' => $value]
])->firstOrFail();
}
为 Route::resource() 方法指定 withTrashed()
在 Laravel 9.35 之前,这个功能只适用于 Route::get()
。
Route::get('/users/{user}', function (User $user) {
return $user->email;
})->withTrashed();
从 Laravel 9.35 开始,这个功能也适用于 Route::resource()
!
Route::resource('users', UserController::class)
->withTrashed();
或者,甚至通过方法:
Route::resource('users', UserController::class)
->withTrashed(['show']);
跳过输入规范化
Laravel 会自动修剪请求中的所有字符串字段。这被称为输入规范化。
有时,你可能不希望这种行为。
你可以在 TrimStrings 中间件上使用 skipWhen 方法,并返回 true 以跳过它。
public function boot()
{
TrimStrings::skipWhen(function ($request) {
return $request->is('admin/*);
});
}
通配符子域名
您可以通过动态子域名创建路由组,并将其值传递给每个路由。
Route::domain('{username}.workspace.com')->group(function () {
Route::get('user/{id}', function ($username, $id) {
//
});
});
路由调用后是什么?
如果你使用 Laravel UI 包,你可能想知道 Auth::routes()
背后的实际路由是什么?
你可以查看文件 /vendor/laravel/ui/src/AuthRouteMethods.php
。
public function auth()
{
return function ($options = []) {
// 身份验证路由...
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');
// 注册路由...
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
}
// 密码重置路由...
if ($options['reset'] ?? true) {
$this->resetPassword();
}
// 密码确认路由...
if ($options['confirm'] ?? class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) {
$this->confirmPassword();
}
// 电子邮件验证路由...
if ($options['verify'] ?? false) {
$this->emailVerification();
}
};
}
默认情况下,该函数的使用方式非常简单:
Auth::routes(); // 无参数
但你可以通过提供参数来启用或禁用某些路由:
Auth::routes([
'login' => true,
'logout' => true,
'register' => true,
'reset' => true, // 用于重置密码
'confirm' => false, // 用于额外的密码确认
'verify' => false, // 用于电子邮件验证
]);
路由模型绑定:你可以定义一个键
你可以像 Route::get('api/users/{user}', function (App\User $user) { … }
这样来进行路由模型绑定,但不仅仅是 ID 字段,如果你想让 {user} 是 username,你可以把它放在模型中
public function getRouteKeyName() {
return 'username';
}
路由回退:当没有其他路由匹配时
如果你想为未找到的路由指定额外的逻辑,而不是仅仅抛出默认的 404 页面,你可以在你的路由文件的最后创建一个特殊的 Route。
Route::group(['middleware' => ['auth'], 'prefix' => 'admin', 'as' => 'admin.'], function () {
Route::get('/home', [HomeController::class, 'index']);
Route::resource('tasks', [Admin\TasksController::class]);
});
// 更多路由...
Route::fallback(function() {
return '哎呀,你是怎么来到这里的?';
});
使用正则验证路由参数
我们可以在路由中直接验证参数,使用 where
参数。一个典型的情况是将路由前缀设置为语言环境,例如 fr/blog
和 en/article/333
。我们如何确保这两个字母只用于语言?
routes/web.php:
Route::group([
'prefix' => '{locale}',
'where' => ['locale' => '[a-zA-Z]{2}']
], function () {
Route::get('/', [HomeController::class, 'index']);
Route::get('article/{id}', [ArticleController::class, 'show']);;
});
频率限制-全局配置和按用户配置
你可以使用 throttle:60,1
来限制一些 URL 在每分钟内最多被访问 60 次。
Route::middleware('auth:api', 'throttle:60,1')->group(function () {
Route::get('/user', function () {
//
});
});
但是,你也可以分别对公共用户和已登录用户进行限制:
// 访客的最大请求次数为10次,已认证用户的最大请求次数为60次
Route::middleware('throttle:10|60,1')->group(function () {
//
});
你还可以根据用户的数据库字段 users.rate_limit
来限制特定用户的请求次数:
Route::middleware('auth:api', 'throttle:rate_limit,1')->group(function () {
Route::get('/user', function () {
//
});
});
将查询字符串参数传递给路由
如果你在路由中传递额外的参数,这些键值对会自动添加到生成的 URL 的查询字符串中。
Route::get('user/{id}/profile', function ($id) {
//
})->name('profile');
$url = route('profile', ['id' => 1, 'photos' => 'yes']); // 结果: /user/1/profile?photos=yes
将路由按文件分离
如果你有一组与某些功能相关的路由,你可以将它们放在一个特殊的文件 routes/XXXXX.php
中,然后在 routes/web.php
中使用 include 引入它。
Taylor Otwell 在 Laravel Breeze 中的例子: routes/auth.php
Route::get('/', function () {
return view('welcome');
});
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
require __DIR__.'/auth.php';
然后,在routes/auth.php中:
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\RegisteredUserController;
// ... 更多控制器
use Illuminate\Support\Facades\Route;
Route::get('/register', [RegisteredUserController::class, 'create'])
->middleware('guest')
->name('register');
Route::post('/register', [RegisteredUserController::class, 'store'])
->middleware('guest');
// ... 十几个其他路由
但你应该只在这个单独的路由文件具有相同的前缀/中间件设置时才使用include(),否则最好将它们分组在app/Providers/RouteServiceProvider中:
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
// ... 在这里列出你的路由文件
});
}
翻译资源动词
如果你使用了资源控制器,但为了 SEO 目的想要将 URL 动词更改为非英语(例如,将如果你使用了资源控制器,但为了 SEO 目的想要将 URL 动词更改为非英语(例如,将 /create
更改为西班牙语 /crear
),你可以在 App\Providers\RouteServiceProvider
中使用 Route::resourceVerbs()
方法进行配置:
public function boot()
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
// ...
}
自定义资源路由名称
在使用资源控制器时,在 routes/web.php
中你可以指定 ->names()
参数,这样浏览器中的 URL 前缀和在整个 Laravel 项目中使用的路由名称前缀可能会不同。
Route::resource('p', ProductController::class)->names('products');
上面的代码会生成类似 /p
、/p/{id}
、/p/{id}/edit
等的 URL。但在代码中,你会通过 route('products.index')
、route('products.create')
等来调用它们。
load 预加载
如果你使用了路由模型绑定,并且你认为不会在绑定关系中使用预加载,请你再想一想。 所以当你用了这样的路由模型绑定:
public function show(Product $product) {
//
}
但是你有一个 belongsTo 关系,不能使用 $product->with('category')
进行预加载?
实际上你可以!使用 ->load()
方法加载关系:
本地化资源 URI
如果你使用了资源控制器,但是想要将 URL 谓词变为非英语形式的,比如你想要西班牙语的 /crear
而不是 /create
,你可以使用 Route::resourceVerbs()
方法来配置。
public function boot()
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
//
}
资源控制器命名
在资源控制器中,在 routes/web.php
文件中,你可以指定 ->names()
参数,这样URL前缀和路由名称前缀可能会不同。
这将生成类似 /p
、/p/{id}
、/p/{id}/edit
等的URL。但是你会这样调用它们:
- route('products.index')
- route('products.create')
- 等等
Route::resource('p', \App\Http\Controllers\ProductController::class)->names('products');
更简单的高亮你的导航栏
使用Route::is('route-name')
来更简单的高亮你的导航栏
<ul>
<li @if(Route::is('home')) class="active" @endif>
<a href="/">Home</a>
</li>
<li @if(Route::is('contact-us')) class="active" @endif>
<a href="/contact-us">Contact us</a>
</li>
</ul>
使用 route() 辅助函数生成绝对路径
route('page.show', $page->id);
// http://laravel.test/pages/1
route('page.show', $page->id, false);
// /pages/1
为你的每个模型重写路由绑定解析器
你可以为每个模型覆盖路由绑定解析器。在这个例子中,我无法控制 URL 中的 @ 符号,所以使用 resolveRouteBinding
方法,我能够移除 @ 符号并解析模型。
// 路由
Route::get('{product:slug}', Controller::class);
// 请求
https://nodejs.pub/@unlock/hello-world
// Product Model
public function resolveRouteBinding($value, $field = null)
{
$value = str_replace('@', '', $value);
return parent::resolveRouteBinding($value, $field);
}
如果你需要一个公共URL但是你想让他们更安全
如果你需要一个公共URL但是你想让他们更安全,使用 Laravel signed URL
class AccountController extends Controller
{
public function destroy(Request $request)
{
$confirmDeleteUrl = URL::signedRoute('confirm-destroy', [
$user => $request->user()
]);
// 通过电子邮件发送链接...
}
public function confirmDestroy(Request $request, User $user)
{
if (! $request->hasValidSignature()) {
abort(403);
}
// 用户通过点击电子邮件确认
$user->delete();
return redirect()->route('home');
}
}
在中间件中使用 Gate
你可以在中间件中使用在 App\Providers\AuthServiceProvider
设置的Gate
怎么做呢?你可以在路由中添加can:
和必要gate
的名字
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新帖子...
})->middleware('can:update,post');
使用箭头函数
你可以使用 PHP 箭头函数在路由中,而无需使用匿名函数。
要实现这一点,你可以使用 fn() =>
,看起来更简单。
// 而不是
Route::get('/example', function () {
return User::all();
});
// 你可以使用
Route::get('/example', fn () => User::all());
路由视图
你可以使用 Route::view($uri , $bladePage)
直接返回一个视图,而无需使用控制器函数。
//这将返回home.blade.php视图
Route::view('/home', 'home');
路由目录而不是路由文件
你可以创建一个 /routes/web/
的目录,并只填充 /routes/web.php
:
foreach(glob(dirname(__FILE__).'/web/*', GLOB_NOSORT) as $route_file){
include $route_file;
}
现在,/routes/web/
中的每个文件都充当 Web 路由器文件,你可以将路由组织到不同的文件中。
路由资源分组
如果你的路由有很多资源控制器,你可以将它们分组并调用一个 Route::resources()
,而不是许多单个 Route::resource() 语句。
Route::resources([
'photos' => PhotoController::class,
'posts' => PostController::class,
]);
自定义路由绑定
你知道在Laravel中可以定义自定义路由绑定吗?
在这个例子中,我需要通过 slug 解析一个 portfolio。但是 slug 不是唯一的,因为多个用户可以有一个名为 Foo 的 portfolio
所以我从路由参数中定义了 Laravel 应该如何解析它们
class RouteServiceProvider extends ServiceProvider
{
public const HOME = '/dashboard';
public function boot()
{
Route::bind('portfolio', function (string $slug) {
return Portfolio::query()
->whereBelongsto(request()->user())
->whereSlug($slug)
->firstOrFail();
});
}
}
Route::get('portfolios/{portfolio}', function (Portfolio $portfolio) {
/*
* $portfolio将是RouteServiceProvider中定义的查询的结果
*/
})
检查路由名称的两种方法
检查路由名称的两种方法
// #1
<a
href="{{ route('home') }}"
@class="['navbar-link', 'active' => Route::current()->getName() === 'home']"
>
Home
</a>
// #2
<a
href="{{ route('home') }}"
@class="['navbar-link', 'active' => request()->routeIs('home)]"
>
Home
</a>
绑定软删除模型的路由模型
默认情况下,使用路由模型绑定时不会检索已软删除的模型。您可以通过在路由中使用 withTrashed
来更改此行为。
Route::get('/posts/{post}', function (Post $post) {
return $post;
})->withTrashed();
删除不带查询参数的 URL
如果出于某种原因,你的URL有查询参数,你可以使用请求的 fullUrlWithoutQuery
方法来检索没有查询参数的 URL,就像这样。
// 原始URL: https://www.amitmerchant.com?search=laravel&lang=en&sort=desc
$urlWithQueryString = $request->fullUrlWithoutQuery([
'lang',
'sort'
]);
echo $urlWithQueryString;
// 输出: https://www.amitmerchant.com?search=laravel
自定义路由模型绑定中的缺失模型行为
默认情况下,Laravel 在无法绑定模型时会抛出 404 错误,但您可以通过向缺少的方法传递闭包来更改该行为。
Route::get('/users/{user}', [UsersController::class, 'show'])
->missing(function ($parameters) {
return Redirect::route('users.index');
});
从路由中排除中间件
您可以使用 withoutMiddleware
方法在 Laravel中排除路由级别的中间件
Route::post('/some/route', SomeController::class)
->withoutMiddleware([VerifyCsrfToken::class]);
控制器组
考虑使用路由控制器组,而不是在每条路由中使用控制器。自 v8.80 起添加到Laravel
// Before
Route::get('users', [UserController::class, 'index']);
Route::post('users', [UserController::class, 'store']);
Route::get('users/{user}', [UserController::class, 'show']);
Route::get('users/{user}/ban', [UserController::class, 'ban']);
// After
Route::controller(UsersController::class)->group(function () {
Route::get('users', 'index');
Route::post('users', 'store');
Route::get('users/{user}', 'show');
Route::get('users/{user}/ban', 'ban');
});