client_body_buffer_size
Nginx 分配给请求数据的 Buffer 大小,如果请求的数据小于 client_body_buffer_size
,那么 Nginx 会在内存中存储数据,如果请求的内容大小大于 client_body_buffer_size
,但是小于 client_max_body_size
,会先将数据存储到临时文件中。
默认的情况下,这个缓存大小是等于两个 memory pages,也就是在 x86 机器上是 8K,在 64-bit 平台上是 16K。
这个空间只有当请求有上传的时候才会被用到,一旦数据被传输到后端服务,内存就会被清空。这意味着你需要足够的内存空间来保存并发的上传,否则服务器会开始 swapping。
client_body_temp
指定的路径,默认是 /tmp/
所配置的 client_body_temp 地址,一定让 Nginx 用户组有读写权限。否则当传输的数据大于 client_body_buffer_size
是写入临时文件会报错。
open() /nginx/client_body_temp/0000000019” failed (13: Permission denied)
在接口层面的表现为接口请求返回 403
client_max_body_size
默认大小是 1M,客户端请求服务器最大允许大小,如果请求正文数据大于 client_max_body_size
的值,那么 HTTP 协议会报错 413 Request Entity Too Large。 正文大于 client_max_body_size
一定是失败的,如果要上传大文件需要修改该值。
http{ }
中设置:client_max_body_size 20m;server{ }
中设置:client_max_body_size 20m;location{ }
中设置:client_max_body_size 20m;三者到区别是:http{} 中控制着所有nginx收到的请求。而报文大小限制设置在server{}中,则控制该server收到的请求报文大小,同理,如果配置在location中,则报文大小限制,只对匹配了location 路由规则的请求生效。
Blade 模板中的 Components 提供了和 section
, layout
和 includes
相似的机制。都可以用来复用构造的 Blade 模板。
但 Component 更容易理解,提供了两种方式:
使用命令创建:
php artisan make:component Alert
创建的文件在 App\View\Components
目录。
make:component
命令会创建一个 template
在 resources/views/components
目录中。
也能在子目录中创建 Components:
php artisan make:component Forms/Input
如果传参 --view
:
php artisan make:component forms.input --view
就不会创建 class,只会创建模板。
在本地开发调试的时候使用了 Laravel 提供的 Sail 依赖本地的 Docker 环境,Sail 提供了 Nginx,MySQL,Redis,等等容器,还提供了一个用于测试的 SMTP mailhog,但是生产环境可以使用更加稳定的组件。
Laravel 应用需要一些基础的系统依赖,需要确保Web 服务器有如下的最低要求:
Web 服务器就用 Nginx。
记住 Web 服务器所有的请求都会先到 public/index.php
文件,千万不要将此文件放到项目的根目录,或者 Web 服务器的根目录,如果 Web 服务器可以访问项目根目录会造成带有敏感信息的配置文件泄漏。
server {
listen 80;
server_name example.com;
root /srv/example.com/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
这里使用 [[aapanel]] 来新建一个站点。
然后将代码 push 到 GitHub,然后到机器上 push 来下。
cp .env.example .env
根据自己的配置,修改数据库、Redis、SMTP 相关的配置。
执行:
sudo composer install
注意将站点的所有者修改为 www
:
sudo chown -R www:www .
然后执行:
sudo php artisan key:generate
sudo php artisan migrate
禁用 php 方法:
proc_open
symlink
pcntl_signal
pcntl_alarm
安装 Composer:
wget https://getcomposer.org/download/1.8.0/composer.phar
mv composer.phar /usr/local/bin/composer
chmod u+x /usr/local/bin/composer
composer -V
自动加载优化:
composer install --optimize-autoloader --no-dev
优化配置加载,将配置文件压缩到一个缓存中
php artisan config:cache
优化路由加载:
php artisan route:cache
优化视图加载
php artisan view:cache
如果遇到如下问题:
PHP Fatal error: Uncaught Error: Call to undefined function Composer\XdebugHandler\putenv() in phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/Process.php:101
Stack trace:
#0 phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/Status.php(59): Composer\XdebugHandler\Process::setEnv()
#1 phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/XdebugHandler.php(99): Composer\XdebugHandler\Status->__construct()
#2 phar:///usr/local/bin/composer/bin/composer(18): Composer\XdebugHandler\XdebugHandler->__construct()
#3 /usr/local/bin/composer(29): require('...')
#4 {main}
thrown in phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/Process.php on line 101
Fatal error: Uncaught Error: Call to undefined function Composer\XdebugHandler\putenv() in phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/Process.php:101
Stack trace:
#0 phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/Status.php(59): Composer\XdebugHandler\Process::setEnv()
#1 phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/XdebugHandler.php(99): Composer\XdebugHandler\Status->__construct()
#2 phar:///usr/local/bin/composer/bin/composer(18): Composer\XdebugHandler\XdebugHandler->__construct()
#3 /usr/local/bin/composer(29): require('...')
#4 {main}
thrown in phar:///usr/local/bin/composer/vendor/composer/xdebug-handler/src/Process.php on line 101
需要禁用 php 的 putenv
方法。
通过 Laravel 的样例项目也应该能看到 Laravel 对本地化多语言的支持代码了。
观察一下项目的目录结构就能猜出来语言文件在 resources/lang
中。目录结构需要按照 ISO 15897 标准来命令,简体中文 zh_CN
/resources
/lang
/en
messages.php
/es
messages.php
可以看到所有的语言文件都是返回一个 key-value 结构。
Laravel 还可以定义 JSON 文件,存放在 resources/lang
下,如果是中文则是 resources/lang/zh_CN.json
文件:
{
"welcome": "欢迎来到 EV 的 Blog"
}
在 config/app.php
中可以配置网站语言。
可以使用 __
辅助函数来从语言文件中获取翻译。
echo __('welcome')
在 Blade 模板引擎中,可以直接在 `` 中使用:
如果翻译字符不存在,则直接返回字符串。
如果翻译字符串中有需要变动的变量,可以使用 :
来将其定义为占位符:
'welcome' => 'Welcome, :name',
然后在获取的时候传入一个数组用于替换:
echo __('welcome', ['name' => 'laravel']);
早在 2022 年年初的时候 Cloudflare 就推出了 Email Routing 的服务,在第一时间就从 Google Domains 中迁移到了 Cloudflare,中间好像也没有遇到什么问题,正常的收到域名邮箱的邮件,转发到 Gmail。
Cloudflare Email Routing (beta) is designed to simplify the way you create and manage email addresses, without needing to keep an eye on additional mailboxes. With Email Routing, you can create any number of custom email addresses that you can use in situations where you do not want to share your primary email address, such as when you subscribe to a new service or newsletter. Emails are then routed to your preferred email inbox, without you ever having to expose your primary email address.
Email Routing is free and private by design. Cloudflare will not store or access the emails routed to your inbox.
Cloudflare 接收邮件的设置非常简单,在页面中可以创建自定义地址的域名邮箱,然后自动转发至指定的首选邮箱。也可以设置 Catch-all 将所有发送至域名邮箱的邮件,即使没有定义前缀也全部转发到指定邮箱。
具体步骤:
随后发往该域名邮箱的所有邮件都会通过 Cloudflare 转发到指定的 Gmail 中。
使用 Cloudflare 的域名邮箱发送邮件则需要用到 Gmail 中的设定。Cloudflare Email Routing 自身是不支持发送邮件的。但可以通过如下方法实现域名邮箱的发送:
首先要生成应用专用密码,主要用来代替密码来登录 Gmail,如下图,记住生成的密码。
然后打开 Gmail,点击「Settings」,在所有的设置中,找到 「Accounts and Import」,在「Send mail as」中,点击「Add another email address」。
在弹出的对话框中设置「Name」和 「Email address」:
然后进入下一步:
smtp.gmail.com
填写成功之后,需要填入验证码,域名邮箱会收到一份邮件,包含验证码,在页面上填入即可。
完成配置之后,在发送邮件的时候就可以选择自定义的邮箱了。
不过需要注意的是,通过 Gmail 代发的邮件在 QQ 邮箱,163 邮箱等邮箱中会显示代发邮箱本身,并且会出现「此地址未验证,请注意识别」等等字样。如果介意这一点,可能还是需要找一家正规的域名邮箱服务提供商比较合适。
ed25519 是一个相对较新的加密算法,实现了 Edwards-curve Digital Signature Algorithm(EdDSA)。但实际上 ed25519 早已经在 5 年前就被 OpenSSH 实现,并不算什么前沿科技。但很多人,即使是每天都使用 SSH/SCP 的人可能并不清楚这个新类型 key。
不过要注意的是并不是所有的软件目前都实现了 ed25519,但是大多数最近的操作系统 SSH 都已经支持了。
ssh-keygen -t ed25519 -C "your@gmail.com"
可以检查 ~/.ssh
目录下的 key,会发现 ed25519 的公钥只有简短的一行:
ssh-ed25519 AAAACxxxx your@gmail.com
在开发环节要测试的时候,如果想要在数据库中批量插入一些假数据,这个时候就可以使用 model factories
。
在 database/factories/
目录下面默认定义了一个 UserFactory.php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}
}
可以看到在这个类中给 User 的每一个字段都设置了一个 faker 方法。
通过命令:
php artisan make:factory UserFactory
这个时候在 database/factories
下面就会有一个 UserFactory
,你需要按照 faker 的方式给 Model 每一个自定义的字段都加上 fake 方式。
然后执行 tinker:
php artisan tinker
进入交互式命令行之后:
use App\Models\User;
User::factory(10)->create();
执行完成之后就会往数据库中插入 10 条假数据。
Laravel 的分页实现集成了 Query Builder 和 Eloquent ORM,提供了一种非常方便的分页接口。
最简单的方式就是使用 query builder 和 Eloquent query 的 paginate
方法,这个方法会自动处理请求的 limit 和 offset 参数。
默认情况下,当前页面的参数使用 page
表示。
所以在 Controller 中直接指定一页请求的条数即可:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
/**
* Show all application users.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('user.index', [
'users' => DB::table('users')->paginate(15)
]);
}
}
默认情况下 paginate
方法会统计总条数,如果不需要,可以使用
$users = DB::table('users')->simplePaginate(15);
如果使用 Eloquent,可以直接在 Model 上调用:
$users = User::paginate(15);
如果要倒序来分页,有两种写法,一种是直接使用 DB:
$ondata = DB::table('official_news')->orderBy('created_date', 'desc')->paginate(10);
另外一种就是使用 Model:
$posts = Post::orderBy('id', 'desc')->paginate(10);
除了使用 offset
方式分页,还可以使用游标:
$users = DB::table('users')->orderBy('id')->cursorPaginate(15);
这样每一次请求,会在分页请求中带上 cursor
参数:
http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0
表示了下一页的开始。
order by
,并且有索引的情况下分页的一些问题和局限:
simplePaginate
只能显示 Next
和 Previous
链接,无法支持展示页码默认情况下,分页器产生的链接会匹配请求的 URI。withPath
方法允许自定义 URL。
如果想要产生的链接是 http://example.com/admin/users?page=N
需要传入 /admin/users
use App\Models\User;
Route::get('/users', function () {
$users = User::paginate(15);
$users->withPath('/admin/users');
//
});
当调用 paginate
方法时,会获得一个 Illuminate\Pagination\LengthAwarePaginator
实例,当调用 simplePaginate
方法时,会获得 Illuminate\Pagination\Paginator
.
cursorPaginate
会获得 Illuminate\Pagination\CursorPaginator
。
这些对象都提供了一些方法来获取结果集。
<div class="container">
@foreach ($users as $user)
@endforeach
</div>
Laravel 使用的 Eloquent ORM 中的 Model 可以用一种非常易读的方式去定义 Model 和 Model 之间的关系。
比如 User 和 Phone 都是一个 Model,要去表示用户和 Phone 的关系,可以:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get the phone associated with the user.
*/
public function phone()
{
return $this->hasOne(Phone::class);
}
}
在 User Model 中定义 phone()
方法,然后使用 Illuminate\Database\Eloquent\Model
中定义的 hasOne()
方法。
hasOne()
方法的第一个参数是 Model 的类名。一旦定义了就可以动态的直接通过用户 Model 去访问 Phone
$phone = User::find(1)->phone;
上面的方式默认 Phone 这个 Model 中有一个 user_id
的外键。如果定义了其他名字,可以将外键名字传入第二个参数:
return $this->hasOne(Phone::class, 'foreign_key');
比如 Phone 属于用户:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* Get the user that owns the phone.
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
Eloquent 会按照约定,假设 Phone model 中含有一个 user_id
列。
一对多关系通常用来定义一个 Model 是其他 Model 的父节点。比如一篇文章可嗯有无数的评论。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* Get the comments for the blog post.
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
}
注意 Eloquent 会自动决定外键的列名,按约定,Eloquent 会自动使用 parent model 的 snake case 名字然后加上 _id
作为外键。所以上面的例子中,Eloquent 会自动认为 Comment model 中有一个 post_id
的外键。
一旦定义了关系,就可以动态的获取:
use App\Models\Post;
$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
//
}
使用 belongsTo
方法:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* Get the post that owns the comment.
*/
public function post()
{
return $this->belongsTo(Post::class);
}
}
获取:
use App\Models\Comment;
$comment = Comment::find(1);
return $comment->post->title;
最常见的多对多关系就是,用户-角色,用户可能有多重角色,同一个角色也会有不同的用户。另外一个比较常见的场景就是标签系统,一本书会有标签1,2,3,标签1也会包含多本书。
class User extends Model
{
public function roles() {
return $this->belongsToMany(
Role:class,
// pivot table
'role_user',
'user_id',
'role_id'
)
}
}
Role 定义:
class Role extends Model {
public function users() {
return $this->belongsToMany(
User:class,
'role_user',
'role_id',
'user_id'
)
}
}
三个 Model 之间都是一对一关系,那么就可以建立远程一对一关系。
Laravel Event 提供了一个最简单的观察者模式实现,可以订阅监听应用中发生的事件。事件通常放在 app/Events
目录,监听器放在 app/Listeners
。
事件是应用功能模块之间解耦的有效方法。单个事件可以有多个监听器,监听器之间相互没有影响。
比如说每次有订单产生时,发送给用户一个 Slack 通知,通过事件,可以将处理订单的代码和 Slack 通知代码解耦,只需要发起一个事件,监听器监听订单产生事件,然后触发响应的动作即可。
可以使用如下的命令创建 Event
php artisan event:generate
或者分别单独创建 Event 和 Listener:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
然后需要手动添加到 EventServiceProvider
的 boot
方法中。
EventServiceProvider
中的 $listen
数组配置了监听器:
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
Event class 是一个包含了 Event 信息的数据容器。
<?php
namespace App\Events;
use App\Models\Subscribe;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class SubscribeEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $subscribe;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Subscribe $subscribe)
{
$this->$subscribe = $subscribe;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
可以看到 Event 中几乎没有什么逻辑,只是保存了一个 Subscribe Model。
Event listeners
会在 handle
方法中被触发。也 handle
方法中可以执行对应的事件响应。
<?php
namespace App\Listeners;
use App\Events\SubscribeEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SubscribeListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(SubscribeEvent $event)
{
//
}
}
handle 方法会接受一个 Event 参数,这个参数就是定义的 Event。
定义好 Event 和 Listener 之后,在 EventServiceProvider
注册,就可以通过
event(new \App\Events\SubscribeEvent($subscribe));
来触发事件。
如果你要在 Listener 中执行一些繁重的操作,那么可以使用 Queued Event Listener
:
在 Listener
上指定实现 ShouldQueue
,然后记得配置好队列。
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
//
}
这样,当一个事件发生后,Listener 会自动被添加到队列中。
可以调用 Events
的 dispatch()
方法。