Completed
Push — 5.1 ( 1afeb5...3bf20e )
by Jarek
04:44
created

Mappable   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 571
Duplicated Lines 7.01 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 16
Bugs 1 Features 2
Metric Value
wmc 67
c 16
b 1
f 2
lcom 1
cbo 8
dl 40
loc 571
rs 5.7097

30 Methods

Rating   Name   Duplication   Size   Complexity  
A mappedSingleResult() 0 4 1
A getMappedOperator() 0 12 4
A relationMapping() 0 4 1
A getMaps() 0 4 2
A bootMappable() 15 15 2
A mappedQuery() 0 12 2
B mappedSelect() 0 33 5
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 mappedHasQuery() 0 12 1
A getMappedWhereConstraint() 0 6 1
A getMappedBoolean() 0 8 1
A getMappingForAttribute() 0 6 2
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

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
            ] as $method) {
54
            static::hook($method, $hooks->{$method}());
55
        }
56
    }
57
58
    /**
59
     * Custom query handler for querying mapped attributes.
60
     *
61
     * @param  \Sofa\Eloquence\Builder $query
62
     * @param  string $method
63
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
64
     * @return mixed
65
     */
66
    protected function mappedQuery(Builder $query, $method, ArgumentBag $args)
67
    {
68
        $mapping = $this->getMappingForAttribute($args->get('column'));
69
70
        if ($this->relationMapping($mapping)) {
71
            return $this->mappedRelationQuery($query, $method, $args, $mapping);
72
        }
73
74
        $args->set('column', $mapping);
75
76
        return $query->callParent($method, $args->all());
77
    }
78
79
    /**
80
     * Adjust mapped columns for select statement.
81
     *
82
     * @param  \Sofa\Eloquence\Builder $query
83
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
84
     * @return void
85
     */
86
    protected function mappedSelect(Builder $query, ArgumentBag $args)
87
    {
88
        $columns = $args->get('columns');
89
90
        foreach ($columns as $key => $column) {
91
            list($column) = $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...
92
93
            // Each mapped column will be selected appropriately. If it's alias
94
            // then prefix it with current table and use original field name
95
            // otherwise join required mapped tables and select the field.
96
            if ($this->hasMapping($column)) {
97
                $mapping = $this->getMappingForAttribute($column);
98
99
                if ($this->relationMapping($mapping)) {
100
                    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...
101
102
                    $table = $this->joinMapped($query, $target);
103
                } else {
104
                    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...
105
                }
106
107
                $columns[$key] = "{$table}.{$mapped}";
108
109
            // For non mapped columns present on this table we will simply
110
            // add the prefix, in order to avoid any column collisions,
111
            // that are likely to happen when we are joining tables.
112
            } 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...
113
                $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...
114
            }
115
        }
116
117
        $args->set('columns', $columns);
118
    }
119
120
    /**
121
     * Handle querying relational mappings.
122
     *
123
     * @param  \Sofa\Eloquence\Builder $query
124
     * @param  string $method
125
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
126
     * @param  string $mapping
127
     * @return mixed
128
     */
129
    protected function mappedRelationQuery($query, $method, ArgumentBag $args, $mapping)
130
    {
131
        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...
132
133 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...
134
            return $this->mappedJoinQuery($query, $method, $args, $target, $column);
135
        }
136
137
        return $this->mappedHasQuery($query, $method, $args, $target, $column);
138
    }
139
140
    /**
141
     * Join mapped table(s) in order to call given method.
142
     *
143
     * @param  \Sofa\Eloquence\Builder $query
144
     * @param  string $method
145
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
146
     * @param  string $target
147
     * @param  string $column
148
     * @return mixed
149
     */
150
    protected function mappedJoinQuery($query, $method, ArgumentBag $args, $target, $column)
151
    {
152
        $table = $this->joinMapped($query, $target);
153
154
        // For aggregates we need the actual function name
155
        // so it can be called directly on the builder.
156
        $method = $args->get('function') ?: $method;
157
158
        return (in_array($method, ['orderBy', 'lists']))
159
            ? $this->{"{$method}Mapped"}($query, $args, $table, $column, $target)
160
            : $this->mappedSingleResult($query, $method, "{$table}.{$column}");
161
    }
162
163
    /**
164
     * Order query by mapped attribute.
165
     *
166
     * @param  \Sofa\Eloquence\Builder $query
167
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
168
     * @param  string $table
169
     * @param  string $column
170
     * @param  string $target
171
     * @return \Sofa\Eloquence\Builder
172
     */
173
    protected function orderByMapped(Builder $query, ArgumentBag $args, $table, $column, $target)
174
    {
175
        $query->with($target)->getQuery()->orderBy("{$table}.{$column}", $args->get('direction'));
176
177
        return $query;
178
    }
179
180
    /**
181
     * Get an array with the values of given mapped attribute.
182
     *
183
     * @param  \Sofa\Eloquence\Builder $query
184
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
185
     * @param  string $table
186
     * @param  string $column
187
     * @return array
188
     */
189
    protected function listsMapped(Builder $query, ArgumentBag $args, $table, $column)
190
    {
191
        $query->select("{$table}.{$column}");
192
193
        if (!is_null($args->get('key'))) {
194
            $this->mappedSelectListsKey($query, $args->get('key'));
195
        }
196
197
        $args->set('column', $column);
198
199
        return $query->callParent('lists', $args->all());
200
    }
201
202
    /**
203
     * Add select clause for key of the list array.
204
     *
205
     * @param  \Sofa\Eloquence\Builder $query
206
     * @param  string $key
207
     * @return \Sofa\Eloquence\Builder
208
     */
209
    protected function mappedSelectListsKey(Builder $query, $key)
210
    {
211
        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...
212
            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...
213
        }
214
215
        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...
216
    }
217
218
    /**
219
     * Join mapped table(s).
220
     *
221
     * @param  \Sofa\Eloquence\Builder $query
222
     * @param  string $target
223
     * @return string
224
     */
225
    protected function joinMapped(Builder $query, $target)
226
    {
227
        $query->prefixColumnsForJoin();
228
229
        $parent = $this;
230
231
        foreach (explode('.', $target) as $segment) {
232
            list($table, $parent) = $this->joinSegment($query, $segment, $parent);
233
        }
234
235
        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...
236
    }
237
238
    /**
239
     * Join relation's table accordingly.
240
     *
241
     * @param  \Sofa\Eloquence\Builder $query
242
     * @param  string $segment
243
     * @param  \Illuminate\Database\Eloquent\Model $parent
244
     * @return array
245
     */
246
    protected function joinSegment(Builder $query, $segment, EloquentModel $parent)
247
    {
248
        $relation = $parent->{$segment}();
249
        $related  = $relation->getRelated();
250
        $table    = $related->getTable();
251
252
        // If the table has been already joined let's skip it. Otherwise we will left join
253
        // it in order to allow using some query methods on mapped columns. Polymorphic
254
        // relations require also additional constraints, so let's handle it as well.
255
        if (!$this->alreadyJoined($query, $table)) {
256
            list($fk, $pk) = $this->getJoinKeys($relation);
257
258
            $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...
259
                $join->on($fk, '=', $pk);
260
261
                if ($relation instanceof MorphOne || $relation instanceof MorphTo) {
262
                    $morphClass = ($relation instanceof MorphOne)
263
                        ? $parent->getMorphClass()
264
                        : $related->getMorphClass();
265
266
                    $join->where($relation->getMorphType(), '=', $morphClass);
267
                }
268
            });
269
        }
270
271
        return [$table, $related];
272
    }
273
274
    /**
275
     * Determine whether given table has been already joined.
276
     *
277
     * @param  \Sofa\Eloquence\Builder $query
278
     * @param  string  $table
279
     * @return boolean
280
     */
281
    protected function alreadyJoined(Builder $query, $table)
282
    {
283
        $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...
284
285
        return in_array($table, $joined);
286
    }
287
288
    /**
289
     * Get the keys from relation in order to join the table.
290
     *
291
     * @param  \Illuminate\Database\Eloquent\Relations\Relation $relation
292
     * @return array
293
     *
294
     * @throws \LogicException
295
     */
296
    protected function getJoinKeys(Relation $relation)
297
    {
298
        if ($relation instanceof HasOne || $relation instanceof MorphOne) {
299
            return [$relation->getForeignKey(), $relation->getQualifiedParentKeyName()];
300
        }
301
302
        if ($relation instanceof BelongsTo && !$relation instanceof MorphTo) {
303
            return [$relation->getQualifiedForeignKey(), $relation->getQualifiedOtherKeyName()];
304
        }
305
306
        $class = get_class($relation);
307
308
        throw new LogicException(
309
            "Only HasOne, MorphOne and BelongsTo mappings can be queried. {$class} given."
310
        );
311
    }
312
313
    /**
314
     * Get single value result from the mapped attribute.
315
     *
316
     * @param  \Sofa\Eloquence\Builder $query
317
     * @param  string $method
318
     * @param  string $qualifiedColumn
319
     * @return mixed
320
     */
321
    protected function mappedSingleResult(Builder $query, $method, $qualifiedColumn)
322
    {
323
        return $query->getQuery()->select("{$qualifiedColumn}")->{$method}("{$qualifiedColumn}");
324
    }
325
326
    /**
327
     * Add whereHas subquery on the mapped attribute relation.
328
     *
329
     * @param  \Sofa\Eloquence\Builder $query
330
     * @param  string $method
331
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
332
     * @param  string $target
333
     * @param  string $column
334
     * @return \Sofa\Eloquence\Builder
335
     */
336
    protected function mappedHasQuery(Builder $query, $method, ArgumentBag $args, $target, $column)
337
    {
338
        $boolean = $this->getMappedBoolean($args);
339
340
        $operator = $this->getMappedOperator($method, $args);
341
342
        $args->set('column', $column);
343
344
        return $query
345
            ->has($target, $operator, 1, $boolean, $this->getMappedWhereConstraint($method, $args))
346
            ->with($target);
347
    }
348
349
    /**
350
     * Get the relation constraint closure.
351
     *
352
     * @param  string $method
353
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
354
     * @return \Closure
355
     */
356
    protected function getMappedWhereConstraint($method, ArgumentBag $args)
357
    {
358
        return function ($query) use ($method, $args) {
359
            call_user_func_array([$query, $method], $args->all());
360
        };
361
    }
362
363
    /**
364
     * Get boolean called on the original method and set it to default.
365
     *
366
     * @param  \Sofa\EloquenceArgumentBag $args
367
     * @return string
368
     */
369
    protected function getMappedBoolean(ArgumentBag $args)
370
    {
371
        $boolean = $args->get('boolean');
372
373
        $args->set('boolean', 'and');
374
375
        return $boolean;
376
    }
377
378
    /**
379
     * Determine the operator for count relation query and set 'not' appropriately.
380
     *
381
     * @param  string $method
382
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
383
     * @return string
384
     */
385
    protected function getMappedOperator($method, ArgumentBag $args)
386
    {
387
        if ($not = $args->get('not')) {
388
            $args->set('not', false);
389
        }
390
391
        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...
392
            $args->set('not', true);
393
        }
394
395
        return ($not ^ $null) ? '<' : '>=';
396
    }
397
398
    /**
399
     * Get the mapping key.
400
     *
401
     * @param  string $key
402
     * @return string|null
403
     */
404
    public function getMappingForAttribute($key)
405
    {
406
        if ($this->hasMapping($key)) {
407
            return static::$mappedAttributes[$key];
408
        }
409
    }
410
411
    /**
412
     * Determine whether the mapping points to relation.
413
     *
414
     * @param  string $mapping
415
     * @return boolean
416
     */
417
    protected function relationMapping($mapping)
418
    {
419
        return strpos($mapping, '.') !== false;
420
    }
421
422
    /**
423
     * Determine whether a mapping exists for an attribute.
424
     *
425
     * @param  string $key
426
     * @return boolean
427
     */
428
    public function hasMapping($key)
429
    {
430
        if (is_null(static::$mappedAttributes)) {
431
            $this->parseMappings();
432
        }
433
434
        return array_key_exists((string) $key, static::$mappedAttributes);
435
    }
436
437
    /**
438
     * Parse defined mappings into flat array.
439
     *
440
     * @return void
441
     */
442
    protected function parseMappings()
443
    {
444
        static::$mappedAttributes = [];
445
446
        foreach ($this->getMaps() as $attribute => $mapping) {
447
            if (is_array($mapping)) {
448
                $this->parseImplicitMapping($mapping, $attribute);
449
            } else {
450
                static::$mappedAttributes[$attribute] = $mapping;
451
            }
452
        }
453
    }
454
455
    /**
456
     * Parse implicit mappings.
457
     *
458
     * @param  array  $attributes
459
     * @param  string $target
460
     * @return void
461
     */
462
    protected function parseImplicitMapping($attributes, $target)
463
    {
464
        foreach ($attributes as $attribute) {
465
            static::$mappedAttributes[$attribute] = "{$target}.{$attribute}";
466
        }
467
    }
468
469
    /**
470
     * Map an attribute to a value.
471
     *
472
     * @param  string $key
473
     * @return mixed
474
     */
475
    protected function mapAttribute($key)
476
    {
477
        $segments = explode('.', $this->getMappingForAttribute($key));
478
479
        return $this->getTarget($this, $segments);
480
    }
481
482
    /**
483
     * Get mapped value.
484
     *
485
     * @param  \Illuminate\Database\Eloquent\Model $target
486
     * @param  array  $segments
487
     * @return mixed
488
     */
489
    protected function getTarget($target, array $segments)
490
    {
491
        foreach ($segments as $segment) {
492
            if (!$target) {
493
                return;
494
            }
495
496
            $target = $target->{$segment};
497
        }
498
499
        return $target;
500
    }
501
502
    /**
503
     * Set value of a mapped attribute.
504
     *
505
     * @param string $key
506
     * @param mixed  $value
507
     */
508 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...
509
    {
510
        $segments = explode('.', $this->getMappingForAttribute($key));
511
512
        $attribute = array_pop($segments);
513
514
        if ($target = $this->getTarget($this, $segments)) {
515
            $this->addTargetToSave($target);
516
517
            $target->{$attribute} = $value;
518
        }
519
    }
520
521
    /**
522
     * Flag mapped model to be saved along with this model.
523
     *
524
     * @param \Illuminate\Database\Eloquent\Model $target
525
     */
526
    protected function addTargetToSave($target)
527
    {
528
        if ($this !== $target) {
529
            $this->targetsToSave[] = $target;
530
        }
531
    }
532
533
    /**
534
     * Save mapped relations.
535
     *
536
     * @return void
537
     */
538
    protected function saveMapped()
539
    {
540
        foreach (array_unique($this->targetsToSave) as $target) {
541
            $target->save();
542
        }
543
544
        $this->targetsToSave = [];
545
    }
546
547
    /**
548
     * Unset mapped attribute.
549
     *
550
     * @param  string $key
551
     * @return void
552
     */
553 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...
554
    {
555
        $mapping = $this->getMappingForAttribute($key);
556
557
        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...
558
559
        $target = $target ? $this->getTarget($this, explode('.', $target)) : $this;
560
561
        unset($target->{$attribute});
562
    }
563
564
    /**
565
     * @codeCoverageIgnore
566
     *
567
     * @inheritdoc
568
     */
569
    protected function mutateAttributeForArray($key, $value)
570
    {
571
        if ($this->hasMapping($key)) {
572
            $value = $this->mapAttribute($key);
573
574
            return $value instanceof Arrayable ? $value->toArray() : $value;
575
        }
576
577
        return parent::mutateAttributeForArray($key, $value);
578
    }
579
580
    /**
581
     * Get the array of attribute mappings.
582
     *
583
     * @return array
584
     */
585
    public function getMaps()
586
    {
587
        return (property_exists($this, 'maps')) ? $this->maps : [];
588
    }
589
}
590