Completed
Push — 6.0 ( 41214e...845f81 )
by liu
06:11 queued 01:15
created

BelongsToMany   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 689
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 227
dl 0
loc 689
ccs 0
cts 242
cp 0
rs 2
c 0
b 0
f 0
wmc 87

28 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 4 1
A hydratePivot() 0 17 5
A pivot() 0 4 1
A buildQuery() 0 11 1
A newPivot() 0 9 3
A __construct() 0 16 2
A relationCount() 0 17 3
A select() 0 6 1
A getRelation() 0 14 2
A hasWhere() 0 3 1
A has() 0 3 1
A wherePivot() 0 4 1
A selectOrFail() 0 3 1
A paginate() 0 6 1
A findOrFail() 0 3 1
A find() 0 9 2
A getRelationCountQuery() 0 11 2
A eagerlyResultSet() 0 29 6
A eagerlyResult() 0 17 3
B attach() 0 42 9
B sync() 0 41 9
A belongsToManyQuery() 0 18 2
A save() 0 4 1
A saveAll() 0 15 4
B eagerlyManyToMany() 0 37 9
A baseQuery() 0 10 3
B detach() 0 31 9
A attached() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like BelongsToMany often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BelongsToMany, and based on these observations, apply Extract Interface, too.

1
<?php
2
// +----------------------------------------------------------------------
1 ignored issue
show
Coding Style introduced by
You must use "/**" style comments for a file comment
Loading history...
3
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
8
// +----------------------------------------------------------------------
9
// | Author: liu21st <[email protected]>
10
// +----------------------------------------------------------------------
11
12
namespace think\model\relation;
13
14
use Closure;
15
use think\App;
16
use think\Collection;
17
use think\db\Query;
18
use think\db\Raw;
19
use think\Exception;
20
use think\Model;
21
use think\model\Pivot;
22
use think\model\Relation;
23
use think\Paginator;
24
25
/**
26
 * 多对多关联类
27
 */
5 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
28
class BelongsToMany extends Relation
29
{
30
    /**
31
     * 中间表表名
32
     * @var string
33
     */
34
    protected $middle;
35
36
    /**
37
     * 中间表模型名称
38
     * @var string
39
     */
40
    protected $pivotName;
41
42
    /**
43
     * 中间表模型对象
44
     * @var Pivot
45
     */
46
    protected $pivot;
47
48
    /**
49
     * 中间表数据名称
50
     * @var string
51
     */
52
    protected $pivotDataName = 'pivot';
53
54
    /**
55
     * 架构函数
56
     * @access public
57
     * @param  Model  $parent     上级模型对象
58
     * @param  string $model      模型名
59
     * @param  string $table      中间表名
60
     * @param  string $foreignKey 关联模型外键
61
     * @param  string $localKey   当前模型关联键
62
     */
63
    public function __construct(Model $parent, string $model, string $table, string $foreignKey, string $localKey)
64
    {
65
        $this->parent     = $parent;
66
        $this->model      = $model;
67
        $this->foreignKey = $foreignKey;
68
        $this->localKey   = $localKey;
69
70
        if (false !== strpos($table, '\\')) {
71
            $this->pivotName = $table;
72
            $this->middle    = App::classBaseName($table);
73
        } else {
74
            $this->middle = $table;
75
        }
76
77
        $this->query = (new $model)->db();
78
        $this->pivot = $this->newPivot();
79
    }
80
81
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $pivot should have a doc-comment as per coding-style.
Loading history...
82
     * 设置中间表模型
83
     * @access public
84
     * @param  $pivot
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
85
     * @return $this
86
     */
87
    public function pivot(string $pivot)
88
    {
89
        $this->pivotName = $pivot;
90
        return $this;
91
    }
92
93
    /**
94
     * 设置中间表数据名称
95
     * @access public
96
     * @param  string $name
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
97
     * @return $this
98
     */
99
    public function name(string $name)
100
    {
101
        $this->pivotDataName = $name;
102
        return $this;
103
    }
104
105
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $data should have a doc-comment as per coding-style.
Loading history...
106
     * 实例化中间表模型
107
     * @access public
108
     * @param  $data
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
109
     * @return Pivot
110
     * @throws Exception
111
     */
112
    protected function newPivot(array $data = []): Pivot
113
    {
114
        $class = $this->pivotName ?: '\\think\\model\\Pivot';
115
        $pivot = new $class($data, $this->parent, $this->middle);
116
117
        if ($pivot instanceof Pivot) {
118
            return $pivot;
119
        } else {
120
            throw new Exception('pivot model must extends: \think\model\Pivot');
121
        }
122
    }
123
124
    /**
125
     * 合成中间表模型
126
     * @access protected
127
     * @param  array|Collection|Paginator $models
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
128
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
129
    protected function hydratePivot(iterable $models)
130
    {
131
        foreach ($models as $model) {
132
            $pivot = [];
133
134
            foreach ($model->getData() as $key => $val) {
135
                if (strpos($key, '__')) {
136
                    list($name, $attr) = explode('__', $key, 2);
137
138
                    if ('pivot' == $name) {
139
                        $pivot[$attr] = $val;
140
                        unset($model->$key);
141
                    }
142
                }
143
            }
144
145
            $model->setRelation($this->pivotDataName, $this->newPivot($pivot));
146
        }
147
    }
148
149
    /**
150
     * 创建关联查询Query对象
151
     * @access protected
152
     * @return Query
153
     */
154
    protected function buildQuery(): Query
155
    {
156
        $foreignKey = $this->foreignKey;
157
        $localKey   = $this->localKey;
158
159
        // 关联查询
160
        $pk = $this->parent->getPk();
161
162
        $condition = ['pivot.' . $localKey, '=', $this->parent->$pk];
163
164
        return $this->belongsToManyQuery($foreignKey, $localKey, [$condition]);
165
    }
166
167
    /**
168
     * 延迟获取关联数据
169
     * @access public
170
     * @param  array    $subRelation 子关联名
171
     * @param  Closure  $closure     闭包查询条件
172
     * @return Collection
173
     */
174
    public function getRelation(array $subRelation = [], Closure $closure = null): Collection
175
    {
176
        if ($closure) {
177
            $closure($this);
178
        }
179
180
        $result = $this->buildQuery()
181
            ->relation($subRelation)
182
            ->select()
183
            ->setParent(clone $this->parent);
0 ignored issues
show
introduced by
The method setParent() does not exist on think\Collection. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
            ->/** @scrutinizer ignore-call */ setParent(clone $this->parent);
Loading history...
184
185
        $this->hydratePivot($result);
186
187
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type array which is incompatible with the type-hinted return think\Collection. Consider adding an additional type-check to rule them out.
Loading history...
188
    }
189
190
    /**
191
     * 重载select方法
192
     * @access public
193
     * @param  mixed $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
194
     * @return Collection
195
     */
196
    public function select($data = null): Collection
197
    {
198
        $result = $this->buildQuery()->select($data);
199
        $this->hydratePivot($result);
200
201
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type array which is incompatible with the type-hinted return think\Collection. Consider adding an additional type-check to rule them out.
Loading history...
202
    }
203
204
    /**
205
     * 重载paginate方法
206
     * @access public
207
     * @param  int|array $listRows
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
208
     * @param  int|bool  $simple
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
209
     * @param  array     $config
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
210
     * @return Paginator
211
     */
212
    public function paginate($listRows = null, $simple = false, $config = []): Paginator
213
    {
214
        $result = $this->buildQuery()->paginate($listRows, $simple, $config);
215
        $this->hydratePivot($result);
216
217
        return $result;
218
    }
219
220
    /**
221
     * 重载find方法
222
     * @access public
223
     * @param  mixed $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
224
     * @return Model
225
     */
226
    public function find($data = null)
227
    {
228
        $result = $this->buildQuery()->find($data);
229
230
        if (!$result->isEmpty()) {
231
            $this->hydratePivot([$result]);
232
        }
233
234
        return $result;
235
    }
236
237
    /**
238
     * 查找多条记录 如果不存在则抛出异常
239
     * @access public
240
     * @param  array|string|Query|\Closure $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
241
     * @return Collection
242
     */
243
    public function selectOrFail($data = null): Collection
244
    {
245
        return $this->buildQuery()->failException(true)->select($data);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->buildQuery...on(true)->select($data) could return the type array which is incompatible with the type-hinted return think\Collection. Consider adding an additional type-check to rule them out.
Loading history...
246
    }
247
248
    /**
249
     * 查找单条记录 如果不存在则抛出异常
250
     * @access public
251
     * @param  array|string|Query|\Closure $data
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
252
     * @return Model
253
     */
254
    public function findOrFail($data = null): Model
255
    {
256
        return $this->buildQuery()->failException(true)->find($data);
257
    }
258
259
    /**
260
     * 根据关联条件查询当前模型
261
     * @access public
262
     * @param  string  $operator 比较操作符
263
     * @param  integer $count    个数
264
     * @param  string  $id       关联表的统计字段
265
     * @param  string  $joinType JOIN类型
266
     * @return Model
267
     */
268
    public function has(string $operator = '>=', $count = 1, $id = '*', string $joinType = 'INNER')
269
    {
270
        return $this->parent;
271
    }
272
273
    /**
274
     * 根据关联条件查询当前模型
275
     * @access public
276
     * @param  mixed  $where 查询条件(数组或者闭包)
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
277
     * @param  mixed  $fields 字段
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
278
     * @param  string $joinType JOIN类型
279
     * @return Query
280
     * @throws Exception
281
     */
282
    public function hasWhere($where = [], $fields = null, string $joinType = '')
283
    {
284
        throw new Exception('relation not support: hasWhere');
285
    }
286
287
    /**
288
     * 设置中间表的查询条件
289
     * @access public
290
     * @param  string $field
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
291
     * @param  string $op
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
292
     * @param  mixed  $condition
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
293
     * @return $this
294
     */
295
    public function wherePivot($field, $op = null, $condition = null)
296
    {
297
        $this->query->where('pivot.' . $field, $op, $condition);
298
        return $this;
299
    }
300
301
    /**
302
     * 预载入关联查询(数据集)
303
     * @access public
304
     * @param  array   $resultSet   数据集
305
     * @param  string  $relation    当前关联名
306
     * @param  array   $subRelation 子关联名
307
     * @param  Closure $closure     闭包
308
     * @return void
309
     */
310
    public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, Closure $closure = null): void
311
    {
312
        $localKey = $this->localKey;
313
        $pk       = $resultSet[0]->getPk();
314
        $range    = [];
315
316
        foreach ($resultSet as $result) {
317
            // 获取关联外键列表
318
            if (isset($result->$pk)) {
319
                $range[] = $result->$pk;
320
            }
321
        }
322
323
        if (!empty($range)) {
324
            // 查询关联数据
325
            $data = $this->eagerlyManyToMany([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
326
                ['pivot.' . $localKey, 'in', $range],
327
            ], $relation, $subRelation, $closure);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
328
329
            // 关联属性名
330
            $attr = App::parseName($relation);
331
332
            // 关联数据封装
333
            foreach ($resultSet as $result) {
334
                if (!isset($data[$result->$pk])) {
335
                    $data[$result->$pk] = [];
336
                }
337
338
                $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk], clone $this->parent));
339
            }
340
        }
341
    }
342
343
    /**
344
     * 预载入关联查询(单个数据)
345
     * @access public
346
     * @param  Model   $result      数据对象
347
     * @param  string  $relation    当前关联名
348
     * @param  array   $subRelation 子关联名
349
     * @param  Closure $closure     闭包
350
     * @return void
351
     */
352
    public function eagerlyResult(Model $result, string $relation, array $subRelation, Closure $closure = null): void
353
    {
354
        $pk = $result->getPk();
355
356
        if (isset($result->$pk)) {
357
            $pk = $result->$pk;
358
            // 查询管理数据
359
            $data = $this->eagerlyManyToMany([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
360
                ['pivot.' . $this->localKey, '=', $pk],
361
            ], $relation, $subRelation, $closure);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
362
363
            // 关联数据封装
364
            if (!isset($data[$pk])) {
365
                $data[$pk] = [];
366
            }
367
368
            $result->setRelation(App::parseName($relation), $this->resultSetBuild($data[$pk], clone $this->parent));
369
        }
370
    }
371
372
    /**
373
     * 关联统计
374
     * @access public
375
     * @param  Model   $result  数据对象
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 2 found
Loading history...
376
     * @param  Closure $closure 闭包
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
377
     * @param  string  $aggregate 聚合查询方法
378
     * @param  string  $field 字段
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
379
     * @param  string  $name 统计字段别名
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
380
     * @return integer
381
     */
382
    public function relationCount(Model $result, Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): float
383
    {
384
        $pk = $result->getPk();
385
386
        if (!isset($result->$pk)) {
387
            return 0;
388
        }
389
390
        $pk = $result->$pk;
391
392
        if ($closure) {
393
            $closure($this, $name);
394
        }
395
396
        return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
397
            ['pivot.' . $this->localKey, '=', $pk],
398
        ])->$aggregate($field);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
399
    }
400
401
    /**
402
     * 获取关联统计子查询
403
     * @access public
404
     * @param  Closure $closure 闭包
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
405
     * @param  string  $aggregate 聚合查询方法
406
     * @param  string  $field 字段
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
407
     * @param  string  $name 统计字段别名
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
408
     * @return string
409
     */
410
    public function getRelationCountQuery(Closure $closure = null, string $aggregate = 'count', string $field = '*', string &$name = null): string
411
    {
412
        if ($closure) {
413
            $closure($this, $name);
414
        }
415
416
        return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
417
            [
418
                'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . $this->parent->getPk()),
0 ignored issues
show
Bug introduced by
Are you sure $this->parent->getPk() of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

418
                'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(false)->getTable() . '.' . /** @scrutinizer ignore-type */ $this->parent->getPk()),
Loading history...
Bug introduced by
false of type false is incompatible with the type array expected by parameter $scope of think\Model::db(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

418
                'pivot.' . $this->localKey, 'exp', new Raw('=' . $this->parent->db(/** @scrutinizer ignore-type */ false)->getTable() . '.' . $this->parent->getPk()),
Loading history...
419
            ],
420
        ])->fetchSql()->$aggregate($field);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
421
    }
422
423
    /**
424
     * 多对多 关联模型预查询
425
     * @access protected
426
     * @param  array   $where       关联预查询条件
427
     * @param  string  $relation    关联名
428
     * @param  array   $subRelation 子关联
429
     * @param  Closure $closure     闭包
430
     * @return array
431
     */
432
    protected function eagerlyManyToMany(array $where, string $relation, array $subRelation = [], Closure $closure = null): array
433
    {
434
        if ($closure) {
435
            $closure($this);
436
        }
437
438
        // 预载入关联查询 支持嵌套预载入
439
        $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
440
            ->with($subRelation)
441
            ->select();
442
443
        // 组装模型数据
444
        $data = [];
445
        foreach ($list as $set) {
446
            $pivot = [];
447
            foreach ($set->getData() as $key => $val) {
448
                if (strpos($key, '__')) {
449
                    list($name, $attr) = explode('__', $key, 2);
450
                    if ('pivot' == $name) {
451
                        $pivot[$attr] = $val;
452
                        unset($set->$key);
453
                    }
454
                }
455
            }
456
457
            $key = $pivot[$this->localKey];
458
459
            if ($this->withLimit && isset($data[$key]) && count($data[$key]) >= $this->withLimit) {
460
                continue;
461
            }
462
463
            $set->setRelation($this->pivotDataName, $this->newPivot($pivot));
464
465
            $data[$key][] = $set;
466
        }
467
468
        return $data;
469
    }
470
471
    /**
472
     * BELONGS TO MANY 关联查询
473
     * @access protected
474
     * @param  string $foreignKey 关联模型关联键
475
     * @param  string $localKey   当前模型关联键
476
     * @param  array  $condition  关联查询条件
477
     * @return Query
478
     */
479
    protected function belongsToManyQuery(string $foreignKey, string $localKey, array $condition = []): Query
480
    {
481
        // 关联查询封装
482
        $tableName = $this->query->getTable();
483
        $table     = $this->pivot->db()->getTable();
484
        $fields    = $this->getQueryFields($tableName);
485
486
        $query = $this->query
487
            ->field($fields)
488
            ->tableField(true, $table, 'pivot', 'pivot__');
489
490
        if (empty($this->baseQuery)) {
491
            $relationFk = $this->query->getPk();
492
            $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
0 ignored issues
show
Bug introduced by
Are you sure $relationFk of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

492
            $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . /** @scrutinizer ignore-type */ $relationFk)
Loading history...
493
                ->where($condition);
494
        }
495
496
        return $query;
497
    }
498
499
    /**
500
     * 保存(新增)当前关联数据对象
501
     * @access public
502
     * @param  mixed $data  数据 可以使用数组 关联模型对象 和 关联对象的主键
503
     * @param  array $pivot 中间表额外数据
504
     * @return array|Pivot
505
     */
506
    public function save($data, array $pivot = [])
507
    {
508
        // 保存关联表/中间表数据
509
        return $this->attach($data, $pivot);
510
    }
511
512
    /**
513
     * 批量保存当前关联数据对象
514
     * @access public
515
     * @param  iterable $dataSet   数据集
516
     * @param  array    $pivot     中间表额外数据
517
     * @param  bool     $samePivot 额外数据是否相同
518
     * @return array|false
519
     */
520
    public function saveAll(iterable $dataSet, array $pivot = [], bool $samePivot = false)
521
    {
522
        $result = [];
523
524
        foreach ($dataSet as $key => $data) {
525
            if (!$samePivot) {
526
                $pivotData = $pivot[$key] ?? [];
527
            } else {
528
                $pivotData = $pivot;
529
            }
530
531
            $result[] = $this->attach($data, $pivotData);
532
        }
533
534
        return empty($result) ? false : $result;
535
    }
536
537
    /**
538
     * 附加关联的一个中间表数据
539
     * @access public
540
     * @param  mixed $data  数据 可以使用数组、关联模型对象 或者 关联对象的主键
541
     * @param  array $pivot 中间表额外数据
542
     * @return array|Pivot
543
     * @throws Exception
544
     */
545
    public function attach($data, array $pivot = [])
546
    {
547
        if (is_array($data)) {
548
            if (key($data) === 0) {
549
                $id = $data;
550
            } else {
551
                // 保存关联表数据
552
                $model = new $this->model;
553
                $id    = $model->insertGetId($data);
554
            }
555
        } elseif (is_numeric($data) || is_string($data)) {
556
            // 根据关联表主键直接写入中间表
557
            $id = $data;
558
        } elseif ($data instanceof Model) {
559
            // 根据关联表主键直接写入中间表
560
            $relationFk = $data->getPk();
561
            $id         = $data->$relationFk;
562
        }
563
564
        if (!empty($id)) {
565
            // 保存中间表数据
566
            $pk                     = $this->parent->getPk();
567
            $pivot[$this->localKey] = $this->parent->$pk;
568
            $ids                    = (array) $id;
569
570
            foreach ($ids as $id) {
571
                $pivot[$this->foreignKey] = $id;
572
                $this->pivot->replace()
573
                    ->exists(false)
574
                    ->data([])
575
                    ->save($pivot);
576
                $result[] = $this->newPivot($pivot);
577
            }
578
579
            if (count($result) == 1) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result seems to be defined by a foreach iteration on line 570. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
580
                // 返回中间表模型对象
581
                $result = $result[0];
582
            }
583
584
            return $result;
585
        } else {
586
            throw new Exception('miss relation data');
587
        }
588
    }
589
590
    /**
591
     * 判断是否存在关联数据
592
     * @access public
593
     * @param  mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键
594
     * @return Pivot|false
595
     */
596
    public function attached($data)
597
    {
598
        if ($data instanceof Model) {
599
            $id = $data->getKey();
600
        } else {
601
            $id = $data;
602
        }
603
604
        $pivot = $this->pivot
605
            ->where($this->localKey, $this->parent->getKey())
0 ignored issues
show
Bug introduced by
The method where() does not exist on think\model\Pivot. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

605
            ->/** @scrutinizer ignore-call */ where($this->localKey, $this->parent->getKey())
Loading history...
606
            ->where($this->foreignKey, $id)
607
            ->find();
608
609
        return $pivot ?: false;
610
    }
611
612
    /**
613
     * 解除关联的一个中间表数据
614
     * @access public
615
     * @param  integer|array $data        数据 可以使用关联对象的主键
616
     * @param  bool          $relationDel 是否同时删除关联表数据
617
     * @return integer
618
     */
619
    public function detach($data = null, bool $relationDel = false): int
620
    {
621
        if (is_array($data)) {
622
            $id = $data;
623
        } elseif (is_numeric($data) || is_string($data)) {
624
            // 根据关联表主键直接写入中间表
625
            $id = $data;
626
        } elseif ($data instanceof Model) {
627
            // 根据关联表主键直接写入中间表
628
            $relationFk = $data->getPk();
629
            $id         = $data->$relationFk;
630
        }
631
632
        // 删除中间表数据
633
        $pk      = $this->parent->getPk();
634
        $pivot   = [];
635
        $pivot[] = [$this->localKey, '=', $this->parent->$pk];
636
637
        if (isset($id)) {
638
            $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
639
        }
640
641
        $result = $this->pivot->where($pivot)->delete();
642
643
        // 删除关联表数据
644
        if (isset($id) && $relationDel) {
645
            $model = $this->model;
646
            $model::destroy($id);
647
        }
648
649
        return $result;
650
    }
651
652
    /**
653
     * 数据同步
654
     * @access public
655
     * @param  array $ids
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
656
     * @param  bool  $detaching
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
657
     * @return array
658
     */
659
    public function sync(array $ids, bool $detaching = true): array
660
    {
661
        $changes = [
662
            'attached' => [],
663
            'detached' => [],
664
            'updated'  => [],
665
        ];
666
667
        $pk = $this->parent->getPk();
668
669
        $current = $this->pivot
670
            ->where($this->localKey, $this->parent->$pk)
671
            ->column($this->foreignKey);
672
673
        $records = [];
674
675
        foreach ($ids as $key => $value) {
676
            if (!is_array($value)) {
677
                $records[$value] = [];
678
            } else {
679
                $records[$key] = $value;
680
            }
681
        }
682
683
        $detach = array_diff($current, array_keys($records));
684
685
        if ($detaching && count($detach) > 0) {
686
            $this->detach($detach);
687
            $changes['detached'] = $detach;
688
        }
689
690
        foreach ($records as $id => $attributes) {
691
            if (!in_array($id, $current)) {
692
                $this->attach($id, $attributes);
693
                $changes['attached'][] = $id;
694
            } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) {
695
                $changes['updated'][] = $id;
696
            }
697
        }
698
699
        return $changes;
700
    }
701
702
    /**
703
     * 执行基础查询(仅执行一次)
704
     * @access protected
705
     * @return void
706
     */
707
    protected function baseQuery(): void
708
    {
709
        if (empty($this->baseQuery) && $this->parent->getData()) {
710
            $pk    = $this->parent->getPk();
711
            $table = $this->pivot->getTable();
0 ignored issues
show
Bug introduced by
The method getTable() does not exist on think\model\Pivot. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

711
            /** @scrutinizer ignore-call */ 
712
            $table = $this->pivot->getTable();
Loading history...
712
713
            $this->query
714
                ->join([$table => 'pivot'], 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())
0 ignored issues
show
Bug introduced by
Are you sure $this->query->getPk() of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

714
                ->join([$table => 'pivot'], 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . /** @scrutinizer ignore-type */ $this->query->getPk())
Loading history...
715
                ->where('pivot.' . $this->localKey, $this->parent->$pk);
716
            $this->baseQuery = true;
717
        }
718
    }
719
720
}
721