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 GedcomRecord 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 GedcomRecord, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 26 | class GedcomRecord { |
||
| 27 | const RECORD_TYPE = 'UNKNOWN'; |
||
| 28 | const URL_PREFIX = 'gedrecord.php?pid='; |
||
| 29 | |||
| 30 | /** @var string The record identifier */ |
||
| 31 | protected $xref; |
||
| 32 | |||
| 33 | /** @var Tree The family tree to which this record belongs */ |
||
| 34 | protected $tree; |
||
| 35 | |||
| 36 | /** @var string GEDCOM data (before any pending edits) */ |
||
| 37 | protected $gedcom; |
||
| 38 | |||
| 39 | /** @var string|null GEDCOM data (after any pending edits) */ |
||
| 40 | protected $pending; |
||
| 41 | |||
| 42 | /** @var Fact[] facts extracted from $gedcom/$pending */ |
||
| 43 | protected $facts; |
||
| 44 | |||
| 45 | /** @var bool Can we display details of this record to Auth::PRIV_PRIVATE */ |
||
| 46 | private $disp_public; |
||
| 47 | |||
| 48 | /** @var bool Can we display details of this record to Auth::PRIV_USER */ |
||
| 49 | private $disp_user; |
||
| 50 | |||
| 51 | /** @var bool Can we display details of this record to Auth::PRIV_NONE */ |
||
| 52 | private $disp_none; |
||
| 53 | |||
| 54 | /** @var string[][] All the names of this individual */ |
||
| 55 | protected $_getAllNames; |
||
| 56 | |||
| 57 | /** @var int Cached result */ |
||
| 58 | protected $_getPrimaryName; |
||
| 59 | |||
| 60 | /** @var int Cached result */ |
||
| 61 | protected $_getSecondaryName; |
||
| 62 | |||
| 63 | /** @var GedcomRecord[][] Allow getInstance() to return references to existing objects */ |
||
| 64 | protected static $gedcom_record_cache; |
||
| 65 | |||
| 66 | /** @var \stdClass[][] Fetch all pending edits in one database query */ |
||
| 67 | private static $pending_record_cache; |
||
| 68 | |||
| 69 | /** |
||
| 70 | * Create a GedcomRecord object from raw GEDCOM data. |
||
| 71 | * |
||
| 72 | * @param string $xref |
||
| 73 | * @param string $gedcom an empty string for new/pending records |
||
| 74 | * @param string|null $pending null for a record with no pending edits, |
||
| 75 | * empty string for records with pending deletions |
||
| 76 | * @param Tree $tree |
||
| 77 | */ |
||
| 78 | public function __construct($xref, $gedcom, $pending, $tree) { |
||
| 86 | |||
| 87 | /** |
||
| 88 | * Split the record into facts |
||
| 89 | */ |
||
| 90 | private function parseFacts() { |
||
| 122 | |||
| 123 | /** |
||
| 124 | * Get an instance of a GedcomRecord object. For single records, |
||
| 125 | * we just receive the XREF. For bulk records (such as lists |
||
| 126 | * and search results) we can receive the GEDCOM data as well. |
||
| 127 | * |
||
| 128 | * @param string $xref |
||
| 129 | * @param Tree $tree |
||
| 130 | * @param string|null $gedcom |
||
| 131 | * |
||
| 132 | * @throws \Exception |
||
| 133 | * |
||
| 134 | * @return GedcomRecord|Individual|Family|Source|Repository|Media|Note|null |
||
| 135 | */ |
||
| 136 | public static function getInstance($xref, Tree $tree, $gedcom = null) { |
||
| 229 | |||
| 230 | /** |
||
| 231 | * Fetch data from the database |
||
| 232 | * |
||
| 233 | * @param string $xref |
||
| 234 | * @param int $tree_id |
||
| 235 | * |
||
| 236 | * @return null|string |
||
| 237 | */ |
||
| 238 | protected static function fetchGedcomRecord($xref, $tree_id) { |
||
| 273 | |||
| 274 | /** |
||
| 275 | * Get the XREF for this record |
||
| 276 | * |
||
| 277 | * @return string |
||
| 278 | */ |
||
| 279 | public function getXref() { |
||
| 282 | |||
| 283 | /** |
||
| 284 | * Get the tree to which this record belongs |
||
| 285 | * |
||
| 286 | * @return Tree |
||
| 287 | */ |
||
| 288 | public function getTree() { |
||
| 291 | |||
| 292 | /** |
||
| 293 | * Application code should access data via Fact objects. |
||
| 294 | * This function exists to support old code. |
||
| 295 | * |
||
| 296 | * @return string |
||
| 297 | */ |
||
| 298 | public function getGedcom() { |
||
| 305 | |||
| 306 | /** |
||
| 307 | * Does this record have a pending change? |
||
| 308 | * |
||
| 309 | * @return bool |
||
| 310 | */ |
||
| 311 | public function isPendingAddtion() { |
||
| 314 | |||
| 315 | /** |
||
| 316 | * Does this record have a pending deletion? |
||
| 317 | * |
||
| 318 | * @return bool |
||
| 319 | */ |
||
| 320 | public function isPendingDeletion() { |
||
| 323 | |||
| 324 | /** |
||
| 325 | * Generate a URL to this record, suitable for use in HTML, etc. |
||
| 326 | * |
||
| 327 | * @return string |
||
| 328 | */ |
||
| 329 | public function getHtmlUrl() { |
||
| 332 | |||
| 333 | /** |
||
| 334 | * Generate a URL to this record, suitable for use in javascript, HTTP headers, etc. |
||
| 335 | * |
||
| 336 | * @return string |
||
| 337 | */ |
||
| 338 | public function getRawUrl() { |
||
| 341 | |||
| 342 | /** |
||
| 343 | * Generate a URL to this record. |
||
| 344 | * |
||
| 345 | * @param string $link |
||
| 346 | * @param string $separator |
||
| 347 | * |
||
| 348 | * @return string |
||
| 349 | */ |
||
| 350 | private function getLinkUrl($link, $separator) { |
||
| 353 | |||
| 354 | /** |
||
| 355 | * Work out whether this record can be shown to a user with a given access level |
||
| 356 | * |
||
| 357 | * @param int $access_level |
||
| 358 | * |
||
| 359 | * @return bool |
||
| 360 | */ |
||
| 361 | private function canShowRecord($access_level) { |
||
| 397 | |||
| 398 | /** |
||
| 399 | * Each object type may have its own special rules, and re-implement this function. |
||
| 400 | * |
||
| 401 | * @param int $access_level |
||
| 402 | * |
||
| 403 | * @return bool |
||
| 404 | */ |
||
| 405 | protected function canShowByType($access_level) { |
||
| 416 | |||
| 417 | /** |
||
| 418 | * Can the details of this record be shown? |
||
| 419 | * |
||
| 420 | * @param int|null $access_level |
||
| 421 | * |
||
| 422 | * @return bool |
||
| 423 | */ |
||
| 424 | public function canShow($access_level = null) { |
||
| 459 | |||
| 460 | /** |
||
| 461 | * Can the name of this record be shown? |
||
| 462 | * |
||
| 463 | * @param int|null $access_level |
||
| 464 | * |
||
| 465 | * @return bool |
||
| 466 | */ |
||
| 467 | public function canShowName($access_level = null) { |
||
| 474 | |||
| 475 | /** |
||
| 476 | * Can we edit this record? |
||
| 477 | * |
||
| 478 | * @return bool |
||
| 479 | */ |
||
| 480 | public function canEdit() { |
||
| 483 | |||
| 484 | /** |
||
| 485 | * Remove private data from the raw gedcom record. |
||
| 486 | * Return both the visible and invisible data. We need the invisible data when editing. |
||
| 487 | * |
||
| 488 | * @param int $access_level |
||
| 489 | * |
||
| 490 | * @return string |
||
| 491 | */ |
||
| 492 | public function privatizeGedcom($access_level) { |
||
| 514 | |||
| 515 | /** |
||
| 516 | * Generate a private version of this record |
||
| 517 | * |
||
| 518 | * @param int $access_level |
||
| 519 | * |
||
| 520 | * @return string |
||
| 521 | */ |
||
| 522 | protected function createPrivateGedcomRecord($access_level) { |
||
| 525 | |||
| 526 | /** |
||
| 527 | * Convert a name record into sortable and full/display versions. This default |
||
| 528 | * should be OK for simple record types. INDI/FAM records will need to redefine it. |
||
| 529 | * |
||
| 530 | * @param string $type |
||
| 531 | * @param string $value |
||
| 532 | * @param string $gedcom |
||
| 533 | */ |
||
| 534 | protected function addName($type, $value, $gedcom) { |
||
| 542 | |||
| 543 | /** |
||
| 544 | * Get all the names of a record, including ROMN, FONE and _HEB alternatives. |
||
| 545 | * Records without a name (e.g. FAM) will need to redefine this function. |
||
| 546 | * Parameters: the level 1 fact containing the name. |
||
| 547 | * Return value: an array of name structures, each containing |
||
| 548 | * ['type'] = the gedcom fact, e.g. NAME, TITL, FONE, _HEB, etc. |
||
| 549 | * ['full'] = the name as specified in the record, e.g. 'Vincent van Gogh' or 'John Unknown' |
||
| 550 | * ['sort'] = a sortable version of the name (not for display), e.g. 'Gogh, Vincent' or '@N.N., John' |
||
| 551 | * |
||
| 552 | * @param int $level |
||
| 553 | * @param string $fact_type |
||
| 554 | * @param Fact[] $facts |
||
| 555 | */ |
||
| 556 | protected function extractNamesFromFacts($level, $fact_type, $facts) { |
||
| 577 | |||
| 578 | /** |
||
| 579 | * Default for "other" object types |
||
| 580 | */ |
||
| 581 | public function extractNames() { |
||
| 584 | |||
| 585 | /** |
||
| 586 | * Derived classes should redefine this function, otherwise the object will have no name |
||
| 587 | * |
||
| 588 | * @return string[][] |
||
| 589 | */ |
||
| 590 | public function getAllNames() { |
||
| 607 | |||
| 608 | /** |
||
| 609 | * If this object has no name, what do we call it? |
||
| 610 | * |
||
| 611 | * @return string |
||
| 612 | */ |
||
| 613 | public function getFallBackName() { |
||
| 616 | |||
| 617 | /** |
||
| 618 | * Which of the (possibly several) names of this record is the primary one. |
||
| 619 | * |
||
| 620 | * @return int |
||
| 621 | */ |
||
| 622 | public function getPrimaryName() { |
||
| 645 | |||
| 646 | /** |
||
| 647 | * Which of the (possibly several) names of this record is the secondary one. |
||
| 648 | * |
||
| 649 | * @return int |
||
| 650 | */ |
||
| 651 | public function getSecondaryName() { |
||
| 670 | |||
| 671 | /** |
||
| 672 | * Allow the choice of primary name to be overidden, e.g. in a search result |
||
| 673 | * |
||
| 674 | * @param int $n |
||
| 675 | */ |
||
| 676 | public function setPrimaryName($n) { |
||
| 680 | |||
| 681 | /** |
||
| 682 | * Allow native PHP functions such as array_unique() to work with objects |
||
| 683 | * |
||
| 684 | * @return string |
||
| 685 | */ |
||
| 686 | public function __toString() { |
||
| 689 | |||
| 690 | /** |
||
| 691 | * Static helper function to sort an array of objects by name |
||
| 692 | * Records whose names cannot be displayed are sorted at the end. |
||
| 693 | * |
||
| 694 | * @param GedcomRecord $x |
||
| 695 | * @param GedcomRecord $y |
||
| 696 | * |
||
| 697 | * @return int |
||
| 698 | */ |
||
| 699 | public static function compare(GedcomRecord $x, GedcomRecord $y) { |
||
| 714 | |||
| 715 | /** |
||
| 716 | * Get variants of the name |
||
| 717 | * |
||
| 718 | * @return string |
||
| 719 | */ |
||
| 720 | public function getFullName() { |
||
| 729 | |||
| 730 | /** |
||
| 731 | * Get a sortable version of the name. Do not display this! |
||
| 732 | * |
||
| 733 | * @return string |
||
| 734 | */ |
||
| 735 | public function getSortName() { |
||
| 741 | |||
| 742 | /** |
||
| 743 | * Get the full name in an alternative character set |
||
| 744 | * |
||
| 745 | * @return null|string |
||
| 746 | */ |
||
| 747 | public function getAddName() { |
||
| 756 | |||
| 757 | /** |
||
| 758 | * Format this object for display in a list |
||
| 759 | * If $find is set, then we are displaying items from a selection list. |
||
| 760 | * $name allows us to use something other than the record name. |
||
| 761 | * |
||
| 762 | * @param string $tag |
||
| 763 | * @param bool $find |
||
| 764 | * @param null $name |
||
| 765 | * |
||
| 766 | * @return string |
||
| 767 | */ |
||
| 768 | public function formatList($tag = 'li', $find = false, $name = null) { |
||
| 782 | |||
| 783 | /** |
||
| 784 | * This function should be redefined in derived classes to show any major |
||
| 785 | * identifying characteristics of this record. |
||
| 786 | * |
||
| 787 | * @return string |
||
| 788 | */ |
||
| 789 | public function formatListDetails() { |
||
| 792 | |||
| 793 | /** |
||
| 794 | * Extract/format the first fact from a list of facts. |
||
| 795 | * |
||
| 796 | * @param string $facts |
||
| 797 | * @param int $style |
||
| 798 | * |
||
| 799 | * @return string |
||
| 800 | */ |
||
| 801 | public function formatFirstMajorFact($facts, $style) { |
||
| 821 | |||
| 822 | /** |
||
| 823 | * Find individuals linked to this record. |
||
| 824 | * |
||
| 825 | * @param string $link |
||
| 826 | * |
||
| 827 | * @return Individual[] |
||
| 828 | */ |
||
| 829 | View Code Duplication | public function linkedIndividuals($link) { |
|
| 854 | |||
| 855 | /** |
||
| 856 | * Find families linked to this record. |
||
| 857 | * |
||
| 858 | * @param string $link |
||
| 859 | * |
||
| 860 | * @return Family[] |
||
| 861 | */ |
||
| 862 | View Code Duplication | public function linkedFamilies($link) { |
|
| 885 | |||
| 886 | /** |
||
| 887 | * Find sources linked to this record. |
||
| 888 | * |
||
| 889 | * @param string $link |
||
| 890 | * |
||
| 891 | * @return Source[] |
||
| 892 | */ |
||
| 893 | View Code Duplication | public function linkedSources($link) { |
|
| 917 | |||
| 918 | /** |
||
| 919 | * Find media objects linked to this record. |
||
| 920 | * |
||
| 921 | * @param string $link |
||
| 922 | * |
||
| 923 | * @return Media[] |
||
| 924 | */ |
||
| 925 | View Code Duplication | public function linkedMedia($link) { |
|
| 949 | |||
| 950 | /** |
||
| 951 | * Find notes linked to this record. |
||
| 952 | * |
||
| 953 | * @param string $link |
||
| 954 | * |
||
| 955 | * @return Note[] |
||
| 956 | */ |
||
| 957 | View Code Duplication | public function linkedNotes($link) { |
|
| 982 | |||
| 983 | /** |
||
| 984 | * Find repositories linked to this record. |
||
| 985 | * |
||
| 986 | * @param string $link |
||
| 987 | * |
||
| 988 | * @return Repository[] |
||
| 989 | */ |
||
| 990 | View Code Duplication | public function linkedRepositories($link) { |
|
| 1015 | |||
| 1016 | /** |
||
| 1017 | * Get all attributes (e.g. DATE or PLAC) from an event (e.g. BIRT or MARR). |
||
| 1018 | * This is used to display multiple events on the individual/family lists. |
||
| 1019 | * Multiple events can exist because of uncertainty in dates, dates in different |
||
| 1020 | * calendars, place-names in both latin and hebrew character sets, etc. |
||
| 1021 | * It also allows us to combine dates/places from different events in the summaries. |
||
| 1022 | * |
||
| 1023 | * @param string $event_type |
||
| 1024 | * |
||
| 1025 | * @return Date[] |
||
| 1026 | */ |
||
| 1027 | public function getAllEventDates($event_type) { |
||
| 1037 | |||
| 1038 | /** |
||
| 1039 | * Get all the places for a particular type of event |
||
| 1040 | * |
||
| 1041 | * @param string $event_type |
||
| 1042 | * |
||
| 1043 | * @return array |
||
| 1044 | */ |
||
| 1045 | public function getAllEventPlaces($event_type) { |
||
| 1057 | |||
| 1058 | /** |
||
| 1059 | * Get the first (i.e. prefered) Fact for the given fact type |
||
| 1060 | * |
||
| 1061 | * @param string $tag |
||
| 1062 | * |
||
| 1063 | * @return Fact|null |
||
| 1064 | */ |
||
| 1065 | public function getFirstFact($tag) { |
||
| 1074 | |||
| 1075 | /** |
||
| 1076 | * The facts and events for this record. |
||
| 1077 | * |
||
| 1078 | * @param string $filter |
||
| 1079 | * @param bool $sort |
||
| 1080 | * @param int|null $access_level |
||
| 1081 | * @param bool $override Include private records, to allow us to implement $SHOW_PRIVATE_RELATIONSHIPS and $SHOW_LIVING_NAMES. |
||
| 1082 | * |
||
| 1083 | * @return Fact[] |
||
| 1084 | */ |
||
| 1085 | public function getFacts($filter = null, $sort = false, $access_level = null, $override = false) { |
||
| 1104 | |||
| 1105 | /** |
||
| 1106 | * Get the last-change timestamp for this record, either as a formatted string |
||
| 1107 | * (for display) or as a unix timestamp (for sorting) |
||
| 1108 | * |
||
| 1109 | * @param bool $sorting |
||
| 1110 | * |
||
| 1111 | * @return string |
||
| 1112 | */ |
||
| 1113 | public function lastChangeTimestamp($sorting = false) { |
||
| 1140 | |||
| 1141 | /** |
||
| 1142 | * Get the last-change user for this record |
||
| 1143 | * |
||
| 1144 | * @return string |
||
| 1145 | */ |
||
| 1146 | public function lastChangeUser() { |
||
| 1160 | |||
| 1161 | /** |
||
| 1162 | * Add a new fact to this record |
||
| 1163 | * |
||
| 1164 | * @param string $gedcom |
||
| 1165 | * @param bool $update_chan |
||
| 1166 | */ |
||
| 1167 | public function createFact($gedcom, $update_chan) { |
||
| 1170 | |||
| 1171 | /** |
||
| 1172 | * Delete a fact from this record |
||
| 1173 | * |
||
| 1174 | * @param string $fact_id |
||
| 1175 | * @param bool $update_chan |
||
| 1176 | */ |
||
| 1177 | public function deleteFact($fact_id, $update_chan) { |
||
| 1180 | |||
| 1181 | /** |
||
| 1182 | * Replace a fact with a new gedcom data. |
||
| 1183 | * |
||
| 1184 | * @param string $fact_id |
||
| 1185 | * @param string $gedcom |
||
| 1186 | * @param bool $update_chan |
||
| 1187 | * |
||
| 1188 | * @throws \Exception |
||
| 1189 | */ |
||
| 1190 | public function updateFact($fact_id, $gedcom, $update_chan) { |
||
| 1255 | |||
| 1256 | /** |
||
| 1257 | * Update this record |
||
| 1258 | * |
||
| 1259 | * @param string $gedcom |
||
| 1260 | * @param bool $update_chan |
||
| 1261 | */ |
||
| 1262 | public function updateRecord($gedcom, $update_chan) { |
||
| 1298 | |||
| 1299 | /** |
||
| 1300 | * Delete this record |
||
| 1301 | */ |
||
| 1302 | public function deleteRecord() { |
||
| 1326 | |||
| 1327 | /** |
||
| 1328 | * Remove all links from this record to $xref |
||
| 1329 | * |
||
| 1330 | * @param string $xref |
||
| 1331 | * @param bool $update_chan |
||
| 1332 | */ |
||
| 1333 | public function removeLinks($xref, $update_chan) { |
||
| 1350 | } |
||
| 1351 |
In PHP, under loose comparison (like
==, or!=, orswitchconditions), values of different types might be equal.For
stringvalues, the empty string''is a special case, in particular the following results might be unexpected: