envoy 是什么?

envoy 是一个支持 blade 语法的 ssh 远程命令执行的工具。具体来说就是,通过配置 ssh 的账号、密码、key 这些,然后可以使用 envoy 运行一些预定义的命令(比如 git 更新什么的)。

安装
1
composer global require laravel/envoy
怎么使用?

配置(~/.ssh/config):

关于 ssh key 配置官网文档没有说,但是 Google 出来了,需要在 ~/.ssh/config 下面定义你每个 server 对应的 key file,我们假设里面有如下内容:

1
2
3
Host 12.34.56.78
IdentityFile ~/.ssh/id_rsa
User root

如果我们有另外一个服务器,那么可以按如上格式再另写三行,以此类推。

配置有了,接下来就是写具体命令了:

在项目根目录新建一个 Envoy.blade.php 文件,这个官网文档有说,第一行定义 servers,注意,不能换行,否则会报错,也许是个 bug,如:

1
@servers(['web' => '127.0.0.1', 'test' => '12.34.56.78'])

我们假设有一个更新项目的操作,如:

1
2
3
4
5
6
7
@task('update', ['on' => ['web', 'test']])
cd "/path/to/project"
{{PHP_BINARY}} artisan config:cache
{{PHP_BINARY}} artisan route:cache
{{PHP_BINARY}} artisan optimize --force
"/composer/binary" dumpautoload
@endtask

这样我们运行下面命令的时候,就会进行一些操作:

1
envoy run update

我们可以使用一些 blade 的语法,如,在 task 中使用变量,这个变量可以是我们在 @setup 中设置的。

tips:对于另外一些操作,比如需要交互的,如可能 git pull 的时候需要输入账号密码的,可以尝试一下 except(当然,不支持 windows)。

更高效率地查询:使用批量查询代替 foreach 查询(多次 io 操作转换为一次 io 操作)

如果想要查看更详尽的介绍,可以看看这篇文章 什么是 N+1 问题,以及如何解决 Laravel 的 N+1 问题?

在维护的项目中, 我发现了有不少需要查询关联数据的时候是这样做的:先查询出列表,然后 foreach 列表去查询列表每一条记录的关联数据,好比如这样:

1
2
3
4
5
6
7
8
$products = \DB::table('product')->where('category_id', 1)->paginate();
foreach ($products as $key => $product) {
$product['some_other_info'] = \DB::table('some_other_info')
->where('product_id', $product['id'])
->get();

$products[$key] = $product;
}
解决方法:使用一次查询代替多次查询
  • 定义模型关联,使用 Modelwith 进行查询。如:
1
AdminUser::with('admin_user_info')->where('id', '>', 1)->get();
  • 获取查询结果集的 id 列(根据实际情况而定),使用 whereIn 一次查询关联数据。对于那些不能定义关联的才用这种方法,因为这种写法有点啰嗦
1
2
3
4
5
6
7
8
9
10
$products = \DB::table('product')->where('category_id', 1)->paginate();
$product_ids = $products->pluck('id')->toArray(); // 获取所有 id , 下面第二点有说到这个用法<br>
$some_other_infos = \DB::table('some_other_info') // 根据 id 数组一次查询所有关联数据
->whereIn('product_id', $product_ids)
->get();<br>
$some_other_infos = array_column($some_other_infos, null, 'product_id'); // 使用 id 把结果集转换为关联数组,这样下面可以更高效地操作,否则我们只能两次 foreach 了
foreach ($products as $key => $product) {
$product['some_other_info'] = array_get($some_other_infos, $product['id']);
$products[$key] = $product;
}

先说说 关联查询:我们在 Model 类里定义的关联,我们不一定在第一次查询就全部查出来,我们可以在需要的时候再去查询 , 使用 load 方法,可以实现这个目标,但是这个 load 只是针对单个 model 对象的,如果我们 Model::xxx()->xx() 链式操作最后是 get(), 我们得到的结果将会是一个 Collection 实例,最后调用的方法是 first() 或其他类似的 firstOrFail() 的时候,返回的才是一个 Model 实例。也就是说 load 方法只针对 Model 实例。如下:

我们假设有如下模型定义:

AdminUser

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Model;

class AdminUser extends BaseModel
{

public function admin_user_info()
{
return $this->hasOne(AdminUserInfo::class);
}
}

AdminUserInfo

1
2
3
4
5
6
7
<?php

namespace App\Model;

class AdminUserInfo
{
}

使用 load 方法加载关联

1
2
$adminUsers = AdminUser::limit(10)->get();
$adminUsers->load('admin_user_info');

更优雅地处理你的 paginate() 结果(利用 Collection 里的方法)

这里应该有一小点,我们想要实现某些功能的时候可以先看看框架有没有提供相关的支持,如命名在驼峰式、下划线式转换; 字符串是否以某些子串开始、结束;数组排序、过滤等等,其实很多常用的功能 Laravel 都有现成的轮子, 最常用的一些功能可以查看 ArrStrCollection 的源码,利用好的话可以把代码写得简洁一些。

这个其实最近才发现的,最近脑洞开了一下,因为想从 paginate() 返回的对象中获取结果集的 id 列,所以就想能不能 $paginator->pluck('id')->toArray() 这样操作,最后发现真的可以,然后去看了源码发现 AbstractPaginator 这个抽象类里有一个 __call 的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* Make dynamic calls into the collection.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->getCollection()->$method(...$parameters);
}

我们会发现,这个魔术方法调用的是 Collection 里面的方法,也就是说,我们可以把 paginate() 返回的结果当作一个 Collection 实例来操作。 所有的 Collection 方法我们都可以使用,什么 filtermapsort 等等等等。 而我之前获取 id 列都是使用 array_column($paginator->items(), 'id'),两种写法差不多, 但是对于另外一些如根据某个字段排序等操作,利用 Collectionsort 方法明显就简洁多了。

利用 Macroable Trait 更优雅地对框架某些功能进行扩展

我们想想,如果有一天,我们需要自定义一些 Query Builder 的方法的时候,我们会怎么做? 当然我们可以利用 Global Scopes 这个特性来实现某些类似的功能,但是如果我们想要 DB 类也用上这个扩展的方法的话,这样就行不通了。 也不是完全没有办法,Laravel 的开发者很早开始就考虑到了扩展性的问题,我们去查看 Query Builder 的源码就会发现,里面有以下的 trait 调用:

1
2
3
use BuildsQueries, Macroable {
__call as macroCall;
}

而这个 MacroableIlluminate\Support\Traits\Macroable,我们可以从这个 trait 中发现, 其实这个 trait 提供了一个很强大的扩展功能,对于所有 use Macroable 的类, 我们都可以通过 XXX::macro('xx', function (){}) 的方式来对这个类进行扩展。

我们全局搜索这个 Macroable,会发现框架不少地方都有使用:如 CacheConsoleEloquentSchema 等等,如果我们想要对框架中某些类进行扩展的时候,不妨先看看这个类有没有 use Macroable, 如果有,我们就可以省去一大部分功夫。

x

当然,我们利用 macro 方法来扩展的功能,IDE 不会有任何提示,除非,我们像 ide-helper 那样处理。具体可参照另外一篇文章:Laravel query builder/ Eloquent builder 添加自定义方法

利用 array_getdata_get 方法代替多个 isset 判断

最常用到的地方是,我们利用 with 关联查询出多层关联,但是我们不确定所有层级关联是否存在,所以我们可能就要一层层地 isset 来判断。

假设有一个这样的关联 User -> HasOne -> Article -> HasOne -> Comment,这个假设不太妥当,但可以说明问题

我们查询的时候按如下方式查询:

1
2
3
4
// 获取一个用户下文章的一条评论
$user = User::with(['article.comment'])->first();
$content = isset($user->article) ? (isset($user->article->comment) ? $user->article->comment : '') : ''; // 传统写法
$content = data_get($user, 'article.comment.content'); // 评论内容

上面的例子中,我们可以发现 data_get 写出的代码更简洁、可读性也更强,array_get 功能类似,但是 array_get 不能针对对象操作。 利用 array_get 可以更方便完成地进行对多维数组的操作,因为 Laravel 把多维数组抽象成了点号分隔的一维数组,不得不说,说 Laravel 优雅不是没有道理的。

多对多关联更方便的操作方法 attachdetachsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 附加多对多关系
$user->roles()->attach($roleId);
$user->roles()->attach($roleId, ['expires' => $expires]);
// 从用户上移除单一身份...
$user->roles()->detach($roleId);
// 从用户上移除所有身份...
$user->roles()->detach();
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([1 => ['expires' => $expires], 2, 3]);

// 任何不在给定数组中的 IDs 将会从中介表中被删除。
$user->roles()->sync([1, 2, 3]);
// 你也可以传递中介表上该 IDs 额外的值:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);

使用 sync 方法的时候需要注意,如果第二个参数不设置 false,会把不在给定数组中的 ID 的关联数据全部删除。

上面的关联是 User -> belongsToMany -> Role,通过多对多表的关联,具体可查看另一篇文章:Laravel5.1 使用中间表的多对多关联,我们通过下面的例子来说明一下 attach 的用法,其他几个方法也是类似的功能,

x x x

 我们查看以下数据库中间表记录:

x

我们可以发现,user 关联了 id 为 1 和 2 的两个 role,同时 attach 第二个参数可以让我们传递一些额外数据保存到中间表。

sync 的功能和 attach 类似,但是 sync 还有个功能是,可以把不在指定数组的关联数据删除(如果我们不需要删除可以传递第二个参数 false,或者直接使用 attach)。

使用 debugbar 尽早发现性能问题

这个其实使用 Laravel 的人基本上都知道,但是可能我们没有用到其中一些功能, 其实 Laravel-debugbar 可以通过配置获取更加详情的信息(如 sql 的堆栈信息、是否 explain 等等非常多实用的功能), 具体功能还是得查看其官方文档,有很多实用的功能,可以让我们对我们的代码了解更多。

使用 chunk 处理表全部数据

有时候,我们给表新增了一个字段,这个字段是由其他字段算出来的,这时候我们就需要跑一遍该表,进行该字段的更新。 一种做法是一次性全表查询出来,foreach 循环处理,这种做法在数据量小的时候问题不大, 但是数据量达到一定程度的时候会产生比较严重的问题:内存耗尽,更严重的是把服务器也弄挂。 另外一种方法是使用 Builderchunk 方法进行批量处理,可以解决内存占用过大的问题。如下:

1
2
3
4
5
6
7
DB::table('users')->chunk(100, function($users)
{
foreach ($users as $user)
{
//
}
});

关联数据分页、统计或者其他类似操作

我们先来看看 Relation 的定义(5.6),我们可以发现里面有个 __call 方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Handle dynamic method calls to the relationship.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
$result = $this->query->{$method}(...$parameters);
if ($result === $this->query) {
return $this;
}
return $result;
}

我们可以发现它会先看 有没有定义相关的 macro,如果没有则去调用 \Illuminate\Database\Eloquent\Builder 里面的方法, 而我们继续看 \Illuminate\Database\Eloquent\Builder 里面的源码(5.6)发现,里面也有一个 __call 方法,

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
/**
* Dynamically handle calls into the query instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if ($method === 'macro') {
$this->localMacros[$parameters[0]] = $parameters[1];
return;
}
if (isset($this->localMacros[$method])) {
array_unshift($parameters, $this);
return $this->localMacros[$method](...$parameters);
}
if (isset(static::$macros[$method])) {
if (static::$macros[$method] instanceof Closure) {
return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters);
}
return call_user_func_array(static::$macros[$method], $parameters);
}
if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
return $this->callScope([$this->model, $scope], $parameters);
}
if (in_array($method, $this->passthru)) {
return $this->toBase()->{$method}(...$parameters);
}
$this->query->{$method}(...$parameters);
return $this;
}

上面是 5.6 版本的 __call,5.1 版本的 __call 相对来说,功能少一些,但是都有的功能就是,先去看 macro 有没有定义, 如果前面条件都不满足就去调用 Query Builder 中对应的方法,当然,如果 Query Builder 里面也没有这个方法就会抛出异常。而实现这些功能都得益于 php 的 __call 魔术方法。

关于 Eloquent BuilderQuery Builder 的区别:
  • 我们使用 Eloquent Model 类或 Eloquent Model 子类进行操作的时候,如果使用的都是 Eloquent Builder 里面的方法, 那么返回的也是一个 Eloquent Builder 或者是最终结果(视操作而定,有可能是 Collection 实例、null、某个 Model 子类的实例等)。 虽然 __call 中可能会调用到 Query Builder 的方法,而 Query Builder 的方法里面可能会返回 $this,这时候我们可能会有一种错觉, 我们通过 __call 间接调用了 Query Builder 里面方法的时候,返回的是不是一个 Query Builder 的实例。 看起来好像的确是这样,但我们实际操作一下就发现,返回的还是 Eloquent Builder 实例。这是因为上面 __call 最后两行,如果写成
1
return $this->query->{$method}(...$parameters);

那么返回的就是 Query Builder 了,但实际上,调用 Query Builder 里面的方法和 return 语句是分开的,当然,这里的 $this 肯定是 Eloquent Builder 的实例了。    

  • Eloquent Builder可以使用 Query Builder 的方法,但是 Query Builder 不能使用 Eloquent Builder 的方法。 所以开发中使用 Model 进行 curd 操作的话,相对来说操控性更强,再加上有模型关联这些强大的功能,可以使用 Model 就没必要用 DB 类了。

好像又说得有点远了,这一点应该说的是,Relation 的相关操作,因为 Relation__call 里面也用到了 Query Builder,所以我们可以直接在关联上调用 Query Builder 里面的方法。但是这个时候关联使用需要加上括号。如:

1
2
3
$user->roles; // 用户所有角色
$user->roles()->count(); // roles() 调用返回 BelongsToMany 实例,
// 可以在此后调用 Query Builder 里面的方法

分页也类似 $user->roles()->paginate(); 这在我们需要查看某条记录的关联数据时候非常有用。

表名使用单数命名时候 Model 类不用定义 protected $table

Laravel 中表名默认是复数形式的,不知道大家有没有用复数做表名,我是没有这种习惯。如果我们想用单数命名,又不想每个 Model 类里面写一个 protected $table;可以重写 Model 类的 getTable 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Get the table associated with the model.
*
* @return string
*/
public function getTable()
{
if (isset($this->table)) {
return $this->table;
}

return str_replace('\\', '', Str::snake(class_basename($this)));
}

把转复数的调用去掉就好了。

为复杂表单创建一个 validator

这个其实也不算是什么技巧,可能一开始写得舒服就一个个 if 判断,但是这样子到最后会发现我们的代码越来越长,然后可读性也会越来越差。我们可以尝试使用一下 validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$validator = \Validator::make(request()->all(), [
'buyer_id' => 'required',
'amount' => 'required|numeric|gt:0'
], [
'buyer_id.required' => ':attribute不能为空',
'amount.required' => '请填写:attribute',
'amount.numeric' => ':attribute必须为数字',
'amount.gt' => ':attribute必须大于0'
], [
'buyer_id' => '客户id',
'amount' => '金额'
]);
if ($validator->fails()) {
return $this->ajaxFail($validator->messages()->first());
}

有些看起来是没什么用的,如 buyer_id,一般情况下,我们会用一个隐藏域保存这个 id,但是我们还是得防止一些非法的请求, 同时也是为了保持数据的完整性,因为这的确是必须的数据,如果有一天某个错误导致这个关联的 id 没有保存到,可能会导致一些神奇的 bug 出现。

返回页面的同时返回一些额外的信息(如警告、错误)

1
return redirect()->back()->withErrors('some error');

前端获取:

1
2
3
@if(Session::has('error'))
toastr.warning('{{Session::get('error')}}', '', options);
@endif

其实 Laravel 在处理表单的时候也有一些类似的处理, 如 redirect('form')->withInput();

在控制器以外的地方返回响应

这种做法可能会导致难以调试的 bug(因为维护的人可能不知道在哪里返回了),不过实在需要的时候,可以用一用。

1
response()->json(['message' => 'test send response directly'])->send();exit;

使用 EloquentCollection 加载关联

我们在使用 User::get() 的时候获取到的是一个 Illuminate\Database\Eloquent\Collection 实例,这个实例继承了 Illuminate\Support\Collection, 可以直接使用 Illuminate\Support\Collection 的方法。除此之外,还有一些自身特有的方法,比如 load,我们可以使用 load 方法加载每一项的关联数据, 好比如,如果每一个 User 有一个 info 关联,我们可以使用 $users->load('info') 关联,这种方法的好处是,避免了 n+1 的问题

函数、 类方法的依赖注入正确使用姿势

  • 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
class TestService
{
public $name = 'testService';
}

function test(TestService $testService)
{
var_dump($testService->name);
}

// 通过依赖注入的方式调用这个 test 函数
// test 的参数会通过依赖注入的方式注入
app()->call('test');
  • 类方法
1
2
3
4
5
6
7
8
9
10
class TestService
{
public function test(stdClass $stdClass)
{
var_dump($stdClass);
}
}

// 类方法的依赖注入(非构造方法)
app()->call('TestService@test');

最后推荐一个好东西:Laravel 5.1 LTS 速查表

1
$tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();

需要 doctrine/dbal 扩展,Laravel 本身也依赖这个扩展,所以无需额外安装

  1. 在修改 queue 相关代码后,必须要使用 php artisan queue:restart 来重启队列服务,否则所做的修改可能不会生效(没法重现了,按理说应该和使用 queue:listenqueue:work 有关,不过最好还是重启;可能和 supervisor 开启多个 queue:work 进程也有关系,本地测试的时候只有一个进程)。

文档: x

2、开发环境下以同步的方式执行队列,将 queue driver 的值改为 sync,注意,如果 queue 有输出的话,可能会导致一些问题,如:本来应该只返回 json 串的,然后 queue 里面有输出,导致前端 json 解析失败。

3、什么时候使用 queue:listen ?什么时候使用 queue:work

官网文档有一段描述是:

queue:work Artisan 命令里包含了 --daemon 选项,强制队列服务器持续处理任务,而不需要重新启动整个框架。比起 queue:listen 命令,这将明显的减少 CPU 的用量。

使用 queue:work 的时候不需要重现启动整个框架,这可能是 1 中可能修改 Job 后不生效的问题。

4、多个项目同时部署时候的冲突

Laravel 中队列任务使用 redis 驱动情况下保存的时候的缓存 key 是不带前缀的,比如 A 项目 dispatch 了一个 a job,保存在了 queues:default,然后我们去 B 项目 dispatch 另一个 job,我们发现它们保存在了相同的 redis key

x x

这样会导致的问题是:在一个项目中跑 php artisan queue:work 会拿到另外一个项目的 job,这样就会导致一些不必要的异常,因为在反序列化的过程中会找不到对应的类。

原因:config/queue.php 中配置的默认 queue 都是 default

1
2
3
4
5
6
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'expire' => 60,
]

解决办法:

a、自己用的是 5.1 版本,网上有说可以修改 cache prefix 解决,但是 laravel 5.1 行不通,可能新版本可以

b、为 job 指定不同的 queue,如 dispatch job 的时候可以 dispatch((new xxJob())->onQueue('xxQueue')),这样一来,job 就保存在了 queues:xxQueue 中, 但是还是得注意,如果还有其他项目,不要取相同名字。同时,这样一来,我们的 queue:work 或者 queue:listen 命令也要加上 --queue 参数了, 如 php artisan queue:work --queue=testQueue,否则还是会去 queues:default 里面找。如下:

x

c、直接修改 config/queue.php,修改 redis.queue 为一个唯一的名字。如:

1
2
3
4
5
6
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'peper',
'expire' => 60,
]
x

这样一来,我们的 job 就不会和其他项目的混在一起了。

个人看法:最好的实践应该还是,不同的队列使用不同的名字(即使是同一个项目),这样会更便于管理。

  1. 给队列的 Job 对象设置模型对象属性的时候,最后处理队列的时候会重新查询这个模型的数据。

详细见:\Illuminate\Queue\SerializesModels

也就是说,我们如果想在新建 Job 实例的时候,通过 setAttribute 设置了一个模型实例的属性,想在 handle 里面获取这个属性的话,是获取不到的。因为序列化队列任务的时候只会保存模型实例的几个关键属性,详细见:\Illuminate\Contracts\Database\ModelIdentifier

扩展:

1、监控 redis:

1
redis-cli > monitor
x

2、关于 Laravel 队列基本工作方式:dispatch 一个 job 的时候,Laravel 把 job 序列化保存到相应的 driver 中(redis、database、file...),然后 queue:listenqueue:work 的时候会从对应的 driver 里面取出这个 job,对 payload 反序列化,然后调用 job 里面的 handle 方法进行 job 的处理。

在 Laravel 中,有时候我们想修改模型对象的某个属性的时候,可能会报错 Indirect modification of overloaded property User::$info has no effect

这是因为我们给模型设置的属性获取我们通过属性的方式获取对象信息的时候,实际上是通过 Model 的 __get 魔术方法获取的。在模型的 $attributes 属性数组里面,保存了我们设置的对象属性,比如下面的操作:

定义模型

1
2
class User extends \Illuminate\Database\Eloquent\Model
{}
1
2
$user = new User;
$user->age = 26;

实际上是通过 Model 的 __set 方法在 Model 的 $attributes 数组属性里面加了一个属性:

1
2
3
4
$user = new User;
$user->age = 26;

dd($user->toArray(), $user->getAttributes()); // ['age' => 26], ['age' => 26]

::__get 方法定义

1
2
3
4
5
6
7
8
9
10
/**
* Dynamically retrieve attributes on the model.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->getAttribute($key);
}

前面我们设置了 $user->age = 26; 但是 property_exists($user, 'age') 会是 false。

我们获取这个属性的时候 $user->age,实际上等同于 $user->__get('age'),也就是说,我们想修改里面属性的时候比如 $user->info['job'] = 'PHP';,实际上等同于:

1
$user->__get('info')['job'] = 'PHP';

也就是说,等同于:

1
2
$temp = $user->__get('info');
$temp['job'] = 'PHP';

也就是说,我们在设置有层级的属性的时候,中间产生了临时变量。

假设大家知道了值传递、引用传递的区别。

这个时候问题就来了,如果这个 $user->info 是一个数组,我们最终修改的是一个临时数组,而 $user->info 还是不变。这个时候就报错了,因为这个修改是完全没有意义的。

而如果 $user->info 是一个对象的时候,我们临时变量也是一个对象,而对象是通过引用传递的,我们修改了这个对象的时候,实际上也修改了 $user->info,因为这个临时变量指向了原始对象。

完整例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14

$user = app(User::class);

$user->infoObj = new stdClass;
$user->infoObj->age = 26;
$user->infoObj->job = 'PHP';

// dd($user->toArray());

// Indirect modification of overloaded property User::$info has no effect
$user->info = [
'age' => 26,
];
$user->info['job'] = 'PHP';
0%