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 classes like Repository 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 Repository, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | class Repository |
||
32 | { |
||
33 | const RESULTS_ARRAY = 'array'; |
||
34 | const RESULTS_OBJECT = 'object'; |
||
35 | const RESULTS_RAW = 'raw'; |
||
36 | const RESULTS_RAW_ITERATOR = 'raw_iterator'; |
||
37 | |||
38 | /** |
||
39 | * @var Manager |
||
40 | */ |
||
41 | private $manager; |
||
42 | |||
43 | /** |
||
44 | * @var array |
||
45 | */ |
||
46 | private $namespaces = []; |
||
47 | |||
48 | /** |
||
49 | * @var array |
||
50 | */ |
||
51 | private $types = []; |
||
52 | |||
53 | /** |
||
54 | * @var array |
||
55 | */ |
||
56 | private $fieldsCache = []; |
||
57 | |||
58 | /** |
||
59 | * Constructor. |
||
60 | * |
||
61 | * @param Manager $manager |
||
62 | * @param array $repositories |
||
63 | */ |
||
64 | public function __construct($manager, $repositories) |
||
65 | { |
||
66 | $this->manager = $manager; |
||
67 | $this->namespaces = $repositories; |
||
68 | $this->types = $this->getTypes(); |
||
69 | } |
||
70 | |||
71 | /** |
||
72 | * @return array |
||
73 | */ |
||
74 | public function getTypes() |
||
75 | { |
||
76 | $types = []; |
||
77 | $meta = $this->getManager()->getBundlesMapping($this->namespaces); |
||
78 | |||
79 | foreach ($meta as $namespace => $metadata) { |
||
80 | $types[] = $metadata->getType(); |
||
81 | } |
||
82 | |||
83 | return $types; |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * Returns a single document data by ID or null if document is not found. |
||
88 | * |
||
89 | * @param string $id Document Id to find. |
||
90 | * @param string $resultType Result type returned. |
||
91 | * |
||
92 | * @return DocumentInterface|null |
||
93 | * |
||
94 | * @throws \LogicException |
||
95 | */ |
||
96 | public function find($id, $resultType = self::RESULTS_OBJECT) |
||
97 | { |
||
98 | if (count($this->types) !== 1) { |
||
99 | throw new \LogicException('Only one type must be specified for the find() method'); |
||
100 | } |
||
101 | |||
102 | $params = [ |
||
103 | 'index' => $this->getManager()->getConnection()->getIndexName(), |
||
104 | 'type' => $this->types[0], |
||
105 | 'id' => $id, |
||
106 | ]; |
||
107 | |||
108 | try { |
||
109 | $result = $this->getManager()->getConnection()->getClient()->get($params); |
||
110 | } catch (Missing404Exception $e) { |
||
111 | return null; |
||
112 | } |
||
113 | |||
114 | if ($resultType === self::RESULTS_OBJECT) { |
||
115 | return (new Converter( |
||
116 | $this->getManager()->getTypesMapping(), |
||
117 | $this->getManager()->getBundlesMapping() |
||
118 | ))->convertToDocument($result); |
||
119 | } |
||
120 | |||
121 | return $this->parseResult($result, $resultType, ''); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Finds entities by a set of criteria. |
||
126 | * |
||
127 | * @param array $criteria Example: ['group' => ['best', 'worst'], 'job' => 'medic']. |
||
128 | * @param array|null $orderBy Example: ['name' => 'ASC', 'surname' => 'DESC']. |
||
129 | * @param int|null $limit Example: 5. |
||
130 | * @param int|null $offset Example: 30. |
||
131 | * @param string $resultType Result type returned. |
||
132 | * |
||
133 | * @return array|DocumentIterator The objects. |
||
134 | */ |
||
135 | public function findBy( |
||
136 | array $criteria, |
||
137 | array $orderBy = [], |
||
138 | $limit = null, |
||
139 | $offset = null, |
||
140 | $resultType = self::RESULTS_OBJECT |
||
141 | ) { |
||
142 | $search = $this->createSearch(); |
||
143 | |||
144 | if ($limit !== null) { |
||
145 | $search->setSize($limit); |
||
146 | } |
||
147 | if ($offset !== null) { |
||
148 | $search->setFrom($offset); |
||
149 | } |
||
150 | |||
151 | View Code Duplication | foreach ($criteria as $field => $value) { |
|
|
|||
152 | $search->addQuery(new TermsQuery($field, is_array($value) ? $value : [$value]), 'must'); |
||
153 | } |
||
154 | |||
155 | View Code Duplication | foreach ($orderBy as $field => $direction) { |
|
156 | $search->addSort(new Sort($field, strcasecmp($direction, 'asc') == 0 ? Sort::ORDER_ASC : Sort::ORDER_DESC)); |
||
157 | } |
||
158 | |||
159 | return $this->execute($search, $resultType); |
||
160 | } |
||
161 | |||
162 | /** |
||
163 | * Finds only one entity by a set of criteria. |
||
164 | * |
||
165 | * @param array $criteria Example: ['group' => ['best', 'worst'], 'job' => 'medic']. |
||
166 | * @param array|null $orderBy Example: ['name' => 'ASC', 'surname' => 'DESC']. |
||
167 | * @param string $resultType Result type returned. |
||
168 | * |
||
169 | * @return DocumentInterface|null The object. |
||
170 | */ |
||
171 | public function findOneBy(array $criteria, array $orderBy = [], $resultType = self::RESULTS_OBJECT) |
||
172 | { |
||
173 | $search = $this->createSearch(); |
||
174 | $search->setSize(1); |
||
175 | |||
176 | View Code Duplication | foreach ($criteria as $field => $value) { |
|
177 | $search->addQuery(new TermsQuery($field, is_array($value) ? $value : [$value]), 'must'); |
||
178 | } |
||
179 | |||
180 | View Code Duplication | foreach ($orderBy as $field => $direction) { |
|
181 | $search->addSort(new Sort($field, strcasecmp($direction, 'asc') == 0 ? Sort::ORDER_ASC : Sort::ORDER_DESC)); |
||
182 | } |
||
183 | |||
184 | $result = $this |
||
185 | ->getManager() |
||
186 | ->getConnection() |
||
187 | ->search($this->types, $this->checkFields($search->toArray()), $search->getQueryParams()); |
||
188 | |||
189 | if ($resultType === self::RESULTS_OBJECT) { |
||
190 | $rawData = $result['hits']['hits']; |
||
191 | if (!count($rawData)) { |
||
192 | return null; |
||
193 | } |
||
194 | |||
195 | return (new Converter( |
||
196 | $this->getManager()->getTypesMapping(), |
||
197 | $this->getManager()->getBundlesMapping() |
||
198 | ))->convertToDocument($rawData[0]); |
||
199 | } |
||
200 | |||
201 | return $this->parseResult($result, $resultType, ''); |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * Returns search instance. |
||
206 | * |
||
207 | * @return Search |
||
208 | */ |
||
209 | public function createSearch() |
||
213 | |||
214 | /** |
||
215 | * Executes given search. |
||
216 | * |
||
217 | * @param Search $search |
||
218 | * @param string $resultsType |
||
219 | * |
||
220 | * @return DocumentIterator|DocumentScanIterator|RawResultIterator|array |
||
221 | * |
||
222 | * @throws \Exception |
||
223 | */ |
||
224 | public function execute(Search $search, $resultsType = self::RESULTS_OBJECT) |
||
225 | { |
||
226 | $results = $this |
||
227 | ->getManager() |
||
228 | ->getConnection() |
||
229 | ->search($this->types, $this->checkFields($search->toArray()), $search->getQueryParams()); |
||
230 | |||
231 | return $this->parseResult($results, $resultsType, $search->getScroll()); |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * Delete by query. |
||
236 | * |
||
237 | * @param Search $search |
||
238 | * |
||
239 | * @return array |
||
240 | */ |
||
241 | public function deleteByQuery(Search $search) |
||
242 | { |
||
243 | $results = $this |
||
244 | ->getManager() |
||
245 | ->getConnection() |
||
246 | ->deleteByQuery($this->types, $search->toArray()); |
||
247 | |||
248 | return new IndicesResult($results); |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Fetches next set of results. |
||
253 | * |
||
254 | * @param string $scrollId |
||
255 | * @param string $scrollDuration |
||
256 | * @param string $resultsType |
||
257 | * |
||
258 | * @return array|DocumentScanIterator |
||
259 | * |
||
260 | * @throws \Exception |
||
261 | */ |
||
262 | public function scan( |
||
263 | $scrollId, |
||
264 | $scrollDuration = '5m', |
||
265 | $resultsType = self::RESULTS_OBJECT |
||
266 | ) { |
||
267 | $results = $this->getManager()->getConnection()->scroll($scrollId, $scrollDuration); |
||
268 | |||
269 | return $this->parseResult($results, $resultsType, $scrollDuration); |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * Get suggestions using suggest api. |
||
274 | * |
||
275 | * @param AbstractSuggester[]|AbstractSuggester $suggesters |
||
276 | * |
||
277 | * @return SuggestionIterator |
||
278 | */ |
||
279 | public function suggest($suggesters) |
||
280 | { |
||
281 | if (!is_array($suggesters)) { |
||
282 | $suggesters = [$suggesters]; |
||
283 | } |
||
284 | |||
285 | $body = []; |
||
286 | /** @var AbstractSuggester $suggester */ |
||
287 | foreach ($suggesters as $suggester) { |
||
288 | $body = array_merge($suggester->toArray(), $body); |
||
289 | } |
||
290 | $results = $this->getManager()->getConnection()->getClient()->suggest(['body' => $body]); |
||
291 | unset($results['_shards']); |
||
292 | |||
293 | return new SuggestionIterator($results); |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Removes a single document data by ID. |
||
298 | * |
||
299 | * @param string $id Document ID to remove. |
||
300 | * |
||
301 | * @return array |
||
302 | * |
||
303 | * @throws \LogicException |
||
304 | */ |
||
305 | public function remove($id) |
||
306 | { |
||
307 | if (count($this->types) == 1) { |
||
308 | $params = [ |
||
309 | 'index' => $this->getManager()->getConnection()->getIndexName(), |
||
310 | 'type' => $this->types[0], |
||
311 | 'id' => $id, |
||
312 | ]; |
||
313 | |||
314 | $response = $this->getManager()->getConnection()->delete($params); |
||
315 | |||
316 | return $response; |
||
317 | } else { |
||
318 | throw new \LogicException('Only one type must be specified for the find() method'); |
||
319 | } |
||
320 | } |
||
321 | |||
322 | /** |
||
323 | * Checks if all required fields are added. |
||
324 | * |
||
325 | * @param array $searchArray |
||
326 | * @param array $fields |
||
327 | * |
||
328 | * @return array |
||
329 | */ |
||
330 | private function checkFields($searchArray, $fields = ['_parent', '_ttl']) |
||
331 | { |
||
332 | if (empty($fields)) { |
||
333 | return $searchArray; |
||
334 | } |
||
335 | |||
336 | // Checks if cache is loaded. |
||
337 | if (empty($this->fieldsCache)) { |
||
338 | foreach ($this->getManager()->getBundlesMapping($this->namespaces) as $ns => $properties) { |
||
339 | $this->fieldsCache = array_unique( |
||
340 | array_merge( |
||
341 | $this->fieldsCache, |
||
342 | array_keys($properties->getFields()) |
||
343 | ) |
||
344 | ); |
||
345 | } |
||
346 | } |
||
347 | |||
348 | // Adds cached fields to fields array. |
||
349 | foreach (array_intersect($this->fieldsCache, $fields) as $field) { |
||
350 | $searchArray['fields'][] = $field; |
||
351 | } |
||
352 | |||
353 | // Removes duplicates and checks if its needed to add _source. |
||
354 | if (!empty($searchArray['fields'])) { |
||
355 | $searchArray['fields'] = array_unique($searchArray['fields']); |
||
356 | if (array_diff($searchArray['fields'], $fields) === []) { |
||
357 | $searchArray['fields'][] = '_source'; |
||
358 | } |
||
359 | } |
||
360 | |||
361 | return $searchArray; |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * Parses raw result. |
||
366 | * |
||
367 | * @param array $raw |
||
368 | * @param string $resultsType |
||
369 | * @param string $scrollDuration |
||
370 | * |
||
371 | * @return DocumentIterator|DocumentScanIterator|RawResultIterator|array |
||
372 | * |
||
373 | * @throws \Exception |
||
374 | */ |
||
375 | private function parseResult($raw, $resultsType, $scrollDuration) |
||
376 | { |
||
377 | switch ($resultsType) { |
||
378 | case self::RESULTS_OBJECT: |
||
379 | if (isset($raw['_scroll_id'])) { |
||
380 | $iterator = new DocumentScanIterator( |
||
381 | $raw, |
||
382 | $this->getManager()->getTypesMapping(), |
||
383 | $this->getManager()->getBundlesMapping() |
||
384 | ); |
||
385 | $iterator |
||
386 | ->setRepository($this) |
||
387 | ->setScrollDuration($scrollDuration) |
||
388 | ->setScrollId($raw['_scroll_id']); |
||
389 | |||
390 | return $iterator; |
||
391 | } |
||
392 | |||
393 | return new DocumentIterator( |
||
394 | $raw, |
||
395 | $this->getManager()->getTypesMapping(), |
||
396 | $this->getManager()->getBundlesMapping() |
||
397 | ); |
||
398 | case self::RESULTS_ARRAY: |
||
399 | return $this->convertToNormalizedArray($raw); |
||
400 | case self::RESULTS_RAW: |
||
401 | return $raw; |
||
402 | case self::RESULTS_RAW_ITERATOR: |
||
403 | if (isset($raw['_scroll_id'])) { |
||
404 | $iterator = new RawResultScanIterator($raw); |
||
405 | $iterator |
||
406 | ->setRepository($this) |
||
407 | ->setScrollDuration($scrollDuration) |
||
408 | ->setScrollId($raw['_scroll_id']); |
||
409 | |||
410 | return $iterator; |
||
411 | } |
||
412 | |||
413 | return new RawResultIterator($raw); |
||
414 | default: |
||
415 | throw new \Exception('Wrong results type selected'); |
||
416 | } |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * Normalizes response array. |
||
421 | * |
||
422 | * @param array $data |
||
423 | * |
||
424 | * @return array |
||
425 | */ |
||
426 | private function convertToNormalizedArray($data) |
||
427 | { |
||
428 | if (array_key_exists('_source', $data)) { |
||
429 | return $data['_source']; |
||
430 | } |
||
431 | |||
432 | $output = []; |
||
433 | |||
434 | if (isset($data['hits']['hits'][0]['_source'])) { |
||
435 | foreach ($data['hits']['hits'] as $item) { |
||
436 | $output[] = $item['_source']; |
||
437 | } |
||
438 | } elseif (isset($data['hits']['hits'][0]['fields'])) { |
||
439 | foreach ($data['hits']['hits'] as $item) { |
||
440 | $output[] = array_map('reset', $item['fields']); |
||
441 | } |
||
442 | } |
||
443 | |||
444 | return $output; |
||
445 | } |
||
446 | |||
447 | /** |
||
448 | * Creates new instance of document. |
||
449 | * |
||
450 | * @return DocumentInterface |
||
451 | * |
||
452 | * @throws \LogicException |
||
453 | */ |
||
454 | public function createDocument() |
||
455 | { |
||
456 | if (count($this->namespaces) > 1) { |
||
457 | throw new \LogicException( |
||
458 | 'Repository can not create new document when it is associated with multiple namespaces' |
||
459 | ); |
||
460 | } |
||
461 | |||
462 | $class = $this->getManager()->getBundlesMapping()[reset($this->namespaces)]->getProxyNamespace(); |
||
463 | |||
464 | return new $class(); |
||
465 | } |
||
466 | |||
467 | /** |
||
468 | * Returns elasticsearch manager used in this repository for getting/setting documents. |
||
469 | * |
||
470 | * @return Manager |
||
471 | */ |
||
472 | public function getManager() |
||
476 | } |
||
477 |
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.