Passed
Push — 5.2 ( 61517e...2a99ba )
by liu
02:49
created

RelationShip::morphTo()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 2
dl 0
loc 18
rs 9.9332
c 0
b 0
f 0
1
<?php
2
// +----------------------------------------------------------------------
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
declare (strict_types = 1);
12
13
namespace think\model\concern;
14
15
use think\App;
16
use think\Collection;
17
use think\db\Query;
18
use think\Model;
19
use think\model\Relation;
20
use think\model\relation\BelongsTo;
21
use think\model\relation\BelongsToMany;
22
use think\model\relation\HasMany;
23
use think\model\relation\HasManyThrough;
24
use think\model\relation\HasOne;
25
use think\model\relation\MorphMany;
26
use think\model\relation\MorphOne;
27
use think\model\relation\MorphTo;
28
29
/**
30
 * 模型关联处理
31
 */
32
trait RelationShip
33
{
34
    /**
35
     * 父关联模型对象
36
     * @var object
37
     */
38
    private $parent;
39
40
    /**
41
     * 模型关联数据
42
     * @var array
43
     */
44
    private $relation = [];
45
46
    /**
47
     * 关联写入定义信息
48
     * @var array
49
     */
50
    private $together = [];
51
52
    /**
53
     * 关联自动写入信息
54
     * @var array
55
     */
56
    protected $relationWrite = [];
57
58
    /**
59
     * 设置父关联对象
60
     * @access public
61
     * @param  Model $model  模型对象
62
     * @return $this
63
     */
64
    public function setParent(Model $model)
65
    {
66
        $this->parent = $model;
67
68
        return $this;
69
    }
70
71
    /**
72
     * 获取父关联对象
73
     * @access public
74
     * @return Model
75
     */
76
    public function getParent(): Model
77
    {
78
        return $this->parent;
79
    }
80
81
    /**
82
     * 获取当前模型的关联模型数据
83
     * @access public
84
     * @param  string $name 关联方法名
85
     * @return mixed
86
     */
87
    public function getRelation(string $name = null)
88
    {
89
        if (is_null($name)) {
90
            return $this->relation;
91
        }
92
93
        if (array_key_exists($name, $this->relation)) {
94
            return $this->relation[$name];
95
        }
96
    }
97
98
    /**
99
     * 设置关联数据对象值
100
     * @access public
101
     * @param  string $name  属性名
102
     * @param  mixed  $value 属性值
103
     * @param  array  $data  数据
104
     * @return $this
105
     */
106
    public function setRelation(string $name, $value, array $data = [])
107
    {
108
        // 检测修改器
109
        $method = 'set' . App::parseName($name, 1) . 'Attr';
110
111
        if (method_exists($this, $method)) {
112
            $value = $this->$method($value, array_merge($this->data, $data));
113
        }
114
115
        $this->relation[$name] = $value;
116
117
        return $this;
118
    }
119
120
    /**
121
     * 查询当前模型的关联数据
122
     * @access public
123
     * @param  array $relations 关联名
124
     * @return void
125
     */
126
    public function relationQuery(array $relations): void
127
    {
128
        foreach ($relations as $key => $relation) {
129
            $subRelation = '';
130
            $closure     = null;
131
132
            if ($relation instanceof \Closure) {
133
                // 支持闭包查询过滤关联条件
134
                $closure  = $relation;
135
                $relation = $key;
136
            }
137
138
            if (is_array($relation)) {
139
                $subRelation = $relation;
140
                $relation    = $key;
141
            } elseif (strpos($relation, '.')) {
142
                list($relation, $subRelation) = explode('.', $relation, 2);
143
            }
144
145
            $method = App::parseName($relation, 1, false);
146
147
            $this->relation[$relation] = $this->$method()->getRelation($subRelation, $closure);
148
        }
149
    }
150
151
    /**
152
     * 关联数据写入
153
     * @access public
154
     * @param  array $relation 关联
155
     * @return $this
156
     */
157
    public function together(array $relation)
158
    {
159
        $this->together = $relation;
160
161
        $this->checkAutoRelationWrite();
162
163
        return $this;
164
    }
165
166
    /**
167
     * 根据关联条件查询当前模型
168
     * @access public
169
     * @param  string  $relation 关联方法名
170
     * @param  mixed   $operator 比较操作符
171
     * @param  integer $count    个数
172
     * @param  string  $id       关联表的统计字段
173
     * @param  string  $joinType JOIN类型
174
     * @return Query
175
     */
176
    public static function has(string $relation, string $operator = '>=', int $count = 1, string $id = '*', string $joinType = ''): Query
177
    {
178
        return (new static())
179
            ->$relation()
180
            ->has($operator, $count, $id, $joinType);
181
    }
182
183
    /**
184
     * 根据关联条件查询当前模型
185
     * @access public
186
     * @param  string $relation 关联方法名
187
     * @param  mixed  $where    查询条件(数组或者闭包)
188
     * @param  mixed  $fields   字段
189
     * @param  string $joinType JOIN类型
190
     * @return Query
191
     */
192
    public static function hasWhere(string $relation, $where = [], string $fields = '*', string $joinType = ''): Query
193
    {
194
        return (new static())
195
            ->$relation()
196
            ->hasWhere($where, $fields, $joinType);
197
    }
198
199
    /**
200
     * 预载入关联查询 返回数据集
201
     * @access public
202
     * @param  array  $resultSet 数据集
203
     * @param  string $relation  关联名
204
     * @param  array  $withRelationAttr 关联获取器
205
     * @param  bool   $join      是否为JOIN方式
206
     * @return void
207
     */
208
    public function eagerlyResultSet(array &$resultSet, array $relations, array $withRelationAttr = [], bool $join = false): void
209
    {
210
        foreach ($relations as $key => $relation) {
211
            $subRelation = [];
212
            $closure     = null;
213
214
            if ($relation instanceof \Closure) {
215
                $closure  = $relation;
216
                $relation = $key;
217
            }
218
219
            if (is_array($relation)) {
220
                $subRelation = $relation;
221
                $relation    = $key;
222
            } elseif (strpos($relation, '.')) {
223
                list($relation, $subRelation) = explode('.', $relation, 2);
224
225
                $subRelation = [$subRelation];
226
            }
227
228
            $relation     = App::parseName($relation, 1, false);
229
            $relationName = App::parseName($relation);
230
231
            $relationResult = $this->$relation();
232
233
            if (isset($withRelationAttr[$relationName])) {
234
                $relationResult->withAttr($withRelationAttr[$relationName]);
235
            }
236
237
            $relationResult->eagerlyResultSet($resultSet, $relation, $subRelation, $closure, $join);
238
        }
239
    }
240
241
    /**
242
     * 预载入关联查询 返回模型对象
243
     * @access public
244
     * @param  Model    $result    数据对象
245
     * @param  array    $relations 关联
246
     * @param  array    $withRelationAttr 关联获取器
247
     * @param  bool     $join      是否为JOIN方式
248
     * @return void
249
     */
250
    public function eagerlyResult(Model $result, array $relations, array $withRelationAttr = [], bool $join = false): void
251
    {
252
        foreach ($relations as $key => $relation) {
253
            $subRelation = [];
254
            $closure     = null;
255
256
            if ($relation instanceof \Closure) {
257
                $closure  = $relation;
258
                $relation = $key;
259
            }
260
261
            if (is_array($relation)) {
262
                $subRelation = $relation;
263
                $relation    = $key;
264
            } elseif (strpos($relation, '.')) {
265
                list($relation, $subRelation) = explode('.', $relation, 2);
266
267
                $subRelation = [$subRelation];
268
            }
269
270
            $relation     = App::parseName($relation, 1, false);
271
            $relationName = App::parseName($relation);
272
273
            $relationResult = $this->$relation();
274
275
            if (isset($withRelationAttr[$relationName])) {
276
                $relationResult->withAttr($withRelationAttr[$relationName]);
277
            }
278
279
            $relationResult->eagerlyResult($result, $relation, $subRelation, $closure, $join);
280
        }
281
    }
282
283
    /**
284
     * 绑定(一对一)关联属性到当前模型
285
     * @access protected
286
     * @param  string   $relation    关联名称
287
     * @param  array    $attrs       绑定属性
288
     * @return $this
289
     * @throws Exception
290
     */
291
    public function bindAttr(string $relation, array $attrs = [])
292
    {
293
        $relation = $this->getRelation($relation);
294
295
        foreach ($attrs as $key => $attr) {
296
            $key   = is_numeric($key) ? $attr : $key;
297
            $value = $this->getOrigin($key);
0 ignored issues
show
Bug introduced by
It seems like getOrigin() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

297
            /** @scrutinizer ignore-call */ 
298
            $value = $this->getOrigin($key);
Loading history...
298
299
            if (!is_null($value)) {
300
                throw new Exception('bind attr has exists:' . $key);
0 ignored issues
show
Bug introduced by
The type think\model\concern\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
301
            } else {
302
                $this->set($key, $relation ? $relation->$attr : null);
0 ignored issues
show
Bug introduced by
It seems like set() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

302
                $this->/** @scrutinizer ignore-call */ 
303
                       set($key, $relation ? $relation->$attr : null);
Loading history...
303
            }
304
        }
305
306
        return $this;
307
    }
308
309
    /**
310
     * 关联统计
311
     * @access public
312
     * @param  Model    $result     数据对象
313
     * @param  array    $relations  关联名
314
     * @param  string   $aggregate  聚合查询方法
315
     * @param  string   $field      字段
316
     * @return void
317
     */
318
    public function relationCount(Model $result, array $relations, string $aggregate = 'sum', string $field = '*'): void
319
    {
320
        foreach ($relations as $key => $relation) {
321
            $closure = $name = null;
322
323
            if ($relation instanceof \Closure) {
324
                $closure  = $relation;
325
                $relation = $key;
326
            } elseif (is_string($key)) {
327
                $name     = $relation;
328
                $relation = $key;
329
            }
330
331
            $relation = App::parseName($relation, 1, false);
332
            $count    = $this->$relation()->relationCount($result, $closure, $aggregate, $field, $name);
333
334
            if (empty($name)) {
335
                $name = App::parseName($relation) . '_' . $aggregate;
336
            }
337
338
            $result->setAttr($name, $count);
339
        }
340
    }
341
342
    /**
343
     * HAS ONE 关联定义
344
     * @access public
345
     * @param  string $model      模型名
346
     * @param  string $foreignKey 关联外键
347
     * @param  string $localKey   当前主键
348
     * @return HasOne
349
     */
350
    public function hasOne(string $model, string $foreignKey = '', string $localKey = ''): HasOne
351
    {
352
        // 记录当前关联信息
353
        $model      = $this->parseModel($model);
354
        $localKey   = $localKey ?: $this->getPk();
0 ignored issues
show
Bug introduced by
It seems like getPk() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

354
        $localKey   = $localKey ?: $this->/** @scrutinizer ignore-call */ getPk();
Loading history...
355
        $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
356
357
        return new HasOne($this, $model, $foreignKey, $localKey);
358
    }
359
360
    /**
361
     * BELONGS TO 关联定义
362
     * @access public
363
     * @param  string $model      模型名
364
     * @param  string $foreignKey 关联外键
365
     * @param  string $localKey   关联主键
366
     * @return BelongsTo
367
     */
368
    public function belongsTo(string $model, string $foreignKey = '', string $localKey = ''): BelongsTo
369
    {
370
        // 记录当前关联信息
371
        $model      = $this->parseModel($model);
372
        $foreignKey = $foreignKey ?: $this->getForeignKey((new $model)->getName());
373
        $localKey   = $localKey ?: (new $model)->getPk();
374
        $trace      = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
375
        $relation   = App::parseName($trace[1]['function']);
376
377
        return new BelongsTo($this, $model, $foreignKey, $localKey, $relation);
378
    }
379
380
    /**
381
     * HAS MANY 关联定义
382
     * @access public
383
     * @param  string $model      模型名
384
     * @param  string $foreignKey 关联外键
385
     * @param  string $localKey   当前主键
386
     * @return HasMany
387
     */
388
    public function hasMany(string $model, string $foreignKey = '', string $localKey = ''): HasMany
389
    {
390
        // 记录当前关联信息
391
        $model      = $this->parseModel($model);
392
        $localKey   = $localKey ?: $this->getPk();
393
        $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
394
395
        return new HasMany($this, $model, $foreignKey, $localKey);
396
    }
397
398
    /**
399
     * HAS MANY 远程关联定义
400
     * @access public
401
     * @param  string $model      模型名
402
     * @param  string $through    中间模型名
403
     * @param  string $foreignKey 关联外键
404
     * @param  string $throughKey 关联外键
405
     * @param  string $localKey   当前主键
406
     * @return HasManyThrough
407
     */
408
    public function hasManyThrough(string $model, string $through, string $foreignKey = '', string $throughKey = '', string $localKey = ''): HasManyThrough
409
    {
410
        // 记录当前关联信息
411
        $model      = $this->parseModel($model);
412
        $through    = $this->parseModel($through);
413
        $localKey   = $localKey ?: $this->getPk();
414
        $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
415
        $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
416
417
        return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey);
418
    }
419
420
    /**
421
     * BELONGS TO MANY 关联定义
422
     * @access public
423
     * @param  string $model      模型名
424
     * @param  string $table      中间表名
425
     * @param  string $foreignKey 关联外键
426
     * @param  string $localKey   当前模型关联键
427
     * @return BelongsToMany
428
     */
429
    public function belongsToMany(string $model, string $table = '', string $foreignKey = '', string $localKey = ''): BelongsToMany
430
    {
431
        // 记录当前关联信息
432
        $model      = $this->parseModel($model);
433
        $name       = App::parseName(App::classBaseName($model));
434
        $table      = $table ?: App::parseName($this->name) . '_' . $name;
435
        $foreignKey = $foreignKey ?: $name . '_id';
436
        $localKey   = $localKey ?: $this->getForeignKey($this->name);
437
438
        return new BelongsToMany($this, $model, $table, $foreignKey, $localKey);
439
    }
440
441
    /**
442
     * MORPH  One 关联定义
443
     * @access public
444
     * @param  string       $model 模型名
445
     * @param  string|array $morph 多态字段信息
446
     * @param  string       $type  多态类型
447
     * @return MorphOne
448
     */
449
    public function morphOne(string $model, $morph = null, string $type = ''): MorphOne
450
    {
451
        // 记录当前关联信息
452
        $model = $this->parseModel($model);
453
454
        if (is_null($morph)) {
455
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
456
            $morph = App::parseName($trace[1]['function']);
457
        }
458
459
        if (is_array($morph)) {
460
            list($morphType, $foreignKey) = $morph;
461
        } else {
462
            $morphType  = $morph . '_type';
463
            $foreignKey = $morph . '_id';
464
        }
465
466
        $type = $type ?: get_class($this);
467
468
        return new MorphOne($this, $model, $foreignKey, $morphType, $type);
469
    }
470
471
    /**
472
     * MORPH  MANY 关联定义
473
     * @access public
474
     * @param  string       $model 模型名
475
     * @param  string|array $morph 多态字段信息
476
     * @param  string       $type  多态类型
477
     * @return MorphMany
478
     */
479
    public function morphMany(string $model, $morph = null, string $type = ''): MorphMany
480
    {
481
        // 记录当前关联信息
482
        $model = $this->parseModel($model);
483
484
        if (is_null($morph)) {
485
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
486
            $morph = App::parseName($trace[1]['function']);
487
        }
488
489
        $type = $type ?: get_class($this);
490
491
        if (is_array($morph)) {
492
            list($morphType, $foreignKey) = $morph;
493
        } else {
494
            $morphType  = $morph . '_type';
495
            $foreignKey = $morph . '_id';
496
        }
497
498
        return new MorphMany($this, $model, $foreignKey, $morphType, $type);
499
    }
500
501
    /**
502
     * MORPH TO 关联定义
503
     * @access public
504
     * @param  string|array $morph 多态字段信息
505
     * @param  array        $alias 多态别名定义
506
     * @return MorphTo
507
     */
508
    public function morphTo($morph = null, array $alias = []): MorphTo
509
    {
510
        $trace    = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
511
        $relation = App::parseName($trace[1]['function']);
512
513
        if (is_null($morph)) {
514
            $morph = $relation;
515
        }
516
517
        // 记录当前关联信息
518
        if (is_array($morph)) {
519
            list($morphType, $foreignKey) = $morph;
520
        } else {
521
            $morphType  = $morph . '_type';
522
            $foreignKey = $morph . '_id';
523
        }
524
525
        return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
526
    }
527
528
    /**
529
     * 解析模型的完整命名空间
530
     * @access protected
531
     * @param  string $model 模型名(或者完整类名)
532
     * @return string
533
     */
534
    protected function parseModel(string $model): string
535
    {
536
        if (false === strpos($model, '\\')) {
537
            $path = explode('\\', static::class);
538
            array_pop($path);
539
            array_push($path, App::parseName($model, 1));
540
            $model = implode('\\', $path);
541
        }
542
543
        return $model;
544
    }
545
546
    /**
547
     * 获取模型的默认外键名
548
     * @access protected
549
     * @param  string $name 模型名
550
     * @return string
551
     */
552
    protected function getForeignKey(string $name): string
553
    {
554
        if (strpos($name, '\\')) {
555
            $name = App::classBaseName($name);
556
        }
557
558
        return App::parseName($name) . '_id';
559
    }
560
561
    /**
562
     * 检查属性是否为关联属性 如果是则返回关联方法名
563
     * @access protected
564
     * @param  string $attr 关联属性名
565
     * @return string|false
566
     */
567
    protected function isRelationAttr(string $attr)
568
    {
569
        $relation = App::parseName($attr, 1, false);
570
571
        if (method_exists($this, $relation) && !method_exists('think\Model', $relation)) {
572
            return $relation;
573
        }
574
575
        return false;
576
    }
577
578
    /**
579
     * 智能获取关联模型数据
580
     * @access protected
581
     * @param  Relation  $modelRelation 模型关联对象
582
     * @return mixed
583
     */
584
    protected function getRelationData(Relation $modelRelation)
585
    {
586
        if ($this->parent && !$modelRelation->isSelfRelation() && get_class($this->parent) == get_class($modelRelation->getModel())) {
587
            return $this->parent;
588
        }
589
590
        // 获取关联数据
591
        return $modelRelation->getRelation();
0 ignored issues
show
Bug introduced by
The method getRelation() does not exist on think\model\Relation. 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

591
        return $modelRelation->/** @scrutinizer ignore-call */ getRelation();
Loading history...
592
    }
593
594
    /**
595
     * 关联数据自动写入检查
596
     * @access protected
597
     * @return void
598
     */
599
    protected function checkAutoRelationWrite(): void
600
    {
601
        foreach ($this->together as $key => $name) {
602
            if (is_array($name)) {
603
                if (key($name) === 0) {
604
                    $this->relationWrite[$key] = [];
605
                    // 绑定关联属性
606
                    foreach ($name as $val) {
607
                        if (isset($this->data[$val])) {
608
                            $this->relationWrite[$key][$val] = $this->data[$val];
609
                        }
610
                    }
611
                } else {
612
                    // 直接传入关联数据
613
                    $this->relationWrite[$key] = $name;
614
                }
615
            } elseif (isset($this->relation[$name])) {
616
                $this->relationWrite[$name] = $this->relation[$name];
617
            } elseif (isset($this->data[$name])) {
618
                $this->relationWrite[$name] = $this->data[$name];
619
                unset($this->data[$name]);
620
            }
621
        }
622
    }
623
624
    /**
625
     * 自动关联数据更新(针对一对一关联)
626
     * @access protected
627
     * @return void
628
     */
629
    protected function autoRelationUpdate(): void
630
    {
631
        foreach ($this->relationWrite as $name => $val) {
632
            if ($val instanceof Model) {
633
                $val->isUpdate()->save();
634
            } else {
635
                $model = $this->getRelation($name);
636
637
                if ($model instanceof Model) {
638
                    $model->isUpdate()->save($val);
639
                }
640
            }
641
        }
642
    }
643
644
    /**
645
     * 自动关联数据写入(针对一对一关联)
646
     * @access protected
647
     * @return void
648
     */
649
    protected function autoRelationInsert(): void
650
    {
651
        foreach ($this->relationWrite as $name => $val) {
652
            $method = App::parseName($name, 1, false);
653
            $this->$method()->save($val);
654
        }
655
    }
656
657
    /**
658
     * 自动关联数据删除(支持一对一及一对多关联)
659
     * @access protected
660
     * @return void
661
     */
662
    protected function autoRelationDelete(): void
663
    {
664
        foreach ($this->relationWrite as $key => $name) {
665
            $name   = is_numeric($key) ? $name : $key;
666
            $result = $this->getRelation($name);
667
668
            if ($result instanceof Model) {
669
                $result->delete();
670
            } elseif ($result instanceof Collection) {
671
                foreach ($result as $model) {
672
                    $model->delete();
673
                }
674
            }
675
        }
676
    }
677
}
678