Laravel 编码技巧 - 路由

作者: 温新

图书: 【Laravel 编码技巧】

阅读: 561

时间: 2024-10-11 13:56:13

路由分组

在路由中,你可以在一个组内创建子组,并为 "父" 组中的某些 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/blogen/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');
});
请登录后再评论