Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jarektkaczyk / eloquence / 204

2 Jul 2017 - 19:12 coverage: 91.815% (-0.2%) from 92.012%
204

Pull #185

travis-ci

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
fix: missing/wrong PHPDoc
Pull Request #185: fix: missing/wrong PHPDoc

931 of 1014 relevant lines covered (91.81%)

14.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.17
/src/Mappable.php
1
<?php
2

3
namespace Sofa\Eloquence;
4

5
use LogicException;
6
use Illuminate\Support\Arr;
7
use Sofa\Eloquence\Mappable\Hooks;
8
use Sofa\Hookable\Contracts\ArgumentBag;
9
use Illuminate\Contracts\Support\Arrayable;
10
use Illuminate\Database\Eloquent\Relations\HasOne;
11
use Illuminate\Database\Eloquent\Relations\MorphTo;
12
use Illuminate\Database\Eloquent\Relations\MorphOne;
13
use Illuminate\Database\Eloquent\Relations\Relation;
14
use Illuminate\Database\Eloquent\Relations\BelongsTo;
15
use Illuminate\Database\Eloquent\Model as EloquentModel;
16

17
/**
18
 * @property array $maps
19
 */
20
trait Mappable
21
{
22
    /**
23
     * Flat array representation of mapped attributes.
24
     *
25
     * @var array
26
     */
27
    protected $mappedAttributes;
28

29
    /**
30
     * Related mapped objects to save along with the mappable instance.
31
     *
32
     * @var array
33
     */
34
    protected $targetsToSave = [];
35

36
    /**
37
     * Register hooks for the trait.
38
     *
39
     * @codeCoverageIgnore
40
     *
41
     * @return void
42
     */
43
    public static function bootMappable()
44
    {
45
        $hooks = new Hooks;
46

47
        foreach ([
48
                'getAttribute',
49
                'setAttribute',
50
                'save',
51
                '__isset',
52
                '__unset',
53
                'queryHook',
54
            ] as $method) {
55
            static::hook($method, $hooks->{$method}());
56
        }
57
    }
58

59
    /**
60
     * Custom query handler for querying mapped attributes.
61
     *
62
     * @param  \Sofa\Eloquence\Builder $query
63
     * @param  string $method
64
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
65
     * @return mixed
66
     */
67
    protected function mappedQuery(Builder $query, $method, ArgumentBag $args)
68
    {
69
        $mapping = $this->getMappingForAttribute($args->get('column'));
44×
70

71
        if ($this->relationMapping($mapping)) {
44×
72
            return $this->mappedRelationQuery($query, $method, $args, $mapping);
40×
73
        }
74

75
        $args->set('column', $mapping);
6×
76

77
        return $query->callParent($method, $args->all());
6×
78
    }
79

80
    /**
81
     * Adjust mapped columns for select statement.
82
     *
83
     * @param  \Sofa\Eloquence\Builder $query
84
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
85
     * @return void
86
     */
87
    protected function mappedSelect(Builder $query, ArgumentBag $args)
88
    {
89
        $columns = $args->get('columns');
30×
90

91
        foreach ($columns as $key => $column) {
30×
92
            list($column, $as) = $this->extractColumnAlias($column);
30×
93

94
            // Each mapped column will be selected appropriately. If it's alias
95
            // then prefix it with current table and use original field name
96
            // otherwise join required mapped tables and select the field.
97
            if ($this->hasMapping($column)) {
30×
98
                $mapping = $this->getMappingForAttribute($column);
4×
99

100
                if ($this->relationMapping($mapping)) {
4×
101
                    list($target, $mapped) = $this->parseMappedColumn($mapping);
2×
102

103
                    $table = $this->joinMapped($query, $target);
2×
104
                } else {
only 204.1 - 1×
105
                    list($table, $mapped) = [$this->getTable(), $mapping];
4×
106
                }
107

108
                $columns[$key] = "{$table}.{$mapped}";
4×
109

110
                if ($as !== $column) {
4×
UNCOV
111
                    $columns[$key] .= " as {$as}";
!
112
                }
!
113

114
            // For non mapped columns present on this table we will simply
115
            // add the prefix, in order to avoid any column collisions,
116
            // that are likely to happen when we are joining tables.
117
            } elseif ($this->hasColumn($column)) {
30×
118
                $columns[$key] = "{$this->getTable()}.{$column}";
2×
119
            }
only 204.1 - 1×
120
        }
only 204.1 - 15×
121

122
        $args->set('columns', $columns);
30×
123
    }
30×
124

125
    /**
126
     * Handle querying relational mappings.
127
     *
128
     * @param  \Sofa\Eloquence\Builder $query
129
     * @param  string $method
130
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
131
     * @param  string $mapping
132
     * @return mixed
133
     */
134
    protected function mappedRelationQuery($query, $method, ArgumentBag $args, $mapping)
135
    {
136
        list($target, $column) = $this->parseMappedColumn($mapping);
40×
137

138
        if (in_array($method, ['pluck', 'value', 'aggregate', 'orderBy', 'lists'])) {
40×
139
            return $this->mappedJoinQuery($query, $method, $args, $target, $column);
26×
140
        }
141

142
        return $this->mappedHasQuery($query, $method, $args, $target, $column);
14×
143
    }
144

145
    /**
146
     * Join mapped table(s) in order to call given method.
147
     *
148
     * @param  \Sofa\Eloquence\Builder $query
149
     * @param  string $method
150
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
151
     * @param  string $target
152
     * @param  string $column
153
     * @return mixed
154
     */
155
    protected function mappedJoinQuery($query, $method, ArgumentBag $args, $target, $column)
156
    {
157
        $table = $this->joinMapped($query, $target);
26×
158

159
        // For aggregates we need the actual function name
160
        // so it can be called directly on the builder.
161
        $method = $args->get('function') ?: $method;
24×
162

163
        return (in_array($method, ['orderBy', 'lists', 'pluck']))
24×
164
            ? $this->{"{$method}Mapped"}($query, $args, $table, $column, $target)
17×
165
            : $this->mappedSingleResult($query, $method, "{$table}.{$column}");
24×
166
    }
167

168
    /**
169
     * Order query by mapped attribute.
170
     *
171
     * @param  \Sofa\Eloquence\Builder $query
172
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
173
     * @param  string $table
174
     * @param  string $column
175
     * @param  string $target
176
     * @return \Sofa\Eloquence\Builder
177
     */
178
    protected function orderByMapped(Builder $query, ArgumentBag $args, $table, $column, $target)
179
    {
180
        $query->with($target)->getQuery()->orderBy("{$table}.{$column}", $args->get('direction'));
6×
181

182
        return $query;
6×
183
    }
184

185
  /**
186
   * @param  \Sofa\Eloquence\Builder $query
187
   * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
188
   * @param  string $table
189
   * @param  string $column
190
   *
191
   * @return array
192
   */
193
    protected function listsMapped(Builder $query, ArgumentBag $args, $table, $column)
194
    {
195
        return $this->pluckMapped($query, $args, $table, $column);
!
196
    }
197

198
    /**
199
     * Get an array with the values of given mapped attribute.
200
     *
201
     * @param  \Sofa\Eloquence\Builder $query
202
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
203
     * @param  string $table
204
     * @param  string $column
205
     * @return array
206
     */
207
    protected function pluckMapped(Builder $query, ArgumentBag $args, $table, $column)
208
    {
209
        $query->select("{$table}.{$column}");
4×
210

211
        if (!is_null($args->get('key'))) {
4×
212
            $this->mappedSelectListsKey($query, $args->get('key'));
4×
213
        }
only 204.1 - 2×
214

215
        $args->set('column', $column);
4×
216

217
        return $query->callParent('pluck', $args->all());
4×
218
    }
219

220
    /**
221
     * Add select clause for key of the list array.
222
     *
223
     * @param  \Sofa\Eloquence\Builder $query
224
     * @param  string $key
225
     * @return \Sofa\Eloquence\Builder
226
     */
227
    protected function mappedSelectListsKey(Builder $query, $key)
228
    {
229
        if ($this->hasColumn($key)) {
4×
230
            return $query->addSelect($this->getTable() . '.' . $key);
2×
231
        }
232

233
        return $query->addSelect($key);
2×
234
    }
235

236
    /**
237
     * Join mapped table(s).
238
     *
239
     * @param  \Sofa\Eloquence\Builder $query
240
     * @param  string $target
241
     * @return string
242
     */
243
    protected function joinMapped(Builder $query, $target)
244
    {
245
        $query->prefixColumnsForJoin();
28×
246

247
        $parent = $this;
28×
248

249
        foreach (explode('.', $target) as $segment) {
28×
250
            list($table, $parent) = $this->joinSegment($query, $segment, $parent);
28×
251
        }
only 204.1 - 13×
252

253
        return $table;
26×
254
    }
255

256
    /**
257
     * Join relation's table accordingly.
258
     *
259
     * @param  \Sofa\Eloquence\Builder $query
260
     * @param  string $segment
261
     * @param  \Illuminate\Database\Eloquent\Model $parent
262
     * @return array
263
     */
264
    protected function joinSegment(Builder $query, $segment, EloquentModel $parent)
265
    {
266
        $relation = $parent->{$segment}();
28×
267
        $related  = $relation->getRelated();
28×
268
        $table    = $related->getTable();
28×
269

270
        // If the table has been already joined let's skip it. Otherwise we will left join
271
        // it in order to allow using some query methods on mapped columns. Polymorphic
272
        // relations require also additional constraints, so let's handle it as well.
273
        if (!$this->alreadyJoined($query, $table)) {
28×
274
            list($fk, $pk) = $this->getJoinKeys($relation);
28×
275

276
            $query->leftJoin($table, function ($join) use ($fk, $pk, $relation, $parent, $related) {
277
                $join->on($fk, '=', $pk);
26×
278

279
                if ($relation instanceof MorphOne || $relation instanceof MorphTo) {
26×
280
                    $morphClass = ($relation instanceof MorphOne)
2×
281
                        ? $parent->getMorphClass()
2×
282
                        : $related->getMorphClass();
2×
283

284
                    $join->where($relation->getQualifiedMorphType(), '=', $morphClass);
2×
285
                }
only 204.1 - 1×
286
            });
26×
287
        }
only 204.1 - 13×
288

289
        return [$table, $related];
26×
290
    }
291

292
    /**
293
     * Determine whether given table has been already joined.
294
     *
295
     * @param  \Sofa\Eloquence\Builder $query
296
     * @param  string  $table
297
     * @return boolean
298
     */
299
    protected function alreadyJoined(Builder $query, $table)
300
    {
301
        $joined = Arr::pluck((array) $query->getQuery()->joins, 'table');
28×
302

303
        return in_array($table, $joined);
28×
304
    }
305

306
    /**
307
     * Get the keys from relation in order to join the table.
308
     *
309
     * @param  \Illuminate\Database\Eloquent\Relations\Relation $relation
310
     * @return array
311
     *
312
     * @throws \LogicException
313
     */
314
    protected function getJoinKeys(Relation $relation)
315
    {
316
        if ($relation instanceof HasOne || $relation instanceof MorphOne) {
28×
317
            return [$relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName()];
18×
318
        }
319

320
        if ($relation instanceof BelongsTo && !$relation instanceof MorphTo) {
24×
321
            return [$relation->getQualifiedForeignKey(), $relation->getQualifiedOwnerKeyName()];
22×
322
        }
323

324
        $class = get_class($relation);
2×
325

326
        throw new LogicException(
2×
327
            "Only HasOne, MorphOne and BelongsTo mappings can be queried. {$class} given."
2×
328
        );
only 204.1 - 1×
329
    }
330

331
    /**
332
     * Get single value result from the mapped attribute.
333
     *
334
     * @param  \Sofa\Eloquence\Builder $query
335
     * @param  string $method
336
     * @param  string $qualifiedColumn
337
     * @return mixed
338
     */
339
    protected function mappedSingleResult(Builder $query, $method, $qualifiedColumn)
340
    {
341
        return $query->getQuery()->select("{$qualifiedColumn}")->{$method}("{$qualifiedColumn}");
14×
342
    }
343

344
    /**
345
     * Add whereHas subquery on the mapped attribute relation.
346
     *
347
     * @param  \Sofa\Eloquence\Builder $query
348
     * @param  string $method
349
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
350
     * @param  string $target
351
     * @param  string $column
352
     * @return \Sofa\Eloquence\Builder
353
     */
354
    protected function mappedHasQuery(Builder $query, $method, ArgumentBag $args, $target, $column)
355
    {
356
        $boolean = $this->getMappedBoolean($args);
14×
357

358
        $operator = $this->getMappedOperator($method, $args);
14×
359

360
        $args->set('column', $column);
14×
361

362
        return $query
363
            ->has($target, $operator, 1, $boolean, $this->getMappedWhereConstraint($method, $args))
14×
364
            ->with($target);
14×
365
    }
366

367
    /**
368
     * Get the relation constraint closure.
369
     *
370
     * @param  string $method
371
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
372
     * @return \Closure
373
     */
374
    protected function getMappedWhereConstraint($method, ArgumentBag $args)
375
    {
376
        return function ($query) use ($method, $args) {
377
            call_user_func_array([$query, $method], $args->all());
14×
378
        };
14×
379
    }
380

381
    /**
382
     * Get boolean called on the original method and set it to default.
383
     *
384
     * @param  \Sofa\EloquenceArgumentBag $args
385
     * @return string
386
     */
387
    protected function getMappedBoolean(ArgumentBag $args)
388
    {
389
        $boolean = $args->get('boolean');
14×
390

391
        $args->set('boolean', 'and');
14×
392

393
        return $boolean;
14×
394
    }
395

396
    /**
397
     * Determine the operator for count relation query and set 'not' appropriately.
398
     *
399
     * @param  string $method
400
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
401
     * @return string
402
     */
403
    protected function getMappedOperator($method, ArgumentBag $args)
404
    {
405
        if ($not = $args->get('not')) {
14×
406
            $args->set('not', false);
6×
407
        }
only 204.1 - 3×
408

409
        if ($null = $this->isWhereNull($method, $args)) {
14×
410
            $args->set('not', true);
4×
411
        }
only 204.1 - 2×
412

413
        return ($not ^ $null) ? '<' : '>=';
14×
414
    }
415

416
    /**
417
     * Get the mapping key.
418
     *
419
     * @param  string $key
420
     * @return string|null
421
     */
422
    public function getMappingForAttribute($key)
423
    {
424
        if ($this->hasMapping($key)) {
50×
425
            return $this->mappedAttributes[$key];
50×
426
        }
427
    }
2×
428

429
    /**
430
     * Determine whether the mapping points to relation.
431
     *
432
     * @param  string $mapping
433
     * @return boolean
434
     */
435
    protected function relationMapping($mapping)
436
    {
437
        return strpos($mapping, '.') !== false;
46×
438
    }
439

440
    /**
441
     * Determine whether a mapping exists for an attribute.
442
     *
443
     * @param  string $key
444
     * @return boolean
445
     */
446
    public function hasMapping($key)
447
    {
448
        if (is_null($this->mappedAttributes)) {
50×
449
            $this->parseMappings();
50×
450
        }
only 204.1 - 25×
451

452
        return array_key_exists((string) $key, $this->mappedAttributes);
50×
453
    }
454

455
    /**
456
     * Parse defined mappings into flat array.
457
     *
458
     * @return void
459
     */
460
    protected function parseMappings()
461
    {
462
        $this->mappedAttributes = [];
46×
463

464
        foreach ($this->getMaps() as $attribute => $mapping) {
46×
465
            if (is_array($mapping)) {
46×
466
                $this->parseImplicitMapping($mapping, $attribute);
44×
467
            } else {
only 204.1 - 22×
468
                $this->mappedAttributes[$attribute] = $mapping;
46×
469
            }
470
        }
only 204.1 - 23×
471
    }
46×
472

473
    /**
474
     * Parse implicit mappings.
475
     *
476
     * @param  array  $attributes
477
     * @param  string $target
478
     * @return void
479
     */
480
    protected function parseImplicitMapping($attributes, $target)
481
    {
482
        foreach ($attributes as $attribute) {
44×
483
            $this->mappedAttributes[$attribute] = "{$target}.{$attribute}";
44×
484
        }
only 204.1 - 22×
485
    }
44×
486

487
    /**
488
     * Map an attribute to a value.
489
     *
490
     * @param  string $key
491
     * @return mixed
492
     */
493
    protected function mapAttribute($key)
494
    {
495
        $segments = explode('.', $this->getMappingForAttribute($key));
2×
496

497
        return $this->getTarget($this, $segments);
2×
498
    }
499

500
    /**
501
     * Get mapped value.
502
     *
503
     * @param  \Illuminate\Database\Eloquent\Model $target
504
     * @param  array  $segments
505
     * @return mixed
506
     */
507
    protected function getTarget($target, array $segments)
508
    {
509
        foreach ($segments as $segment) {
4×
510
            if (!$target) {
4×
511
                return;
2×
512
            }
513

514
            $target = $target->{$segment};
4×
515
        }
only 204.1 - 2×
516

517
        return $target;
4×
518
    }
519

520
    /**
521
     * Set value of a mapped attribute.
522
     *
523
     * @param string $key
524
     * @param mixed  $value
525
     */
526
    protected function setMappedAttribute($key, $value)
527
    {
528
        $segments = explode('.', $this->getMappingForAttribute($key));
2×
529

530
        $attribute = array_pop($segments);
2×
531

532
        if ($target = $this->getTarget($this, $segments)) {
2×
533
            $this->addTargetToSave($target);
2×
534

535
            $target->{$attribute} = $value;
2×
536
        }
only 204.1 - 1×
537
    }
2×
538

539
    /**
540
     * Flag mapped model to be saved along with this model.
541
     *
542
     * @param \Illuminate\Database\Eloquent\Model $target
543
     */
544
    protected function addTargetToSave($target)
545
    {
546
        if ($this !== $target) {
!
547
            $this->targetsToSave[] = $target;
!
548
        }
!
549
    }
!
550

551
    /**
552
     * Save mapped relations.
553
     *
554
     * @return void
555
     */
556
    protected function saveMapped()
557
    {
558
        foreach (array_unique($this->targetsToSave) as $target) {
2×
559
            $target->save();
2×
560
        }
only 204.1 - 1×
561

562
        $this->targetsToSave = [];
2×
563
    }
2×
564

565
    /**
566
     * Unset mapped attribute.
567
     *
568
     * @param  string $key
569
     * @return void
570
     */
571
    protected function forget($key)
572
    {
573
        $mapping = $this->getMappingForAttribute($key);
2×
574

575
        list($target, $attribute) = $this->parseMappedColumn($mapping);
2×
576

577
        $target = $target ? $this->getTarget($this, explode('.', $target)) : $this;
2×
578

579
        unset($target->{$attribute});
2×
580
    }
2×
581

582
    /**
583
     * @codeCoverageIgnore
584
     *
585
     * @param  string  $key
586
     * @param  mixed  $value
587
     *
588
     * @inheritdoc
589
     */
590
    protected function mutateAttributeForArray($key, $value)
591
    {
592
        if ($this->hasMapping($key)) {
593
            $value = $this->mapAttribute($key);
594

595
            return $value instanceof Arrayable ? $value->toArray() : $value;
596
        }
597

598
        return parent::mutateAttributeForArray($key, $value);
599
    }
600

601
    /**
602
     * Get the array of attribute mappings.
603
     *
604
     * @return array
605
     */
606
    public function getMaps()
607
    {
608
        return (property_exists($this, 'maps')) ? $this->maps : [];
48×
609
    }
610
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc