Completed
Push — 5.1 ( c7eca6...1f8916 )
by Jarek
05:49
created

Mappable   C

Complexity

Total Complexity 68

Size/Duplication

Total Lines 576
Duplicated Lines 7.12 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 18
Bugs 1 Features 2
Metric Value
c 18
b 1
f 2
dl 41
loc 576
rs 5.6756
wmc 68
lcom 1
cbo 8

30 Methods

Rating   Name   Duplication   Size   Complexity  
A mappedQuery() 0 12 2
A bootMappable() 16 16 2
B mappedSelect() 0 37 6
A mappedRelationQuery() 3 10 2
A mappedJoinQuery() 0 12 3
A orderByMapped() 0 6 1
A listsMapped() 0 12 2
A mappedSelectListsKey() 0 8 2
A joinMapped() 0 12 2
B joinSegment() 0 27 5
A alreadyJoined() 0 6 1
B getJoinKeys() 0 16 5
A mappedSingleResult() 0 4 1
A mappedHasQuery() 0 12 1
A getMappedWhereConstraint() 0 6 1
A getMappedBoolean() 0 8 1
A getMappedOperator() 0 12 4
A getMappingForAttribute() 0 6 2
A relationMapping() 0 4 1
A hasMapping() 0 8 2
A parseMappings() 0 12 3
A parseImplicitMapping() 0 6 2
A mapAttribute() 0 6 1
A getTarget() 0 12 3
A setMappedAttribute() 12 12 2
A addTargetToSave() 0 6 2
A saveMapped() 0 8 2
A forget() 10 10 2
A mutateAttributeForArray() 0 10 3
A getMaps() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Mappable 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Mappable, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Sofa\Eloquence;
4
5
use LogicException;
6
use Sofa\Eloquence\Mappable\Hooks;
7
use Sofa\Hookable\Contracts\ArgumentBag;
8
use Illuminate\Contracts\Support\Arrayable;
9
use Illuminate\Database\Eloquent\Relations\HasOne;
10
use Illuminate\Database\Eloquent\Relations\MorphTo;
11
use Illuminate\Database\Eloquent\Relations\MorphOne;
12
use Illuminate\Database\Eloquent\Relations\Relation;
13
use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
use Illuminate\Database\Eloquent\Model as EloquentModel;
15
16
/**
17
 * @property array $maps
18
 */
19
trait Mappable
20
{
21
    /**
22
     * Flat array representation of mapped attributes.
23
     *
24
     * @var array
25
     */
26
    protected static $mappedAttributes;
27
28
    /**
29
     * Related mapped objects to save along with the mappable instance.
30
     *
31
     * @var array
32
     */
33
    protected $targetsToSave = [];
34
35
    /**
36
     * Register hooks for the trait.
37
     *
38
     * @codeCoverageIgnore
39
     *
40
     * @return void
41
     */
42 View Code Duplication
    public static function bootMappable()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
43
    {
44
        $hooks = new Hooks;
45
46
        foreach ([
47
                'getAttribute',
48
                'setAttribute',
49
                'save',
50
                '__isset',
51
                '__unset',
52
                'queryHook',
53
                'isDirty',
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'));
70
71
        if ($this->relationMapping($mapping)) {
72
            return $this->mappedRelationQuery($query, $method, $args, $mapping);
73
        }
74
75
        $args->set('column', $mapping);
76
77
        return $query->callParent($method, $args->all());
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');
90
91
        foreach ($columns as $key => $column) {
92
            list($column, $as) = $this->extractColumnAlias($column);
0 ignored issues
show
Bug introduced by
It seems like extractColumnAlias() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
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)) {
98
                $mapping = $this->getMappingForAttribute($column);
99
100
                if ($this->relationMapping($mapping)) {
101
                    list($target, $mapped) = $this->parseMappedColumn($mapping);
0 ignored issues
show
Bug introduced by
It seems like parseMappedColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
102
103
                    $table = $this->joinMapped($query, $target);
104
                } else {
105
                    list($table, $mapped) = [$this->getTable(), $mapping];
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
106
                }
107
108
                $columns[$key] = "{$table}.{$mapped}";
109
110
                if ($as !== $mapped) {
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)) {
0 ignored issues
show
Bug introduced by
It seems like hasColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
118
                $columns[$key] = "{$this->getTable()}.{$column}";
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
119
            }
120
        }
121
122
        $args->set('columns', $columns);
123
    }
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);
0 ignored issues
show
Bug introduced by
It seems like parseMappedColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
137
138 View Code Duplication
        if (in_array($method, ['pluck', 'value', 'aggregate', 'orderBy', 'lists'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
139
            return $this->mappedJoinQuery($query, $method, $args, $target, $column);
140
        }
141
142
        return $this->mappedHasQuery($query, $method, $args, $target, $column);
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);
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;
162
163
        return (in_array($method, ['orderBy', 'lists']))
164
            ? $this->{"{$method}Mapped"}($query, $args, $table, $column, $target)
165
            : $this->mappedSingleResult($query, $method, "{$table}.{$column}");
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'));
181
182
        return $query;
183
    }
184
185
    /**
186
     * Get an array with the values of given mapped attribute.
187
     *
188
     * @param  \Sofa\Eloquence\Builder $query
189
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
190
     * @param  string $table
191
     * @param  string $column
192
     * @return array
193
     */
194
    protected function listsMapped(Builder $query, ArgumentBag $args, $table, $column)
195
    {
196
        $query->select("{$table}.{$column}");
197
198
        if (!is_null($args->get('key'))) {
199
            $this->mappedSelectListsKey($query, $args->get('key'));
200
        }
201
202
        $args->set('column', $column);
203
204
        return $query->callParent('lists', $args->all());
205
    }
206
207
    /**
208
     * Add select clause for key of the list array.
209
     *
210
     * @param  \Sofa\Eloquence\Builder $query
211
     * @param  string $key
212
     * @return \Sofa\Eloquence\Builder
213
     */
214
    protected function mappedSelectListsKey(Builder $query, $key)
215
    {
216
        if ($this->hasColumn($key)) {
0 ignored issues
show
Bug introduced by
It seems like hasColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
217
            return $query->addSelect($this->getTable() . '.' . $key);
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
The method addSelect() does not exist on Sofa\Eloquence\Builder. Did you maybe mean select()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
218
        }
219
220
        return $query->addSelect($key);
0 ignored issues
show
Bug introduced by
The method addSelect() does not exist on Sofa\Eloquence\Builder. Did you maybe mean select()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
221
    }
222
223
    /**
224
     * Join mapped table(s).
225
     *
226
     * @param  \Sofa\Eloquence\Builder $query
227
     * @param  string $target
228
     * @return string
229
     */
230
    protected function joinMapped(Builder $query, $target)
231
    {
232
        $query->prefixColumnsForJoin();
233
234
        $parent = $this;
235
236
        foreach (explode('.', $target) as $segment) {
237
            list($table, $parent) = $this->joinSegment($query, $segment, $parent);
238
        }
239
240
        return $table;
0 ignored issues
show
Bug introduced by
The variable $table does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
241
    }
242
243
    /**
244
     * Join relation's table accordingly.
245
     *
246
     * @param  \Sofa\Eloquence\Builder $query
247
     * @param  string $segment
248
     * @param  \Illuminate\Database\Eloquent\Model $parent
249
     * @return array
250
     */
251
    protected function joinSegment(Builder $query, $segment, EloquentModel $parent)
252
    {
253
        $relation = $parent->{$segment}();
254
        $related  = $relation->getRelated();
255
        $table    = $related->getTable();
256
257
        // If the table has been already joined let's skip it. Otherwise we will left join
258
        // it in order to allow using some query methods on mapped columns. Polymorphic
259
        // relations require also additional constraints, so let's handle it as well.
260
        if (!$this->alreadyJoined($query, $table)) {
261
            list($fk, $pk) = $this->getJoinKeys($relation);
262
263
            $query->leftJoin($table, function ($join) use ($fk, $pk, $relation, $parent, $related) {
0 ignored issues
show
Bug introduced by
The call to leftJoin() misses some required arguments starting with $operator.
Loading history...
264
                $join->on($fk, '=', $pk);
265
266
                if ($relation instanceof MorphOne || $relation instanceof MorphTo) {
267
                    $morphClass = ($relation instanceof MorphOne)
268
                        ? $parent->getMorphClass()
269
                        : $related->getMorphClass();
270
271
                    $join->where($relation->getMorphType(), '=', $morphClass);
272
                }
273
            });
274
        }
275
276
        return [$table, $related];
277
    }
278
279
    /**
280
     * Determine whether given table has been already joined.
281
     *
282
     * @param  \Sofa\Eloquence\Builder $query
283
     * @param  string  $table
284
     * @return boolean
285
     */
286
    protected function alreadyJoined(Builder $query, $table)
287
    {
288
        $joined = array_fetch((array) $query->getQuery()->joins, 'table');
0 ignored issues
show
Deprecated Code introduced by
The function array_fetch() has been deprecated with message: since version 5.1. Use array_pluck instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
289
290
        return in_array($table, $joined);
291
    }
292
293
    /**
294
     * Get the keys from relation in order to join the table.
295
     *
296
     * @param  \Illuminate\Database\Eloquent\Relations\Relation $relation
297
     * @return array
298
     *
299
     * @throws \LogicException
300
     */
301
    protected function getJoinKeys(Relation $relation)
302
    {
303
        if ($relation instanceof HasOne || $relation instanceof MorphOne) {
304
            return [$relation->getForeignKey(), $relation->getQualifiedParentKeyName()];
305
        }
306
307
        if ($relation instanceof BelongsTo && !$relation instanceof MorphTo) {
308
            return [$relation->getQualifiedForeignKey(), $relation->getQualifiedOtherKeyName()];
309
        }
310
311
        $class = get_class($relation);
312
313
        throw new LogicException(
314
            "Only HasOne, MorphOne and BelongsTo mappings can be queried. {$class} given."
315
        );
316
    }
317
318
    /**
319
     * Get single value result from the mapped attribute.
320
     *
321
     * @param  \Sofa\Eloquence\Builder $query
322
     * @param  string $method
323
     * @param  string $qualifiedColumn
324
     * @return mixed
325
     */
326
    protected function mappedSingleResult(Builder $query, $method, $qualifiedColumn)
327
    {
328
        return $query->getQuery()->select("{$qualifiedColumn}")->{$method}("{$qualifiedColumn}");
329
    }
330
331
    /**
332
     * Add whereHas subquery on the mapped attribute relation.
333
     *
334
     * @param  \Sofa\Eloquence\Builder $query
335
     * @param  string $method
336
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
337
     * @param  string $target
338
     * @param  string $column
339
     * @return \Sofa\Eloquence\Builder
340
     */
341
    protected function mappedHasQuery(Builder $query, $method, ArgumentBag $args, $target, $column)
342
    {
343
        $boolean = $this->getMappedBoolean($args);
344
345
        $operator = $this->getMappedOperator($method, $args);
346
347
        $args->set('column', $column);
348
349
        return $query
350
            ->has($target, $operator, 1, $boolean, $this->getMappedWhereConstraint($method, $args))
351
            ->with($target);
352
    }
353
354
    /**
355
     * Get the relation constraint closure.
356
     *
357
     * @param  string $method
358
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
359
     * @return \Closure
360
     */
361
    protected function getMappedWhereConstraint($method, ArgumentBag $args)
362
    {
363
        return function ($query) use ($method, $args) {
364
            call_user_func_array([$query, $method], $args->all());
365
        };
366
    }
367
368
    /**
369
     * Get boolean called on the original method and set it to default.
370
     *
371
     * @param  \Sofa\EloquenceArgumentBag $args
372
     * @return string
373
     */
374
    protected function getMappedBoolean(ArgumentBag $args)
375
    {
376
        $boolean = $args->get('boolean');
377
378
        $args->set('boolean', 'and');
379
380
        return $boolean;
381
    }
382
383
    /**
384
     * Determine the operator for count relation query and set 'not' appropriately.
385
     *
386
     * @param  string $method
387
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
388
     * @return string
389
     */
390
    protected function getMappedOperator($method, ArgumentBag $args)
391
    {
392
        if ($not = $args->get('not')) {
393
            $args->set('not', false);
394
        }
395
396
        if ($null = $this->isWhereNull($method, $args)) {
0 ignored issues
show
Bug introduced by
It seems like isWhereNull() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
397
            $args->set('not', true);
398
        }
399
400
        return ($not ^ $null) ? '<' : '>=';
401
    }
402
403
    /**
404
     * Get the mapping key.
405
     *
406
     * @param  string $key
407
     * @return string|null
408
     */
409
    public function getMappingForAttribute($key)
410
    {
411
        if ($this->hasMapping($key)) {
412
            return static::$mappedAttributes[$key];
413
        }
414
    }
415
416
    /**
417
     * Determine whether the mapping points to relation.
418
     *
419
     * @param  string $mapping
420
     * @return boolean
421
     */
422
    protected function relationMapping($mapping)
423
    {
424
        return strpos($mapping, '.') !== false;
425
    }
426
427
    /**
428
     * Determine whether a mapping exists for an attribute.
429
     *
430
     * @param  string $key
431
     * @return boolean
432
     */
433
    public function hasMapping($key)
434
    {
435
        if (is_null(static::$mappedAttributes)) {
436
            $this->parseMappings();
437
        }
438
439
        return array_key_exists((string) $key, static::$mappedAttributes);
440
    }
441
442
    /**
443
     * Parse defined mappings into flat array.
444
     *
445
     * @return void
446
     */
447
    protected function parseMappings()
448
    {
449
        static::$mappedAttributes = [];
450
451
        foreach ($this->getMaps() as $attribute => $mapping) {
452
            if (is_array($mapping)) {
453
                $this->parseImplicitMapping($mapping, $attribute);
454
            } else {
455
                static::$mappedAttributes[$attribute] = $mapping;
456
            }
457
        }
458
    }
459
460
    /**
461
     * Parse implicit mappings.
462
     *
463
     * @param  array  $attributes
464
     * @param  string $target
465
     * @return void
466
     */
467
    protected function parseImplicitMapping($attributes, $target)
468
    {
469
        foreach ($attributes as $attribute) {
470
            static::$mappedAttributes[$attribute] = "{$target}.{$attribute}";
471
        }
472
    }
473
474
    /**
475
     * Map an attribute to a value.
476
     *
477
     * @param  string $key
478
     * @return mixed
479
     */
480
    protected function mapAttribute($key)
481
    {
482
        $segments = explode('.', $this->getMappingForAttribute($key));
483
484
        return $this->getTarget($this, $segments);
485
    }
486
487
    /**
488
     * Get mapped value.
489
     *
490
     * @param  \Illuminate\Database\Eloquent\Model $target
491
     * @param  array  $segments
492
     * @return mixed
493
     */
494
    protected function getTarget($target, array $segments)
495
    {
496
        foreach ($segments as $segment) {
497
            if (!$target) {
498
                return;
499
            }
500
501
            $target = $target->{$segment};
502
        }
503
504
        return $target;
505
    }
506
507
    /**
508
     * Set value of a mapped attribute.
509
     *
510
     * @param string $key
511
     * @param mixed  $value
512
     */
513 View Code Duplication
    protected function setMappedAttribute($key, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
514
    {
515
        $segments = explode('.', $this->getMappingForAttribute($key));
516
517
        $attribute = array_pop($segments);
518
519
        if ($target = $this->getTarget($this, $segments)) {
520
            $this->addTargetToSave($target);
521
522
            $target->{$attribute} = $value;
523
        }
524
    }
525
526
    /**
527
     * Flag mapped model to be saved along with this model.
528
     *
529
     * @param \Illuminate\Database\Eloquent\Model $target
530
     */
531
    protected function addTargetToSave($target)
532
    {
533
        if ($this !== $target) {
534
            $this->targetsToSave[] = $target;
535
        }
536
    }
537
538
    /**
539
     * Save mapped relations.
540
     *
541
     * @return void
542
     */
543
    protected function saveMapped()
544
    {
545
        foreach (array_unique($this->targetsToSave) as $target) {
546
            $target->save();
547
        }
548
549
        $this->targetsToSave = [];
550
    }
551
552
    /**
553
     * Unset mapped attribute.
554
     *
555
     * @param  string $key
556
     * @return void
557
     */
558 View Code Duplication
    protected function forget($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
559
    {
560
        $mapping = $this->getMappingForAttribute($key);
561
562
        list($target, $attribute) = $this->parseMappedColumn($mapping);
0 ignored issues
show
Bug introduced by
It seems like parseMappedColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
563
564
        $target = $target ? $this->getTarget($this, explode('.', $target)) : $this;
565
566
        unset($target->{$attribute});
567
    }
568
569
    /**
570
     * @codeCoverageIgnore
571
     *
572
     * @inheritdoc
573
     */
574
    protected function mutateAttributeForArray($key, $value)
575
    {
576
        if ($this->hasMapping($key)) {
577
            $value = $this->mapAttribute($key);
578
579
            return $value instanceof Arrayable ? $value->toArray() : $value;
580
        }
581
582
        return parent::mutateAttributeForArray($key, $value);
583
    }
584
585
    /**
586
     * Get the array of attribute mappings.
587
     *
588
     * @return array
589
     */
590
    public function getMaps()
591
    {
592
        return (property_exists($this, 'maps')) ? $this->maps : [];
593
    }
594
}
595