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 UrlRewriteObserver 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 UrlRewriteObserver, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
38 | class UrlRewriteObserver extends AbstractProductImportObserver |
||
39 | { |
||
40 | |||
41 | /** |
||
42 | * The entity type to load the URL rewrites for. |
||
43 | * |
||
44 | * @var string |
||
45 | */ |
||
46 | const ENTITY_TYPE = 'product'; |
||
47 | |||
48 | /** |
||
49 | * Will be invoked by the action on the events the listener has been registered for. |
||
50 | * |
||
51 | * @param array $row The row to handle |
||
52 | * |
||
53 | * @return array The modified row |
||
54 | * @see \TechDivision\Import\Product\Observers\ImportObserverInterface::handle() |
||
55 | */ |
||
56 | 3 | public function handle(array $row) |
|
57 | { |
||
58 | |||
59 | // load the header information |
||
60 | 3 | $headers = $this->getHeaders(); |
|
61 | |||
62 | // query whether or not, we've found a new SKU => means we've found a new product |
||
63 | 3 | if ($this->isLastSku($row[$headers[ColumnKeys::SKU]])) { |
|
64 | return $row; |
||
65 | } |
||
66 | |||
67 | // prepare the URL key, return immediately if not available |
||
68 | 3 | if ($this->prepareUrlKey($row) == null) { |
|
69 | return $row; |
||
70 | } |
||
71 | |||
72 | // initialize the store view code |
||
73 | 3 | $this->setStoreViewCode($row[$headers[ColumnKeys::STORE_VIEW_CODE]] ?: StoreViewCodes::DEF); |
|
74 | |||
75 | // load the ID of the last entity |
||
76 | 3 | $lastEntityId = $this->getLastEntityId(); |
|
77 | |||
78 | // initialize the entity type to use |
||
79 | 3 | $entityType = UrlRewriteObserver::ENTITY_TYPE; |
|
80 | |||
81 | // load the product category IDs |
||
82 | 3 | $productCategoryIds = $this->getProductCategoryIds(); |
|
83 | |||
84 | // load the URL rewrites for the entity type and ID |
||
85 | 3 | $urlRewrites = $this->getUrlRewritesByEntityTypeAndEntityId($entityType, $lastEntityId); |
|
86 | |||
87 | // prepare the existing URLs => unserialize the metadata |
||
88 | 3 | $existingProductCategoryUrlRewrites = $this->prepareExistingCategoryUrlRewrites($urlRewrites); |
|
89 | |||
90 | // delete/create/update the URL rewrites |
||
91 | 3 | $this->deleteUrlRewrites($existingProductCategoryUrlRewrites); |
|
92 | 3 | $this->updateUrlRewrites(array_intersect_key($existingProductCategoryUrlRewrites, $productCategoryIds)); |
|
93 | 3 | $this->createUrlRewrites($productCategoryIds); |
|
94 | |||
95 | // returns the row |
||
96 | 3 | return $row; |
|
97 | } |
||
98 | |||
99 | /** |
||
100 | * Prepare's and set's the URL key from the passed row of the CSV file. |
||
101 | * |
||
102 | * @param array $row The row with the CSV data |
||
103 | * |
||
104 | * @return boolean TRUE, if the URL key has been prepared, else FALSE |
||
105 | */ |
||
106 | 3 | protected function prepareUrlKey($row) |
|
107 | { |
||
108 | |||
109 | // load the header information |
||
110 | 3 | $headers = $this->getHeaders(); |
|
111 | |||
112 | // query whether or not we've a URL key available in the CSV file row |
||
113 | 3 | if (isset($row[$headers[ColumnKeys::URL_KEY]])) { |
|
114 | 3 | $urlKey = $row[$headers[ColumnKeys::URL_KEY]]; |
|
115 | } |
||
116 | |||
117 | // query whether or not an URL key has been specified in the CSV file |
||
118 | 3 | if (empty($urlKey)) { |
|
119 | // if not, try to use the product name |
||
120 | if (isset($row[$headers[ColumnKeys::NAME]])) { |
||
121 | $productName = $row[$headers[ColumnKeys::NAME]]; |
||
122 | } |
||
123 | |||
124 | // if nor URL key AND product name are empty, return immediately |
||
125 | if (empty($productName)) { |
||
126 | return false; |
||
127 | } |
||
128 | |||
129 | // initialize the URL key with product name |
||
130 | $urlKey = $productName; |
||
131 | } |
||
132 | |||
133 | // convert and set the URL key |
||
134 | 3 | $this->setUrlKey($this->convertNameToUrlKey($urlKey)); |
|
135 | |||
136 | // return TRUE if the URL key has been prepared |
||
137 | 3 | return true; |
|
138 | } |
||
139 | |||
140 | /** |
||
141 | * Set's the prepared URL key. |
||
142 | * |
||
143 | * @param string $urlKey The prepared URL key |
||
144 | * |
||
145 | * @return void |
||
146 | */ |
||
147 | 3 | protected function setUrlKey($urlKey) |
|
151 | |||
152 | /** |
||
153 | * Return's the prepared URL key. |
||
154 | * |
||
155 | * @return string The prepared URL key |
||
156 | */ |
||
157 | 3 | protected function getUrlKey() |
|
161 | |||
162 | /** |
||
163 | * Initialize's and return's the URL key filter. |
||
164 | * |
||
165 | * @return \TechDivision\Import\Product\Utils\ConvertLiteralUrl The URL key filter |
||
166 | */ |
||
167 | 3 | protected function getUrlKeyFilter() |
|
171 | |||
172 | /** |
||
173 | * Convert's the passed string into a valid URL key. |
||
174 | * |
||
175 | * @param string $string The string to be converted, e. g. the product name |
||
176 | * |
||
177 | * @return string The converted string as valid URL key |
||
178 | */ |
||
179 | 3 | protected function convertNameToUrlKey($string) |
|
183 | |||
184 | /** |
||
185 | * Convert's the passed URL rewrites into an array with the category ID from the |
||
186 | * metadata as key and the URL rewrite as value. |
||
187 | * |
||
188 | * If now category ID can be found in the metadata, the ID of the store's root |
||
189 | * category is used. |
||
190 | * |
||
191 | * @param array $urlRewrites The URL rewrites to convert |
||
192 | * |
||
193 | * @return array The converted array with the de-serialized category IDs as key |
||
194 | */ |
||
195 | 3 | protected function prepareExistingCategoryUrlRewrites(array $urlRewrites) |
|
196 | { |
||
197 | |||
198 | // initialize the array for the existing URL rewrites |
||
199 | 3 | $existingProductCategoryUrlRewrites = array(); |
|
200 | |||
201 | // load the store's root category |
||
202 | 3 | $rootCategory = $this->getRootCategory(); |
|
203 | |||
204 | // iterate over the URL rewrites and convert them |
||
205 | 3 | foreach ($urlRewrites as $urlRewrite) { |
|
206 | // initialize the array with the metadata |
||
207 | 2 | $metadata = array(); |
|
208 | |||
209 | // de-serialize the category ID from the metadata |
||
210 | 2 | if ($md = $urlRewrite['metadata']) { |
|
211 | 2 | $metadata = unserialize($md); |
|
212 | } |
||
213 | |||
214 | // use the store's category ID if not serialized metadata is available |
||
215 | 2 | if (!isset($metadata['category_id'])) { |
|
216 | 2 | $metadata['category_id'] = $rootCategory[MemberNames::ENTITY_ID]; |
|
217 | } |
||
218 | |||
219 | // append the URL rewrite with the found category ID |
||
220 | 2 | $existingProductCategoryUrlRewrites[$metadata['category_id']] = $urlRewrite; |
|
221 | } |
||
222 | |||
223 | // return the array with the existing URL rewrites |
||
224 | 3 | return $existingProductCategoryUrlRewrites; |
|
225 | } |
||
226 | |||
227 | /** |
||
228 | * Remove's the URL rewrites with the passed data. |
||
229 | * |
||
230 | * @param array $existingProductCategoryUrlRewrites The array with the URL rewrites to remove |
||
231 | * |
||
232 | * @return void |
||
233 | */ |
||
234 | 3 | protected function deleteUrlRewrites(array $existingProductCategoryUrlRewrites) |
|
235 | { |
||
236 | |||
237 | // query whether or not we've any URL rewrites that have to be removed |
||
238 | 3 | if (sizeof($existingProductCategoryUrlRewrites) === 0) { |
|
239 | 1 | return; |
|
240 | } |
||
241 | |||
242 | // remove the URL rewrites |
||
243 | 2 | foreach ($existingProductCategoryUrlRewrites as $urlRewrite) { |
|
244 | 2 | $this->removeUrlRewrite(array(MemberNames::URL_REWRITE_ID => $urlRewrite[MemberNames::URL_REWRITE_ID])); |
|
245 | } |
||
246 | 2 | } |
|
247 | |||
248 | /** |
||
249 | * Create's the URL rewrites from the passed data. |
||
250 | * |
||
251 | * @param array $productCategoryIds The categories to create a URL rewrite for |
||
252 | * |
||
253 | * @return void |
||
254 | */ |
||
255 | 3 | protected function createUrlRewrites(array $productCategoryIds) |
|
256 | { |
||
257 | |||
258 | // query whether or not if there is any category to create a URL rewrite for |
||
259 | 3 | if (sizeof($productCategoryIds) === 0) { |
|
260 | return; |
||
261 | } |
||
262 | |||
263 | // iterate over the categories and create the URL rewrites |
||
264 | 3 | foreach ($productCategoryIds as $categoryId => $entityId) { |
|
265 | // load the category to create the URL rewrite for |
||
266 | 3 | $category = $this->getCategory($categoryId); |
|
267 | |||
268 | // initialize the values |
||
269 | 3 | $requestPath = $this->prepareRequestPath($category); |
|
270 | 3 | $targetPath = $this->prepareTargetPath($category); |
|
271 | 3 | $metadata = serialize($this->prepareMetadata($category)); |
|
272 | |||
273 | // initialize the URL rewrite data |
||
274 | 3 | $params = array('product', $entityId, $requestPath, $targetPath, 0, 1, null, 1, $metadata); |
|
275 | |||
276 | // create the URL rewrite |
||
277 | 3 | $this->persistUrlRewrite($params); |
|
278 | } |
||
279 | 3 | } |
|
280 | |||
281 | /** |
||
282 | * Update's existing URL rewrites by creating 301 redirect URL rewrites for each. |
||
283 | * |
||
284 | * @param array $existingProductCategoryUrlRewrites The array with the existing URL rewrites |
||
285 | * |
||
286 | * @return void |
||
287 | */ |
||
288 | 3 | protected function updateUrlRewrites(array $existingProductCategoryUrlRewrites) |
|
289 | { |
||
290 | |||
291 | // query whether or not, we've existing URL rewrites that need to be redirected |
||
292 | 3 | if (sizeof($existingProductCategoryUrlRewrites) === 0) { |
|
293 | 1 | return; |
|
294 | } |
||
295 | |||
296 | // iterate over the URL redirects that have to be redirected |
||
297 | 2 | foreach ($existingProductCategoryUrlRewrites as $categoryId => $urlRewrite) { |
|
298 | // load the category data |
||
299 | 2 | $category = $this->getCategory($categoryId); |
|
300 | |||
301 | // initialize the values |
||
302 | 2 | $entityId = $urlRewrite[MemberNames::ENTITY_ID]; |
|
303 | 2 | $requestPath = sprintf('%s', $urlRewrite['request_path']); |
|
304 | 2 | $targetPath = $this->prepareTargetPathForRedirect($category); |
|
305 | 2 | $metadata = serialize($this->prepareMetadata($category)); |
|
306 | |||
307 | // initialize the URL rewrite data |
||
308 | 2 | $params = array('product', $entityId, $requestPath, $targetPath, 301, 1, null, 0, $metadata); |
|
309 | |||
310 | // create the 301 redirect URL rewrite |
||
311 | 2 | $this->persistUrlRewrite($params); |
|
312 | } |
||
313 | 2 | } |
|
314 | |||
315 | /** |
||
316 | * Prepare's the target path for a URL rewrite. |
||
317 | * |
||
318 | * @param array $category The categroy with the URL path |
||
319 | * |
||
320 | * @return string The target path |
||
321 | */ |
||
322 | 3 | protected function prepareTargetPath(array $category) |
|
323 | { |
||
324 | |||
325 | // load the actual entity ID |
||
326 | 3 | $lastEntityId = $this->getLastEntityId(); |
|
327 | |||
328 | // initialize the target path |
||
329 | 3 | $targetPath = ''; |
|
330 | |||
331 | // query whether or not, the category is the root category |
||
332 | 3 | if ($this->isRootCategory($category)) { |
|
333 | 3 | $targetPath = sprintf('catalog/product/view/id/%d', $lastEntityId); |
|
334 | } else { |
||
335 | 2 | $targetPath = sprintf('catalog/product/view/id/%d/category/%d', $lastEntityId, $category[MemberNames::ENTITY_ID]); |
|
336 | } |
||
337 | |||
338 | // return the target path |
||
339 | 3 | return $targetPath; |
|
340 | } |
||
341 | |||
342 | /** |
||
343 | * Prepare's the request path for a URL rewrite. |
||
344 | * |
||
345 | * @param array $category The categroy with the URL path |
||
346 | * |
||
347 | * @return string The request path |
||
348 | */ |
||
349 | 3 | View Code Duplication | protected function prepareRequestPath(array $category) |
350 | { |
||
351 | |||
352 | // initialize the request path |
||
353 | 3 | $requestPath = ''; |
|
354 | |||
355 | // query whether or not, the category is the root category |
||
356 | 3 | if ($this->isRootCategory($category)) { |
|
357 | 3 | $requestPath = sprintf('%s.html', $this->getUrlKey()); |
|
358 | } else { |
||
359 | 2 | $requestPath = sprintf('%s/%s.html', $category[MemberNames::URL_PATH], $this->getUrlKey()); |
|
360 | } |
||
361 | |||
362 | // return the request path |
||
363 | 3 | return $requestPath; |
|
364 | } |
||
365 | |||
366 | /** |
||
367 | * Prepare's the target path for a 301 redirect URL rewrite. |
||
368 | * |
||
369 | * @param array $category The categroy with the URL path |
||
370 | * |
||
371 | * @return string The target path |
||
372 | */ |
||
373 | 2 | View Code Duplication | protected function prepareTargetPathForRedirect(array $category) |
374 | { |
||
375 | |||
376 | // initialize the target path |
||
377 | 2 | $targetPath = ''; |
|
378 | |||
379 | // query whether or not, the category is the root category |
||
380 | 2 | if ($this->isRootCategory($category)) { |
|
381 | 2 | $targetPath = sprintf('%s.html', $this->getUrlKey()); |
|
382 | } else { |
||
383 | 1 | $targetPath = sprintf('%s/%s.html', $category[MemberNames::URL_PATH], $this->getUrlKey()); |
|
384 | } |
||
385 | |||
386 | // return the target path |
||
387 | 2 | return $targetPath; |
|
388 | } |
||
389 | |||
390 | /** |
||
391 | * Prepare's the URL rewrite's metadata with the passed category values. |
||
392 | * |
||
393 | * @param array $category The category used for preparation |
||
394 | * |
||
395 | * @return array The metadata |
||
396 | */ |
||
397 | 3 | protected function prepareMetadata(array $category) |
|
414 | |||
415 | /** |
||
416 | * Set's the store view code the create the product/attributes for. |
||
417 | * |
||
418 | * @param string $storeViewCode The store view code |
||
419 | * |
||
420 | * @return void |
||
421 | */ |
||
422 | 3 | public function setStoreViewCode($storeViewCode) |
|
426 | |||
427 | /** |
||
428 | * Return's the root category for the actual view store. |
||
429 | * |
||
430 | * @return array The store's root category |
||
431 | * @throws \Exception Is thrown if the root category for the passed store code is NOT available |
||
432 | */ |
||
433 | 3 | public function getRootCategory() |
|
437 | |||
438 | /** |
||
439 | * Return's TRUE if the passed category IS the root category, else FALSE. |
||
440 | * |
||
441 | * @param array $category The category to query |
||
442 | * |
||
443 | * @return boolean TRUE if the passed category IS the root category |
||
444 | */ |
||
445 | 3 | public function isRootCategory(array $category) |
|
454 | |||
455 | /** |
||
456 | * Return's the list with category IDs the product is related with. |
||
457 | * |
||
458 | * @return array The product's category IDs |
||
459 | */ |
||
460 | 4 | public function getProductCategoryIds() |
|
464 | |||
465 | /** |
||
466 | * Return's the category with the passed ID. |
||
467 | * |
||
468 | * @param integer $categoryId The ID of the category to return |
||
469 | * |
||
470 | * @return array The category data |
||
471 | */ |
||
472 | 3 | public function getCategory($categoryId) |
|
476 | |||
477 | /** |
||
478 | * Return's the URL rewrites for the passed URL entity type and ID. |
||
479 | * |
||
480 | * @param string $entityType The entity type to load the URL rewrites for |
||
481 | * @param integer $entityId The entity ID to laod the rewrites for |
||
482 | * |
||
483 | * @return array The URL rewrites |
||
484 | */ |
||
485 | 4 | public function getUrlRewritesByEntityTypeAndEntityId($entityType, $entityId) |
|
489 | |||
490 | /** |
||
491 | * Persist's the URL write with the passed data. |
||
492 | * |
||
493 | * @param array $row The URL rewrite to persist |
||
494 | * |
||
495 | * @return void |
||
496 | */ |
||
497 | 4 | public function persistUrlRewrite($row) |
|
501 | |||
502 | /** |
||
503 | * Delete's the URL rewrite with the passed attributes. |
||
504 | * |
||
505 | * @param array $row The attributes of the entity to remove |
||
506 | * |
||
507 | * @return void |
||
508 | */ |
||
509 | 3 | public function removeUrlRewrite($row) |
|
513 | } |
||
514 |
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: