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 Handler 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 Handler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
32 | class Handler implements UrlAliasHandlerInterface |
||
33 | { |
||
34 | const ROOT_LOCATION_ID = 1; |
||
35 | |||
36 | /** |
||
37 | * This is intentionally hardcoded for now as: |
||
38 | * 1. We don't implement this configuration option. |
||
39 | * 2. Such option should not be in this layer, should be handled higher up. |
||
40 | * |
||
41 | * @deprecated |
||
42 | */ |
||
43 | const CONTENT_REPOSITORY_ROOT_LOCATION_ID = 2; |
||
44 | |||
45 | /** |
||
46 | * The maximum level of alias depth. |
||
47 | */ |
||
48 | const MAX_URL_ALIAS_DEPTH_LEVEL = 60; |
||
49 | |||
50 | /** |
||
51 | * UrlAlias Gateway. |
||
52 | * |
||
53 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway |
||
54 | */ |
||
55 | protected $gateway; |
||
56 | |||
57 | /** |
||
58 | * Gateway for handling location data. |
||
59 | * |
||
60 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway |
||
61 | */ |
||
62 | protected $locationGateway; |
||
63 | |||
64 | /** |
||
65 | * UrlAlias Mapper. |
||
66 | * |
||
67 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper |
||
68 | */ |
||
69 | protected $mapper; |
||
70 | |||
71 | /** |
||
72 | * Caching language handler. |
||
73 | * |
||
74 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler |
||
75 | */ |
||
76 | protected $languageHandler; |
||
77 | |||
78 | /** |
||
79 | * URL slug converter. |
||
80 | * |
||
81 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter |
||
82 | */ |
||
83 | protected $slugConverter; |
||
84 | |||
85 | /** |
||
86 | * Gateway for handling content data. |
||
87 | * |
||
88 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\Gateway |
||
89 | */ |
||
90 | protected $contentGateway; |
||
91 | |||
92 | /** |
||
93 | * Language mask generator. |
||
94 | * |
||
95 | * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator |
||
96 | */ |
||
97 | protected $maskGenerator; |
||
98 | |||
99 | /** @var \eZ\Publish\SPI\Persistence\TransactionHandler */ |
||
100 | private $transactionHandler; |
||
101 | |||
102 | /** |
||
103 | * Creates a new UrlAlias Handler. |
||
104 | * |
||
105 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Gateway $gateway |
||
106 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Mapper $mapper |
||
107 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\Location\Gateway $locationGateway |
||
108 | * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler |
||
109 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter $slugConverter |
||
110 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\Gateway $contentGateway |
||
111 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\Language\MaskGenerator $maskGenerator |
||
112 | * @param \eZ\Publish\SPI\Persistence\TransactionHandler $transactionHandler |
||
113 | */ |
||
114 | public function __construct( |
||
133 | |||
134 | public function publishUrlAliasForLocation( |
||
153 | |||
154 | /** |
||
155 | * Internal publish method, accepting language ID instead of language code and optionally |
||
156 | * new alias ID (used when swapping Locations). |
||
157 | * |
||
158 | * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped() |
||
159 | * |
||
160 | * @param int $locationId |
||
161 | * @param int $parentLocationId |
||
162 | * @param string $name |
||
163 | * @param int $languageId |
||
164 | * @param bool $alwaysAvailable |
||
165 | * @param bool $updatePathIdentificationString legacy storage specific for updating ezcontentobject_tree.path_identification_string |
||
166 | * @param int $newId |
||
167 | */ |
||
168 | private function internalPublishUrlAliasForLocation( |
||
301 | |||
302 | /** |
||
303 | * Create a user chosen $alias pointing to $locationId in $languageCode. |
||
304 | * |
||
305 | * If $languageCode is null the $alias is created in the system's default |
||
306 | * language. $alwaysAvailable makes the alias available in all languages. |
||
307 | * |
||
308 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException |
||
309 | * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException |
||
310 | * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException |
||
311 | * |
||
312 | * @param mixed $locationId |
||
313 | * @param string $path |
||
314 | * @param bool $forwarding |
||
315 | * @param string $languageCode |
||
316 | * @param bool $alwaysAvailable |
||
317 | * |
||
318 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias |
||
319 | */ |
||
320 | public function createCustomUrlAlias($locationId, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false) |
||
330 | |||
331 | /** |
||
332 | * Create a user chosen $alias pointing to a resource in $languageCode. |
||
333 | * This method does not handle location resources - if a user enters a location target |
||
334 | * the createCustomUrlAlias method has to be used. |
||
335 | * |
||
336 | * If $languageCode is null the $alias is created in the system's default |
||
337 | * language. $alwaysAvailable makes the alias available in all languages. |
||
338 | * |
||
339 | * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException if the path already exists for the given language |
||
340 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the path is broken |
||
341 | * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException |
||
342 | * |
||
343 | * @param string $resource |
||
344 | * @param string $path |
||
345 | * @param bool $forwarding |
||
346 | * @param string $languageCode |
||
347 | * @param bool $alwaysAvailable |
||
348 | * |
||
349 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias |
||
350 | */ |
||
351 | public function createGlobalUrlAlias($resource, $path, $forwarding = false, $languageCode = null, $alwaysAvailable = false) |
||
361 | |||
362 | /** |
||
363 | * Internal method for creating global or custom URL alias (these are handled in the same way). |
||
364 | * |
||
365 | * @throws \eZ\Publish\Core\Base\Exceptions\ForbiddenException if the path already exists for the given language |
||
366 | * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException |
||
367 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException |
||
368 | * |
||
369 | * @param string $action |
||
370 | * @param string $path |
||
371 | * @param bool $forward |
||
372 | * @param string|null $languageCode |
||
373 | * @param bool $alwaysAvailable |
||
374 | * |
||
375 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias |
||
376 | */ |
||
377 | protected function createUrlAlias($action, $path, $forward, $languageCode, $alwaysAvailable) |
||
461 | |||
462 | /** |
||
463 | * Convenience method for inserting nop type row. |
||
464 | * |
||
465 | * @param mixed $parentId |
||
466 | * @param string $text |
||
467 | * @param string $textMD5 |
||
468 | * |
||
469 | * @return mixed |
||
470 | */ |
||
471 | protected function insertNopEntry($parentId, $text, $textMD5) |
||
483 | |||
484 | /** |
||
485 | * List of user generated or autogenerated url entries, pointing to $locationId. |
||
486 | * |
||
487 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException |
||
488 | * |
||
489 | * @param mixed $locationId |
||
490 | * @param bool $custom if true the user generated aliases are listed otherwise the autogenerated |
||
491 | * |
||
492 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[] |
||
493 | */ |
||
494 | View Code Duplication | public function listURLAliasesForLocation($locationId, $custom = false) |
|
503 | |||
504 | /** |
||
505 | * List global aliases. |
||
506 | * |
||
507 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException |
||
508 | * |
||
509 | * @param string|null $languageCode |
||
510 | * @param int $offset |
||
511 | * @param int $limit |
||
512 | * |
||
513 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias[] |
||
514 | */ |
||
515 | View Code Duplication | public function listGlobalURLAliases($languageCode = null, $offset = 0, $limit = -1) |
|
524 | |||
525 | /** |
||
526 | * Removes url aliases. |
||
527 | * |
||
528 | * Autogenerated aliases are not removed by this method. |
||
529 | * |
||
530 | * @param \eZ\Publish\SPI\Persistence\Content\UrlAlias[] $urlAliases |
||
531 | * |
||
532 | * @return bool |
||
533 | */ |
||
534 | public function removeURLAliases(array $urlAliases) |
||
547 | |||
548 | /** |
||
549 | * Looks up a url alias for the given url. |
||
550 | * |
||
551 | * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException |
||
552 | * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException |
||
553 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException |
||
554 | * |
||
555 | * @param string $url |
||
556 | * |
||
557 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias |
||
558 | */ |
||
559 | public function lookup($url) |
||
595 | |||
596 | /** |
||
597 | * Loads URL alias by given $id. |
||
598 | * |
||
599 | * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException |
||
600 | * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException |
||
601 | * |
||
602 | * @param string $id |
||
603 | * |
||
604 | * @return \eZ\Publish\SPI\Persistence\Content\UrlAlias |
||
605 | */ |
||
606 | public function loadUrlAlias($id) |
||
619 | |||
620 | /** |
||
621 | * Notifies the underlying engine that a location has moved. |
||
622 | * |
||
623 | * This method triggers the change of the autogenerated aliases. |
||
624 | * |
||
625 | * @param mixed $locationId |
||
626 | * @param mixed $oldParentId |
||
627 | * @param mixed $newParentId |
||
628 | */ |
||
629 | public function locationMoved($locationId, $oldParentId, $newParentId) |
||
649 | |||
650 | /** |
||
651 | * Notifies the underlying engine that a location was copied. |
||
652 | * |
||
653 | * This method triggers the creation of the autogenerated aliases for the copied locations |
||
654 | * |
||
655 | * @param mixed $locationId |
||
656 | * @param mixed $newLocationId |
||
657 | * @param mixed $newParentId |
||
658 | */ |
||
659 | public function locationCopied($locationId, $newLocationId, $newParentId) |
||
672 | |||
673 | /** |
||
674 | * Notify the underlying engine that a Location has been swapped. |
||
675 | * |
||
676 | * This method triggers the change of the autogenerated aliases. |
||
677 | * |
||
678 | * @param int $location1Id |
||
679 | * @param int $location1ParentId |
||
680 | * @param int $location2Id |
||
681 | * @param int $location2ParentId |
||
682 | */ |
||
683 | public function locationSwapped($location1Id, $location1ParentId, $location2Id, $location2ParentId) |
||
733 | |||
734 | /** |
||
735 | * @param array $contentInfo |
||
736 | * |
||
737 | * @return array |
||
738 | */ |
||
739 | private function getNamesForAllLanguages(array $contentInfo) |
||
756 | |||
757 | /** |
||
758 | * Historizes given existing active entries for two swapped Locations. |
||
759 | * |
||
760 | * This should be done before republishing URL aliases, in order to avoid unnecessary |
||
761 | * conflicts when swapped Locations are siblings. |
||
762 | * |
||
763 | * We need to historize everything separately per language (mask), in case the entries |
||
764 | * remain history future publishing reusages need to be able to take them over cleanly. |
||
765 | * |
||
766 | * @see \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\Handler::locationSwapped() |
||
767 | * |
||
768 | * @param array $location1Entries |
||
769 | * @param array $location2Entries |
||
770 | */ |
||
771 | private function historizeBeforeSwap($location1Entries, $location2Entries) |
||
781 | |||
782 | /** |
||
783 | * Decides if UrlAlias for $location2 should be published first. |
||
784 | * |
||
785 | * The order in which Locations are published only matters if swapped Locations are siblings and they have the same |
||
786 | * name in a given language. In this case, the UrlAlias for Location which previously had lower number at the end of |
||
787 | * its UrlAlias text (or no number at all) should be published first. This ensures that the number still stays lower |
||
788 | * for this Location after the swap. If it wouldn't stay lower, then swapping Locations in conjunction with swapping |
||
789 | * UrlAliases would effectively cancel each other. |
||
790 | * |
||
791 | * @param array $location1Entries |
||
792 | * @param int $location1ParentId |
||
793 | * @param string $name1 |
||
794 | * @param array $location2Entries |
||
795 | * @param int $location2ParentId |
||
796 | * @param string $name2 |
||
797 | * @param int $languageId |
||
798 | * |
||
799 | * @return bool |
||
800 | */ |
||
801 | private function shouldUrlAliasForSecondLocationBePublishedFirst( |
||
825 | |||
826 | /** |
||
827 | * Get in a proper order - to be published - a list of URL aliases for swapped Locations. |
||
828 | * |
||
829 | * @see shouldUrlAliasForSecondLocationBePublishedFirst |
||
830 | * |
||
831 | * @param \eZ\Publish\SPI\Persistence\Content\Language $language |
||
832 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties $location1 |
||
833 | * @param \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\SwappedLocationProperties $location2 |
||
834 | * |
||
835 | * @return \eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\DTO\UrlAliasForSwappedLocation[] |
||
836 | */ |
||
837 | private function getUrlAliasesForSwappedLocations( |
||
881 | |||
882 | /** |
||
883 | * @param array $locationEntries |
||
884 | * @param int $languageId |
||
885 | * |
||
886 | * @return array|null |
||
887 | */ |
||
888 | private function getLocationEntryInLanguage(array $locationEntries, $languageId) |
||
899 | |||
900 | /** |
||
901 | * Returns possibly corrected alias id for given $locationId !! For use as parent id in logic. |
||
902 | * |
||
903 | * First level entries must have parent id set to 0 instead of their parent location alias id. |
||
904 | * There are two cases when alias id needs to be corrected: |
||
905 | * 1) location is special location without URL alias (location with id=1 in standard installation) |
||
906 | * 2) location is site root location, having special root entry in the ezurlalias_ml table (location with id=2 |
||
907 | * in standard installation) |
||
908 | * |
||
909 | * @param mixed $locationId |
||
910 | * |
||
911 | * @return mixed |
||
912 | */ |
||
913 | protected function getRealAliasId($locationId) |
||
931 | |||
932 | /** |
||
933 | * Recursively copies aliases from old parent under new parent. |
||
934 | * |
||
935 | * @param array $actionMap |
||
936 | * @param mixed $oldParentAliasId |
||
937 | * @param mixed $newParentAliasId |
||
938 | */ |
||
939 | protected function copySubtree($actionMap, $oldParentAliasId, $newParentAliasId) |
||
963 | |||
964 | /** |
||
965 | * @param mixed $oldParentId |
||
966 | * @param mixed $newParentId |
||
967 | * |
||
968 | * @return array |
||
969 | */ |
||
970 | protected function getCopiedLocationsMap($oldParentId, $newParentId) |
||
982 | |||
983 | public function locationDeleted($locationId): array |
||
984 | { |
||
985 | $action = 'eznode:' . $locationId; |
||
986 | $entry = $this->gateway->loadAutogeneratedEntry($action); |
||
987 | $entryId = $entry['id']; |
||
988 | |||
989 | $this->removeSubtree($entryId, $action, $entry['is_original']); |
||
990 | |||
991 | // after location-delete process completed |
||
992 | // check if entry had children; if yes - then restore them as nop-type |
||
993 | // for historical aliases relates to that entry |
||
994 | $notDeletedChildrenAliases = $this->gateway->getAllChildrenAliases($entryId); |
||
995 | if (count($notDeletedChildrenAliases) > 0) { |
||
996 | $this->insertAliasEntryAsNop($entry); |
||
997 | } |
||
998 | |||
999 | return $notDeletedChildrenAliases; |
||
1000 | } |
||
1001 | |||
1002 | /** |
||
1003 | * Notifies the underlying engine that Locations Content Translation was removed. |
||
1004 | * |
||
1005 | * @param int[] $locationIds all Locations of the Content that got Translation removed |
||
1006 | * @param string $languageCode language code of the removed Translation |
||
1007 | */ |
||
1008 | public function translationRemoved(array $locationIds, $languageCode) |
||
1009 | { |
||
1010 | $languageId = $this->languageHandler->loadByLanguageCode($languageCode)->id; |
||
1011 | |||
1012 | $actions = []; |
||
1013 | foreach ($locationIds as $locationId) { |
||
1014 | $actions[] = 'eznode:' . $locationId; |
||
1015 | } |
||
1016 | $this->gateway->bulkRemoveTranslation($languageId, $actions); |
||
1017 | } |
||
1018 | |||
1019 | /** |
||
1020 | * Recursively removes aliases by given $id and $action. |
||
1021 | * |
||
1022 | * $original parameter is used to limit removal of moved Location aliases to history entries only. |
||
1023 | * |
||
1024 | * @param mixed $id |
||
1025 | * @param string $action |
||
1026 | * @param mixed $original |
||
1027 | */ |
||
1028 | protected function removeSubtree($id, $action, $original) |
||
1029 | { |
||
1030 | // Remove first to avoid unnecessary recursion. |
||
1031 | if ($original) { |
||
1032 | // If entry is original remove all for action (history and custom entries included). |
||
1033 | $this->gateway->remove($action); |
||
1034 | } else { |
||
1035 | // Else entry is history, so remove only for action with the id. |
||
1036 | // This means $id grouped history entries are removed, other history, active autogenerated |
||
1037 | // and custom are left alone. |
||
1038 | $this->gateway->remove($action, $id); |
||
1039 | } |
||
1040 | |||
1041 | // Load all autogenerated for parent $id, including history. |
||
1042 | $entries = $this->gateway->loadAutogeneratedEntries($id, true); |
||
1043 | |||
1044 | foreach ($entries as $entry) { |
||
1045 | $this->removeSubtree($entry['id'], $entry['action'], $entry['is_original']); |
||
1046 | } |
||
1047 | } |
||
1048 | |||
1049 | /** |
||
1050 | * @param string $text |
||
1051 | * |
||
1052 | * @return string |
||
1053 | */ |
||
1054 | protected function getHash($text) |
||
1055 | { |
||
1056 | return md5(mb_strtolower($text, 'UTF-8')); |
||
1057 | } |
||
1058 | |||
1059 | /** |
||
1060 | * {@inheritdoc} |
||
1061 | */ |
||
1062 | public function archiveUrlAliasesForDeletedTranslations($locationId, $parentLocationId, array $languageCodes) |
||
1063 | { |
||
1064 | $parentId = $this->getRealAliasId($parentLocationId); |
||
1065 | |||
1066 | $data = $this->gateway->loadLocationEntries($locationId); |
||
1067 | // filter removed Translations |
||
1068 | $removedLanguages = array_diff( |
||
1069 | $this->mapper->extractLanguageCodesFromData($data), |
||
1070 | $languageCodes |
||
1071 | ); |
||
1072 | |||
1073 | if (empty($removedLanguages)) { |
||
1074 | return; |
||
1075 | } |
||
1076 | |||
1077 | // map languageCodes to their IDs |
||
1078 | $languageIds = array_map( |
||
1079 | function ($languageCode) { |
||
1080 | return $this->languageHandler->loadByLanguageCode($languageCode)->id; |
||
1081 | }, |
||
1082 | $removedLanguages |
||
1083 | ); |
||
1084 | |||
1085 | $this->gateway->archiveUrlAliasesForDeletedTranslations($locationId, $parentId, $languageIds); |
||
1086 | } |
||
1087 | |||
1088 | /** |
||
1089 | * Remove corrupted URL aliases (global, custom and system). |
||
1090 | * |
||
1091 | * @return int Number of removed URL aliases |
||
1092 | * |
||
1093 | * @throws \Exception |
||
1094 | */ |
||
1095 | public function deleteCorruptedUrlAliases() |
||
1096 | { |
||
1097 | $this->transactionHandler->beginTransaction(); |
||
1098 | try { |
||
1099 | $totalCount = $this->gateway->deleteUrlAliasesWithoutLocation(); |
||
1100 | $totalCount += $this->gateway->deleteUrlAliasesWithoutParent(); |
||
1101 | $totalCount += $this->gateway->deleteUrlAliasesWithBrokenLink(); |
||
1102 | $totalCount += $this->gateway->deleteUrlNopAliasesWithoutChildren(); |
||
1103 | |||
1104 | $this->transactionHandler->commit(); |
||
1105 | |||
1106 | return $totalCount; |
||
1107 | } catch (\Exception $e) { |
||
1108 | $this->transactionHandler->rollback(); |
||
1109 | throw $e; |
||
1110 | } |
||
1111 | } |
||
1112 | |||
1113 | /** |
||
1114 | * Attempt repairing auto-generated URL aliases for the given Location (including history). |
||
1115 | * |
||
1116 | * Note: it is assumed that at this point original, working, URL Alias for Location is published. |
||
1117 | * |
||
1118 | * @param int $locationId |
||
1119 | * |
||
1120 | * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException |
||
1121 | */ |
||
1122 | public function repairBrokenUrlAliasesForLocation(int $locationId) |
||
1130 | |||
1131 | private function insertAliasEntryAsNop(array $aliasEntry): void |
||
1132 | { |
||
1133 | $aliasEntry['action'] = 'nop:'; |
||
1134 | $aliasEntry['action_type'] = 'nop'; |
||
1135 | |||
1136 | $this->gateway->insertRow($aliasEntry); |
||
1138 | } |
||
1139 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.
Either this assignment is in error or an instanceof check should be added for that assignment.