0%

一直以来,想调试框架中的某些东西,如想知道 Elpquentcreate 方法返回值是个什么东西,

以前的话,应该就是在 create 方法调用之后,使用 dd 或者 var_dump 之类的函数打印出来

如:

1
2
3
4
5
public function getTest()
{
$user = \App\User::create(['name' => 'tinker']);
dd($user);
}

这样一来,这个流程似乎有点冗长,因为我们还要打开浏览器查看

有了 tinker,我们就可以直接在命令行运行我们想要测试的代码,如:

tinker

我们可以看到,create 方法返回了一个模型对象。

我们就把这个 tinker 当作可以交互式的 代码执行工具好了,我们在学习 Laravel 的过程中可以在这里直接测试 Laravel 框架中的一些用法,也可以进行调试什么的。

通过控制台的 cookie 信息我们会发现,每次请求之后,关键的 cookie,如PHPSESSIDXSRF-TOKEN 都会发生变化,并且都是很长的一串字符串。

其实这是一个 json 数组,其中包含了 ivvaluemac 三个字段:

cookie

这些字段都是在框架加密解密的时候使用的,加密方法是 openssl_encrypt

openssl_encrypt

openssl 不太了解的可以看下下面的例子:

1
2
3
4
5
6
7
8
$data = 'laravel';
$iv = random_bytes(16);
$key = 'this is key';

$encrypt = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);

var_dump($encrypt);
var_dump(openssl_decrypt($encrypt, 'AES-256-CBC', $key, 0, $iv));

Laravel 中的话,key 就是 .env 配置文件里面的 APP_KEY,除了 key 还有两个变化的参数就是 加密、解密的数据以及 iv

也就是说,如果我们需要加密 cookie 的话,我们至少得保存下 加密后的数据以及 iv

这样看来,mac 字段似乎有点多余,但是我们可以使用该字段来验证数据的合法性:

x

如果验证不通过,Laravel 也就不会对 data 进行解密操作。

虽然每次请求 cookie 都会发生变化,但是实际数据是没有变的,发生变化只是因为用来加密的 iv 变了(使用 random_bytes 方法生成)。

由于 iv 每次都变化,所以需要把 iv 也一同返回给浏览器,加上验证数据合法性的 mac,最后返回的就是下面的数组的 json 编码后在 base64 编码的数据:

1
2
3
4
5
[
'iv' => random_bytes(16), // 16位随机字节串
'value' => 'xxxx...', // 加密后的数据
'mac' => 'xxx...' // 后续请求验证数据合法性的字符串
]

模型定义

App\User

1
2
3
4
5
6
7
class User extends Model
{
public function profile()
{
return $this->hasOne('App\UserProfile');
}
}

使用 with 查询某个 user 及其的 profile:

1
2
3
4
5
6
7
8
App\User::with([
'profile' => function($query) {
$query->select(['id']);
}
])
->find(4)
->toArray();

上面的用法中,我们会发现,即使数据库有记录,sql 也记录了对应的查询语句,但是 profile 关联却是空的:

但是加上外键就可以得到正确结果了:

1
2
3
4
5
6
7
8
App\User::with([
'profile' => function($query) {
$query->select(['id', 'user_id']); // 多了 user_id
}
])
->find(4)
->toArray();

可以查找到正确的 profile 了。

这和 Laravel 框架的工作方式相关,我们先看看下面的例子:

sql

我们使用 DB::listen 方法去记录相关的 sql 语句

我们查看 log 可以发现有以下语句:

1
2
select * from `users` where `id` in (?, ?) [3,4]
select * from `profiles` where `profiles`.`user_id` in (?, ?) [3,4]

我们可以明显发现,laravel 对于 userprofile 是独立查询的,

也就是说会得到两个集合,一个是 User、一个是 Profile,但是这并不是我们想要的结果,我们需要的结果是,只有一个 User 集合, 并且这个 User 集合里面有 Profile 关联。

但是结果就是这样,如果是你,你会怎么把这些数据关联起来呢?

对了,我们定义关联的时候不是定义了它们的关联方式么?

上面的 hasMany 方法默认第二第三个参数其实就是这两个集合建立关联的关键,第三个参数 user_id、第四个参数 id;这样一来我们就可以通过比较 Profileuser_idUser 里面的 id,如果相等,则这个 Profile 是属于这个 User 的,我们就把该 Profile 放进 Userprofile 关联中,最后就得到我们想要的结果了。

Xdebug 验证

xdebug 证实一下我们的想法:

match getRelationValue matchOneOrMany

如我们所想的那样,图一的 match 方法,顾名思义就是匹配了,通过 user 模型集合和 profile 模型集合进行匹配。

图二,也证实了我们模型建立关联需要通过关联中外键的值得想法。

图三,是通过获取 userlocalkey,也就是 id 的值,来查找 $dictonary 中是否有对应的值,buildDictonary 方法会建立一个关联数组,keyuser_id(外键)的值,值是关联的数据。这样一样,由于我们没有把 user_idselect 出来,最后得到的 $dictonary 的结构并不是预期的那样:

xdebug

其实我们本来是想要得到下面的这种:

1
2
3
[
3 => xxx(UserProfile对象) // 3 是关联的 user_id
]

但是我们得到的却是,所有的 UserProfile 都在一个嵌套的数组里面了,这样一来,下面的 getRelationValue 得到的结果自然就是空的了。

总结

好了,总结一下,就是:Laravel 先查询主要的数据(不带 with),查询完了之后,取出其中的 id 列数组(不一定都是id啊,只是举个例子),将这个数组作为条件去查找关联,有多少个关联就会再去查找多少次,查找完关联之后通过得到的结果的主键和关联数据的外键比对,相等则建立关联。

总结:在关联筛选 field 的时候,也必须要把关联的外键写进去,否则,即使产生了正确的 sql 语句,但是它们建立不了关联,通过 $user->profile 得到的还是一个空集合。

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
<?php

namespace Illuminate\Database\Eloquent;

/**
* 下面提到某些词的含义:
* 1、覆盖: 在继承该类 \Illuminate\Database\Eloquent\Model 的自定义的模型类中, 定义一个同名 field,值不一样
*/
abstract class Model1 implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
/**
* 数据库连接,我们在 config/database.php 里面的 connections 数组定义的连接。
* usage:
* (new xxModel)->connection('mysql')
* DB::connection('pgsql')
* 或者在模型定义里面覆盖该属性,如: protected $connection = 'pgsql';
*/
protected $connection;

/**
* 模型关联的数据库表名,默认是 模型名->下划线命名->复数形式,
* 好比如定义了 User 模型,class User extend Model,那么默认的表名是 users。
*
* usage:
* (new xxModel)->setTable('xxx')
* (new xxModel)->getModel()
* 或者在模型定义的时候覆盖该属性,如: protected $table = 'tb_user';
*/
protected $table;

/**
* 主键字段,默认为 id,也可以覆盖该属性
*/
protected $primaryKey = 'id';

/**
* 不知道哪里用到,除了该文件的 getter 和 setter
*/
protected $perPage = 15;

/**
* 主键是否默认自增长
*/
public $incrementing = true;

/**
* 是否由模型去维护时间戳字段,如果我们想手动去维护,可以设置为 false
* usage:
* 默认的时间戳字段是 created_at、updated_at,
* 我们如果想要使用自定义的字段,则要在模型里面覆盖 CREATED_AT、UPDATED_AT 这两个常量(下面有定义)
* 其他:
* 默认使用 mysql 的 datetime 类型,如果需要更改为 10 位整型,可以设置 protected $dateFormat = 'U'; ()
*/
public $timestamps = true;

/**
* 我们在给模型对象设置非 public 属性的时候,会通过 setAttributes 方法,保存到该数组中
* usage:
* $user = new User();
* $user->name = 'laravel';
* User 中没有定义 public $name 的时候, $attributes 就会多了 'name' => 'laravel' 的键值对
*/
protected $attributes = [];

/**
* 保存模型的原始数据,后续修改模型属性只会修改 $attributes,以便侦测变化
*/
protected $original = [];

/**
* 模型的关联数据
*/
protected $relations = [];

/**
* 隐藏的属性,我们调用模型的 toArray 方法的时候不会得到该数组中的属性,
* 如果需要也得到隐藏属性,可以通过 withHidden 方法
*/
protected $hidden = [];

/**
* 与 hidden 数组作用差不多,共同作用
*/
protected $visible = [];

/**
* 其中一个用法,根据现有某几个属性,计算出新属性,并在 模型 toArray 的时候显示
* usage:
* 模型里面定义: protected $appends = ['full_name'];
* public function getFullNameAttribute() { return $this->firstName . ' ' . $this->lastName; }
*/
protected $appends = [];

/**
* mass assignment 的时候可以批量设置的属性,目的是防止用户提交我们不想更新的字段
* 注意:
* 和 $guarded 同时使用的时候, $guard 设置的会无效
*/
protected $fillable = [];

/**
* 不能批量赋值的属性
*/
protected $guarded = ['*'];

/**
* 需要进行时间格式转换的字段
* 应用场景:
* 一般情况我们只定义了 created_at、updated_at,我们还可能会保存用户注册时间这些,register_time,
* 这样我们就可以定义,protected $dates = ['register_time'];
* 好比如:
* 我们定义的 $dateFormat 为 mysql 的 datetime 格式,我们即使把 register_time 设置为 time(),
* 实际保存的其实是 datetime 格式的
*/
protected $dates = [];

/**
* created_at、updated_at、$dates数组 进行时间格式转换的时候使用的格式
*/
protected $dateFormat;

/**
* 自动格式转换,定义方式: protected $casts = ['info' => 'json'];
* 所有可用格式: int、integer、real、float、double、string、bool、boolean、
* object、array、json、collection、date、datetime
* 应用场景:
* 我们想要保存某些特殊格式到数据库(如json、object),我们可以使用该数组对我们的数据进行自动转换,
* usage:
* $user = User::create([
* 'name' => 'ruby',
* 'info' => ['city' => 'Guangzhou']
* ]);
*
* $query_user = User::find($user['id']);
* dd($query_user->info);
* 这里我们可以看数据库保存的 info 字段,实际上是 json 编码的,并且取出来的是 json 解码后的数据
*/
protected $casts = [];

/**
* 需要同步更新 updated_at 的关联,调用 save 方法的时候会更新该数组里面定义的关联的 updated_at 字段
*/
protected $touches = [];

/**
* todo 自定义事件,目前还没用过 -_-
*/
protected $observables = [];

/**
* 需要预加载的关联
*/
protected $with = [];

/**
* The class name to be used in polymorphic relations.
*
* todo 也不知道
*/
protected $morphClass;

/**
* 模型是否存在
*/
public $exists = false;

/**
* 判断模型是否是当前请求插入的
*/
public $wasRecentlyCreated = false;

/**
* 模型属性名是否是下划线形式的
*/
public static $snakeAttributes = true;

/**
* 用来建立数据库连接
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected static $resolver;

/**
* 用来分发模型事件
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected static $dispatcher;

/**
* 模型 new 的时候调用模型的 bootXXX 方法, XXX 是模型名称
*/
protected static $booted = [];

/**
* 全局查询条件,需要定义
*
* @see \Illuminate\Database\Eloquent\ScopeInterface
*/
protected static $globalScopes = [];

/**
* 批量设置属性的时候是否不需要筛选字段
*/
protected static $unguarded = false;

/**
* 缓存 getXXAttribute 的值
*/
protected static $mutatorCache = [];

/**
* 多对多关联方法,不知道哪里用了
*/
public static $manyMethods = ['belongsToMany', 'morphToMany', 'morphedByMany'];

/**
* 创建时间戳字段名称
*/
const CREATED_AT = 'created_at';

/**
* 更新时间戳字段名称
*/
const UPDATED_AT = 'updated_at';
}

模型定义

App\User

1
2
3
4
5
6
7
class User
{
public function customer()
{
return $this->hasOne('App\Customer');
}
}

App\Customer

1
2
3
4
5
6
7
class Customer
{
public function user()
{
return $this->belongsTo('App\User');
}
}

需要注意的是 associate 方法是 BelongsTo 类才有,所以正确的调用方法如下:

1
2
3
4
5
6
7
8
use App\User;
use App\Customer;

$user = new User($data);
$customer = new Customer($customerData);

$customer->user()->associate($user);
$customer->save();

与此相反的方法是 disassociate 方法:取消两个模型之间的 belongsTo 关联

1
2
$customer->user()->disassociate();
$customer->save();

此方法需要注意的是,disassociate 并不会删除记录,只是更新关联的字段为 null。上面这种操作会把 customer 表的 user_id 设置为 null

附(通过关联的模型保存):

1
2
3
4
5
6
7
8
use App\User;
use App\Customer;

$user = new User($data);
$user->save();

$customer = new Customer($customerData);
$user->customer()->save($customer);