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 |
||
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); |
||
|
|||
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) |
||
107 | |||
108 | /** |
||
109 | * @param $entities |
||
110 | * @throws \InvalidArgumentException |
||
111 | */ |
||
112 | protected function detachExcept($entities) |
||
113 | { |
||
114 | $query = $this->query->getQuery()->from($this->relatedMap->getTable()); |
||
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()); |
||
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)); |
||
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) |
|
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) |
|
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() |
||
327 | } |
||
328 |
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.