Total Complexity | 65 |
Total Lines | 388 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Family 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.
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 Family, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
21 | class Family extends GedcomRecord { |
||
22 | const RECORD_TYPE = 'FAM'; |
||
23 | const ROUTE_NAME = 'family'; |
||
24 | |||
25 | /** @var Individual|null The husband (or first spouse for same-sex couples) */ |
||
26 | private $husb; |
||
27 | |||
28 | /** @var Individual|null The wife (or second spouse for same-sex couples) */ |
||
29 | private $wife; |
||
30 | |||
31 | /** |
||
32 | * Create a GedcomRecord object from raw GEDCOM data. |
||
33 | * |
||
34 | * @param string $xref |
||
35 | * @param string $gedcom an empty string for new/pending records |
||
36 | * @param string|null $pending null for a record with no pending edits, |
||
37 | * empty string for records with pending deletions |
||
38 | * @param Tree $tree |
||
39 | */ |
||
40 | public function __construct($xref, $gedcom, $pending, $tree) { |
||
41 | parent::__construct($xref, $gedcom, $pending, $tree); |
||
42 | |||
43 | // Fetch family members |
||
44 | if (preg_match_all('/^1 (?:HUSB|WIFE|CHIL) @(.+)@/m', $gedcom . $pending, $match)) { |
||
45 | Individual::load($tree, $match[1]); |
||
46 | } |
||
47 | |||
48 | if (preg_match('/^1 HUSB @(.+)@/m', $gedcom . $pending, $match)) { |
||
49 | $this->husb = Individual::getInstance($match[1], $tree); |
||
|
|||
50 | } |
||
51 | if (preg_match('/^1 WIFE @(.+)@/m', $gedcom . $pending, $match)) { |
||
52 | $this->wife = Individual::getInstance($match[1], $tree); |
||
53 | } |
||
54 | |||
55 | // Make sure husb/wife are the right way round. |
||
56 | if ($this->husb && $this->husb->getSex() === 'F' || $this->wife && $this->wife->getSex() === 'M') { |
||
57 | list($this->husb, $this->wife) = [$this->wife, $this->husb]; |
||
58 | } |
||
59 | } |
||
60 | |||
61 | /** |
||
62 | * Generate a private version of this record |
||
63 | * |
||
64 | * @param int $access_level |
||
65 | * |
||
66 | * @return string |
||
67 | */ |
||
68 | protected function createPrivateGedcomRecord($access_level) { |
||
69 | $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); |
||
70 | |||
71 | $rec = '0 @' . $this->xref . '@ FAM'; |
||
72 | // Just show the 1 CHIL/HUSB/WIFE tag, not any subtags, which may contain private data |
||
73 | preg_match_all('/\n1 (?:CHIL|HUSB|WIFE) @(' . WT_REGEX_XREF . ')@/', $this->gedcom, $matches, PREG_SET_ORDER); |
||
74 | foreach ($matches as $match) { |
||
75 | $rela = Individual::getInstance($match[1], $this->tree); |
||
76 | if ($rela && ($SHOW_PRIVATE_RELATIONSHIPS || $rela->canShow($access_level))) { |
||
77 | $rec .= $match[0]; |
||
78 | } |
||
79 | } |
||
80 | |||
81 | return $rec; |
||
82 | } |
||
83 | |||
84 | /** |
||
85 | * Fetch data from the database |
||
86 | * |
||
87 | * @param string $xref |
||
88 | * @param int $tree_id |
||
89 | * |
||
90 | * @return null|string |
||
91 | */ |
||
92 | protected static function fetchGedcomRecord($xref, $tree_id) { |
||
93 | return Database::prepare( |
||
94 | "SELECT f_gedcom FROM `##families` WHERE f_id = :xref AND f_file = :tree_id" |
||
95 | )->execute([ |
||
96 | 'xref' => $xref, |
||
97 | 'tree_id' => $tree_id, |
||
98 | ])->fetchOne(); |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * Get the male (or first female) partner of the family |
||
103 | * |
||
104 | * @param $access_level int|null |
||
105 | * |
||
106 | * @return Individual|null |
||
107 | */ |
||
108 | public function getHusband($access_level = null) { |
||
109 | $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); |
||
110 | |||
111 | if ($this->husb && ($SHOW_PRIVATE_RELATIONSHIPS || $this->husb->canShowName($access_level))) { |
||
112 | return $this->husb; |
||
113 | } else { |
||
114 | return null; |
||
115 | } |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * Get the female (or second male) partner of the family |
||
120 | * |
||
121 | * @param $access_level int|null |
||
122 | * |
||
123 | * @return Individual|null |
||
124 | */ |
||
125 | public function getWife($access_level = null) { |
||
126 | $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); |
||
127 | |||
128 | if ($this->wife && ($SHOW_PRIVATE_RELATIONSHIPS || $this->wife->canShowName($access_level))) { |
||
129 | return $this->wife; |
||
130 | } else { |
||
131 | return null; |
||
132 | } |
||
133 | } |
||
134 | |||
135 | /** |
||
136 | * Each object type may have its own special rules, and re-implement this function. |
||
137 | * |
||
138 | * @param int $access_level |
||
139 | * |
||
140 | * @return bool |
||
141 | */ |
||
142 | protected function canShowByType($access_level) { |
||
143 | // Hide a family if any member is private |
||
144 | preg_match_all('/\n1 (?:CHIL|HUSB|WIFE) @(' . WT_REGEX_XREF . ')@/', $this->gedcom, $matches); |
||
145 | foreach ($matches[1] as $match) { |
||
146 | $person = Individual::getInstance($match, $this->tree); |
||
147 | if ($person && !$person->canShow($access_level)) { |
||
148 | return false; |
||
149 | } |
||
150 | } |
||
151 | |||
152 | return true; |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * Can the name of this record be shown? |
||
157 | * |
||
158 | * @param int|null $access_level |
||
159 | * |
||
160 | * @return bool |
||
161 | */ |
||
162 | public function canShowName($access_level = null) { |
||
163 | // We can always see the name (Husband-name + Wife-name), however, |
||
164 | // the name will often be "private + private" |
||
165 | return true; |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * Find the spouse of a person. |
||
170 | * |
||
171 | * @param Individual $person |
||
172 | * @param int|null $access_level |
||
173 | * |
||
174 | * @return Individual|null |
||
175 | */ |
||
176 | public function getSpouse(Individual $person, $access_level = null) { |
||
177 | if ($person === $this->wife) { |
||
178 | return $this->getHusband($access_level); |
||
179 | } else { |
||
180 | return $this->getWife($access_level); |
||
181 | } |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * Get the (zero, one or two) spouses from this family. |
||
186 | * |
||
187 | * @param int|null $access_level |
||
188 | * |
||
189 | * @return Individual[] |
||
190 | */ |
||
191 | public function getSpouses($access_level = null) { |
||
192 | return array_filter([ |
||
193 | $this->getHusband($access_level), |
||
194 | $this->getWife($access_level), |
||
195 | ]); |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * Get a list of this family’s children. |
||
200 | * |
||
201 | * @param int|null $access_level |
||
202 | * |
||
203 | * @return Individual[] |
||
204 | */ |
||
205 | public function getChildren($access_level = null) { |
||
206 | if ($access_level === null) { |
||
207 | $access_level = Auth::accessLevel($this->tree); |
||
208 | } |
||
209 | |||
210 | $SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS'); |
||
211 | |||
212 | $children = []; |
||
213 | foreach ($this->getFacts('CHIL', false, $access_level, $SHOW_PRIVATE_RELATIONSHIPS) as $fact) { |
||
214 | $child = $fact->getTarget(); |
||
215 | if ($child && ($SHOW_PRIVATE_RELATIONSHIPS || $child->canShowName($access_level))) { |
||
216 | $children[] = $child; |
||
217 | } |
||
218 | } |
||
219 | |||
220 | return $children; |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Static helper function to sort an array of families by marriage date |
||
225 | * |
||
226 | * @param Family $x |
||
227 | * @param Family $y |
||
228 | * |
||
229 | * @return int |
||
230 | */ |
||
231 | public static function compareMarrDate(Family $x, Family $y) { |
||
232 | return Date::compare($x->getMarriageDate(), $y->getMarriageDate()); |
||
233 | } |
||
234 | |||
235 | /** |
||
236 | * Number of children - for the individual list |
||
237 | * |
||
238 | * @return int |
||
239 | */ |
||
240 | public function getNumberOfChildren() { |
||
241 | $nchi = count($this->getChildren()); |
||
242 | foreach ($this->getFacts('NCHI') as $fact) { |
||
243 | $nchi = max($nchi, (int) $fact->getValue()); |
||
244 | } |
||
245 | |||
246 | return $nchi; |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * get the marriage event |
||
251 | * |
||
252 | * @return Fact |
||
253 | */ |
||
254 | public function getMarriage() { |
||
255 | return $this->getFirstFact('MARR'); |
||
256 | } |
||
257 | |||
258 | /** |
||
259 | * Get marriage date |
||
260 | * |
||
261 | * @return Date |
||
262 | */ |
||
263 | public function getMarriageDate() { |
||
264 | $marriage = $this->getMarriage(); |
||
265 | if ($marriage) { |
||
266 | return $marriage->getDate(); |
||
267 | } else { |
||
268 | return new Date(''); |
||
269 | } |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * Get the marriage year - displayed on lists of families |
||
274 | * |
||
275 | * @return int |
||
276 | */ |
||
277 | public function getMarriageYear() { |
||
278 | return $this->getMarriageDate()->minimumDate()->y; |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * Get the marriage place |
||
283 | * |
||
284 | * @return Place |
||
285 | */ |
||
286 | public function getMarriagePlace() { |
||
287 | $marriage = $this->getMarriage(); |
||
288 | |||
289 | return $marriage->getPlace(); |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * Get a list of all marriage dates - for the family lists. |
||
294 | * |
||
295 | * @return Date[] |
||
296 | */ |
||
297 | public function getAllMarriageDates() { |
||
298 | foreach (explode('|', WT_EVENTS_MARR) as $event) { |
||
299 | if ($array = $this->getAllEventDates($event)) { |
||
300 | return $array; |
||
301 | } |
||
302 | } |
||
303 | |||
304 | return []; |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Get a list of all marriage places - for the family lists. |
||
309 | * |
||
310 | * @return Place[] |
||
311 | */ |
||
312 | public function getAllMarriagePlaces() { |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * Derived classes should redefine this function, otherwise the object will have no name |
||
325 | * |
||
326 | * @return string[][] |
||
327 | */ |
||
328 | public function getAllNames() { |
||
329 | if (is_null($this->_getAllNames)) { |
||
330 | // Check the script used by each name, so we can match cyrillic with cyrillic, greek with greek, etc. |
||
331 | $husb_names = []; |
||
332 | if ($this->husb) { |
||
333 | $husb_names = array_filter($this->husb->getAllNames(), function (array $x) { |
||
334 | return $x['type'] !== '_MARNM'; |
||
335 | } ); |
||
336 | } |
||
337 | // If the individual only has married names, create a dummy birth name. |
||
338 | if (empty($husb_names)) { |
||
339 | $husb_names[] = [ |
||
340 | 'type' => 'BIRT', |
||
341 | 'sort' => '@N.N.', |
||
342 | 'full' => I18N::translateContext('Unknown given name', '…') . ' ' . I18N::translateContext('Unknown surname', '…'), |
||
343 | ]; |
||
344 | } |
||
345 | foreach ($husb_names as $n => $husb_name) { |
||
346 | $husb_names[$n]['script'] = I18N::textScript($husb_name['full']); |
||
347 | } |
||
348 | |||
349 | $wife_names = []; |
||
350 | if ($this->wife) { |
||
351 | $wife_names = array_filter($this->wife->getAllNames(), function (array $x) { |
||
352 | return $x['type'] !== '_MARNM'; |
||
353 | } ); |
||
354 | } |
||
355 | // If the individual only has married names, create a dummy birth name. |
||
356 | if (empty($wife_names)) { |
||
357 | $wife_names[] = [ |
||
358 | 'type' => 'BIRT', |
||
359 | 'sort' => '@N.N.', |
||
360 | 'full' => I18N::translateContext('Unknown given name', '…') . ' ' . I18N::translateContext('Unknown surname', '…'), |
||
361 | ]; |
||
362 | } |
||
363 | foreach ($wife_names as $n => $wife_name) { |
||
364 | $wife_names[$n]['script'] = I18N::textScript($wife_name['full']); |
||
365 | } |
||
366 | |||
367 | // Add the matched names first |
||
368 | foreach ($husb_names as $husb_name) { |
||
369 | foreach ($wife_names as $wife_name) { |
||
370 | if ($husb_name['script'] == $wife_name['script']) { |
||
371 | $this->_getAllNames[] = [ |
||
372 | 'type' => $husb_name['type'], |
||
373 | 'sort' => $husb_name['sort'] . ' + ' . $wife_name['sort'], |
||
374 | 'full' => $husb_name['full'] . ' + ' . $wife_name['full'], |
||
375 | // No need for a fullNN entry - we do not currently store FAM names in the database |
||
376 | ]; |
||
377 | } |
||
378 | } |
||
379 | } |
||
380 | |||
381 | // Add the unmatched names second (there may be no matched names) |
||
382 | foreach ($husb_names as $husb_name) { |
||
383 | foreach ($wife_names as $wife_name) { |
||
384 | if ($husb_name['script'] != $wife_name['script']) { |
||
385 | $this->_getAllNames[] = [ |
||
386 | 'type' => $husb_name['type'], |
||
387 | 'sort' => $husb_name['sort'] . ' + ' . $wife_name['sort'], |
||
388 | 'full' => $husb_name['full'] . ' + ' . $wife_name['full'], |
||
389 | // No need for a fullNN entry - we do not currently store FAM names in the database |
||
390 | ]; |
||
391 | } |
||
392 | } |
||
393 | } |
||
394 | } |
||
395 | |||
396 | return $this->_getAllNames; |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * This function should be redefined in derived classes to show any major |
||
401 | * identifying characteristics of this record. |
||
402 | * |
||
403 | * @return string |
||
404 | */ |
||
405 | public function formatListDetails() { |
||
409 | } |
||
410 | } |
||
411 |
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 mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.