slpi1

slpi1


  • 首页

  • 归档

laravel 中 where 闭包条件使用注意事项

发表于 2021-03-01

先看两个使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use App\User;

// 示例1. where 闭包条件
$this->query = User::query();
$this->query->where('id', 1)->where('name', 'slpi1');
$this->query->where(function ($query) {
$query->orWhere('created', '2021-02-01');
$query->orWhere('updated', '2021-03-01');
});
$sql = $this->query->toSql();
dd($sql);
// select * from `users` where `id` = ? and `name` = ? and (`created` = ? or `updated` = ?)


// 示例2. where 闭包条件
$this->query = User::query();
$this->query->where('id', 1)->where('name', 'slpi1');
$this->query->where(function ($query) {
$this->query->orWhere('created', '2021-02-01');
$this->query->orWhere('updated', '2021-03-01');
});
$sql = $this->query->toSql();
dd($sql);
// select * from `users` where `id` = ? and `name` = ? or `created` = ? or `updated` = ?

通过上述两个示例,可以看出:

  • 在 where 闭包条件中,引用原查询来构造查询条件,也可以正常运行
  • 在 where 闭包条件中,引用原查询来构造查询条件,其结果并不满足闭包 where 的语义,属于使用错误

这一点在使用中应当特别注意,接下来再看看形成上述区别的原因是什么。

首先定位到框架关于 where() 方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Illuminate\Database\Eloquent\Builder

/**
* Add a basic where clause to the query.
*
* @param string|array|\Closure $column
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column instanceof Closure) {
$column($query = $this->model->newModelQuery());

$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
} else {
$this->query->where(...func_get_args());
}

return $this;
}

方法内容比较简单,仅有一个判断,当参数是一个闭包时,应该执行的流程,以及参数不是闭包时,应该执行的流程。其中方法中的 $this->query 指的是 Illuminate\Database\Query\Builder 的实例。为了方便描述,对两种 $query 做出描述上的区别:

  • Illuminate\Database\Eloquent\Builder 的实例对象 $query,称为模型查询对象
  • Illuminate\Database\Query\Builder 的实例对象 $query, 称为数据库查询对象
  • 每个模型查询对象内部,都有一个数据库查询对象:获取该对象的方法是 $query->getQuery();在对象内部的表示是 $this->query

对 模型查询对象A,当 where() 方法参数是一个闭包时,先以当前模型初始化一个空的 模型查询对象B,然后传入闭包执行。所以,在 where 闭包查询中的局部变量 $query 也就是这个空的 模型查询对象B。 闭包执行完毕后, 模型查询对象A 中的 数据库查询对象a 以 模型查询对象B 中的 数据库查询对象b 为参数,执行方法 addNestedWhereQuery()。在来看看 addNestedWhereQuery() 方法的实现,定位到相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Illuminate\Database\Query\Builder

/**
* Add another query builder as a nested where to the query builder.
*
* @param \Illuminate\Database\Query\Builder|static $query
* @param string $boolean
* @return $this
*/
public function addNestedWhereQuery($query, $boolean = 'and')
{
if (count($query->wheres)) {
$type = 'Nested';

$this->wheres[] = compact('type', 'query', 'boolean');

$this->addBinding($query->getBindings(), 'where');
}

return $this;
}

这段代码作用是,将两个 数据库查询对象 以 Nested 的方式,合并为一个数据库查询对象,其中作为参数的 数据库查询对象 会以 Nested where 的形式,保存到调用对象的 wheres 条件数组中。在后面 Grammar 拼接 SQL 语句的过程中,针对 wheres 条件中 Nested 类型的查询条件,会以括号的新式,合并为一个条件组。

自此,就大概解释清楚 where 闭包条件中,最终查询语句中的 () 是如何形成的。

如果在 where 闭包条件中,通过引用原 模型查询对象A 来续写查询条件的话,那么即便在闭包执行期间,执行了上述过程,但由于 模型查询对象B 并未被调用,那么他的 数据库查询对象b 中的 wheres 条件数据就是空的,在调用 addNestedWhereQuery() 方法时,if 条件判断为假,不会执行相应逻辑。所以这种情况下,其运行实质是如下代码:

1
2
3
4
5
6
7
8
9
10
// 示例3
$this->query = User::query();
$this->query->where('id', 1)->where('name', 'slpi1');
//$this->query->where(function ($query) {
$this->query->orWhere('created', '2021-02-01');
$this->query->orWhere('updated', '2021-03-01');
//});
$sql = $this->query->toSql();
dd($sql);
// select * from `users` where `id` = ? and `name` = ? or `created` = ? or `updated` = ?

php实现variable-precision SWAR算法

发表于 2021-02-19

在看 <<redis的设计与实现>> 时遇到一个问题,如何统计一个二进制串中 1 出现的次数。 书上一个介绍了三种方法:循环检查,查表统计以及 swar 算法。

首先看看 swar 算法的实现:

1
2
3
4
5
6
$int = ($int & 0x55555555) + (($int >> 1) & 0x55555555);
$int = ($int & 0x33333333) + (($int >> 2) & 0x33333333);
$int = ($int & 0x0f0f0f0f) + (($int >> 4) & 0x0f0f0f0f);
$int = ($int * 0x01010101) >> 24;

return $int & 0xff;

再简单介绍一下 swar 算法的原理。对任意一个长度为2的二进制数 x ,公式 x & 0b01 + (x >> 1) & 0b01 的结果,就是 x 中包含的 1 的个数,具体情况如下表:

x x & 0b01 + (x >> 1) & 0b01 结果
0b00 0b00 0
0b01 0b01 1
0b10 0b01 1
0b11 0b10 2

那么对于长度大于 2 的值呢?可以将数值进行两两分组,看作多个长度为 2 的二进制串的组合,然后对每一组运用上述公式进行计算,也就是算法实现的第一行。

通过第一行的计算后,原数值的两位一组的包含 1 个数的结果,保存在计算结果中,剩下的工作,就是对第一行的计算结果,两两分组后,进行求和。以十进制数为例,对 123 的各位数求和,公式为 3 + 20 /10 + 100 / 100。而对一个 4 位的二进制串 y,计算前两位与后两位的和公式为: y & 0b0011 + (y >> 2) & 0b0011,得到的 4 位二进制串表示的数,就是其结果,也就是算法实现的第二行。以次类推4位、8位、16位分组的求和,即可得到最终结果。

算法实现的第三行,就是计算按4位分组的和数,得到的结果是8位的结果组合。如果继续按8位分组算和的化,表达式应该是 $int = ($int & 0x00ff00ff) + (($int >> 8) & 0x00ff00ff),但算法实现的第四行却并不是这样,为什么呢?因为表达式 $int * 0x01010101 计算的结果的第25到32位,实际上就是原值 $int 的 1到8位,9到16位,17到24位,25到32位 四个分组的数值和,这个结论写一下竖式乘法就可以验证。所以,按八位分组的和通过一个算式求和到第25到32位,然后右移24位到低位即可。

最后对结果进行 $int & 0xff 运算就可以得到最后的结果。因为在 php 中,整型数值溢出后会自动转化为浮点表示,如果第四行的乘法结果超出整形的表示返回,或者 php 是64位的版本,总之就是乘积的结果表示超过了32位,那么右移24位后的结果依然超出8位,所以还要对第四行的结果取低八位,得到最终的结果。

再谈Filesystem及Storage驱动的扩展

发表于 2020-11-21

之前写过一篇文章来讲 laravel 中的文件系统,提到框架中使用到两套文件系统,一套是 NativeFilesystem,主要用于框架内部一些用到文件管理的场景,比如:生成缓存文件、生成模板文件、操作扩展包的文件等。另外一套是 Flysystem基于 league/flysystem 扩展,提供对本地文件系统、FTP、云盘等具备文件存储功能的系统的操作。也就是使用文档中的 Storage 。

Storage 面向更广泛、更普遍、更严格的文件管理的场景,所以逻辑也更为严谨与复杂。框架对 Storage 的封装有四个层次,每层解决不同的问题,各层都起到承上启下的作用,他们分别是:

  • Storage: 最上层的对用户友好的操作接口,语义化程度较高。
  • FilesystemAdapter: 对文件系统的高级抽象,包含一定的使用场景原语,是对文件系统的运用范围的一定程度的扩展。
  • Filesystem: 统一文件管理的公共接口,而不需要关心文件最终会存储到什么地方,这一层已经回归到对文件的管理,这一单一职责上。
  • Adapter: 承接具体的文件管理职责,这里明确文件的存储方式,能对外提供的接口。

在了解到框架 Storage 的具体实现逻辑后,再来谈谈为什么我们要了解这些。在我负责的美术相关业务系统中,有大量的文件存储的需求,原本是单独部署的一个存储服务器,挂载到 web 服务器,统一保存这些文件资源。 后来 IT 部门对外采购了云存储服务,用来统一存储公司各个业务部门的业务文件,刚好我们系统也可以接入。由于前期的架构,代码中大量使用 Storage 的 local 驱动管理文件,而在接入云盘的过程中,需要实现新的驱动,来取代原始的 local 驱动。由此,在了解到框架对 Storage 的封装,以及云供应商提供的接口文档后,开始着手实现云存储的驱动。

在了解框架实现之后,很容易做出判断,在原有应用场景不变的情况下,可以跳过对 Storage 前三层的修改,直接在第四层 Adapter 进行封装,而这一层的主要职责,是对接统一的文件管理接口与云供应商提供的接口。具体需要实现的接口类是 League\Flysystem\AdapterInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace League\Flysystem;

interface AdapterInterface extends ReadInterface
{
const VISIBILITY_PUBLIC = 'public';
const VISIBILITY_PRIVATE = 'private';

public function write($path, $contents, Config $config);
public function writeStream($path, $resource, Config $config);
public function update($path, $contents, Config $config);
public function updateStream($path, $resource, Config $config);
public function rename($path, $newpath);
public function copy($path, $newpath);
public function delete($path);
public function deleteDir($dirname);
public function createDir($dirname, Config $config);
public function setVisibility($path, $visibility);
}

考虑到后期可能对文件管理行为添加权限等场景,决定在 Adapter 下层再添加一层对云供应商接口的封装,其主要职责是对接 AdapterInterface 以及预留后期可能需要实现的功能接口。到此,就可以规划各层级的职责,建立基础的代码框架。

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
/**
* 一、云供应商接口封装类
* 负责处理云服务的登录,会话保存,接口调用
*/
class CloudDriverHandler
{

}


/**
* 二、云存储驱动
* 通过调用CloudDriverHandler的接口实现AdapterInterface的方法,
*/
class CloudAdapter extends AbstractAdapter /*implements AdapterInterface*/
{
protected $handler;
public function __construct(CloudDriverHandler $handler)
{
$this->handler = $handler;
}

}

/**
* 三、 通过 Provider 注入实现,扩展Stroage驱动
*/
class CloudServiceProvider extends ServiceProvider
{
public function boot()
{
Storage::extend('cloud', function ($app, $config) {

// 当 Storage 扩展驱动被解析时,为 CloudDriverHandler 实例导入配置
// 并绑定实例到容器,后续容器解析该对象时,返回的就是已配置过的实例
$handler = new CloudDriverHandler($app['cache']);
$handler->init($config);
$app->instance('cloud', $handler);

$adaper = new CloudAdapter($handler);
if (isset($config['root'])) {
$adaper->setPathPrefix($config['root']);
}

return new FilesystemAdapter(new Filesystem($adaper, $config));
});
}

/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(CloudDriverHandler::class, function ($app) {
// 触发 Storage::extend('cloud') 的解析,达到注入配置的目的
// 需要配置 filesystems.cloud
Storage::cloud();
if ($app->resolved('cloud')) {
return $app['cloud'];
}
throw new PanException('请配置默认 cloud 驱动');
});

$this->app->alias(CloudDriverHandler::class, 'cloud');
}

public function provides()
{
return [
'cloud' => CloudDriverHandler::class,
];
}
}

代码开发完毕后,配置好云存储的参数,就可以开始使用,使用方面也可以区分两个层级。第一层级是以 Storage 完成对文件管理的操作,第二层级是以服务注入的方式,调用云供应商提供的原生接口,以次来实现存储功能外的需求。

1
2
3
4
5
6
7
8
9
10
// 1. 作为 Storage 的云盘驱动使用
Storage::disk('cloud')->has('test.txt');
Storage::disk('cloud')->copy('test.txt', '/folder/test.txt');
Storage::disk('cloud')->rename('test.txt', 'newName.txt');
Storage::disk('cloud')->delete('test.txt');

// 2. 调用云盘服务的 filesystem 以外的接口
$pan = app('cloud');
$pan->getHost();
$pan->getUserInfoByAccount('songzhp');

CloudDriverHandler::class/CloudAdapter::class 的具体实现如下:

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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
<?php

namespace Youzu\Pan;

use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Config;
use League\Flysystem\Handler;
use League\Flysystem\Util;

class CloudAdapter extends AbstractAdapter
{
protected $handler;
public function __construct(CloudDriverHandler $handler)
{
$this->handler = $handler;
}

/**
* Write a new file.
*
* @param string $path
* @param string $contents
* @param Config $config Config object
*
* @return array|false false on failure file meta data on success
*/
public function write($path, $contents, Config $config)
{
$path = $this->applyPathPrefix($path);

$info = pathinfo($path);
$folderPath = $info['dirname'];
$filename = $info['basename'];

$folderInfo = $this->mkdir($folderPath);
$folderId = $folderInfo['folderId'];

$fileId = $this->handler->upload($folderId, $filename, $contents);

$type = 'file';
$size = strlen($contents);
$result = compact('contents', 'type', 'size', 'path', 'fileId', 'folderId');
return $result;
}

/**
* Write a new file using a stream.
*
* @param string $path
* @param resource $resource
* @param Config $config Config object
*
* @return array|false false on failure file meta data on success
*/
public function writeStream($path, $resource, Config $config)
{
$path = $this->applyPathPrefix($path);

$info = pathinfo($path);
$folderPath = $info['dirname'];
$filename = $info['basename'];

$folderInfo = $this->mkdir($folderPath);
$folderId = $folderInfo['folderId'];

$fileId = $this->handler->uploadStream($folderId, $filename, $resource);

$type = 'file';
$result = compact('type', 'path', 'fileId', 'folderId');
return $result;
}

/**
* Update a file.
*
* @param string $path
* @param string $contents
* @param Config $config Config object
*
* @return array|false false on failure file meta data on success
*/
public function update($path, $contents, Config $config)
{
$path = $this->applyPathPrefix($path);

$info = pathinfo($path);
$folderPath = $info['dirname'];
$filename = $info['basename'];

$folderId = $this->handler->getFolderId($folderPath);

$fileId = $this->handler->upload($folderId, $filename, $contents, true);
$type = 'file';
$size = strlen($contents);
$result = compact('type', 'path', 'size', 'contents', 'fileId', 'folderId');
return $result;
}

/**
* Update a file using a stream.
*
* @param string $path
* @param resource $resource
* @param Config $config Config object
*
* @return array|false false on failure file meta data on success
*/
public function updateStream($path, $resource, Config $config)
{
$path = $this->applyPathPrefix($path);

$info = pathinfo($path);
$folderPath = $info['dirname'];
$filename = $info['basename'];
$folderId = $this->handler->getFolderId($folderPath);

$fileId = $this->handler->uploadStream($folderId, $filename, $resource, true);
$type = 'file';
$result = compact('type', 'path', 'fileId', 'folderId');
return $result;
}

/**
* Rename a file.
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function rename($path, $newpath)
{
$path = $this->applyPathPrefix($path);
$newpath = $this->applyPathPrefix($newpath);

$originName = pathinfo($path, PATHINFO_BASENAME);
$newName = pathinfo($newpath, PATHINFO_BASENAME);

$parentFolder = pathinfo($newpath, PATHINFO_DIRNAME);
$parentFolderInfo = $this->mkdir($parentFolder);
$parentFolderId = $parentFolderInfo['folderId'];

if ($fileId = $this->handler->getFileId($path)) {
// 如果目标文件夹中有同名的文件
if ($this->handler->fileExists($originName, $parentFolderId)) {
// 1. 重命名为临时文件
$templateName = $this->templateName();
$this->handler->renameFile($fileId, $templateName);
}

// 移动文件
$this->handler->moveFile($fileId, $parentFolderId);
// 重命名为新的文件名
return $this->handler->renameFile($fileId, $newName);
} elseif ($folderId = $this->handler->getFolderId($path)) {

// 如果目标文件夹中有同名的文件
if ($this->handler->folderExists($originName, $parentFolderId)) {
// 1. 重命名为临时文件
$templateName = $this->templateName();
$this->handler->renameFolder($folderId, $templateName);
}

// 移动文件
$this->handler->moveFolder($folderId, $parentFolderId);
// 重命名为新的文件名
return $this->handler->renameFolder($folderId, $newName);
}
}

/**
* 生成临时文档名
*
* @method templateName
* @author 雷行 songzhp@yoozoo.com 2020-10-22T17:06:18+0800
* @return string
*/
protected function templateName()
{
return uniqid() . '_' . rand(1000, 9999);
}

/**
* Copy a file.
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function copy($path, $newpath)
{
$oldOriginPath = $path;
$oldNewPath = $newpath;

$originName = pathinfo($path, PATHINFO_BASENAME);
$newName = pathinfo($newpath, PATHINFO_BASENAME);

$path = $this->applyPathPrefix($path);
$newpath = $this->applyPathPrefix($newpath);

if ($folderId = $this->handler->getFolderId($path)) {
return false;
}
$fileInfo = $this->handler->getFileInfo($path);
$fileId = $fileInfo['FileId'];
$parentFolderId = $fileInfo['ParentFolderId'];

$newParentFolder = pathinfo($newpath, PATHINFO_DIRNAME);
$newParentFolderInfo = $this->mkdir($newParentFolder);

// 1. 复制到其他文件夹,直接调接口
if ($parentFolderId != $newParentFolderInfo['folderId']) {
// copy 到其他目录后,文件名不变,要更新一次名称
if ($this->handler->copyFile($fileId, $newParentFolderInfo['folderId'])) {
$copyPath = $newParentFolder . '/' . $originName;
$newFileId = $this->handler->getFileId($copyPath);
return $this->handler->renameFile($newFileId, $newName);
}
return false;
}

// 2. 复制到原文件夹,需要重命名
return (bool) $this->write($oldNewPath, $this->read($oldOriginPath)['contents'], new Config);

}

/**
* Delete a file.
*
* @param string $path
*
* @return bool
*/
public function delete($path)
{
$path = $this->applyPathPrefix($path);

if ($fileId = $this->handler->getFileId($path)) {
return $this->handler->deleteFileById($fileId);
} elseif ($folderId = $this->handler->getFolderId($path)) {
return $this->handler->deleteFolderById($folderId);
}
}

/**
* Delete a directory.
*
* @param string $path
*
* @return bool
*/
public function deleteDir($path)
{
return $this->delete($path);
}

/**
* Create a directory.
*
* @param string $path directory name
* @param Config $config
*
* @return array|false
*/
public function createDir($path, Config $config)
{
$path = $this->applyPathPrefix($path);
return $this->mkdir($path);
}

protected function mkdir($path)
{
if ($folderId = $this->handler->getFolderId($path)) {
return ['path' => $path, 'type' => 'dir', 'folderId' => $folderId];
}
$folderId = $this->handler->createFolder($path);

return ['path' => $path, 'type' => 'dir', 'folderId' => $folderId];
}

/**
* Set the visibility for a file.
*
* @param string $path
* @param string $visibility
*
* @return array|false file meta data
*/
public function setVisibility($path, $visibility)
{
$visibility = false;
return compact('path', 'visibility');
}

/**
* Get the visibility of a file.
*
* @param string $path
*
* @return array|false
*/
public function getVisibility($path)
{
$visibility = false;
return compact('path', 'visibility');
}
/**
* 返回文件连接
*
* @method getUrl
* @author 雷行 songzhp@yoozoo.com 2020-10-27T16:24:38+0800
* @param string $path 文件路径
* @return string
*/
public function getUrl($path)
{
$path = $this->applyPathPrefix($path);

$host = $this->handler->getHost();
if ($fileId = $this->handler->getFileId($path)) {
return $this->handler->publishFile($fileId);
} elseif ($fileId = $this->handler->getFolderId($path)) {
return $this->handler->publishFolder($fileId);
}
return false;
}

/**
* Check whether a file exists.
*
* @param string $path
*
* @return array|bool|null
*/
public function has($path)
{

$path = $this->applyPathPrefix($path);
$rootId = $this->handler->getTopId();
$info = explode('/', $path);
$last = array_pop($info);

// 以目录分割符结尾,目录存在
if ($last == '') {
return (bool) $this->handler->getFolderId($path);
}

// 目录或文件存在
return $this->handler->getFolderId($path) || $this->handler->getFileId($path);
}

/**
* Read a file.
*
* @param string $path
*
* @return array|false
*/
public function read($path)
{

$path = $this->applyPathPrefix($path);

if ($fileId = $this->handler->getFileId($path)) {
$body = $this->handler->download($fileId);

$content = '';
while (!$body->eof()) {
$content .= $body->read(1024);
}

return ['type' => 'file', 'path' => $path, 'contents' => $content];
}
return false;
}

/**
* Read a file as a stream.
*
* @param string $path
*
* @return array|false
*/
public function readStream($path)
{
$path = $this->applyPathPrefix($path);

$temp = tmpfile();
if ($fileId = $this->handler->getFileId($path)) {
$body = $this->handler->download($fileId);

while (!$body->eof()) {
fwrite($temp, $body->read(1024));
}
fseek($temp, 0);

return ['type' => 'file', 'path' => $path, 'stream' => $temp];
}
return false;
}

/**
* List contents of a directory.
*
* @param string $directory
* @param bool $recursive
*
* @return array
*/
public function listContents($directory = '', $recursive = false)
{
$directory = $this->applyPathPrefix($directory);

return $this->listDirectory($directory);
}

/**
* 列出文件夹内容
*
* @method listDirectory
* @author 雷行 songzhp@yoozoo.com 2020-11-05T11:39:53+0800
* @param string $directory
* @param boolean $recursive
* @return array
*/
protected function listDirectory($directory = '', $recursive = false)
{

$data = [];
if ($folderId = $this->handler->getFolderId($directory)) {
$list = $this->handler->listFolder($folderId);

foreach ($list as $item) {
if (isset($item['FolderId'])) {
$path = ($directory ? $directory . '/' : '') . $item['FolderName'];
$data[] = $this->formatFolderInfo($item, $path);

if ($recursive) {
$data = array_merge($data, $this->listDirectory($path, $recursive));
}
} else {

$path = ($directory ? $directory . '/' : '') . $item['FileName'];
$data[] = $this->formatFileInfo($item, $path);
}
}
}

return $data;
}

/**
* Get all the meta data of a file or directory.
*
* @param string $path
*
* @return false|array
*/
public function getMetadata($path)
{
$path = $this->applyPathPrefix($path);
if ($fileInfo = $this->handler->getFileInfo($path)) {
return $this->formatFileInfo($fileInfo, $path);
} elseif ($folderInfo = $this->handler->getFolderInfo($path)) {
return $this->formatFolderInfo($folderInfo, $path);
}
}

protected function formatFileInfo($fileInfo, $path)
{
return [
//TODO:: link?
'fileId' => $fileInfo['FileId'],
'parentFolderId' => $fileInfo['ParentFolderId'],
'type' => 'file',
'path' => $path,
'timestamp' => strtotime($fileInfo['FileModifyTime']),
'mimetype' => Util\MimeType::detectByFilename($fileInfo['FileName']),
'size' => isset($fileInfo['FileSize']) ? $fileInfo['FileSize'] : $fileInfo['FileLastSize'],
];
}

protected function formatFolderInfo($folderInfo, $path)
{
return [
'folderId' => $folderInfo['FolderId'],
'parentFolderId' => $fileInfo['ParentFolderId'],
'type' => 'dir',
'path' => $path,
'timestamp' => isset($folderInfo['ModifyTime'])
? strtotime($folderInfo['ModifyTime'])
: strtotime($folderInfo['FolderModifyTime']),
];
}

/**
* Get all the meta data of a file or directory.
*
* @param string $path
*
* @return false|array
*/
public function getSize($path)
{
return $this->getMetadata($path);
}

/**
* Get the mimetype of a file.
*
* @param string $path
*
* @return false|array
*/
public function getMimetype($path)
{
return $this->getMetadata($path);
}

/**
* Get the timestamp of a file.
*
* @param string $path
*
* @return false|array
*/
public function getTimestamp($path)
{
return $this->getMetadata($path);
}
}
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
<?php

namespace Youzu\Pan;

use GuzzleHttp\Client;
use Log;
use Youzu\Pan\Apis;
use Youzu\Pan\Exceptions\PanException;
use Youzu\Pan\Exceptions\RequestException;

class CloudDriverHandler
{

protected $host;
protected $account;
protected $password;

protected $http;
protected $token = null;

protected $rootType = 'person';
protected $rootTypeList = ['person', 'public'];

// 分片大小
protected $uploadChunkSize = 1024 * 1200;

protected $debug = false;

public function __construct($cache)
{
$this->cache = $cache;
}

/**
* 初始化配置
*
* @method init
* @author 雷行 songzhp@yoozoo.com 2020-11-20T12:22:14+0800
* @param array $config
* @return void
*/
public function init($config)
{
$this->host = $config['host'];
$this->account = $config['account'];
$this->password = $config['password'];

$this->http = new Client([
'base_uri' => $this->host,

// 忽略证书过期的事实 !-_-
'verify' => false,
'headers' => [
//'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36',
'Accept' => 'application/json',
],
]);

if (isset($config['type']) && in_array($config['type'], $this->rootTypeList)) {
$this->setRootType($config['type']);
}

if (isset($config['debug']) && $config['debug']) {
$this->debug = true;
}

}

/**
* 获取host
*
* @method getHost
* @author 雷行 songzhp@yoozoo.com 2020-11-04T17:58:32+0800
* @return string
*/
public function getHost()
{
return $this->host;
}

/**
* 设置根目录类型
*
* @method setRootType
* @author 雷行 songzhp@yoozoo.com 2020-10-22T14:27:50+0800
* @param string $type
*/
protected function setRootType($type)
{
$this->rootType = $type;
self::$topId = null;
}

/**
* 获取Token
*
* @method getToken
* @author 雷行 songzhp@yoozoo.com 2020-11-19T11:28:14+0800
* @param boolean $force 是否强制更新
* @return string
*/
public function getToken($force = false)
{
$key = 'YOUZU:PAN:TOKEN';

if (!$force) {
if (isset($this->token)) {
return $this->token;
}

$token = $this->cache->get($key);

if ($token && $this->validateToken($token)) {
$this->token = $token;
return $this->token;
}
}

$token = $this->login();

// 设置token过期时间一小时
$this->cache->put($key, $token, 60);
$this->token = $token;
return $this->token;
}

/**
* 登录云盘
*
* @method login
* @author 雷行 songzhp@yoozoo.com 2020-10-22T10:35:36+0800
* @return boolean
*/
public function login()
{
$source = new Apis\Org\UserLogin($this->account, $this->password);
return $this->fetch($source);
}

/**
* 验证token是否有效
*
* @method validateToken
* @author 雷行 songzhp@yoozoo.com 2020-10-22T10:35:06+0800
* @param string $token 云盘Token
* @return Boolean
*/
public function validateToken($token)
{
$source = new Apis\Org\CheckUserTokenValidity($token);
return $this->fetch($source);
}

/**
* 验证服务器时间
*
* @method check
* @author 雷行 songzhp@yoozoo.com 2020-10-22T11:01:07+0800
* @return string
*/
public function check()
{
$source = new Apis\Doc\GetServerDateTime($this->getToken());
return $this->fetch($source);
}

private static $topId = null;
/**
* 获取根目录ID
*
* @method getTopId
* @author 雷行 songzhp@yoozoo.com 2020-10-22T11:06:59+0800
* @return integer
*/
public function getTopId()
{
if (!isset(self::$topId)) {
$type = $this->rootType == 'person' ? Apis\Doc\GetTopFolderId::PERSON_FOLDER : Apis\Doc\GetTopFolderId::PUBLIC_FOLDER;
$source = new Apis\Doc\GetTopFolderId($this->getToken(), $type);
self::$topId = $this->fetch($source);
}
return self::$topId;
}

/**
* 判断指定目录中是否存在文件夹名
*
* @method folderExists
* @author 雷行 songzhp@yoozoo.com 2020-10-22T11:16:12+0800
* @param string $folderName 待判定文件夹名称
* @param interger $parentId 父级文件夹ID
* @return boolean
*/
public function folderExists($folderName, $parentId)
{
$source = new Apis\Folder\IsExistfolderInFolderByfolderName($this->getToken(), $folderName, $parentId);
return $this->fetch($source);
}

/**
* 判断指定目录中是否存在某文件
*
* @method fileExists
* @author 雷行 songzhp@yoozoo.com 2020-10-22T11:21:43+0800
* @param string $fileName 待判定文件名
* @param integer $parentId 父级文件夹ID
* @return boolean
*/
public function fileExists($fileName, $parentId)
{
$source = new Apis\File\IsExistFileInFolderByFileName($this->getToken(), $fileName, $parentId);
return $this->fetch($source);
}

/**
* 列出文件目录
*
* @method listFolder
* @author 雷行 songzhp@yoozoo.com 2020-10-26T14:15:17+0800
* @param integer $folderId 目录ID
* @return array
*/
public function listFolder($folderId)
{

$page = 1;
$limit = 20;
$source = new Apis\Doc\GetFileAndFolderList($this->getToken(), $folderId, $page, $limit);
$info = $this->fetch($source);

$page++;
$total = $info['Settings']['TotalCount'];
$lastPage = ceil($total / $limit);

$contents = array_merge($info['FilesInfo'], $info['FoldersInfo']);
while ($page < $lastPage) {

$source = new Apis\Doc\GetFileAndFolderList($this->getToken(), $folderId, $page, $limit);
$info = $this->fetch($source);

$contents = array_merge($contents, $info['FilesInfo'], $info['FoldersInfo']);
}
return $contents;
}

/**
* 根据目录路径,获取目录ID
*
* @method getFolderId
* @author 雷行 songzhp@yoozoo.com 2020-10-22T11:39:51+0800
* @param string $path string
* @return integer
*/
public function getFolderId($path)
{
if ($info = $this->getFolderInfo($path)) {
return $info['FolderId'];
}
return false;
}

/**
* 根据目录路径,获取目录信息
*
* @method getFolderInfo
* @author 雷行 songzhp@yoozoo.com 2020-10-22T13:42:13+0800
* @param string $path
* @return array
*/
public function getFolderInfo($path)
{

$topId = $this->getTopId();
$realpath = $topId . '/' . $path;
$source = new Apis\Folder\GetFolderInfoByNamePath($this->getToken(), $realpath);
$info = $this->fetch($source);
return $info;
}

/**
* 根据文件ID,获取文件夹信息
*
* @method getFolderInfoById
* @author 雷行 songzhp@yoozoo.com 2020-10-23T10:12:04+0800
* @param integer $folderId 文件ID
* @return array
*/
public function getFolderInfoById($folderId)
{
$source = new Apis\Folder\GetFolderInfoById($this->getToken(), $folderId);
if ($info = $this->fetch($source)) {
return $info;
}
return false;
}

/**
* 根据文件路劲,获取文件ID
*
* @method getFileId
* @author 雷行 songzhp@yoozoo.com 2020-10-22T12:06:19+0800
* @param string $path 文件路径
* @return integer
*/
public function getFileId($path)
{
if ($info = $this->getFileInfo($path)) {
return $info['FileId'];
}
return false;
}

/**
* 根据文件路径,获取文件信息
*
* @method getFileInfo
* @author 雷行 songzhp@yoozoo.com 2020-10-22T13:43:58+0800
* @param string $path
* @return array
*/
public function getFileInfo($path)
{

$topId = $this->getTopId();
$realpath = $topId . '/' . $path;
$source = new Apis\File\GetFileInfoByNamePath($this->getToken(), $realpath);
$info = $this->fetch($source);
return $info;
}

/**
* 根据文件ID,获取文件信息
*
* @method getFileInfoById
* @author 雷行 songzhp@yoozoo.com 2020-10-23T10:12:04+0800
* @param integer $fileId 文件ID
* @return array
*/
public function getFileInfoById($fileId)
{
$source = new Apis\File\GetFileInfoById($this->getToken(), $fileId);
if ($info = $this->fetch($source)) {
return $info;
}
return false;
}

/**
* 将文本写入网盘文件
*
* @method uploadStream
* @author 雷行 songzhp@yoozoo.com 2020-10-23T17:12:53+0800
* @param integer $folderId 父级文件夹ID
* @param string $filename 文件名
* @param string $content 资源
* @param boolean $updateVersion 是否更新版本
* @param integer $blockSize 分片块大小
* @return integer
*/
public function upload($folderId, $filename, $content, $updateVersion = false, $blockSize = 0)
{

if (!$blockSize) {
$blockSize = $this->uploadChunkSize;
}

$pos = 0;
$uploadId = uniqid() . time() . rand(100, 999);
$size = strlen($content);

try {

// 1. 启动传输任务
$start = new Apis\Uploads\StartUploadFile($this->getToken(), $uploadId, $filename, $folderId, $size, $updateVersion);
$fileInfo = $this->fetch($start);
if (!$fileInfo) {
$this->stopUpload($folderId, $filename);
return false;
}
$regionHash = $fileInfo['RegionHash'];

// 3. 传输文件
$chunks = str_split($content, $blockSize);
$count = count($chunks);
foreach ($chunks as $key => $chunk) {
if ($key < $count - 1) {
$len = $blockSize;
} else {
$len = strlen($chunk);
}
$chunk = base64_encode($chunk);

$uploader = new Apis\Uploads\UploadFileBlock($this->getToken(), $regionHash, $uploadId, $blockSize, $chunk, $pos);
$uploadInfo = $this->fetch($uploader);

if (!$uploadInfo) {
$this->stopUpload($folderId, $filename);
return false;
}
$pos += $len;
}

// 3. 完成传输
$finish = new Apis\Uploads\EndUploadFile($this->getToken(), $regionHash, $uploadId);
$finishInfo = $this->fetch($finish);
if (!$finishInfo) {
$this->stopUpload($folderId, $filename);
return false;
}
} catch (\ErrorException $e) {
$this->stopUpload($folderId, $filename);
throw $e;
}
return $fileInfo['FileId'];
}

/**
* 将资源写入网盘
*
* @method uploadStream
* @author 雷行 songzhp@yoozoo.com 2020-10-23T17:12:53+0800
* @param integer $folderId 父级文件夹ID
* @param string $filename 文件名
* @param resource $resource 资源
* @param boolean $updateVersion 是否更新版本
* @param integer $blockSize 分片块大小
* @return integer
*/
public function uploadStream($folderId, $filename, $resource, $updateVersion = false, $blockSize = 0)
{
if (!$blockSize) {
$blockSize = $this->uploadChunkSize;
}

$pos = 0;
$uploadId = uniqid() . time() . rand(100, 999);
$size = $this->getStreamSize($resource);

try {
// 1. 启动传输任务
$start = new Apis\Uploads\StartUploadFile($this->getToken(), $uploadId, $filename, $folderId, $size, $updateVersion);
$fileInfo = $this->fetch($start);
if (!$fileInfo) {
$this->stopUpload($folderId, $filename);
return false;
}
$regionHash = $fileInfo['RegionHash'];

rewind($resource);
// 3. 传输文件
while (!feof($resource)) {
$data = fread($resource, $blockSize);
$len = strlen($data);
$chunk = base64_encode($data);

$uploader = new Apis\Uploads\UploadFileBlock($this->getToken(), $regionHash, $uploadId, $len, $chunk, $pos);
$uploadInfo = $this->fetch($uploader);

if (!$uploadInfo) {
$this->stopUpload($folderId, $filename);
return false;
}
$pos += $len;
}

$finish = new Apis\Uploads\EndUploadFile($this->getToken(), $regionHash, $uploadId);
$finishInfo = $this->fetch($finish);

if (!$finishInfo) {
$this->stopUpload($folderId, $filename);
return false;
}
} catch (\ErrorException $e) {
$this->stopUpload($folderId, $filename);
throw $e;
}
return $fileInfo['FileId'];
}

/**
* 获取资源大小
*
* @method getStreamSize
* @author 雷行 songzhp@yoozoo.com 2020-10-23T17:09:32+0800
* @param resource $resource
* @return integer
*/
protected function getStreamSize($resource)
{
$data = null;
$count = 0;
while (!feof($resource)) {
$data = fread($resource, $this->uploadChunkSize);
$count++;
}

return ($count - 1) * $this->uploadChunkSize + strlen($data);
}

/**
* 取消文件上传
*
* @method stopUpload
* @author 雷行 songzhp@yoozoo.com 2020-10-23T16:31:32+0800
* @param integer $folderId 上传文件夹ID
* @param string $filename 文件名
* @return void
*/
public function stopUpload($folderId, $filename)
{

$finish = new Apis\Uploads\UndoCreateFile($this->getToken(), $folderId, $filename);
$finishInfo = $this->fetch($finish);
}

/**
* 创建文件夹
*
* @method createFolder
* @author 雷行 songzhp@yoozoo.com 2020-10-22T16:06:36+0800
* @param string $path
* @return integer
*/
public function createFolder($path)
{
$topId = $this->getTopId();
$parts = explode('/', $path);
$suffix = '';
$parentId = $topId;
$currentId = null;
while ($folderName = array_shift($parts)) {
$suffix .= $folderName . '/';

$currentId = $this->getFolderId($suffix);
if (!$currentId) {
$currentId = $this->createFolderUnder($parentId, $folderName);
}
$parentId = $currentId;
}
return $currentId;
}

/**
* 在指定目录下新建文件夹
*
* @method createFolderUnder
* @author 雷行 songzhp@yoozoo.com 2020-10-22T15:02:59+0800
* @param integer $parentId 父级目录ID
* @param string $folderName 待创建文件名
* @return integer
*/
public function createFolderUnder($parentId, $folderName)
{
$source = new Apis\Folder\CreateFolder($this->getToken(), $parentId, $folderName);
if ($info = $this->fetch($source)) {
return $info['FolderId'];
}
return false;
}

/**
* 移动文件到指定目录
*
* @method moveFile
* @author 雷行 songzhp@yoozoo.com 2020-10-22T17:58:53+0800
* @param integer $fileId 待移动文件ID
* @param integer $targetFolderId 目标目录ID
* @return integer
*/
public function moveFile($fileId, $targetFolderId)
{
$fileId = [$fileId];
$source = new Apis\Doc\MoveFolderListAndFileList($this->getToken(), $targetFolderId, $fileId, []);
return $this->fetch($source);
}

/**
* 移动目录到指定目录
*
* @method moveFolder
* @author 雷行 songzhp@yoozoo.com 2020-10-22T17:59:44+0800
* @param integer $folderId 待移动目录ID
* @param integer $targetFolderId 目标目录ID
* @return integer
*/
public function moveFolder($folderId, $targetFolderId)
{
$folderId = [$folderId];
$source = new Apis\Doc\MoveFolderListAndFileList($this->getToken(), $targetFolderId, [], $folderId);
return $this->fetch($source);
}

/**
* 重命名文件
*
* @method renameFile
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:00:36+0800
* @param integer $fileId 待重命名文件ID
* @param string $newName 新的文件名
* @return boolean
*/
public function renameFile($fileId, $newName)
{
$source = new Apis\File\RenameFile($this->getToken(), $fileId, $newName);
return $this->fetch($source);
}

/**
* 重命名文件夹
*
* @method renameFolder
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:07:39+0800
* @param integer $folderId 待重命名文件夹ID
* @param string $newName 新的文件夹名称
* @return boolean
*/
public function renameFolder($folderId, $newName)
{
$source = new Apis\Folder\RenameFolder($this->getToken(), $folderId, $newName);
return $this->fetch($source);
}

/**
* 复制文件
*
* @method copyFile
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:09:00+0800
* @param integer $fileId 待移动文件ID
* @param integer $targetFolderId 目标文件夹ID
* @return boolean
*/
public function copyFile($fileId, $targetFolderId)
{
$fileId = [$fileId];
$source = new Apis\Doc\CopyFolderListAndFileList($this->getToken(), $targetFolderId, $fileId, []);
return $this->fetch($source);
}

/**
* 复制文件夹
*
* @method copyFolder
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:10:30+0800
* @param integer $folderId 待复制文件夹ID
* @param integer $targetFolderId 目标文件夹ID
* @return boolean
*/
public function copyFolder($folderId, $targetFolderId)
{
$folderId = [$folderId];
$source = new Apis\Doc\CopyFolderListAndFileList($this->getToken(), $targetFolderId, [], $folderId);
return $this->fetch($source);
}

/**
* 通过ID删除文件
*
* @method deleteFileById
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:11:07+0800
* @param integer $fileId 待删除文件ID
* @return boolean
*/
public function deleteFileById($fileId)
{
$fileIds = [$fileId];
$source = new Apis\Doc\RemoveFolderListAndFileList($this->getToken(), $fileIds, [], [], []);
return $this->fetch($source);
}

/**
* 通过ID删除文件夹
*
* @method deleteFolderById
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:11:30+0800
* @param integer $folderId 待删除文件夹ID
* @return boolean
*/
public function deleteFolderById($folderId)
{
$folderIds = [$folderId];
$source = new Apis\Doc\RemoveFolderListAndFileList($this->getToken(), [], $folderIds, [], []);
return $this->fetch($source);
}

/**
* 删除文件
*
* @method deleteFile
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:11:58+0800
* @param string $path 待删除文件路径
* @return boolean
*/
public function deleteFile($path)
{
$path = [$path];
$source = new Apis\Doc\RemoveFolderListAndFileList($this->getToken(), [], [], $path, []);
return $this->fetch($source);
}

/**
* 删除文件夹
*
* @method deleteFolder
* @author 雷行 songzhp@yoozoo.com 2020-10-22T18:12:20+0800
* @param string $path 待删除文件夹路径
* @return boolean
*/
public function deleteFolder($path)
{
$path = [$path];
$source = new Apis\Doc\RemoveFolderListAndFileList($this->getToken(), [], [], [], $path);
return $this->fetch($source);
}

/**
* 下载文件
*
* @method download
* @author 雷行 songzhp@yoozoo.com 2020-10-26T10:39:16+0800
* @param integer $fileId 文件ID
* @return void
*/
public function download($fileId)
{

$source = new Apis\Download\DownLoadCheck($this->getToken(), $fileId);
$checkInfo = $this->fetch($source);

// 下载区域不在主区域的情况
if ($checkInfo['RegionType'] != 1) {
$download = new Apis\Download\HttpDownLoad($checkInfo['RegionUrl']);
} else {
$download = new Apis\Download\DownLoad($checkInfo['RegionHash']);
}

return $this->fetchStream($download);
}

/**
* 文件外发
*
* @method publishFile
* @author 雷行 songzhp@yoozoo.com 2020-11-04T14:11:30+0800
* @param integer $fileId 文件ID
* @return string
*/
public function publishFile($fileId)
{
$endTime = date('Y-m-d', strtotime('+1 day'));
$source = new Apis\DocPublish\CreateFilePublish($this->getToken(), $fileId, $endTime);
$publishCode = $this->fetch($source);

return [
'fileId' => $fileId,
'publishCode' => $publishCode,
'endTime' => $endTime,
'url' => $this->host . '/preview.html?fileid=' . $fileId . '&ispublish=true&ifream=1&code=' . $publishCode,

];
}

/**
* 文件夹外发
*
* @method publishFolder
* @author 雷行 songzhp@yoozoo.com 2020-11-04T14:12:22+0800
* @param integer $folderId
* @param boolean $uploadable
* @return string
*/
public function publishFolder($folderId, $uploadable = false)
{
$endTime = date('Y-m-d', strtotime('+1 day'));
$source = new Apis\DocPublish\CreateFolderPublish($this->getToken(), $folderId, $endTime, $uploadable);
$publishCode = $this->fetch($source);

return [
'folderId' => $folderId,
'publishCode' => $publishCode,
'endTime' => $endTime,
'uploadable' => $uploadable,
];
}

/**
* 按账号获取用户信息
*
* @method getUserInfoByAccount
* @author 雷行 songzhp@yoozoo.com 2020-11-19T18:17:28+0800
* @param string $account
* @return array
*/
public function getUserInfoByAccount($account)
{
$source = new Apis\OrgUser\GetUserInfoByAccount($this->getToken(), $account);
return $this->fetch($source);
}

/**
* 按账号获取ID
*
* @method getUserIdByAccount
* @author 雷行 songzhp@yoozoo.com 2020-11-20T09:41:48+0800
* @param string $account
* @return string
*/
public function getUserIdByAccount($account)
{
$info = $this->getUserInfoByAccount($account);
return $info['ID'];
}

private static $operationUserId = null;

/**
* 获取当前操作人ID
*
* @method getOperationUserId
* @author 雷行 songzhp@yoozoo.com 2020-11-19T18:25:02+0800
* @return string
*/
public function getOperationUserId()
{
if (!isset(self::$operationUserId)) {
$info = $this->getUserInfoByAccount($this->account);
self::$operationUserId = $info['ID'];
}
return self::$operationUserId;
}

/**
* 注销账号
*
* @method logOff
* @author 雷行 songzhp@yoozoo.com 2020-11-20T09:43:03+0800
* @param string $accounts
* @return boolean
*/
public function logOff($accounts)
{
return $this->batchUserOperation($accounts, __FUNCTION__);
}

/**
* 激活用户
*
* @method activeUser
* @author 雷行 songzhp@yoozoo.com 2020-11-20T09:53:39+0800
* @param string $accounts
* @return boolean
*/
public function activeUser($accounts)
{
return $this->batchUserOperation($accounts, __FUNCTION__);
}

/**
* 锁定/解锁用户
*
* @method toggleLockUser
* @author 雷行 songzhp@yoozoo.com 2020-11-20T10:00:33+0800
* @param string $accounts
* @return boolean
*/
public function toggleLockUser($accounts)
{
return $this->batchUserOperation($accounts, __FUNCTION__);
}

/**
* 用户批量操作
*
* @method batchUserOperation
* @author 雷行 songzhp@yoozoo.com 2020-11-20T10:13:38+0800
* @param string $accounts
* @param string $operation
* @return boolean
*/
protected function batchUserOperation($accounts, $operation)
{

$operationUserId = $this->getOperationUserId();
if (is_string($accounts)) {
$accounts = [$accounts];
}

$userIds = [];
foreach ($accounts as $account) {
print_r($account);
$userIds[] = $this->getUserIdByAccount($account);
}

$class = null;
switch ($operation) {
case 'logOff':
$class = Apis\OrgUser\Logoff::class;
break;
case 'activeUser':
$class = Apis\OrgUser\ActivateUser::class;
break;
case 'toggleLockUser':
$class = Apis\OrgUser\ToggleLockUser::class;
break;

default:
# code...
break;
}

if (is_null($class)) {
throw new PanException('用户操作类别不存在');
}

$source = new $class($this->getToken(), $operationUserId, $userIds);
return $this->fetch($source);
}

public function fetchStream(Apis\SourceInterface $source)
{
if ($this->debug) {
Log::info(' ');
Log::info(' ');
Log::info(' ');
Log::info($source->api);
Log::info($this->cut($source->body()));
}
$response = $this->http->request(
$source->method,
$source->api,
$source->body()
);

if ($response->getStatusCode() != 200) {
throw new RequestException('请求错误:' . $response->getStatusCode());
}

return $response->getBody();
}

/**
* 拉取资源
*
* @method fetch
* @author 雷行 songzhp@yoozoo.com 2020-11-19T11:42:46+0800
* @param Apis\SourceInterface $source
* @param integer $tries
* @return mix
*/
public function fetch(Apis\SourceInterface $source, $tries = 0)
{
if ($tries > 3) {
throw new RequestException('retry times too match!');
}

$body = $this->fetchStream($source);

$data = json_decode((string) $body, true);
if ($this->debug) {
Log::info($data);
}
if (isset($data['errorCode'])) {
if ($data['errorCode'] == 4) {
// 出现token失效的时候,先强制重新登录刷新token,然后重试
$source->updateToken($this->getToken(true));
return $this->fetch($source, ++$tries);
}
throw new RequestException('errorCode:' . $data['errorCode'] . ',errorMsg:' . $data['errorMsg']);
}
return $source->parse($data);
}

/**
* 截断太长的信息
*
* @method cut
* @author 雷行 songzhp@yoozoo.com 2020-11-06T17:13:19+0800
* @param array $data
* @return array
*/
protected function cut($data)
{
foreach ($data as $key => $item) {
if (is_string($item) && strlen($item) > 1000) {
$data[$key] = substr($item, 0, 100) . '...';
}

if (is_array($item)) {
$data[$key] = $this->cut($item);
}
}
return $data;
}
}

laravel 路由模块源码分析

发表于 2020-01-09

概述

  • 路由模块是如何进入系统生命周期的?
  • 路由模块包含哪些功能,分别由哪些子模块负责?

需要说明的是,路由基本是针对web应用而言的,所以本文中提及的应用,是指由laravel构建的web应用,而不包括Console应用。

路由模块的生命周期

在由laravel构建的应用当中,功能模块可以统称为服务,所以路由模块也可以称为路由服务。由laravel文档可知,应用中的服务都是由服务提供者来提供的,路由模块也是,所以找到了路由服务提供者,就等于找到了路由模块生命周期的入口。

如果熟悉laravel应用的生命周期的话,就应该会很清楚:在容器的初始化阶段,会注册三个基础服务提供者,分别是EventServiceProvider/LogServiceProvider/RoutingServiceProvider,其中,RoutingServiceProvider即是路由服务提供者;接着,在容器启动阶段,分别完成配置加载/注册配置中的服务提供者/启动服务提供者三项事务,而在注册配置中的服务提供者时,又会注册一个叫做RouteServiceProvider的路由服务提供者;然后,在启动服务提供者阶段,完成路由启动。之后进入应用执行阶段,期间路由服务由开发者自行决定如何使用。

所以,路由模块的生命周期可以简单归纳为三个阶段:

  • 路由服务注册阶段,由RoutingServiceProvider类接管,主要负责路由模块各个功能组件的注册。
  • 路由启动阶段,加载开发者定义的路由,由RouteServiceProvider类接管。
  • 路由应用阶段,不是本文所述重点,可以参考官方手册。

路由模块的组成

路由模块在应用中的命令空间名称是 Illuminate\Routing。在路由服务的注册阶段,服务提供者注册了7个对象到容器当中:

  • router: 路由核心
  • url:url生成器
  • redirect:url跳转服务
  • ServerRequestInterface:略
  • ResponseInterface:略
  • ResponseFactoryContract:略
  • ControllerDispatcherContract:控制器解析器
    这些就是路由模块中的重要组件啦,当然,各个组件还会进行细分,接下来我们会分别对各个组件做出说明。

路由核心

  • 类名: Illuminate\Routing\Router::class
  • 功能:路由定义/路由匹配
  • 子组件:Route/RouteCollection/RouteRegistrar/ResourceRegistrar

在具体介绍路由核心的功能之前,我们先了解一下核心子组件,对他们有一个大致的了解。

Route

  • 类名:Illuminate\Routing\Route::class
  • 功能:路由实例/控制器参数解析/控制器中间件解析

Route就是我们将要定义的一个一个的路由,我们通过命令php artisan route:list列出来的,就是它们了,与Router只有一字之差,但功能大相径庭。在laravel文档 基础功能 - 路由 篇中,主要讲的就是Route,由于文档对它有大篇幅的解释,所以,他不是本文的主角。

RouteCollection

  • 类名:Illuminate\Routing\RouteCollection::class
  • 功能:路由集合/路由表/路由匹配

RouteCollection是一个很直观的名字了,应用中无论在何处定义的路由,最后都会被添加到路由表中,因为RouteCollection的存在,所以route:list命令可以很方便的列出应用中定义的路由,也正是因为它的存在,在收到请求是,可以方便的识别出是请求的哪个路由。

RouteRegistrar

  • 类名:Illuminate\Routing\RouteRegistrar::class
  • 功能:路由定义代理

简单来讲,当我们在routes/web.php文件中定义路由的时候,有可能是Router在定义路由,也有可能是RouteRegistrar在定义路由。

ResourceRegistrar

  • 类名:Illuminate\Routing\ResourceRegistrar::class
  • 功能:路由定义代理(资源路由)

与RouteRegistrar的定位相同,只不过是专门处理资源路由定义时的场景。

功能

路由定义

在laravel文档 基础功能 - 路由 篇中详细说明了如何在应用中定义路由,那么它背后是如何实现的呢?通过上文”路由模块的生命周期”的介绍,我们知道,路由的定义是在路由启动阶段,由RouteServiceProvider类负责。

RouteServiceProvider服务提供者启动时,会检查路由表是否被缓存,是的话会从缓存中直接返回路由表;否则会执行map方法,分别从routes/web.php和routes/api.php路由定义文件中,导入开发者定义的路由。它执行过程如下:

注:Route门面最终会指向Router对象,为了避免引起混淆,路由定义过程中都以Router做说明。

1
2
3
Router::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));

而routes/web.php中的内容可能会出现以下片段:

1
2
3
Router::group(['middleware' => ['auth']], function(){
...
});

很容易发现Router两次调用group时,方法的参数个数不一致,其实,Router并没有
middleware/as/domain/name/namespace/prefix 这些方法,当调用这些方法时,会转发给RouteRegistrar路由定义代理对象去执行,在调用上述方法时,会把对应的参数收集起来,称为路由属性定义方法。而RouteRegistrar也由一个group方法,所以才说的通。

通过Router直接定义的路由,我称之为原生路由定义,而通过RouteRegistrar定义的路由,我称之为代理路由定义。在原生路由定义之下,又可以分为简单路由定义与复杂路由定义。简单路由定义指一次执行定义一个路由,复杂路由定义指一次执行可以定义多个路由,包括Router的group/resource/apiResource等方法。当RouteRegistrar定义路由时,又会转发给Router来进行定义,并收集路由属性定义链上的属性,所以,路由最终都是Router“直接”定义出来的,而RouteRegistrar和ResourceRegistrar都是路由定义中出现的语法糖。

原生定义 代理定义
简单定义 Router::get($uri,$action)
Router::post($uri,$action)…
暂无该场景
复杂定义 Router::group($attribute,$callback/$routeFile) RouteRegistrar::group($routeFile)

路由匹配

在容器启动完毕,请求通过全局中间件过滤后,会开始对请求的路由进行匹配关键方法是Illuminate\Routing\Router::findRoute

1
2
3
4
5
6
7
8
9
10

protected function findRoute($request)
{
// $this->routes 即是路由表,RouteCollection对象,$route既是命中的路由
$this->current = $route = $this->routes->match($request);

$this->container->instance(Route::class, $route);

return $route;
}

Router调用findRoute方法,将请求request转发给RouteCollection的match方法执行,match方法接着会遍历路由对象,由路由对象与request进行匹配,匹配通过则命中路由,否则会抛出路由不存在的异常。匹配会从uri/method/host/sheme四个方面进行,全部通过为命中。

控制器解析

控制器解析分为两个过程,控制器中间件解析和控制器参数解析,这两个过程都是在Route路由对象中执行,由Route转发给ControllerDispatcherContract的实例经行解析。

控制器中间件解析

在路由匹配之前,request已经通过全局中间件,接下来request将要通过路由中间件,通过文档我们知道,路由中间件可以在定义路由时添加(group属性或middleware方法),也可以在控制器中通过middleware方法定义。为了实现这一功能,所以需要在request通过路由中间件之前,实例化控制器,并从中取出定义的中间件。这也导致了在控制器的构造方法中,无法获取路由中间件中获得的状态。

控制器参数解析

控制器参数解析是注入的关键。在容器与注入的概念当中,依赖对象先注入到容器之中(或者是依赖对象的实例化方法),后续执行对象,可以根据执行方法参数的类型,从容器中解析出所需对象,然后将这些对象注入到执行方法中,完成依赖的注入。控制器参数解析,就是通过反射解析出执行方法的参数类型,后续过程交给容器解决即可。回到路由模块的生命周期上来,在request通过路由中间件之后,路由解析出控制器的参数,然后通过callAction方法引导控制器方法的执行,执行参数已经通过参数解析得到。这里的callAction方法,解决了上面控制器中间件解析中提到的问题:控制器的构造函数使用受到限制,可以通过callAction方法代替构造函数来执行一些通用操作。

URL生成器

  • 类名:Illuminate\Routing\UrlGenerator::class
  • 功能:生成url/判断字符串是否是url

URL生成器可以生成的url大致分为两种,一种是普通url,一种是路由url。

普通URL

普通URL的生成有以下几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 当前请求的完整URL
URL::full();

// 不含查询字符串的url
URL::current();

// referer
URL::previous();

URL::to('foo/bar', $parameters, $secure);
URL::secure('foo/bar', $parameters);
URL::asset('css/foo.css', $secure);
URL::secureAsset('css/foo.css');

路由URL

普通URL的生成有以下几个方法:

1
2
3
4
5
6
7
// 根据控制器及方法生成url
URL::action('NewsController@item', ['id'=>123]);
URL::action('Auth\AuthController@logout');
URL::action('FooController@method', $parameters, $absolute);

// 根据路由名称生成url
URL::route('foo', $parameters, $absolute);

判断字符串是否是url

1
URL::isValidUrl('http://example.com');

路由跳转

  • 类名:Illuminate\Routing\Redirector::class
  • 功能:url跳转

一般跳转

1
2
3
4
5
return Redirect::to('foo/bar');
return Redirect::to('foo/bar')->with('key', 'value');
return Redirect::to('foo/bar')->withInput(Input::get());
return Redirect::to('foo/bar')->withInput(Input::except('password'));
return Redirect::to('foo/bar')->withErrors($validator);

相对跳转

1
2
3
4
5
// 后退
return Redirect::back();

// 刷新
return Redirect::refresh();

路由跳转

1
2
3
4
5
6
7
8
9
// 重定向到命名路由(根据命名路由算出 URL)
return Redirect::route('foobar');
return Redirect::route('foobar', array('value'));
return Redirect::route('foobar', array('key' => 'value'));

// 重定向到控制器动作(根据控制器动作算出 URL)
return Redirect::action('FooController@index');
return Redirect::action('FooController@baz', array('value'));
return Redirect::action('FooController@baz', array('key' => 'value'));

授权记忆

在授权服务中,完成授权后跳回到来源地址的功能依赖这两个方法。

1
2
3
4
5
6

// 记住当前url并跳转到指定$path
return Redirect::guest($path);

// 跳转到guest中记住的地址,否则跳转到$path
return Redirect::intended($path);

Laravel Pipeline源码分析

发表于 2020-01-03

Index

  • Pipeline的使用
    • send
    • through
    • then
  • Pipeline的实现
    • 管道的设计0
    • 管道的设计1
    • 管道的设计10

Pipeline 模型最早被使用在 Unix 操作系统中。管道的出现,所要解决的问题,还是软件设计中的设计目标——高内聚,低耦合。它以一种“链式模型”来串接不同的程序或者不同的组件,让它们组成一条直线的工作流。这样给定一个完整的输入,经过各个组件的先后协同处理,得到唯一的最终输出。

Pipeline的使用

1
2
3
4
5
use Illuminate\Pipeline\Pipeline;
$result = (new Pipeline($container))
->send($passable)
->through($pipes)
->then($callable);

send

  • 参数: $passable

给定一个完整的输入就是send所要做的事情。在使用管道模式之前,首先得想清楚,我们是要通过管道来处理什么东西。拿 Laravel 应用来举例,web 程序要通过中间件来处理请求,最终得到响应返回给浏览器。那么,请求就是这里给定的输入。

through

  • 参数: $pipes

经过各个组件的先后协同处理中提及的各个组件,就是由 through 方法来传入。基于管道模式的特点,这里的各个组件,会有相同类型的输入,相同类型的输出以及相同的执行入口。

1
2
3
4
5
6
7
public function handle($request, Closure $next)
{
// do something
$response = $next($request);
// do something
return $response;
}

then

  • 参数: $callable

最终输出就是 then 要做的事。前面 send 以及 through 都只是定义管道执行过程中所需要的参数,真正的执行过程在 then 这个方法中。then 的功能点在于将输入转化为输出。

Pipeline的实现

管道的设计0

先考虑一种最容易想到的管道的实现方式,我们假设如下:

1
2
3
4
5
6
7
8
9
10
11
12
use Input;
use Output;

$input = new Input;

// 通过管道
$result = pipeOne($input);
$result = pipeTwo($result);
$result = pipeThree($result);

// 转化输出
(Output) $output = then($result);

结合我们上文提到的各个组件,会有相同类型的输入,相同类型的输出以及相同的执行入口,如果通过上述方式来实现管道,那么实际上管道中的组件pipeOne/pipeTwo/pipeThree的输入与输出都是同一种数据类型。否则$input通过第一个组件之后,产生的输出就不能作为下一个组件的输入。显然,这种愚蠢的设计方式根本就不能满足需求。

管道的设计1

考虑以下面这种方式来实现管道:

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
use Input;
use Output;

$input = new Input;

function pipeOne(Input $input){
$input = dosomething($input);
(Output) $result = pipeTwo($input);
return $result;

}
function pipeTwo(Input $input){
$input = dosomething($input);
(Output) $result = pipeThree($input);
return $result;

}
function pipeThree(Input $input){
$input = dosomething($input);
(Output) $result = howToGetTheResult($input);
return $result;

}

// 通过管道
$result = pipeOne($input);

// 转化输出
$output = then($result);

通过这种方式来实现管道,仿佛可以满足上文对输入与输出的定义。但显然也存在很大问题,通过这种方式实现的管道,组件执行的顺利被硬编码到了组件的逻辑之中,如果出现了流程变动的问题,要花很大的力气去做修改。其次,最后一个组件怎么来获取 result 呢?获取 result 的过程,应该定义在 then 当中,综上所述,我们要改进的设计,需要解决下面两个问题:

  • 管道中要执行的组件是可配置的。组件的数量与顺序都是可以修改的。
  • 管道要能自行检查到执行的末端,并调用 then 方法,将 input 转化为 output。

管道的设计10

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
use Input;
use Output;

$input = new Input;


function pipeOne(Input $input, Callable $callback){
// dosomething
(Output) $result = $callback($input, $callback);
return $result;
}

function pipeTwo(Input $input, Callable $callback){
// dosomething
(Output) $result = $callback($input, $callback);
return $result;
}

function pipeThree(Input $input, Callable $callback){
// dosomething
(Output) $result = $callback($input, $callback);
return $result;
}


function createCallbackOfPipe($pipes, $index){

return function($input, $callback) use($pipes,$index){
// 自动检测管道的末端
if($index == count($pipes)){
return then($input);
}else{
$index+1;
$nextPipe = $pipes[$index];

return $nextPipe($input,createCallbackOfPipe($pipes, $index));
}
}
}

// 这里可以定义管道中的组件顺序及数量
$pipes = [
'pipeOne',
'pipeTwo',
'pipeThree',
];
$firstPipe = $pipes[0];
(Output) $result = $firstPipe($input, createCallbackOfPipe($pipes, 0));

至此,一个管道的模型就基本实现了。我们重新梳理一下管道设计中要注意的细节问题,可以归纳出以下几点:

  • 管道中的每一个组件,都有相同类型的输入与输出。
  • 管道的参数中还要传递下一次要调用的句柄,组件除了要执行本身的逻辑外,还需要调用这个句柄,来触发下一个组件的执行。
  • 组件的执行过程,最好封装成一个匿名函数,这样可以变得通用,而不需要知道下一个要执行的组件的具体信息,比如方法名。

在 Laravel 框架中,通过一个函数就达到了我们传递下一次要调用的句柄的目的,这个函数就是 array_reduce,这个方法,简直完美的契合管道的思想啊。此外,Laravel 中对管道执行的封装,还考虑到了其他的因素,比如对下一次要调用的句柄的扩展,除了可以使用匿名函数,还兼容了 PHP 中的其他三种可调用结构,以及对容器的使用等,具体 Laravel 是如何实现的,就让大家自行去了解吧。

Laravel 中间件

发表于 2019-12-24

Index

  • 中间件全家福
  • Authenticate
  • CheckForMaintenanceMode
  • EncryptCookies
  • RedirectIfAuthenticated
  • TrimStrings
  • TrustProxies
  • VerifyCsrfToken
  • 待续…
  • ValidatePostSize
  • ConvertEmptyStringsToNull
  • AddQueuedCookiesToResponse
  • StartSession
  • ShareErrorsFromSession
  • SubstituteBindings
  • AuthenticateWithBasicAuth
  • Authorize
  • RedirectIfAuthenticated
  • ThrottleRequests

中间件全家福

先在这里列一下框架中用到的中间件。

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
/**
* 全局中间件
*/

// 检测项目是否处于 维护模式。
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class

// 检查请求数据大小是否超过限制
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class

// 清理请求字段值首位的空格
\App\Http\Middleware\TrimStrings::class

// 将请求中的空字段转化为null值
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class

// 设置可信任代理
\App\Http\Middleware\TrustProxies::class

/**
* web 路由中间件
*/
// 获取请求中的Cookies,解密Cookies的值,加密Headers中的Cookies
\App\Http\Middleware\EncryptCookies::class

// 添加Cookies到Headers
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class

// 启用Session,在Headers中添加Session相关的Cookies
\Illuminate\Session\Middleware\StartSession::class

// 将闪存到Session中的错误信息,共享到视图 - 闪存是指在上一次请求时将数据存入Session的过程,数据会在下一次请求时取出并销毁
\Illuminate\View\Middleware\ShareErrorsFromSession::class

// csrf 令牌验证
\App\Http\Middleware\VerifyCsrfToken::class

// 路由参数模型绑定检查
\Illuminate\Routing\Middleware\SubstituteBindings::class

/**
* 其他路由中间件
*/
[
// 认证中间件
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

// http基础认证
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,

// 路由参数模型绑定检查
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,

// 授权检查
'can' => \Illuminate\Auth\Middleware\Authorize::class,

// 访客认证
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

// 请求节流
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
]

Authenticate

源文件

app\Http\Middleware\Http\Middleware\Authenticate.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}

作用

用户身份验证。可修改 redirectTo 方法,返回未经身份验证的用户应该重定向到的路径。

CheckForMaintenanceMode

源文件

app\Http\Middleware\CheckForMaintenanceMode.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;

class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

作用

检测项目是否处于 维护模式。可通过 $except 数组属性设置在维护模式下仍能访问的网址。

EncryptCookies

源文件

app\Http\Middleware\EncryptCookies.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Http\Middleware;

use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;

class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

作用

对 Cookie 进行加解密处理与验证。可通过 $except 数组属性设置不做加密处理的 cookie。

RedirectIfAuthenticated

源文件

app\Http\Middleware\RedirectIfAuthenticated.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
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}

return $next($request);
}
}

作用

当请求页是 注册、登录、忘记密码 时,检测用户是否已经登录,如果已经登录,那么就重定向到首页,如果没有就打开相应界面。可以在 handle 方法中定制重定向到的路径。

TrimStrings

源文件

app\Http\Middleware\TrimStrings.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;

class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

作用

对请求参数内容进行 前后空白字符清理。可通过 $except 数组属性设置不做处理的参数。

TrustProxies

源文件

app\Http\Middleware\TrustProxies.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware;

class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string
*/
protected $proxies;

/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}

作用

配置可信代理。可通过 $proxies 属性设置可信代理列表,$headers 属性设置用来检测代理的 HTTP 头字段。

VerifyCsrfToken

源文件

app\Http\Middleware\VerifyCsrfToken.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
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;

/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

作用

验证请求里的令牌是否与存储在会话中令牌匹配。可通过 $except 数组属性设置不做 CSRF 验证的网址。

ValidatePostSize

源文件

vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.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
<?php

namespace Illuminate\Foundation\Http\Middleware;

use Closure;
use Illuminate\Http\Exceptions\PostTooLargeException;

class ValidatePostSize
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Illuminate\Http\Exceptions\PostTooLargeException
*/
public function handle($request, Closure $next)
{
$max = $this->getPostMaxSize();

if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) {
throw new PostTooLargeException;
}

return $next($request);
}

/**
* Determine the server 'post_max_size' as bytes.
*
* @return int
*/
protected function getPostMaxSize()
{
if (is_numeric($postMaxSize = ini_get('post_max_size'))) {
return (int) $postMaxSize;
}

$metric = strtoupper(substr($postMaxSize, -1));
$postMaxSize = (int) $postMaxSize;

switch ($metric) {
case 'K':
return $postMaxSize * 1024;
case 'M':
return $postMaxSize * 1048576;
case 'G':
return $postMaxSize * 1073741824;
default:
return $postMaxSize;
}
}
}

作用

检查请求数据大小是否操作限制。

ConvertEmptyStringsToNull

源文件

vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace Illuminate\Foundation\Http\Middleware;

class ConvertEmptyStringsToNull extends TransformsRequest
{
/**
* Transform the given value.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function transform($key, $value)
{
return is_string($value) && $value === '' ? null : $value;
}
}

作用

将请求中的空字符,转化为 NULL 值。

AddQueuedCookiesToResponse

源文件

vendor/laravel/framework/src/Illuminate/Cookie\Middleware/AddQueuedCookiesToResponse.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
<?php

namespace Illuminate\Cookie\Middleware;

use Closure;
use Illuminate\Contracts\Cookie\QueueingFactory as CookieJar;

class AddQueuedCookiesToResponse
{
/**
* The cookie jar instance.
*
* @var \Illuminate\Contracts\Cookie\QueueingFactory
*/
protected $cookies;

/**
* Create a new CookieQueue instance.
*
* @param \Illuminate\Contracts\Cookie\QueueingFactory $cookies
* @return void
*/
public function __construct(CookieJar $cookies)
{
$this->cookies = $cookies;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);

foreach ($this->cookies->getQueuedCookies() as $cookie) {
$response->headers->setCookie($cookie);
}

return $response;
}
}

作用

将程序中通过 Cookies Queue 的方式设置的 cookies 值设置到响应头。

StartSession

源文件

vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.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
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
<?php

namespace Illuminate\Session\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Session\SessionManager;
use Illuminate\Contracts\Session\Session;
use Illuminate\Session\CookieSessionHandler;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;

class StartSession
{
/**
* The session manager.
*
* @var \Illuminate\Session\SessionManager
*/
protected $manager;

/**
* Indicates if the session was handled for the current request.
*
* @var bool
*/
protected $sessionHandled = false;

/**
* Create a new session middleware.
*
* @param \Illuminate\Session\SessionManager $manager
* @return void
*/
public function __construct(SessionManager $manager)
{
$this->manager = $manager;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$this->sessionHandled = true;

// If a session driver has been configured, we will need to start the session here
// so that the data is ready for an application. Note that the Laravel sessions
// do not make use of PHP "native" sessions in any way since they are crappy.
if ($this->sessionConfigured()) {
$request->setLaravelSession(
$session = $this->startSession($request)
);

$this->collectGarbage($session);
}

$response = $next($request);

// Again, if the session has been configured we will need to close out the session
// so that the attributes may be persisted to some storage medium. We will also
// add the session identifier cookie to the application response headers now.
if ($this->sessionConfigured()) {
$this->storeCurrentUrl($request, $session);

$this->addCookieToResponse($response, $session);
}

return $response;
}

/**
* Perform any final actions for the request lifecycle.
*
* @param \Illuminate\Http\Request $request
* @param \Symfony\Component\HttpFoundation\Response $response
* @return void
*/
public function terminate($request, $response)
{
if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
$this->manager->driver()->save();
}
}

/**
* Start the session for the given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Session\Session
*/
protected function startSession(Request $request)
{
return tap($this->getSession($request), function ($session) use ($request) {
$session->setRequestOnHandler($request);

$session->start();
});
}

/**
* Get the session implementation from the manager.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Session\Session
*/
public function getSession(Request $request)
{
return tap($this->manager->driver(), function ($session) use ($request) {
$session->setId($request->cookies->get($session->getName()));
});
}

/**
* Remove the garbage from the session if necessary.
*
* @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
protected function collectGarbage(Session $session)
{
$config = $this->manager->getSessionConfig();

// Here we will see if this request hits the garbage collection lottery by hitting
// the odds needed to perform garbage collection on any given request. If we do
// hit it, we'll call this handler to let it delete all the expired sessions.
if ($this->configHitsLottery($config)) {
$session->getHandler()->gc($this->getSessionLifetimeInSeconds());
}
}

/**
* Determine if the configuration odds hit the lottery.
*
* @param array $config
* @return bool
*/
protected function configHitsLottery(array $config)
{
return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
}

/**
* Store the current URL for the request if necessary.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
protected function storeCurrentUrl(Request $request, $session)
{
if ($request->method() === 'GET' && $request->route() && ! $request->ajax()) {
$session->setPreviousUrl($request->fullUrl());
}
}

/**
* Add the session cookie to the application response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
protected function addCookieToResponse(Response $response, Session $session)
{
if ($this->usingCookieSessions()) {
$this->manager->driver()->save();
}

if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) {
$response->headers->setCookie(new Cookie(
$session->getName(), $session->getId(), $this->getCookieExpirationDate(),
$config['path'], $config['domain'], $config['secure'] ?? false,
$config['http_only'] ?? true, false, $config['same_site'] ?? null
));
}
}

/**
* Get the session lifetime in seconds.
*
* @return int
*/
protected function getSessionLifetimeInSeconds()
{
return ($this->manager->getSessionConfig()['lifetime'] ?? null) * 60;
}

/**
* Get the cookie lifetime in seconds.
*
* @return \DateTimeInterface
*/
protected function getCookieExpirationDate()
{
$config = $this->manager->getSessionConfig();

return $config['expire_on_close'] ? 0 : Carbon::now()->addMinutes($config['lifetime']);
}

/**
* Determine if a session driver has been configured.
*
* @return bool
*/
protected function sessionConfigured()
{
return ! is_null($this->manager->getSessionConfig()['driver'] ?? null);
}

/**
* Determine if the configured session driver is persistent.
*
* @param array|null $config
* @return bool
*/
protected function sessionIsPersistent(array $config = null)
{
$config = $config ?: $this->manager->getSessionConfig();

return ! in_array($config['driver'], [null, 'array']);
}

/**
* Determine if the session is using cookie sessions.
*
* @return bool
*/
protected function usingCookieSessions()
{
if ($this->sessionConfigured()) {
return $this->manager->driver()->getHandler() instanceof CookieSessionHandler;
}

return false;
}
}

作用

启用 session。同时执行 session 垃圾回收、 referer 保存,session ID 保存至 cookies 等操作。

ShareErrorsFromSession

源文件

vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.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
<?php

namespace Illuminate\View\Middleware;

use Closure;
use Illuminate\Support\ViewErrorBag;
use Illuminate\Contracts\View\Factory as ViewFactory;

class ShareErrorsFromSession
{
/**
* The view factory implementation.
*
* @var \Illuminate\Contracts\View\Factory
*/
protected $view;

/**
* Create a new error binder instance.
*
* @param \Illuminate\Contracts\View\Factory $view
* @return void
*/
public function __construct(ViewFactory $view)
{
$this->view = $view;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// If the current session has an "errors" variable bound to it, we will share
// its value with all view instances so the views can easily access errors
// without having to bind. An empty bag is set when there aren't errors.
$this->view->share(
'errors', $request->session()->get('errors') ?: new ViewErrorBag
);

// Putting the errors in the view for every view allows the developer to just
// assume that some errors are always available, which is convenient since
// they don't have to continually run checks for the presence of errors.

return $next($request);
}
}

作用

将闪存到 session 中的错误消息数据,传递到视图中。

SubstituteBindings

源文件

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
<?php

namespace Illuminate\Routing\Middleware;

use Closure;
use Illuminate\Contracts\Routing\Registrar;

class SubstituteBindings
{
/**
* The router instance.
*
* @var \Illuminate\Contracts\Routing\Registrar
*/
protected $router;

/**
* Create a new bindings substitutor.
*
* @param \Illuminate\Contracts\Routing\Registrar $router
* @return void
*/
public function __construct(Registrar $router)
{
$this->router = $router;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$this->router->substituteBindings($route = $request->route());

$this->router->substituteImplicitBindings($route);

return $next($request);
}
}

作用

检查路由模型绑定。

AuthenticateWithBasicAuth

源文件

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
<?php

namespace Illuminate\Auth\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Factory as AuthFactory;

class AuthenticateWithBasicAuth
{
/**
* The guard factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;

/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(AuthFactory $auth)
{
$this->auth = $auth;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
return $this->auth->guard($guard)->basic() ?: $next($request);
}
}

作用

http 基础认证,浏览器弹出账号密码输入框。

Authorize

源文件

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
<?php

namespace Illuminate\Auth\Middleware;

use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Contracts\Auth\Factory as Auth;

class Authorize
{
/**
* The authentication factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;

/**
* The gate instance.
*
* @var \Illuminate\Contracts\Auth\Access\Gate
*/
protected $gate;

/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function __construct(Auth $auth, Gate $gate)
{
$this->auth = $auth;
$this->gate = $gate;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $ability
* @param array|null $models
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function handle($request, Closure $next, $ability, ...$models)
{
$this->auth->authenticate();

$this->gate->authorize($ability, $this->getGateArguments($request, $models));

return $next($request);
}

/**
* Get the arguments parameter for the gate.
*
* @param \Illuminate\Http\Request $request
* @param array|null $models
* @return array|string|\Illuminate\Database\Eloquent\Model
*/
protected function getGateArguments($request, $models)
{
if (is_null($models)) {
return [];
}

return collect($models)->map(function ($model) use ($request) {
return $model instanceof Model ? $model : $this->getModel($request, $model);
})->all();
}

/**
* Get the model to authorize.
*
* @param \Illuminate\Http\Request $request
* @param string $model
* @return \Illuminate\Database\Eloquent\Model|string
*/
protected function getModel($request, $model)
{
return $this->isClassName($model) ? $model : $request->route($model);
}

/**
* Checks if the given string looks like a fully qualified class name.
*
* @param string $value
* @return bool
*/
protected function isClassName($value)
{
return strpos($value, '\\') !== false;
}
}

作用

Gate 授权检查

RedirectIfAuthenticated

源文件

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
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}

return $next($request);
}
}

作用

访客授权检查。仅在用户未登录的情况下通过,否则重定向到统一的地址。

ThrottleRequests

源文件

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
<?php

namespace Illuminate\Routing\Middleware;

use Closure;
use RuntimeException;
use Illuminate\Support\Str;
use Illuminate\Cache\RateLimiter;
use Illuminate\Support\InteractsWithTime;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;

class ThrottleRequests
{
use InteractsWithTime;

/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;

/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int|string $maxAttempts
* @param float|int $decayMinutes
* @return mixed
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);

$maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);

if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
throw $this->buildException($key, $maxAttempts);
}

$this->limiter->hit($key, $decayMinutes);

$response = $next($request);

return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}

/**
* Resolve the number of attempts if the user is authenticated or not.
*
* @param \Illuminate\Http\Request $request
* @param int|string $maxAttempts
* @return int
*/
protected function resolveMaxAttempts($request, $maxAttempts)
{
if (Str::contains($maxAttempts, '|')) {
$maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0];
}

return (int) $maxAttempts;
}

/**
* Resolve request signature.
*
* @param \Illuminate\Http\Request $request
* @return string
* @throws \RuntimeException
*/
protected function resolveRequestSignature($request)
{
if ($user = $request->user()) {
return sha1($user->getAuthIdentifier());
}

if ($route = $request->route()) {
return sha1($route->getDomain().'|'.$request->ip());
}

throw new RuntimeException(
'Unable to generate the request signature. Route unavailable.'
);
}

/**
* Create a 'too many attempts' exception.
*
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpKernel\Exception\HttpException
*/
protected function buildException($key, $maxAttempts)
{
$retryAfter = $this->getTimeUntilNextRetry($key);

$headers = $this->getHeaders(
$maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);

return new HttpException(
429, 'Too Many Attempts.', null, $headers
);
}

/**
* Get the number of seconds until the next retry.
*
* @param string $key
* @return int
*/
protected function getTimeUntilNextRetry($key)
{
return $this->limiter->availableIn($key);
}

/**
* Add the limit header information to the given response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$response->headers->add(
$this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter)
);

return $response;
}

/**
* Get the limit headers information.
*
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return array
*/
protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];

if (! is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter);
}

return $headers;
}

/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (is_null($retryAfter)) {
return $this->limiter->retriesLeft($key, $maxAttempts);
}

return 0;
}
}

作用

请求节流。限定单位时间内,同一客户端访问的评率。

文件系统

发表于 2019-12-12

通过阅读 laravel 的 Filesystem 部分的代码可知,框架提供两套文件系统的操作接口:

1
2
3
4
5
6
7
// Illuminate\Filesystem\FilesystemServiceProvider
public function register()
{
$this->registerNativeFilesystem();

$this->registerFlysystem();
}

他们之间有些差异,以及在使用的过程中,也会有稍许不同。

NativeFilesystem

NativeFilesystem 是由 registerNativeFilesystem() 方法注册的文件系统操作接口,其对应的类是 \Illuminate\Filesystem\Filesystem,对应的 Facade 门面名称是 File,在容器内的别名是 files。

他是框架提供的一组对本地文件系统常用的操作接口,特点是简单易理解,基本就是对 PHP 的一些原生函数的封装。但是在官方入门文档中并不包含对这一套接口的说明,因为上述原因,这部分的接口并没做太多的逻辑业务处理,并不适合在业务流程中使用,但是框架本身有对这部分的接口使用,主要集中在命令行操作的过程中,比如:生成缓存文件、生成模板文件、操作扩展包的文件等。

Flysystem

Flysystem 是框架提供的另一套文件系统操作接口,基于 league/flysystem 扩展,提供对本地文件系统、FTP、云盘等具备文件存储功能的系统的操作。也就是使用文档中的 Storage 。

Storage 经过多次封装

  • Storage 的操作接口由类 Illuminate\Filesystem\FilesystemAdapter::class 提供;
  • Illuminate\Filesystem\FilesystemAdapter::class 类负责与 league/flysystem 的统一接口 League\Flysystem\Filesystem::class 进行对接;
  • League\Flysystem\Filesystem::class 负责与存储驱动交互,完成最后的文件操作。

league/flysystem 默认支持 Ftp/Local/NullAdapter 三种存储驱动,其他的存储驱动需要通过 composer 安装额外的扩展,如: sftp/aws/Azure/Memory/aliyun-oss 等。

laravel 框架 5.5 版本默认支持 league/flysystem 的 Local/Ftp/S3/Rackspace/ 三种驱动,如果需要通过其他储存,可以通过 Storage::extend() 方法,扩展驱动实例。

区别

如果在使用 Storage 时,通过 Local 本地文件驱动来操作文件,与直接通过 File 来操作文件,有哪些区别呢?

路径字符串的规范化

File 操作文件时,如果参数是相对路径,则是相对当前脚本执行路径。并不会对路径参数做额外的处理。
Storage 操作文件时,必须先配置存储的根路径,所有的参数路径都是基于根路径参数。同时,在操作文件之前,会对参数路径进行规范化。规范化包括:

  • 如果根路劲是符号连接,转化为真实路劲
  • 处理 ./.. 目录标识符
  • 转化 window 格式的目录分隔符
  • 处理路径参数中的空白字符:
1
2
3
4
5
6
7
8
9
protected static function removeFunkyWhiteSpace($path) {
// We do this check in a loop, since removing invalid unicode characters
// can lead to new characters being created.
while (preg_match('#\p{C}+|^\./#u', $path)) {
$path = preg_replace('#\p{C}+|^\./#u', '', $path);
}

return $path;
}

解释一下上面出现的正则:

  • # 是分隔符
  • u 是模式修饰符,此修正符打开一个与 perl 不兼容的附加功能 模式修饰符
  • \p{C}+ 是匹配 Unicode 字符中的其他字符。Unicode字符属性

所以上述正则的含义是,将路径中的 起头的./字符或一些不规范的unicode字符 替换为空。

写入文件是否加锁

File 在写入文件时,默认采用不加锁的策略,Storage 在写入文件时,始终会先获取独占锁,然后进行文件的写入。

其他

File 与 Storage 在进行目录迭代时,都使用到了 PHP标准库 中的文件对象和目录迭代器。

  • DirectoryIterator
  • SplFileInfo

邮件发送过滤

发表于 2019-12-05

Index

  • 实现思路
  • 监听事件
  • 过滤

实现思路

开发中无法避免有要发送邮件的情况,在开发或测试环节,一般都需要对邮件发送操作进行拦截,仅对指定的邮箱发送邮件,避免用户收到测试邮件。在 Laravel 中有一个非常简单的办法实现这一功能,其原理如下:

无论是通过 Mail 或 Notification 的方式来发送邮件,最后都会执行到 Illuminate\Mail\Mailer::class 类的 send() 方法:

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
public function send($view, array $data = [], $callback = null)
{
if ($view instanceof MailableContract) {
return $this->sendMailable($view);
}

// First we need to parse the view, which could either be a string or an array
// containing both an HTML and plain text versions of the view which should
// be used when sending an e-mail. We will extract both of them out here.
list($view, $plain, $raw) = $this->parseView($view);

$data['message'] = $message = $this->createMessage();

// Once we have retrieved the view content for the e-mail we will set the body
// of this message using the HTML type, which will provide a simple wrapper
// to creating view based emails that are able to receive arrays of data.
$this->addContent($message, $view, $plain, $raw, $data);

call_user_func($callback, $message);

// If a global "to" address has been set, we will set that address on the mail
// message. This is primarily useful during local development in which each
// message should be delivered into a single mail address for inspection.
if (isset($this->to['address'])) {
$this->setGlobalTo($message);
}

// Next we will determine if the message should be sent. We give the developer
// one final chance to stop this message and then we will send it to all of
// its recipients. We will then fire the sent event for the sent message.
$swiftMessage = $message->getSwiftMessage();

if ($this->shouldSendMessage($swiftMessage)) {
$this->sendSwiftMessage($swiftMessage);

$this->dispatchSentEvent($message);
}
}

在通过一系列的操作之后,得到了一个 $swiftMessage 对象,通过 shouldSendMessage() 方法来检查该对象是否应该发送邮件。此处会触发一个 MessageSending 事件,通过监听这个事件,并做出相应的返回,达到控制邮件是否发送的目的。

监听事件

在 App\Providers\EventServiceProvider::class 类中定义事件监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'Illuminate\Mail\Events\MessageSending' => [
'App\Listeners\SendMailFilter',
],
];
}

然后通过 php artisan event:generate 命令生成监听类 App\Listeners\SendMailFilter::class

过滤

在事件监听器中写入邮箱过滤的逻辑,比如通过添加白名单的方式:

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
class SendMailFilter
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}

// 定义放行的收件邮箱列表
protected $whiteList = [
'aaa@youzu.com',
'bbb@youzu.com',
'ccc@youzu.com',
];

/**
* Handle the event.
*
* @param MessageSending $event
* @return void
*/
public function handle(MessageSending $event)
{
// 如果是正式环境的非调试模式,不启用过滤
if (App::environment('production') && !config('app.debug')) {
return true;
}

// 此处的 $event->message 是Swift_Mime_SimpleMessage 类的一个实例
// 获取收件邮箱列表
$to = $event->message->getTo();

// $cc = $event->message->getCc();
// $bcc = $event->message->getBcc();

foreach ($to as $mail => $name) {
if (!in_array($mail, $this->whiteList)) {
// throw new Exception
return false;
}
}
return true;
}
}

需要注意的是,在 return false 处可以直接抛出异常,也可达到过滤效果,区别在于,通过 return false 的方式不会打断程序原始的执行流程。

通过docker构建本地环境

发表于 2019-11-20

文件及目录说明

文件

  • docker-composer.yml: docker容器编排文件,也是搭建本地环境的入口文件。
  • Dockerfile.php: php环境容器
  • Dockerfile.queue: supervisord环境容器,容器基于php容器,开启了cron,用于实现队列及定时任务。该文件构建的容器用于模拟linux的shell环境,其他项目中所需要的软件可以在此容器中安装。

目录

  • data: mysql与redis数据目录
  • font: 字体库
  • supervisor: supervisor配置文件目录
  • vhost: nginx站点配置目录

其他变量

  • docker网络名: localhost
  • 工作目录:D:\song\www 请根据自身情况修改,用户存放项目代码
  • 容器工作目录: /app 容器中的该目录会映射到本地工作目录
  • nginx容器站点配置目录: /etc/nginx/conf.d nginx容器中的该目录会映射到本地nginx站点配置目录
  • mysql容器数据目录:/var/lib/mysql mysql容器中的该目录映射到本地mysql的数据目录
  • redis容器数据目录:/data redis容器中的该目录映射到本地redis数据目录

请检查上述变量在 docker-composer.yml 配置文件中的映射是否正确,检查各容器的 volumes项。

使用

在上述配置正确,docker引擎正常启动的前提下,在本目录执行 docker-compose up -d 即可启动docker环境。

如何进行代码结构的规划

发表于 2019-11-13

Index

  • 层
    • 从MVC谈起
      • MVC的变种
      • 分层带来的问题
    • AOP与MVC
    • 一个例子
  • 模块
    • 如何构建一个模块
      • 要解决的问题
      • 提供的接口
      • 模块的引用方式
      • 必要的说明文档
  • 其他

层

从MVC谈起

从最开始接触到web编程时,MVC是使用的最多的模型,在当时的一众CMS中,大多都会打着MVC、OOP等旗号推向市场。MVC从何而起,不必深究,只需要知道,他是一种可以指导我们对代码进行规划的模型。如果你的项目中有太多无处安放的代码,那么,用MVC去盘他,总能找到他们合适的位置。

再后来接触到ThinkPHP的时候,又了解到了AOP面向切面编程,“切面”又成了一种可选的组织代码的模型。如果把MVC看做是纵向的拆分方式,那么切面就是横向的拆分方式。这两种模型,都可以在目前的框架中找到其实现,可以作为我们自身代码结构规划的指导思想。

MVC的变种

然而,凡事没有银弹,不存在一个方法可以完美解决世界上的所有问题,MVC与AOP也不例外。由于当前web开发中,后台的业务变得更为复杂,不再只是以前简单的增删改查,一味往MVC上靠,已经变得不可取了。再加上前后端分离的趋势,后端实际只剩MC两层,将复杂的业务逻辑编码到MC两层中,必然导致某一层变得臃肿庞大,日渐难以维护。由此,有一种思路是将MC继续拆分,引入Service,Repository两层,分别与Controller和Model进行对接,使项目代码分为四层:

1
Controller -> Service -> Repositroy -> Model

还可以根据项目自身情况来决定,只引人Service或Repository中的一层,从而解决某一层过大的问题。依据这一思路,其实可以规划出更多的层级来组织代码,不怕你业务逻辑多复杂,就怕你层级分的不够多。

分层带来的问题

将MVC继续细分,固然是解决方案之一,但同时也带来了一些问题,比如:

  • 层级与层级之间的划分依据是什么?
  • 如果某个操作逻辑比较简单,可否跨级调用?

这些问题可能不会有一个标准的答案,而是需要在决定如此分层时,进行统一约束与定义。即使是进行了定义,也会有:如果层级之间的划分依据不明确,会导致层级与层级之间的边界变得模糊;如果可以进行跨级调用,除了导致层级之边界模糊外,还会使人重新思考,层级划分的意义何在?但是反过来,如果层级的划分依据过于明确,是否可以适用于大多数场景?禁止跨级调用,是否会导致无用代码的增加?等等系列问题。

AOP与MVC

上文我们提到过,如果把MVC看做是纵向的拆分方式,那么切面就是横向的拆分方式。就像是代码执行到某个分叉口时,既要横向执行,又要纵向执行,但众所周知,PHP是单线程的,串行执行,不存在分叉一说,所谓切面,实际上就是插队,将“切面”上编码的内容,插入到钩子设置的切面点上执行,然后继续MVC这条线的执行。

如果我们将“切面”看做是我们分的一个层的话,那么,这一层在执行中到底存不存在,实际上由是否在代码中设置了钩子来决定的。如此看来,刚才我们讨论的分层带来的问题,仿佛有了解决办法,至少,不存在跨级调用的这个问题。

“切面”是层的另一种形态,是某一层中的一个细节。当某一层中的“切面”大量出现时,就应该考虑将切面进行归纳总结,是否来划分成一个“层”了。

至此我们可以总结:

  • 在使用MVC这种模型规划代码时,初期可以不必划分过多的层级,通过引入切面这一模型,来进行调节,然后在合适的时机划分出下一个层级。
  • 通过“层”的维度对代码整体进行划分,是个不错的选择。除此之外,还可以通过模块的方式,对代码进行划分。

一个例子

我以做过的一个项目,美术数据驾驶舱,来举例,简单说明一下我是如何进行分层及依据的。 文档

从Controller到Model,这个项目分了这些层次:

  • Controller: 接收请求的参数;确定用于筛选数据的“应用数据”对象
  • Charts/Exports/TableViewers: 应用数据层。根据上层传递的参数及数据展现型式,查询并组合出指定型式的数据,如:图表型数据、表格型数据、Excel文件数据。对于图表型数据,需要将数据转化成适配EChart的数据格式,如果是表格型数据、Excel文件数据,需要对表格单元格进行合并。
  • TablePrototypes: 表格原型数据层。如果应用数据层是Exports/TableViewers,需要经过这一层。根据上层传递的参数,查询数据并组合成原始表格格式的数据
  • Data: 基础数据层。根据上层传递的参数,查询所需要的数据,这一层的数据都是简单的键->值单元,一个上层的调用,在这一层可能会查询出很多组相同主键的键->值单元,然后上层会根据主键,组合成表一样的数据。
  • Model: 数据库层。

模块

模块是区别于层的,从另外一个维度划分代码时,提出的一个概念,与之平行的还有库、包等概念。在说起模块时,可以暂时忘掉我们上文对于层的讨论。

在使用php的过程中,如果说有什么能帮助你理解模块的概念的话,那莫过于composer了。通常我们使用composer来为项目安装依赖后,可能会在任何地方使用到这些依赖。他们大多都是,为了解决某一范围内的问题,提炼出的一套接口。

然而,使用composer安装的依赖都集中在vendor目录中,日常开发中很少会去注意到这里面都有些什么,似乎并不会对我们自己组织代码时起到什么指导作用。对此,我们来说道说道。

如何构建一个模块

要解决的问题

一个模块,必须要有他的边界,有他要着重解决的问题。如果你想把所有问题都放到一个模块中,那么,这个模块实际就是你正在开发的项目了。所以,在构建一个模块时,首先要考虑的是:你打算用他来解决项目中的哪个问题?

  • 由于需要调用其他服务的接口,所以我们划分出了HttpClient模块,来专门处理http请求发送与响应的问题。
  • 由于需要生成图片的缩略图,所以我们划分出了Image模块,来专门处理缩略图生成的问题,后来我们又需要给图片加水印、需要获取图片宽高等信息等等

提供的接口

在明确模块需要解决的问题后,下一步就要定义为了解决这些问题、模块所提供的接口。在解决这一问题时,所需要的上下文信息,都应该通过接口的参数传递过来,尽可能避免在处理问题的过程中,再次向外部获取参数。与之对应,接口的返回也是要明确定义的,如果返回的是一个对象,那么应该始终返回一个对象,不会因为参数传递的区别,导致返回值类型的差异。

其实,这一条可以作为大多数函数参数与返回值的标准,虽然不是必须遵守的,但是如果你的模块,有可能被抽象出来,广泛运用到其他项目中,最好严格遵守这一条。因为你不知道别人调用你的接口之后,到底会不会对返回值进行检查,如果没有而你又恰巧返回了其他类型的值,就可能导致代码运行出错。

模块的引用方式

一般来说,一个模块有一个公共的出口对象,所有的接口都由这个公共对象来调用,是一个比较好的模式。这样在引用模块时,只需要实例化该对象即可。

必要的说明文档

主要介绍模块的引用方式以及接口使用说明

内部模块与外部模块

使用composer安装的依赖,我们可以称之为外部模块,与之对应的内部模块,就是我们项目中封装的模块了。内部模块公用同一个命名空间即可。

其他

代码结构的规划,说到底其实就是封装的过程。无论是层级的划分,还是模块的划分,都会遇到边界的问题,封装的过程,就是要明确问题的边界。这需要一些设计模式相关知识的储备,也需要进行大量编码的实践,没法一蹴而就,如果从现在开始,试着将函数与方法尽量进行拆分,坚持一段时间,或多或少都会提高自己对代码封装的理解。

12…4
slpi1

slpi1

PHP,Laravel,thinkPHP,javascript,css

37 日志
6 标签
© 2021 slpi1
由 Hexo 强力驱动
主题 - NexT.Mist