Completed
Push — 5.1 ( af1a7b...f5b0b1 )
by Rémi
03:26
created

HasOneOrMany   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 320
Duplicated Lines 12.5 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 11
Bugs 4 Features 0
Metric Value
wmc 32
c 11
b 4
f 0
lcom 1
cbo 7
dl 40
loc 320
rs 9.6

22 Methods

Rating   Name   Duplication   Size   Complexity  
A sync() 0 4 1
A getForeignKeyValuePair() 0 4 1
A detachExcept() 0 14 2
A __construct() 0 7 1
A attachTo() 0 7 2
A detachFrom() 0 8 2
A attachOne() 0 10 1
A attachMany() 0 6 2
A detachOne() 0 4 1
A detachMany() 0 14 2
A addConstraints() 0 6 2
A addEagerConstraints() 0 4 1
A matchOne() 0 4 1
A matchMany() 0 4 1
B matchOneOrMany() 25 25 3
A getRelationValue() 0 6 2
A buildDictionary() 15 15 2
A getHasCompareKey() 0 4 1
A getForeignKey() 0 4 1
A getPlainForeignKey() 0 6 1
A getParentKey() 0 4 1
A getQualifiedParentKeyName() 0 4 1

How to fix   Duplicated Code   

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:

1
<?php
2
3
namespace Analogue\ORM\Relationships;
4
5
use Analogue\ORM\System\Mapper;
6
use Analogue\ORM\EntityCollection;
7
8
abstract class HasOneOrMany extends Relationship
9
{
10
    /**
11
     * The foreign key of the parent model.
12
     *
13
     * @var string
14
     */
15
    protected $foreignKey;
16
17
    /**
18
     * The local key of the parent model.
19
     *
20
     * @var string
21
     */
22
    protected $localKey;
23
24
    /**
25
     * Create a new has many relationship instance.
26
     *
27
     * @param Mapper                 $mapper
28
     * @param \Analogue\ORM\Mappable $parentEntity
29
     * @param string                 $foreignKey
30
     * @param string                 $localKey
31
     */
32
    public function __construct(Mapper $mapper, $parentEntity, $foreignKey, $localKey)
33
    {
34
        $this->localKey = $localKey;
35
        $this->foreignKey = $foreignKey;
36
37
        parent::__construct($mapper, $parentEntity);
38
    }
39
40
    /**
41
     * @param \Analogue\ORM\Entity|EntityCollection $entity
42
     * @return void
43
     */
44
    public function attachTo($entity)
45
    {
46
        if ($entity instanceof EntityCollection) {
47
            $this->attachMany($entity);
48
        }
49
        $this->attachOne($entity);
0 ignored issues
show
Bug introduced by
It seems like $entity defined by parameter $entity on line 44 can also be of type object<Analogue\ORM\EntityCollection>; however, Analogue\ORM\Relationshi...sOneOrMany::attachOne() does only seem to accept object<Analogue\ORM\Entity>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
50
    }
51
52
    /**
53
     * @param $entityHash
54
     * @return void
55
     */
56
    public function detachFrom($entityHash)
57
    {
58
        if (is_array($entityHash)) {
59
            $this->detachMany($entityHash);
60
            return;
61
        }
62
        $this->detachMany([$entityHash]);
63
    }
64
65
    /**
66
     * @param \Analogue\ORM\Entity $entity
67
     */
68
    public function attachOne($entity)
69
    {
70
        $wrapper = $this->factory->make($entity);
71
72
        // Ok, we need to guess the inverse of the relation from there.
73
        // Let's assume the inverse of the relation method is the name of
74
        // the entity.
75
76
        $wrapper->setEntityAttribute($this->getPlainForeignKey(), $this->getParentKey());
77
    }
78
79
    /**
80
     * @param EntityCollection $entities
81
     */
82
    public function attachMany(EntityCollection $entities)
83
    {
84
        foreach ($entities as $entity) {
85
            $this->attachOne($entity);
86
        }
87
    }
88
89
    /**
90
     * @param $entityHash
91
     */
92
    protected function detachOne($entityHash)
93
    {
94
        $this->detachMany([$entityHash]);
95
    }
96
97
    /**
98
     * Attach ids that are passed as arguments, and detach any other
99
     * @param  mixed $entities
100
     * @throws \InvalidArgumentException
101
     * @return void
102
     */
103
    public function sync(array $entities)
104
    {
105
        $this->detachExcept($entities);
106
    }
107
108
    /**
109
     * @param  $entities
110
     * @throws \InvalidArgumentException
111
     */
112
    protected function detachExcept($entities)
113
    {
114
        $query = $this->query->getQuery()->from($this->relatedMap->getTable());
0 ignored issues
show
Bug introduced by
The method from() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
115
116
        if (count($entities) > 0) {
117
            $keys = $this->getKeys($entities);
118
            $query->whereNotIn($this->relatedMap->getKeyName(), $keys);
119
        }
120
121
        $parentKey = $this->parentMap->getKeyName();
122
123
        $query->where($this->getPlainForeignKey(), '=', $this->parent->getEntityAttribute($parentKey))
124
            ->update([$this->getPlainForeignKey() => null]);
125
    }
126
127
    /**
128
     * @param array $entityHashes
129
     */
130
    public function detachMany(array $entityHashes)
131
    {
132
        $keys = [];
133
134
        foreach ($entityHashes as $hash) {
135
            $split = explode('.', $hash);
136
            $keys[] = $split[1];
137
        }
138
139
        $query = $this->query->getQuery()->from($this->relatedMap->getTable());
0 ignored issues
show
Bug introduced by
The method from() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
140
141
        $query->whereIn($this->relatedMap->getKeyName(), $keys)
142
            ->update([$this->getPlainForeignKey() => null]);
143
    }
144
145
    /**
146
     * Set the base constraints on the relation query.
147
     *
148
     * @return void
149
     */
150
    public function addConstraints()
151
    {
152
        if (static::$constraints) {
153
            $this->query->where($this->foreignKey, '=', $this->getParentKey());
154
        }
155
    }
156
157
    /**
158
     * Set the constraints for an eager load of the relation.
159
     *
160
     * @param  array $entities
161
     * @return void
162
     */
163
    public function addEagerConstraints(array $entities)
164
    {
165
        $this->query->whereIn($this->foreignKey, $this->getKeys($entities, $this->localKey));
0 ignored issues
show
Documentation Bug introduced by
The method whereIn does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
166
    }
167
168
    /**
169
     * Match the eagerly loaded results to their single parents.
170
     *
171
     * @param  array            $entities
172
     * @param  EntityCollection $results
173
     * @param  string           $relation
174
     * @return array
175
     */
176
    public function matchOne(array $entities, EntityCollection $results, $relation)
177
    {
178
        return $this->matchOneOrMany($entities, $results, $relation, 'one');
179
    }
180
181
    /**
182
     * Match the eagerly loaded results to their many parents.
183
     *
184
     * @param  array            $entities
185
     * @param  EntityCollection $results
186
     * @param  string           $relation
187
     * @return array
188
     */
189
    public function matchMany(array $entities, EntityCollection $results, $relation)
190
    {
191
        return $this->matchOneOrMany($entities, $results, $relation, 'many');
192
    }
193
194
    /**
195
     * Match the eagerly loaded results to their many parents.
196
     *
197
     * @param  array            $entities
198
     * @param  EntityCollection $results
199
     * @param  string           $relation
200
     * @param  string           $type
201
     * @return array
202
     */
203 View Code Duplication
    protected function matchOneOrMany(array $entities, EntityCollection $results, $relation, $type)
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...
204
    {
205
        $dictionary = $this->buildDictionary($results);
206
207
        $cache = $this->parentMapper->getEntityCache();
208
209
        // Once we have the dictionary we can simply spin through the parent models to
210
        // link them up with their children using the keyed dictionary to make the
211
        // matching very convenient and easy work. Then we'll just return them.
212
        foreach ($entities as $entity) {
213
            $entity = $this->factory->make($entity);
214
215
            $key = $entity->getEntityAttribute($this->localKey);
216
217
            if (isset($dictionary[$key])) {
218
                $value = $this->getRelationValue($dictionary, $key, $type);
219
220
                $entity->setEntityAttribute($relation, $value);
221
222
                $cache->cacheLoadedRelationResult($entity, $relation, $value, $this);
223
            }
224
        }
225
226
        return $entities;
227
    }
228
229
    /**
230
     * Get the value of a relationship by one or many type.
231
     *
232
     * @param  array  $dictionary
233
     * @param  string $key
234
     * @param  string $type
235
     * @return mixed
236
     */
237
    protected function getRelationValue(array $dictionary, $key, $type)
238
    {
239
        $value = $dictionary[$key];
240
241
        return $type == 'one' ? reset($value) : $this->relatedMap->newCollection($value);
242
    }
243
244
    /**
245
     * Build model dictionary keyed by the relation's foreign key.
246
     *
247
     * @param  EntityCollection $results
248
     * @return array
249
     */
250 View Code Duplication
    protected function buildDictionary(EntityCollection $results)
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...
251
    {
252
        $dictionary = [];
253
254
        $foreign = $this->getPlainForeignKey();
255
256
        // First we will create a dictionary of models keyed by the foreign key of the
257
        // relationship as this will allow us to quickly access all of the related
258
        // models without having to do nested looping which will be quite slow.
259
        foreach ($results as $result) {
260
            $dictionary[$result->{$foreign}][] = $result;
261
        }
262
263
        return $dictionary;
264
    }
265
266
    /**
267
     * Get the key for comparing against the parent key in "has" query.
268
     *
269
     * @return string
270
     */
271
    public function getHasCompareKey()
272
    {
273
        return $this->getForeignKey();
274
    }
275
276
    /**
277
     * Get the foreign key for the relationship.
278
     *
279
     * @return string
280
     */
281
    public function getForeignKey()
282
    {
283
        return $this->foreignKey;
284
    }
285
286
    /**
287
     * Get the plain foreign key.
288
     *
289
     * @return string
290
     */
291
    public function getPlainForeignKey()
292
    {
293
        $segments = explode('.', $this->getForeignKey());
294
295
        return $segments[count($segments) - 1];
296
    }
297
298
    /**
299
     * Get the key value of the parent's local key.
300
     *
301
     * @return mixed
302
     */
303
    public function getParentKey()
304
    {
305
        return $this->parent->getEntityAttribute($this->localKey);
306
    }
307
308
    /**
309
     * Get the fully qualified parent key name.
310
     *
311
     * @return string
312
     */
313
    public function getQualifiedParentKeyName()
314
    {
315
        return $this->parentMap->getTable() . '.' . $this->localKey;
316
    }
317
318
    /**
319
     * Get the foreign key as value pair for this relation
320
     *
321
     * @return array
322
     */
323
    public function getForeignKeyValuePair()
324
    {
325
        return [$this->getPlainForeignKey() => $this->getParentKey()];
326
    }
327
}
328