理解 Laravel:TrustProxies Middleware

Laravel 项目使用负载均衡(load balancers)或者反向代理(reverse proxies)时,可能会遇到HTTPS检测或客户端IP地址不正确的问题。Laravel 的 TrustProxies 中间件提供了解决方案。

理解 TrustProxies 的必要性

使用负载均衡或反向代理时,传入的请求需要经过这个中间层才能到达你的应用。这可能导致两个问题:

  • HTTPS 检测可能失败,导致生成不安全的 URLs;
  • 客户端的IP地址可能被错误地识别为代理的IP,而不是实际的客户端。

TrustProxies 中间件允许你的应用程序信任代理设置的标头,从而解决这些问题。

配置 TrustProxies

bootstrap/app.php 文件中编辑:

1
2
3
4
5
6
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: [
'192.168.1.1',
'10.0.0.0/8',
]);
})

此配置告诉Laravel信任IP地址为 192.168.1.110.0.0.0/8 范围内的任何IP的代理。

信任所有代理

1
2
3
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})

须慎用。

配置可信任的 Headers

1
2
3
4
5
6
7
8
9
10
use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(headers: Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB
);
})

参考:

https://www.harrisrafto.eu/securing-your-laravel-app-behind-load-balancers-mastering-the-trustproxies-middleware

Laravel Blade:@stack

Laravel Blade 模板中的 @stack 标签对于指定子视图可能需要的 JavaScript 或 CSS 文件等特别有用。

stack 顾名思义是堆栈的意思,这里的操作则是入栈。

基本使用

1
2
3
4
5
// In your layout
<head>
<!--comment0-->
@stack('scripts')
</head>
1
2
3
4
// In a child view
@push('scripts')
<script src="/example.js"></script>
@endpush

进阶使用

@prepend

加入到堆栈的开始

1
2
3
@prepend('scripts')
<script src="/first-to-load.js"></script>
@endprepend

@pushIf

根据条件入栈

1
2
3
@pushIf($shouldPushScript, 'scripts')
<script src="/conditional-script.js"></script>
@endPushIf

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'My App')</title>
<link rel="stylesheet" href="/app.css">
@stack('styles')
</head>
<body>
<nav>
<!--comment1-->
</nav>

<main>
@yield('content')
</main>

<footer>
<!--comment2-->
</footer>

<script src="/app.js"></script>
@stack('scripts')
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// resources/views/posts/show.blade.php
@extends('layouts.app')

@section('title', 'View Post')

@section('content')
<h1>{{ $post->title }}</h1>
<p>{{ $post->content }}</p>
@endsection

@push('styles')
<link rel="stylesheet" href="/posts.css">
@endpush

@push('scripts')
<script src="/posts.js"></script>
@endpush
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// resources/views/posts/create.blade.php
@extends('layouts.app')

@section('title', 'Create Post')

@section('content')
<h1>Create a New Post</h1>
<!--comment3-->
@endsection

@push('styles')
<link rel="stylesheet" href="/markdown-editor.css">
@endpush

@push('scripts')
<script src="/markdown-editor.js"></script>
<script>
initializeMarkdownEditor();
</script>
@endpush

参考:

https://www.harrisrafto.eu/mastering-blade-stacks-organizing-your-laravel-views-with-precision

Laravel 重设 Session ID

在 Web 应用中,Session ID 用户维护浏览器(客户端)和服务器端的连接状态,用户登录时选择“记住我”,下次打开时默认是登录状态。出于一些安全因素的考虑,可以通过重新生成 Session ID 而断开连接:比如用户修改密码后要求重新登陆,一个账号在新的浏览器登陆后强制其他浏览器的登陆状态下线。

手动生成 Session ID

1
$request->session()->regenerate();

销毁 Session (ID)

1
$request->session()->invalidate();

案例:

用户修改密码后重新生成 Session ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function changePassword(Request $request)
{
$request->validate([
'current_password' => ['required', 'current_password'],
'new_password' => ['required', 'confirmed', 'min:8'],
]);

$user = $request->user();
$user->update([
'password' => Hash::make($request->new_password),
]);

// Regenerate the session ID
$request->session()->regenerate();

return redirect()->route('profile')
->with('status', 'Password changed successfully.');
}

使用 remember token 认证后,重新生成 Session ID:

1
2
3
if (Auth::viaRemember()) {
$request->session()->regenerate();
}

参考:

https://www.harrisrafto.eu/enhancing-security-with-session-id-regeneration-in-laravel/

Laravel 预加载(Eager Loaded )限定条数

常见的用例:评论列表中每条评论下最多显示三条回复。

下面的代码示例是查询每个作者最近的三篇文章。

1
2
3
4
5
6
7
8
9
use App\Models\Comment;
use App\Models\Author;
use Illuminate\Contracts\Database\Eloquent\Builder;

Author::query()
->with([
'posts' => fn (Builder $query): Builder => $query->limit(3),
])
->get();

生成的 sql:

1
SELECT * FROM `authors`
1
2
3
4
5
6
7
8
9
10
11
SELECT *
FROM
(
SELECT
*,
row_number() OVER (PARTITION BY `posts`.`author_id`) AS `laravel_row`
FROM `posts`
WHERE `posts`.`author_id` IN (1, 2, 3, 4, 5)
) AS `laravel_table`
WHERE `laravel_row` <= 3
ORDER BY `laravel_row`

参考:
https://ashallendesign.co.uk/blog/limit-eager-loaded-relationships

PHP OPCache:Laravel性能提升的秘密武器

OPCache 优化原理

  • 编译缓存:当执行 PHP 脚本时,OPCache 会将编译后的字节码存储在内存中。
  • 内存存储:对同一脚本的后续请求由内存提供,避免了磁盘 I/O 和编译开销。
  • 自动失效:检测到更改时,OPCache 会自动失效并重新编译脚本。

使用 OPCache 的好处

  • 减少加载时间:减少加载和执行 PHP 脚本所需的时间。
  • 降低 CPU 使用率:通过提供预编译脚本来减少 CPU 负载。
  • 改进的可扩展性:增强了使用相同服务器资源处理更多请求的能力。
  • 成本效益:通过优化资源使用来降低服务器成本。

安装和启用 OPCache

1. 安装 OPCache

使用 php -m 命令可查看有没有安装 OPCache 模块:

1
php -m 

如果未安装,参考如下为 php8.3 版本的命令,下文中都是基于 php8.3 版本:

1
sudo apt install php8.3-opcache

2. 开启 OPCache

需要在 php.ini 文件中配置,可通过 php --ini 命令查看 php.ini 文件的位置:

1
php --ini

php.ini 文件中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; Enable OPCache extension
zend_extension=opcache.so

; Recommended OPCache settings
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.validate_timestamps=1
opcache.file_cache=/path/to/cache
opcache.file_update_protection=2
opcache.max_wasted_percentage=5

3.了解 OPCache 设置

  • opcache.enable=1: Enables OPCache for PHP.
  • opcache.memory_consumption=128: Allocates 128MB of memory for storing precompiled scripts.
  • opcache.interned_strings_buffer=8: Allocates 8MB for interned strings in memory.
  • opcache.max_accelerated_files=10000: Sets the maximum number of files that can be cached.
  • opcache.revalidate_freq=2: Sets the frequency (in seconds) for checking script timestamps to see if they have been updated.
  • opcache.fast_shutdown=1: Enables fast shutdown to reduce memory usage when scripts are terminated.
  • opcache.enable_cli=1: Enables OPCache for the CLI version of PHP. This is useful for speeding up long-running PHP scripts executed from the command line.
  • opcache.validate_timestamps=1: When enabled, OPCache checks the timestamps of files to see if they have been updated. If a file is updated, it is recompiled. By default, it is enabled.
  • opcache.file_cache=/path/to/cache: Specifies the directory where OPCache should store cached scripts if they cannot be stored in shared memory.
  • opcache.file_update_protection=2: Ensures that cached scripts are not accessed until at least this many seconds have passed since they were last modified.
  • opcache.max_wasted_percentage=5: The percentage of “wasted” memory (due to fragmentation, etc.) that OPCache can tolerate before it triggers a restart of the cache to reclaim memory.

4. 为 Laravel 配置 OPCache

为了优化Laravel的OPCache,微调配置参数至关重要。以下是一些推荐的设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; Increase memory consumption to handle more scripts
opcache.memory_consumption=256

; Higher number of interned strings buffer for better performance
opcache.interned_strings_buffer=16

; Max number of scripts that can be cached
opcache.max_accelerated_files=20000

; Frequency of file status checks (in seconds)
opcache.revalidate_freq=60

; Enable file cache for scripts that can\'t be stored in shared memory
opcache.file_cache=/tmp

; Enable optimization for faster execution
opcache.opt_debug_level=0

创建 Laravel 预加载脚本

预加载允许您在启动时将一组PHP文件加载到内存中,使其可用于所有请求。这可以进一步减少常用类的加载时间。

1.创建预加载脚本

在 Laravel 应用程序的根目录中创建一个 `preload.php 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<?php

require_once __DIR__ . '/vendor/autoload.php';

class Preloader
{
private array $ignores = [];

private static int $count = 0;

private array $paths;

private array $fileMap;

public function __construct(string ...$paths)
{
$this->paths = $paths;

// We'll use composer's classmap
// to easily find which classes to autoload,
// based on their filename
$classMap = require __DIR__ . '/vendor/composer/autoload_classmap.php';

$this->fileMap = array_flip($classMap);
}

public function paths(string ...$paths): Preloader
{
$this->paths = array_merge(
$this->paths,
$paths
);

return $this;
}

public function ignore(string ...$names): Preloader
{
$this->ignores = array_merge(
$this->ignores,
$names
);

return $this;
}

public function load(): void
{
// We'll loop over all registered paths
// and load them one by one
foreach ($this->paths as $path) {
$this->loadPath(rtrim($path, '/'));
}

$count = self::$count;

echo "[Preloader] Preloaded {$count} classes" . PHP_EOL;
}

private function loadPath(string $path): void
{
// If the current path is a directory,
// we'll load all files in it
if (is_dir($path)) {
$this->loadDir($path);

return;
}

// Otherwise we'll just load this one file
$this->loadFile($path);
}

private function loadDir(string $path): void
{
$handle = opendir($path);

// We'll loop over all files and directories
// in the current path,
// and load them one by one
while ($file = readdir($handle)) {
if (in_array($file, ['.', '..'])) {
continue;
}

$this->loadPath("{$path}/{$file}");
}

closedir($handle);
}

private function loadFile(string $path): void
{
// We resolve the classname from composer's autoload mapping
$class = $this->fileMap[$path] ?? null;

// And use it to make sure the class shouldn't be ignored
if ($this->shouldIgnore($class)) {
return;
}

// Finally we require the path,
// causing all its dependencies to be loaded as well
require_once($path);

self::$count++;

echo "[Preloader] Preloaded `{$class}`" . PHP_EOL;
}

private function shouldIgnore(?string $name): bool
{
if ($name === null) {
return true;
}

foreach ($this->ignores as $ignore) {
if (strpos($name, $ignore) === 0) {
return true;
}
}

return false;
}
}

(new Preloader())
->paths(__DIR__ . '/vendor/laravel')
->ignore(
\Illuminate\Filesystem\Cache::class,
\Illuminate\Log\LogManager::class,
\Illuminate\Http\Testing\File::class,
\Illuminate\Http\UploadedFile::class,
\Illuminate\Support\Carbon::class,
)
->load();

2.更新 PHP 配置

将预加载脚本添加到 PHP 配置(php.ini)中。

1
2
3
; Path to the preload script
opcache.preload=/path/to/your/laravel/project/preload.php
opcache.preload_user=www-data

3.重新启动 Web 服务器

修改php.ini文件后,重新启动 web 服务器使更改生效。

Apache:

1
sudo systemctl restart apache2

Nginx with PHP-FPM:

1
2
3

sudo systemctl restart php8.3-fpm
sudo systemctl restart nginx

4.检查 OPCache 状态

要验证 OPCache 是否按预期工作并检查预加载细节,请在 Laravel 项目的根目录中创建一个名为 opache.php 的 PHP 脚本,其内容如下:

1
2
3
4
5
<?php

$status = opcache_get_status();

print_r($status);

运行脚本:

1
php opcache.php

输出将提供有关 OPCache 使用、配置和预加载脚本的详细信息。查找统计部分以验证预加载是否按预期工作。

刷新 OPCache

有这两种方法:

1. 重启 PHP or Web 服务器:

  • 最直接的清空 OPCache 方法
  • 可能打断即将到来的请求

2. 使用 PHP 脚本

1
2
3
opcache_reset();

echo 'OPCache flushed successfully.';

监控 OPCache

使用诸如 opcache-guiopcache-status 之类的工具。

使用 opcache-gui

  1. 安装 opcache-gui:
1
git clone https://github.com/amnuts/opcache-gui.git /var/www/html/opcache-gui
  1. 访问 opcache-gui: Open your browser and navigate to http://your-domain/opcache-gui.

使用 opcache-stat

  1. 安装 opcache-status
1
git clone https://github.com/rlerdorf/opcache-status.git /var/www/html/opcache-status
  1. 访问 opcache-status: Open your browser and navigate to http://your-domain/opcache-status.

参考:

https://qirolab.com/posts/php-opcache-the-secret-weapon-for-laravel-performance-boost

Laravel 集合中使用 ensure() 方法验证数据类型

ensure() 方法用于验证集合中的元素类型,验证不通过抛出 UnexpectedValueException 异常。

示例一:

1
2
3
4
5
6
7
8
9
10
use Illuminate\Support\Collection;

$collection = collect([1, 2, 3, 4, 5]);

$validatedCollection = $collection->ensure(function ($item) {
return is_int($item);
});


print_r($validatedCollection->all());

示例二:

1
2
3
4
5
6
7
8
9
10
use Illuminate\Support\Collection;

$numbers = collect([1, 2, 3, 'four']); // Collection with mixed types

try {
$numbers->ensure('int'); // Ensure all items are integers
} catch (UnexpectedValueException $e) {
echo 'Error: Collection contains a non-integer value.';
// Handle the exception as needed (e.g., log the error, provide user feedback)
}

示例三,自定义类型验证:

1
2
3
4
5
6
7
8
9
10
11
class User {}

$users = collect([new User(), new User()]);

try {
$users->ensure(function ($item) {
return $item instanceof User;
});
} catch (Exception $e) {
echo $e->getMessage();
}

示例四,多类型验证:

1
2
3
$data = collect([1, 'hello', new User]);

$data->ensure([int::class, string::class, User::class]); // Valid collection

参考:

https://qirolab.com/posts/data-type-validation-in-laravel-collections-with-the-ensure-method

Laravel API 速率限制(Rate Limiting)

Laravel 提供了强大的内置工具来实现速率限制,防止 API 滥用,本文探讨如何有效地利用这些功能。

基础使用

Laravel 的速率限制通常使用中间件来应用。例如:

1
2
3
4
5
Route::middleware(['auth:api', 'throttle:60,1'])->group(function () {
Route::get('/user', function () {
return auth()->user();
});
});

/user 端点每分钟可被访问 60 次。

自定义速率限制器

在一些复杂的场景,需要自定义速率限制器,在 AppServiceProvider 中定义或者使用特定的服务提供者(Service Provider)。下面示例在 AppServiceProviderboot 方法中定义:

1
2
3
4
5
6
7
8
9
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}

应用:

1
2
3
Route::middleware(['throttle:api'])->group(function () {
// API routes
});

动态速率限制

例如你可以更具用户角色或者订阅级别设置速率:

1
2
3
4
5
6
7
8
9
RateLimiter::for('premium', function (Request $request) {
return $request->user()->isPremium()
? Limit::perMinute(100)
: Limit::perMinute(30);
});

Route::middleware(['auth:api', 'throttle:premium'])->group(function () {
Route::post('/process', [DataController::class, 'process']);
});

高级用户比普通用户有更多的调用次数。

全局限制 vs 特定路由限制

app/Http/Kernel.php 中设置全局限制:

1
2
3
4
5
6
protected $middlewareGroups = [
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

或者对特定路由限制:

1
2
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1'); // 5 attempts per minute

处理速率限制异常

当调用次数超过限制频率, Laravel 抛出 Illuminate\Http\Exceptions\ThrottleRequestsException 异常。Laravel 11 之前的版本在 app/Exceptions/Handler.php 捕获:

1
2
3
4
5
6
7
8
9
10
public function render($request, Throwable $exception)
{
if ($exception instanceof ThrottleRequestsException) {
return response()->json([
'error' => 'Too many requests. Please try again later.'
], 429);
}

return parent::render($request, $exception);
}

进阶用法:

滑动窗口速率限制(Sliding Window Rate Limiting)

想要更加精细的控制,可以使用 Sliding Window Rate Limiting:

1
2
3
RateLimiter::for('sliding', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

使用 Redis 实现速率限制(Rate Limiting with Redis)

对于高流量的应用,使用 Redis 实现速率限制。

1
2
3
4
5
RateLimiter::for('redis', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip())->response(function () {
return response('Custom rate limit exceeded message', 429);
});
});

组合使用

1
2
3
Route::middleware(['throttle:global,throttle:api'])->group(function () {
// Routes that need to satisfy both global and API-specific limits
});

参考:

https://www.harrisrafto.eu/securing-your-laravel-apis-with-built-in-rate-limiting/

Laravel 方法:Str::replaceArray()

Str::replaceArray() 方法是 Illuminate\Support\Str 类的一部分。它允许你使用数组元素替换字符串中的特定元素(占位符)。这对于模板和动态内容生成特别有用。

基本使用:

1
2
3
4
5
6
use Illuminate\Support\Str;

$template = 'The event will take place on ?, at ?';
$replaced = Str::replaceArray('?', ['Monday', '10 AM'], $template);

echo $replaced; // Output: The event will take place on Monday, at 10 AM

实际案例,邮件通知;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Illuminate\Support\Str;

class NotificationController extends Controller
{
public function sendEventNotification($event)
{
$template = 'Hello ?, your event ? is scheduled for ? at ?.';
$replacements = [
$event->user->name,
$event->name,
$event->date->format('l'),
$event->time->format('h:i A'),
];

$message = Str::replaceArray('?', $replacements, $template);

// Send the email using the generated message
Mail::to($event->user->email)->send(new EventNotification($message));

return response()->json(['status' => 'Notification sent successfully.']);
}
}

使用 HTTP 响应缓存加速 Laravel API

HTTP 响应(response)可以通过设置 Cache-ControlETag 响应头(header)实现缓存。

Laravel API 中的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class ProductController extends Controller
{
public function show($id, Request $request)
{
$product = Cache::remember("product:{$id}", 3600, function () use ($id) {
return Product::findOrFail($id);
});

return response()->json($product)
->header('Cache-Control', 'public, max-age=3600')
->setEtag(md5($product->updated_at));
}
}
  1. 使用 Laravel 的 Cache::remember() 方法实现服务器端缓存,实践中应该是 redis 等实现,缓存 3600 秒。
  2. 使用 Cache-Control header 设置响应缓存。
  3. 使用 Etag header 判断响应缓存是否过期。

列表 Etag 设置的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class ProductCatalogController extends Controller
{
public function index(Request $request)
{
$page = $request->get('page', 1);
$perPage = 20;

$cacheKey = "product_catalog:page:{$page}";

$products = Cache::remember($cacheKey, 3600, function () use ($page, $perPage) {
return Product::with('category')
->where('status', 'active')
->orderBy('name')
->paginate($perPage);
});

return response()->json($products)
->header('Cache-Control', 'public, max-age=3600')
->setEtag(md5($products->lastPage() . $products->total() . $products->currentPage()));
}

public function updateProduct($id, Request $request)
{
$product = Product::findOrFail($id);
$product->update($request->all());

// Invalidate cache for all pages
Cache::flush();

return response()->json(['message' => 'Product updated successfully']);
}
}

参考:

https://www.harrisrafto.eu/supercharge-your-laravel-api-with-http-response-caching

CSS 使用 align-content:center 实现 div 居中

以前实现 div 居中比较方便的方法有:

1
2
3
display: flex; 
justify-content: center;
align-items: center;
1
2
display: grid; 
place-content: center;

新实现的属性 align-content: center; ,浏览器支持查看🔗

1
align-content: center;

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div style="display: flex; justify-content: center; align-items: center; height: 200px; background-color:aqua;">
<div style="background-color:antiquewhite; height: 100px;width: 200px;">
display: flex
</div>
</div>
<div style="display: grid; place-content: center; height: 200px; background-color: aquamarine;">
<div style="background-color: antiquewhite; height: 100px; width: 200px;">
display: grid
</div>
</div>
<div style="align-content: center; height: 200px; background-color:blueviolet;">
<div style="background-color:antiquewhite; height: 100px; width: 200px; margin: 0 auto;">
align-content: center
</div>
</div>
</body>
</html>

示例效果:

参考:

https://build-your-own.org/blog/20240813_css_vertical_center/

https://www.bilibili.com/video/BV1V7iNeWE9m