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 |
||
41 | class UrlRewriteObserver extends AbstractProductImportObserver |
||
42 | { |
||
43 | |||
44 | /** |
||
45 | * The entity type to load the URL rewrites for. |
||
46 | * |
||
47 | * @var string |
||
48 | */ |
||
49 | const ENTITY_TYPE = 'product'; |
||
50 | |||
51 | /** |
||
52 | * The key for the category in the metadata. |
||
53 | * |
||
54 | * @var string |
||
55 | */ |
||
56 | const CATEGORY_ID = 'category_id'; |
||
57 | |||
58 | /** |
||
59 | * The URL key from the CSV file column that has to be processed by the observer. |
||
60 | * |
||
61 | * @var string |
||
62 | */ |
||
63 | protected $urlKey; |
||
64 | |||
65 | /** |
||
66 | * The actual category ID to process. |
||
67 | * |
||
68 | * @var integer |
||
69 | */ |
||
70 | protected $categoryId; |
||
71 | |||
72 | /** |
||
73 | * The actual entity ID to process. |
||
74 | * |
||
75 | * @var integer |
||
76 | */ |
||
77 | protected $entityId; |
||
78 | |||
79 | /** |
||
80 | * The ID of the recently created URL rewrite. |
||
81 | * |
||
82 | * @var integer |
||
83 | */ |
||
84 | protected $urlRewriteId; |
||
85 | |||
86 | /** |
||
87 | * The array with the URL rewrites that has to be created. |
||
88 | * |
||
89 | * @var array |
||
90 | */ |
||
91 | protected $urlRewrites = array(); |
||
92 | |||
93 | /** |
||
94 | * The array with the category IDs related with the product. |
||
95 | * |
||
96 | * @var array |
||
97 | */ |
||
98 | protected $productCategoryIds = array(); |
||
99 | |||
100 | /** |
||
101 | * The product bunch processor instance. |
||
102 | * |
||
103 | * @var \TechDivision\Import\Product\UrlRewrite\Services\ProductUrlRewriteProcessorInterface |
||
104 | */ |
||
105 | protected $productUrlRewriteProcessor; |
||
106 | |||
107 | /** |
||
108 | * Initialize the observer with the passed product URL rewrite processor instance. |
||
109 | * |
||
110 | * @param \TechDivision\Import\Product\UrlRewrite\Services\ProductUrlRewriteProcessorInterface $productUrlRewriteProcessor The product URL rewrite processor instance |
||
111 | */ |
||
112 | 3 | public function __construct(ProductUrlRewriteProcessorInterface $productUrlRewriteProcessor) |
|
116 | |||
117 | /** |
||
118 | * Return's the product bunch processor instance. |
||
119 | * |
||
120 | * @return \TechDivision\Import\Product\Services\ProductBunchProcessorInterface The product bunch processor instance |
||
121 | */ |
||
122 | 3 | protected function getProductUrlRewriteProcessor() |
|
126 | |||
127 | /** |
||
128 | * Will be invoked by the action on the events the listener has been registered for. |
||
129 | * |
||
130 | * @param \TechDivision\Import\Subjects\SubjectInterface $subject The subject instance |
||
131 | * |
||
132 | * @return array The modified row |
||
133 | * @throws \Exception Is thrown, if the product is not available or no URL key has been specified |
||
134 | * @see \TechDivision\Import\Observers\ObserverInterface::handle() |
||
135 | */ |
||
136 | 3 | public function handle(SubjectInterface $subject) |
|
258 | |||
259 | /** |
||
260 | * Process the observer's business logic. |
||
261 | * |
||
262 | * @return void |
||
263 | */ |
||
264 | 3 | protected function process() |
|
309 | |||
310 | /** |
||
311 | * Initialize the category product with the passed attributes and returns an instance. |
||
312 | * |
||
313 | * @param array $attr The category product attributes |
||
314 | * |
||
315 | * @return array The initialized category product |
||
316 | */ |
||
317 | 2 | protected function initializeUrlRewrite(array $attr) |
|
321 | |||
322 | /** |
||
323 | * Initialize the URL rewrite product => category relation with the passed attributes |
||
324 | * and returns an instance. |
||
325 | * |
||
326 | * @param array $attr The URL rewrite product => category relation attributes |
||
327 | * |
||
328 | * @return array The initialized URL rewrite product => category relation |
||
329 | */ |
||
330 | 1 | protected function initializeUrlRewriteProductCategory($attr) |
|
334 | |||
335 | /** |
||
336 | * Prepare's the URL rewrites that has to be created/updated. |
||
337 | * |
||
338 | * @return void |
||
339 | */ |
||
340 | 3 | protected function prepareUrlRewrites() |
|
385 | |||
386 | /** |
||
387 | * Resolve's the parent categories of the category with the passed ID and relate's |
||
388 | * it with the product with the passed ID, if the category is top level OR has the |
||
389 | * anchor flag set. |
||
390 | * |
||
391 | * @param integer $categoryId The ID of the category to resolve the parents |
||
392 | * @param boolean $topLevel TRUE if the passed category has top level, else FALSE |
||
393 | * @param string $storeViewCode The store view code to resolve the category IDs for |
||
394 | * |
||
395 | * @return void |
||
396 | */ |
||
397 | 2 | protected function resolveCategoryIds($categoryId, $topLevel = false, $storeViewCode = StoreViewCodes::ADMIN) |
|
398 | { |
||
399 | |||
400 | // return immediately if this is the absolute root node |
||
401 | 2 | if ((integer) $categoryId === 1) { |
|
402 | 2 | return; |
|
403 | } |
||
404 | |||
405 | // load the data of the category with the passed ID |
||
406 | 2 | $category = $this->getCategory($categoryId, $storeViewCode); |
|
407 | |||
408 | // create the product category relation for the current category |
||
409 | 2 | $this->createProductCategoryRelation($category, $topLevel); |
|
410 | 2 | ||
411 | // load the root category |
||
412 | 2 | $rootCategory = $this->getRootCategory(); |
|
413 | 2 | ||
414 | // try to resolve the parent category IDs |
||
415 | if ($rootCategory[MemberNames::ENTITY_ID] !== ($parentId = $category[MemberNames::PARENT_ID])) { |
||
416 | $this->resolveCategoryIds($parentId, false); |
||
417 | 2 | } |
|
418 | 2 | } |
|
419 | 2 | ||
420 | /** |
||
421 | * Adds the entity product relation if necessary. |
||
422 | * |
||
423 | * @param array $category The category to create the relation for |
||
424 | 2 | * @param boolean $topLevel Whether or not the category has top level |
|
425 | * |
||
426 | * @return void |
||
427 | 2 | */ |
|
428 | 2 | protected function createProductCategoryRelation($category, $topLevel) |
|
429 | { |
||
430 | 2 | ||
431 | // query whether or not the product has already been related |
||
432 | if (in_array($category[MemberNames::ENTITY_ID], $this->productCategoryIds)) { |
||
433 | return; |
||
434 | } |
||
435 | |||
436 | // load the backend configuration value for whether or not the catalog product rewrites should be generated |
||
437 | $generateCategoryRewrites = $this->getGenerateCategoryProductRewritesOptionValue(); |
||
438 | |||
439 | 3 | // abort if generating product categories is disabled and category is not root |
|
440 | if ($generateCategoryRewrites === false && $this->isRootCategory($category) === false) { |
||
441 | return; |
||
442 | } |
||
443 | 3 | ||
444 | // create relation if the category is top level or has the anchor flag set |
||
445 | if ($topLevel || (integer) $category[MemberNames::IS_ANCHOR] === 1) { |
||
446 | 3 | $this->productCategoryIds[] = $category[MemberNames::ENTITY_ID]; |
|
447 | return; |
||
448 | } |
||
449 | 3 | ||
450 | 3 | $this->getSubject() |
|
451 | 3 | ->getSystemLogger() |
|
452 | ->debug( |
||
453 | sprintf( |
||
454 | 3 | 'Don\'t create URL rewrite for category "%s" because of missing anchor flag', |
|
455 | $category[MemberNames::PATH] |
||
456 | 3 | ) |
|
457 | 3 | ); |
|
458 | 3 | } |
|
459 | 3 | ||
460 | 3 | /** |
|
461 | 3 | * Returns the option value for whether or not to generate product catalog rewrites as well. |
|
462 | 3 | * |
|
463 | 3 | * @return bool |
|
464 | 3 | */ |
|
465 | protected function getGenerateCategoryProductRewritesOptionValue() |
||
466 | { |
||
467 | return (bool) $this->getSubject()->getCoreConfigData( |
||
468 | CoreConfigDataKeys::CATALOG_SEO_GENERATE_CATEGORY_PRODUCT_REWRITES, |
||
469 | true |
||
470 | ); |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | 2 | * Prepare the attributes of the entity that has to be persisted. |
|
475 | * |
||
476 | * @param string $storeViewCode The store view code to prepare the attributes for |
||
477 | * |
||
478 | 2 | * @return array The prepared attributes |
|
479 | */ |
||
480 | 2 | protected function prepareAttributes($storeViewCode) |
|
481 | 2 | { |
|
482 | 2 | ||
483 | // load the store ID to use |
||
484 | $storeId = $this->getSubject()->getRowStoreId(); |
||
485 | |||
486 | // load the category to create the URL rewrite for |
||
487 | $category = $this->getCategory($this->categoryId, $storeViewCode); |
||
488 | |||
489 | // initialize the values |
||
490 | $metadata = $this->prepareMetadata($category); |
||
491 | $targetPath = $this->prepareTargetPath($category); |
||
492 | $requestPath = $this->prepareRequestPath($category); |
||
493 | |||
494 | 3 | // return the prepared URL rewrite |
|
495 | return $this->initializeEntity( |
||
496 | array( |
||
497 | MemberNames::ENTITY_TYPE => UrlRewriteObserver::ENTITY_TYPE, |
||
498 | 3 | MemberNames::ENTITY_ID => $this->entityId, |
|
499 | 3 | MemberNames::REQUEST_PATH => $requestPath, |
|
500 | MemberNames::TARGET_PATH => $targetPath, |
||
501 | 2 | MemberNames::REDIRECT_TYPE => 0, |
|
502 | MemberNames::STORE_ID => $storeId, |
||
503 | MemberNames::DESCRIPTION => null, |
||
504 | MemberNames::IS_AUTOGENERATED => 1, |
||
505 | 3 | MemberNames::METADATA => $metadata ? json_encode($metadata) : null |
|
506 | ) |
||
507 | ); |
||
508 | } |
||
509 | |||
510 | /** |
||
511 | * Prepare's the URL rewrite product => category relation attributes. |
||
512 | * |
||
513 | * @return array The prepared attributes |
||
514 | */ |
||
515 | protected function prepareUrlRewriteProductCategoryAttributes() |
||
516 | 3 | { |
|
517 | |||
518 | // return the prepared product |
||
519 | return $this->initializeEntity( |
||
520 | 3 | array( |
|
521 | MemberNames::PRODUCT_ID => $this->entityId, |
||
522 | MemberNames::CATEGORY_ID => $this->categoryId, |
||
523 | 3 | MemberNames::URL_REWRITE_ID => $this->urlRewriteId |
|
524 | 3 | ) |
|
525 | ); |
||
526 | } |
||
527 | 2 | ||
528 | 2 | /** |
|
529 | * Prepare's the target path for a URL rewrite. |
||
530 | * |
||
531 | * @param array $category The categroy with the URL path |
||
532 | * |
||
533 | * @return string The target path |
||
534 | */ |
||
535 | protected function prepareTargetPath(array $category) |
||
536 | { |
||
537 | |||
538 | // query whether or not, the category is the root category |
||
539 | if ($this->isRootCategory($category)) { |
||
540 | $targetPath = sprintf('catalog/product/view/id/%d', $this->entityId); |
||
541 | } else { |
||
542 | $targetPath = sprintf('catalog/product/view/id/%d/category/%d', $this->entityId, $category[MemberNames::ENTITY_ID]); |
||
543 | } |
||
544 | |||
545 | // return the target path |
||
546 | return $targetPath; |
||
547 | } |
||
548 | |||
549 | /** |
||
550 | * Prepare's the request path for a URL rewrite or the target path for a 301 redirect. |
||
551 | 3 | * |
|
552 | * @param array $category The categroy with the URL path |
||
553 | * |
||
554 | * @return string The request path |
||
555 | 3 | * @throws \RuntimeException Is thrown, if the passed category has no or an empty value for attribute "url_path" |
|
556 | */ |
||
557 | protected function prepareRequestPath(array $category) |
||
558 | 3 | { |
|
559 | 3 | ||
560 | // load the product URL suffix to use |
||
561 | $urlSuffix = $this->getSubject()->getCoreConfigData(CoreConfigDataKeys::CATALOG_SEO_PRODUCT_URL_SUFFIX, '.html'); |
||
562 | |||
563 | 2 | // query whether or not, the category is the root category |
|
564 | if ($this->isRootCategory($category)) { |
||
565 | return sprintf('%s%s', $this->urlKey, $urlSuffix); |
||
566 | 2 | } else { |
|
567 | // query whether or not the category's "url_path" attribute, necessary to create a valid "request_path", is available |
||
568 | if (isset($category[MemberNames::URL_PATH]) && $category[MemberNames::URL_PATH]) { |
||
569 | return sprintf('%s/%s%s', $category[MemberNames::URL_PATH], $this->urlKey, $urlSuffix); |
||
570 | } |
||
571 | } |
||
572 | |||
573 | // throw an exception if the category's "url_path" attribute is NOT available |
||
574 | 3 | throw new \RuntimeException( |
|
575 | $this->appendExceptionSuffix( |
||
576 | 3 | sprintf( |
|
577 | 'Can\'t find mandatory attribute "%s" for category ID "%d", necessary to build a valid "request_path"', |
||
578 | MemberNames::URL_PATH, |
||
579 | $category[MemberNames::ENTITY_ID] |
||
580 | ) |
||
581 | ) |
||
582 | ); |
||
583 | } |
||
584 | |||
585 | /** |
||
586 | * Prepare's the URL rewrite's metadata with the passed category values. |
||
587 | * |
||
588 | 3 | * @param array $category The category used for preparation |
|
589 | * |
||
590 | 3 | * @return array|null The metadata |
|
591 | */ |
||
592 | protected function prepareMetadata(array $category) |
||
593 | { |
||
594 | |||
595 | // initialize the metadata |
||
596 | $metadata = array(); |
||
597 | |||
598 | // query whether or not, the passed category IS the root category |
||
599 | 3 | if ($this->isRootCategory($category)) { |
|
600 | return; |
||
601 | 3 | } |
|
602 | |||
603 | // if not, set the category ID in the metadata |
||
604 | $metadata[UrlRewriteObserver::CATEGORY_ID] = (string) $category[MemberNames::ENTITY_ID]; |
||
605 | |||
606 | // return the metadata |
||
607 | return $metadata; |
||
608 | } |
||
609 | |||
610 | /** |
||
611 | 3 | * Query whether or not the actual entity is visible. |
|
612 | * |
||
613 | * @return boolean TRUE if the entity is visible, else FALSE |
||
614 | */ |
||
615 | 3 | protected function isVisible() |
|
619 | |||
620 | /** |
||
621 | * Return's the visibility for the passed entity ID, if it already has been mapped. The mapping will be created |
||
622 | * by calling <code>\TechDivision\Import\Product\Subjects\BunchSubject::getVisibilityIdByValue</code> which will |
||
623 | * be done by the <code>\TechDivision\Import\Product\Callbacks\VisibilityCallback</code>. |
||
624 | * |
||
625 | * @return integer The visibility ID |
||
626 | * @throws \Exception Is thrown, if the entity ID has not been mapped |
||
627 | * @see \TechDivision\Import\Product\Subjects\BunchSubject::getVisibilityIdByValue() |
||
628 | */ |
||
629 | 2 | protected function getEntityIdVisibilityIdMapping() |
|
633 | |||
634 | /** |
||
635 | * Return's the root category for the actual view store. |
||
636 | * |
||
637 | * @return array The store's root category |
||
638 | * @throws \Exception Is thrown if the root category for the passed store code is NOT available |
||
639 | */ |
||
640 | protected function getRootCategory() |
||
641 | { |
||
644 | 2 | ||
645 | /** |
||
646 | * Return's TRUE if the passed category IS the root category, else FALSE. |
||
647 | * |
||
648 | * @param array $category The category to query |
||
649 | * |
||
650 | * @return boolean TRUE if the passed category IS the root category |
||
651 | */ |
||
652 | protected function isRootCategory(array $category) |
||
661 | |||
662 | /** |
||
663 | * Return's the category with the passed path. |
||
664 | * |
||
665 | * @param string $path The path of the category to return |
||
666 | 2 | * @param string $storeViewCode The store view code of the category to return |
|
667 | * |
||
668 | 2 | * @return array The category |
|
669 | */ |
||
670 | protected function getCategoryByPath($path, $storeViewCode = StoreViewCodes::ADMIN) |
||
674 | |||
675 | /** |
||
676 | * Return's the category with the passed ID. |
||
677 | * |
||
678 | * @param integer $categoryId The ID of the category to return |
||
679 | 3 | * @param string $storeViewCode The store view code of category to return |
|
680 | * |
||
681 | 3 | * @return array The category data |
|
682 | */ |
||
683 | protected function getCategory($categoryId, $storeViewCode = StoreViewCodes::ADMIN) |
||
687 | |||
688 | /** |
||
689 | * Persist's the URL rewrite with the passed data. |
||
690 | * |
||
691 | 3 | * @param array $row The URL rewrite to persist |
|
692 | * |
||
693 | 3 | * @return string The ID of the persisted entity |
|
694 | 3 | */ |
|
695 | protected function persistUrlRewrite($row) |
||
699 | |||
700 | /** |
||
701 | * Persist's the URL rewrite product => category relation with the passed data. |
||
702 | * |
||
703 | 3 | * @param array $row The URL rewrite product => category relation to persist |
|
704 | * |
||
705 | 3 | * @return void |
|
706 | 3 | */ |
|
707 | protected function persistUrlRewriteProductCategory($row) |
||
711 | |||
712 | /** |
||
713 | * Queries whether or not the passed SKU and store view code has already been processed. |
||
714 | * |
||
715 | 3 | * @param string $sku The SKU to check been processed |
|
716 | * @param string $storeViewCode The store view code to check been processed |
||
717 | 3 | * |
|
718 | * @return boolean TRUE if the SKU and store view code has been processed, else FALSE |
||
719 | */ |
||
720 | protected function storeViewHasBeenProcessed($sku, $storeViewCode) |
||
724 | |||
725 | /** |
||
726 | * Add the entity ID => visibility mapping for the actual entity ID. |
||
727 | * |
||
728 | * @param string $visibility The visibility of the actual entity to map |
||
729 | * |
||
730 | * @return void |
||
731 | */ |
||
732 | protected function addEntityIdVisibilityIdMapping($visibility) |
||
736 | |||
737 | /** |
||
738 | * Set's the ID of the product that has been created recently. |
||
739 | * |
||
740 | * @param string $lastEntityId The entity ID |
||
741 | * |
||
742 | * @return void |
||
743 | */ |
||
744 | protected function setLastEntityId($lastEntityId) |
||
748 | |||
749 | /** |
||
750 | * Load's and return's the product with the passed SKU. |
||
751 | * |
||
752 | * @param string $sku The SKU of the product to load |
||
753 | * |
||
754 | * @return array The product |
||
755 | */ |
||
756 | protected function loadProduct($sku) |
||
760 | } |
||
761 |
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.