Total Complexity | 59 |
Total Lines | 423 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 1 | Features | 0 |
Complex classes like BranchesListModule 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 BranchesListModule, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
61 | class BranchesListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface |
||
62 | { |
||
63 | use ModuleListTrait; |
||
64 | |||
65 | protected const ROUTE_URL = '/tree/{tree}/branches{/surname}'; |
||
66 | |||
67 | private ModuleService $module_service; |
||
68 | |||
69 | /** |
||
70 | * @param ModuleService $module_service |
||
71 | */ |
||
72 | public function __construct(ModuleService $module_service) |
||
73 | { |
||
74 | $this->module_service = $module_service; |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * Initialization. |
||
79 | * |
||
80 | * @return void |
||
81 | */ |
||
82 | public function boot(): void |
||
83 | { |
||
84 | Registry::routeFactory()->routeMap() |
||
85 | ->get(static::class, static::ROUTE_URL, $this) |
||
86 | ->allows(RequestMethodInterface::METHOD_POST); |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * How should this module be identified in the control panel, etc.? |
||
91 | * |
||
92 | * @return string |
||
93 | */ |
||
94 | public function title(): string |
||
98 | } |
||
99 | |||
100 | public function description(): string |
||
101 | { |
||
102 | /* I18N: Description of the “Branches” module */ |
||
103 | return I18N::translate('A list of branches of a family.'); |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * CSS class for the URL. |
||
108 | * |
||
109 | * @return string |
||
110 | */ |
||
111 | public function listMenuClass(): string |
||
112 | { |
||
113 | return 'menu-branches'; |
||
114 | } |
||
115 | |||
116 | /** |
||
117 | * @param Tree $tree |
||
118 | * @param array<bool|int|string|array<string>|null> $parameters |
||
119 | * |
||
120 | * @return string |
||
121 | */ |
||
122 | public function listUrl(Tree $tree, array $parameters = []): string |
||
123 | { |
||
124 | $request = Registry::container()->get(ServerRequestInterface::class); |
||
125 | $xref = Validator::attributes($request)->isXref()->string('xref', ''); |
||
126 | |||
127 | if ($xref !== '') { |
||
128 | $individual = Registry::individualFactory()->make($xref, $tree); |
||
129 | |||
130 | if ($individual instanceof Individual && $individual->canShow()) { |
||
131 | $parameters['surname'] ??= $individual->getAllNames()[0]['surn'] ?? null; |
||
132 | } |
||
133 | } |
||
134 | |||
135 | $parameters['tree'] = $tree->name(); |
||
136 | |||
137 | return route(static::class, $parameters); |
||
138 | } |
||
139 | |||
140 | /** |
||
141 | * @return array<string> |
||
142 | */ |
||
143 | public function listUrlAttributes(): array |
||
146 | } |
||
147 | |||
148 | /** |
||
149 | * @param ServerRequestInterface $request |
||
150 | * |
||
151 | * @return ResponseInterface |
||
152 | */ |
||
153 | public function handle(ServerRequestInterface $request): ResponseInterface |
||
154 | { |
||
155 | $tree = Validator::attributes($request)->tree(); |
||
156 | $user = Validator::attributes($request)->user(); |
||
157 | |||
158 | Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); |
||
159 | |||
160 | // Convert POST requests into GET requests for pretty URLs. |
||
161 | if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { |
||
162 | return redirect($this->listUrl($tree, [ |
||
163 | 'soundex_dm' => Validator::parsedBody($request)->boolean('soundex_dm', false), |
||
164 | 'soundex_std' => Validator::parsedBody($request)->boolean('soundex_std', false), |
||
165 | 'surname' => Validator::parsedBody($request)->string('surname'), |
||
166 | ])); |
||
167 | } |
||
168 | |||
169 | $surname = Validator::attributes($request)->string('surname', ''); |
||
170 | $soundex_std = Validator::queryParams($request)->boolean('soundex_std', false); |
||
171 | $soundex_dm = Validator::queryParams($request)->boolean('soundex_dm', false); |
||
172 | $ajax = Validator::queryParams($request)->boolean('ajax', false); |
||
173 | |||
174 | if ($ajax) { |
||
175 | $this->layout = 'layouts/ajax'; |
||
176 | |||
177 | // Highlight direct-line ancestors of this individual. |
||
178 | $xref = $tree->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF); |
||
179 | $self = Registry::individualFactory()->make($xref, $tree); |
||
180 | |||
181 | if ($surname !== '') { |
||
182 | $individuals = $this->loadIndividuals($tree, $surname, $soundex_dm, $soundex_std); |
||
183 | } else { |
||
184 | $individuals = []; |
||
185 | } |
||
186 | |||
187 | if ($self instanceof Individual) { |
||
188 | $ancestors = $this->allAncestors($self); |
||
189 | } else { |
||
190 | $ancestors = []; |
||
191 | } |
||
192 | |||
193 | return $this->viewResponse('modules/branches/list', [ |
||
194 | 'branches' => $this->getPatriarchsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std), |
||
195 | ]); |
||
196 | } |
||
197 | |||
198 | if ($surname !== '') { |
||
199 | /* I18N: %s is a surname */ |
||
200 | $title = I18N::translate('Branches of the %s family', e($surname)); |
||
201 | |||
202 | $ajax_url = $this->listUrl($tree, [ |
||
203 | 'ajax' => true, |
||
204 | 'soundex_dm' => $soundex_dm, |
||
205 | 'soundex_std' => $soundex_std, |
||
206 | 'surname' => $surname, |
||
207 | ]); |
||
208 | } else { |
||
209 | /* I18N: Branches of a family tree */ |
||
210 | $title = I18N::translate('Branches'); |
||
211 | |||
212 | $ajax_url = ''; |
||
213 | } |
||
214 | |||
215 | return $this->viewResponse('branches-page', [ |
||
216 | 'ajax_url' => $ajax_url, |
||
217 | 'soundex_dm' => $soundex_dm, |
||
218 | 'soundex_std' => $soundex_std, |
||
219 | 'surname' => $surname, |
||
220 | 'title' => $title, |
||
221 | 'tree' => $tree, |
||
222 | ]); |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * Find all ancestors of an individual, indexed by the Sosa-Stradonitz number. |
||
227 | * |
||
228 | * @param Individual $individual |
||
229 | * |
||
230 | * @return array<Individual> |
||
231 | */ |
||
232 | private function allAncestors(Individual $individual): array |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * Fetch all individuals with a matching surname |
||
258 | * |
||
259 | * @param Tree $tree |
||
260 | * @param string $surname |
||
261 | * @param bool $soundex_dm |
||
262 | * @param bool $soundex_std |
||
263 | * |
||
264 | * @return array<Individual> |
||
265 | */ |
||
266 | private function loadIndividuals(Tree $tree, string $surname, bool $soundex_dm, bool $soundex_std): array |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * For each individual with no ancestors, list their descendants. |
||
313 | * |
||
314 | * @param Tree $tree |
||
315 | * @param array<Individual> $individuals |
||
316 | * @param array<Individual> $ancestors |
||
317 | * @param string $surname |
||
318 | * @param bool $soundex_dm |
||
319 | * @param bool $soundex_std |
||
320 | * |
||
321 | * @return string |
||
322 | */ |
||
323 | private function getPatriarchsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std): string |
||
324 | { |
||
325 | $html = ''; |
||
326 | foreach ($individuals as $individual) { |
||
327 | foreach ($individual->childFamilies() as $family) { |
||
328 | foreach ($family->spouses() as $parent) { |
||
329 | if (in_array($parent, $individuals, true)) { |
||
330 | continue 3; |
||
331 | } |
||
332 | } |
||
333 | } |
||
334 | $html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $individual, null); |
||
335 | } |
||
336 | |||
337 | return $html; |
||
338 | } |
||
339 | |||
340 | /** |
||
341 | * Generate a recursive list of descendants of an individual. |
||
342 | * If parents are specified, we can also show the pedigree (adopted, etc.). |
||
343 | * |
||
344 | * @param Tree $tree |
||
345 | * @param array<Individual> $individuals |
||
346 | * @param array<Individual> $ancestors |
||
347 | * @param string $surname |
||
348 | * @param bool $soundex_dm |
||
349 | * @param bool $soundex_std |
||
350 | * @param Individual $individual |
||
351 | * @param Family|null $parents |
||
352 | * |
||
353 | * @return string |
||
354 | */ |
||
355 | private function getDescendantsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std, Individual $individual, Family|null $parents = null): string |
||
356 | { |
||
357 | $module = $this->module_service |
||
358 | ->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) |
||
359 | ->first(static fn (ModuleInterface $module) => $module instanceof RelationshipsChartModule); |
||
360 | |||
361 | // A person has many names. Select the one that matches the searched surname |
||
362 | $person_name = ''; |
||
363 | foreach ($individual->getAllNames() as $name) { |
||
364 | [$surn1] = explode(',', $name['sort']); |
||
365 | if ($this->surnamesMatch($surn1, $surname, $soundex_std, $soundex_dm)) { |
||
366 | $person_name = $name['full']; |
||
367 | break; |
||
368 | } |
||
369 | } |
||
370 | |||
371 | // No matching name? Typically children with a different surname. The branch stops here. |
||
372 | if ($person_name === '') { |
||
373 | return '<li title="' . strip_tags($individual->fullName()) . '" class="wt-branch-split"><small>' . view('icons/sex', ['sex' => $individual->sex()]) . '</small>…</li>'; |
||
374 | } |
||
375 | |||
376 | // Is this individual one of our ancestors? |
||
377 | $sosa = array_search($individual, $ancestors, true); |
||
378 | if (is_int($sosa) && $module instanceof RelationshipsChartModule) { |
||
379 | $sosa_class = 'search_hit'; |
||
380 | $sosa_html = ' <a class="small wt-chart-box-' . strtolower($individual->sex()) . '" href="' . e($module->chartUrl($individual, ['xref2' => $ancestors[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationship') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa); |
||
381 | } else { |
||
382 | $sosa_class = ''; |
||
383 | $sosa_html = ''; |
||
384 | } |
||
385 | |||
386 | // Generate HTML for this individual, and all their descendants |
||
387 | $indi_html = '<small>' . view('icons/sex', ['sex' => $individual->sex()]) . '</small><a class="' . $sosa_class . '" href="' . e($individual->url()) . '">' . $person_name . '</a> ' . $individual->lifespan() . $sosa_html; |
||
388 | |||
389 | // If this is not a birth pedigree (e.g. an adoption), highlight it |
||
390 | if ($parents instanceof Family) { |
||
391 | foreach ($individual->facts(['FAMC']) as $fact) { |
||
392 | if ($fact->target() === $parents) { |
||
393 | $pedi = $fact->attribute('PEDI'); |
||
394 | |||
395 | if ($pedi !== '' && $pedi !== PedigreeLinkageType::VALUE_BIRTH) { |
||
396 | $pedigree = Registry::elementFactory()->make('INDI:FAMC:PEDI')->value($pedi, $tree); |
||
397 | $indi_html = '<span class="red">' . $pedigree . '</span> ' . $indi_html; |
||
398 | } |
||
399 | break; |
||
400 | } |
||
401 | } |
||
402 | } |
||
403 | |||
404 | // spouses and children |
||
405 | $spouse_families = $individual->spouseFamilies() |
||
406 | ->sort(Family::marriageDateComparator()); |
||
407 | |||
408 | if ($spouse_families->isNotEmpty()) { |
||
409 | $fam_html = ''; |
||
410 | foreach ($spouse_families as $family) { |
||
411 | $fam_html .= $indi_html; // Repeat the individual details for each spouse. |
||
412 | |||
413 | $spouse = $family->spouse($individual); |
||
414 | if ($spouse instanceof Individual) { |
||
415 | $sosa = array_search($spouse, $ancestors, true); |
||
416 | if (is_int($sosa) && $module instanceof RelationshipsChartModule) { |
||
417 | $sosa_class = 'search_hit'; |
||
418 | $sosa_html = ' <a class="small wt-chart-box-' . strtolower($spouse->sex()) . '" href="' . e($module->chartUrl($spouse, ['xref2' => $ancestors[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationship') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa); |
||
419 | } else { |
||
420 | $sosa_class = ''; |
||
421 | $sosa_html = ''; |
||
422 | } |
||
423 | $marriage_year = $family->getMarriageYear(); |
||
424 | if ($marriage_year) { |
||
425 | $fam_html .= ' <a href="' . e($family->url()) . '" title="' . strip_tags($family->getMarriageDate()->display()) . '"><i class="icon-rings"></i>' . $marriage_year . '</a>'; |
||
426 | } elseif ($family->facts(['MARR'])->isNotEmpty()) { |
||
427 | $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Marriage') . '"><i class="icon-rings"></i></a>'; |
||
428 | } else { |
||
429 | $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Not married') . '"><i class="icon-rings"></i></a>'; |
||
430 | } |
||
431 | $fam_html .= ' <small>' . view('icons/sex', ['sex' => $spouse->sex()]) . '</small><a class="' . $sosa_class . '" href="' . e($spouse->url()) . '">' . $spouse->fullName() . '</a> ' . $spouse->lifespan() . ' ' . $sosa_html; |
||
432 | } |
||
433 | |||
434 | $fam_html .= '<ol>'; |
||
435 | foreach ($family->children() as $child) { |
||
436 | $fam_html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $child, $family); |
||
437 | } |
||
438 | $fam_html .= '</ol>'; |
||
439 | } |
||
440 | |||
441 | return '<li>' . $fam_html . '</li>'; |
||
442 | } |
||
443 | |||
444 | // No spouses - just show the individual |
||
445 | return '<li>' . $indi_html . '</li>'; |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Do two surnames match? |
||
450 | * |
||
451 | * @param string $surname1 |
||
452 | * @param string $surname2 |
||
453 | * @param bool $soundex_std |
||
454 | * @param bool $soundex_dm |
||
455 | * |
||
456 | * @return bool |
||
457 | */ |
||
458 | private function surnamesMatch(string $surname1, string $surname2, bool $soundex_std, bool $soundex_dm): bool |
||
459 | { |
||
460 | // One name sounds like another? |
||
461 | if ($soundex_std && Soundex::compare(Soundex::russell($surname1), Soundex::russell($surname2))) { |
||
462 | return true; |
||
463 | } |
||
464 | if ($soundex_dm && Soundex::compare(Soundex::daitchMokotoff($surname1), Soundex::daitchMokotoff($surname2))) { |
||
465 | return true; |
||
466 | } |
||
467 | |||
468 | // One is a substring of the other. e.g. Halen / Van Halen |
||
469 | return stripos($surname1, $surname2) !== false || stripos($surname2, $surname1) !== false; |
||
470 | } |
||
471 | |||
472 | /** |
||
473 | * Convert a SOSA number into a generation number. e.g. 8 = great-grandfather = 3 generations |
||
474 | * |
||
475 | * @param int $sosa |
||
476 | * |
||
477 | * @return string |
||
478 | */ |
||
479 | private static function sosaGeneration(int $sosa): string |
||
484 | } |
||
485 | } |
||
486 |