Passed
Push — master ( 7b2840...06a438 )
by Greg
06:41
created

BranchesListModule   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 171
dl 0
loc 440
rs 3.6
c 0
b 0
f 0
wmc 60

15 Methods

Rating   Name   Duplication   Size   Complexity  
A title() 0 4 1
A listUrlAttributes() 0 3 1
A allAncestors() 0 22 5
B loadIndividuals() 0 43 7
A getPageAction() 0 3 1
A description() 0 4 1
A sosaGeneration() 0 5 1
F getDescendantsHtml() 0 90 19
A getPatriarchsHtml() 0 15 5
A surnamesMatch() 0 12 6
A listUrl() 0 15 4
B handle() 0 65 6
A __construct() 0 3 1
A listMenuClass() 0 3 1
A boot() 0 8 1

How to fix   Complexity   

Complex Class

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
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2020 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Aura\Router\RouterContainer;
23
use Fig\Http\Message\RequestMethodInterface;
24
use Fisharebest\Webtrees\Auth;
25
use Fisharebest\Webtrees\Contracts\UserInterface;
26
use Fisharebest\Webtrees\Factory;
27
use Fisharebest\Webtrees\Family;
28
use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi;
29
use Fisharebest\Webtrees\GedcomRecord;
30
use Fisharebest\Webtrees\I18N;
31
use Fisharebest\Webtrees\Individual;
32
use Fisharebest\Webtrees\Services\ModuleService;
33
use Fisharebest\Webtrees\Soundex;
34
use Fisharebest\Webtrees\Tree;
35
use Fisharebest\Webtrees\User;
36
use Illuminate\Database\Capsule\Manager as DB;
37
use Illuminate\Database\Query\Builder;
38
use Illuminate\Database\Query\JoinClause;
39
use Psr\Http\Message\ResponseInterface;
40
use Psr\Http\Message\ServerRequestInterface;
41
use Psr\Http\Server\RequestHandlerInterface;
42
43
use function app;
44
use function array_search;
45
use function assert;
46
use function e;
47
use function explode;
48
use function in_array;
49
use function is_int;
50
use function key;
51
use function log;
52
use function next;
53
use function redirect;
54
use function route;
55
use function strip_tags;
56
use function stripos;
57
use function usort;
58
use function view;
59
60
/**
61
 * Class BranchesListModule
62
 */
63
class BranchesListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
64
{
65
    use ModuleListTrait;
66
67
    protected const ROUTE_URL  = '/tree/{tree}/branches{/surname}';
68
69
    /** @var ModuleService */
70
    protected $module_service;
71
72
    /**
73
     * BranchesListModule constructor.
74
     *
75
     * @param ModuleService $module_service
76
     */
77
    public function __construct(ModuleService $module_service)
78
    {
79
        $this->module_service = $module_service;
80
    }
81
82
    /**
83
     * Initialization.
84
     *
85
     * @return void
86
     */
87
    public function boot(): void
88
    {
89
        $router_container = app(RouterContainer::class);
90
        assert($router_container instanceof RouterContainer);
91
92
        $router_container->getMap()
93
            ->get(static::class, static::ROUTE_URL, $this)
94
            ->allows(RequestMethodInterface::METHOD_POST);
95
    }
96
97
    /**
98
     * How should this module be identified in the control panel, etc.?
99
     *
100
     * @return string
101
     */
102
    public function title(): string
103
    {
104
        /* I18N: Name of a module/list */
105
        return I18N::translate('Branches');
106
    }
107
108
    /**
109
     * A sentence describing what this module does.
110
     *
111
     * @return string
112
     */
113
    public function description(): string
114
    {
115
        /* I18N: Description of the “Branches” module */
116
        return I18N::translate('A list of branches of a family.');
117
    }
118
119
    /**
120
     * CSS class for the URL.
121
     *
122
     * @return string
123
     */
124
    public function listMenuClass(): string
125
    {
126
        return 'menu-branches';
127
    }
128
129
    /**
130
     * @param Tree    $tree
131
     * @param mixed[] $parameters
132
     *
133
     * @return string
134
     */
135
    public function listUrl(Tree $tree, array $parameters = []): string
136
    {
137
        $xref = app(ServerRequestInterface::class)->getAttribute('xref', '');
138
139
        if ($xref !== '') {
140
            $individual = Factory::individual()->make($xref, $tree);
141
142
            if ($individual instanceof Individual && $individual->canShow()) {
143
                $parameters['surname'] = $parameters['surname'] ?? $individual->getAllNames()[0]['surn'] ?? null;
144
            }
145
        }
146
147
        $parameters['tree'] = $tree->name();
148
149
        return route(static::class, $parameters);
150
    }
151
152
    /**
153
     * @return string[]
154
     */
155
    public function listUrlAttributes(): array
156
    {
157
        return [];
158
    }
159
160
    /**
161
     * Handle URLs generated by older versions of webtrees
162
     *
163
     * @param ServerRequestInterface $request
164
     *
165
     * @return ResponseInterface
166
     */
167
    public function getPageAction(ServerRequestInterface $request): ResponseInterface
168
    {
169
        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
170
    }
171
172
    /**
173
     * @param ServerRequestInterface $request
174
     *
175
     * @return ResponseInterface
176
     */
177
    public function handle(ServerRequestInterface $request): ResponseInterface
178
    {
179
        $tree = $request->getAttribute('tree');
180
        assert($tree instanceof Tree);
181
182
        $user = $request->getAttribute('user');
183
        assert($user instanceof UserInterface);
184
185
        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
186
187
        // Convert POST requests into GET requests for pretty URLs.
188
        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
189
            return redirect($this->listUrl($tree, (array) $request->getParsedBody()));
190
        }
191
192
        $surname = (string) $request->getAttribute('surname');
193
194
        $params      = $request->getQueryParams();
195
        $ajax        = $params['ajax'] ?? '';
196
        $soundex_std = (bool) ($params['soundex_std'] ?? false);
197
        $soundex_dm  = (bool) ($params['soundex_dm'] ?? false);
198
199
        if ($ajax === '1') {
200
            $this->layout = 'layouts/ajax';
201
202
            // Highlight direct-line ancestors of this individual.
203
            $xref = $tree->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF);
204
            $self = Factory::individual()->make($xref, $tree);
205
206
            if ($surname !== '') {
207
                $individuals = $this->loadIndividuals($tree, $surname, $soundex_dm, $soundex_std);
208
            } else {
209
                $individuals = [];
210
            }
211
212
            if ($self instanceof Individual) {
213
                $ancestors = $this->allAncestors($self);
214
            } else {
215
                $ancestors = [];
216
            }
217
218
            return $this->viewResponse('modules/branches/list', [
219
                'branches' => $this->getPatriarchsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std),
220
            ]);
221
        }
222
223
        if ($surname !== '') {
224
            /* I18N: %s is a surname */
225
            $title = I18N::translate('Branches of the %s family', e($surname));
226
227
            $ajax_url = $this->listUrl($tree, $params + ['ajax' => true, 'surname' => $surname]);
228
        } else {
229
            /* I18N: Branches of a family tree */
230
            $title = I18N::translate('Branches');
231
232
            $ajax_url = '';
233
        }
234
235
        return $this->viewResponse('branches-page', [
236
            'ajax_url'    => $ajax_url,
237
            'soundex_dm'  => $soundex_dm,
238
            'soundex_std' => $soundex_std,
239
            'surname'     => $surname,
240
            'title'       => $title,
241
            'tree'        => $tree,
242
        ]);
243
    }
244
245
    /**
246
     * Find all ancestors of an individual, indexed by the Sosa-Stradonitz number.
247
     *
248
     * @param Individual $individual
249
     *
250
     * @return Individual[]
251
     */
252
    private function allAncestors(Individual $individual): array
253
    {
254
        $ancestors = [
255
            1 => $individual,
256
        ];
257
258
        do {
259
            $sosa = key($ancestors);
260
261
            $family = $ancestors[$sosa]->childFamilies()->first();
262
263
            if ($family !== null) {
264
                if ($family->husband() !== null) {
265
                    $ancestors[$sosa * 2] = $family->husband();
266
                }
267
                if ($family->wife() !== null) {
268
                    $ancestors[$sosa * 2 + 1] = $family->wife();
269
                }
270
            }
271
        } while (next($ancestors));
272
273
        return $ancestors;
274
    }
275
276
    /**
277
     * Fetch all individuals with a matching surname
278
     *
279
     * @param Tree   $tree
280
     * @param string $surname
281
     * @param bool   $soundex_dm
282
     * @param bool   $soundex_std
283
     *
284
     * @return Individual[]
285
     */
286
    private function loadIndividuals(Tree $tree, string $surname, bool $soundex_dm, bool $soundex_std): array
287
    {
288
        $individuals = DB::table('individuals')
289
            ->join('name', static function (JoinClause $join): void {
290
                $join
291
                    ->on('name.n_file', '=', 'individuals.i_file')
292
                    ->on('name.n_id', '=', 'individuals.i_id');
293
            })
294
            ->where('i_file', '=', $tree->id())
295
            ->where('n_type', '<>', '_MARNM')
296
            ->where(static function (Builder $query) use ($surname, $soundex_dm, $soundex_std): void {
297
                $query
298
                    ->where('n_surn', '=', $surname)
299
                    ->orWhere('n_surname', '=', $surname);
300
301
                if ($soundex_std) {
302
                    $sdx = Soundex::russell($surname);
303
                    if ($sdx !== '') {
304
                        foreach (explode(':', $sdx) as $value) {
305
                            $query->orWhere('n_soundex_surn_std', 'LIKE', '%' . $value . '%');
306
                        }
307
                    }
308
                }
309
310
                if ($soundex_dm) {
311
                    $sdx = Soundex::daitchMokotoff($surname);
312
                    if ($sdx !== '') {
313
                        foreach (explode(':', $sdx) as $value) {
314
                            $query->orWhere('n_soundex_surn_dm', 'LIKE', '%' . $value . '%');
315
                        }
316
                    }
317
                }
318
            })
319
            ->select(['individuals.*'])
320
            ->distinct()
321
            ->get()
322
            ->map(Factory::individual()->mapper($tree))
323
            ->filter(GedcomRecord::accessFilter())
324
            ->all();
325
326
        usort($individuals, Individual::birthDateComparator());
327
328
        return $individuals;
329
    }
330
331
    /**
332
     * For each individual with no ancestors, list their descendants.
333
     *
334
     * @param Tree         $tree
335
     * @param Individual[] $individuals
336
     * @param Individual[] $ancestors
337
     * @param string       $surname
338
     * @param bool         $soundex_dm
339
     * @param bool         $soundex_std
340
     *
341
     * @return string
342
     */
343
    private function getPatriarchsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std): string
344
    {
345
        $html = '';
346
        foreach ($individuals as $individual) {
347
            foreach ($individual->childFamilies() as $family) {
348
                foreach ($family->spouses() as $parent) {
349
                    if (in_array($parent, $individuals, true)) {
350
                        continue 3;
351
                    }
352
                }
353
            }
354
            $html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $individual, null);
355
        }
356
357
        return $html;
358
    }
359
360
    /**
361
     * Generate a recursive list of descendants of an individual.
362
     * If parents are specified, we can also show the pedigree (adopted, etc.).
363
     *
364
     * @param Tree         $tree
365
     * @param Individual[] $individuals
366
     * @param Individual[] $ancestors
367
     * @param string       $surname
368
     * @param bool         $soundex_dm
369
     * @param bool         $soundex_std
370
     * @param Individual   $individual
371
     * @param Family|null  $parents
372
     *
373
     * @return string
374
     */
375
    private function getDescendantsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std, Individual $individual, Family $parents = null): string
376
    {
377
        $module = $this->module_service->findByComponent(ModuleChartInterface::class, $tree, Auth::user())->first(static function (ModuleInterface $module) {
378
            return $module instanceof RelationshipsChartModule;
379
        });
380
381
        // A person has many names. Select the one that matches the searched surname
382
        $person_name = '';
383
        foreach ($individual->getAllNames() as $name) {
384
            [$surn1] = explode(',', $name['sort']);
385
            if ($this->surnamesMatch($surn1, $surname, $soundex_std, $soundex_dm)) {
386
                $person_name = $name['full'];
387
                break;
388
            }
389
        }
390
391
        // No matching name? Typically children with a different surname. The branch stops here.
392
        if ($person_name === '') {
393
            return '<li title="' . strip_tags($individual->fullName()) . '"><small>' . view('icons/sex', ['sex' => $individual->sex()]) . '</small>…</li>';
394
        }
395
396
        // Is this individual one of our ancestors?
397
        $sosa = array_search($individual, $ancestors, true);
398
        if (is_int($sosa) && $module instanceof RelationshipsChartModule) {
399
            $sosa_class = 'search_hit';
400
            $sosa_html  = '<a class="small ' . $individual->getBoxStyle() . '" href="' . e($module->chartUrl($individual, ['xref2' => $individuals[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationships') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa);
401
        } else {
402
            $sosa_class = '';
403
            $sosa_html  = '';
404
        }
405
406
        // Generate HTML for this individual, and all their descendants
407
        $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;
408
409
        // If this is not a birth pedigree (e.g. an adoption), highlight it
410
        if ($parents) {
411
            $pedi = '';
412
            foreach ($individual->facts(['FAMC']) as $fact) {
413
                if ($fact->target() === $parents) {
414
                    $pedi = $fact->attribute('PEDI');
415
                    break;
416
                }
417
            }
418
            if ($pedi !== '' && $pedi !== 'birth') {
419
                $indi_html = '<span class="red">' . GedcomCodePedi::getValue($pedi, $individual) . '</span> ' . $indi_html;
420
            }
421
        }
422
423
        // spouses and children
424
        $spouse_families = $individual->spouseFamilies()
425
            ->sort(Family::marriageDateComparator());
426
427
        if ($spouse_families->isNotEmpty()) {
428
            $fam_html = '';
429
            foreach ($spouse_families as $family) {
430
                $fam_html .= $indi_html; // Repeat the individual details for each spouse.
431
432
                $spouse = $family->spouse($individual);
433
                if ($spouse instanceof Individual) {
434
                    $sosa = array_search($spouse, $ancestors, true);
435
                    if (is_int($sosa) && $module instanceof RelationshipsChartModule) {
436
                        $sosa_class = 'search_hit';
437
                        $sosa_html  = '<a class="small ' . $spouse->getBoxStyle() . '" href="' . e($module->chartUrl($individual, ['xref2' => $individuals[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationships') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa);
438
                    } else {
439
                        $sosa_class = '';
440
                        $sosa_html  = '';
441
                    }
442
                    $marriage_year = $family->getMarriageYear();
443
                    if ($marriage_year) {
444
                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . strip_tags($family->getMarriageDate()->display()) . '"><i class="icon-rings"></i>' . $marriage_year . '</a>';
445
                    } elseif ($family->facts(['MARR'])->isNotEmpty()) {
446
                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Marriage') . '"><i class="icon-rings"></i></a>';
447
                    } else {
448
                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Not married') . '"><i class="icon-rings"></i></a>';
449
                    }
450
                    $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;
451
                }
452
453
                $fam_html .= '<ol>';
454
                foreach ($family->children() as $child) {
455
                    $fam_html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $child, $family);
456
                }
457
                $fam_html .= '</ol>';
458
            }
459
460
            return '<li>' . $fam_html . '</li>';
461
        }
462
463
        // No spouses - just show the individual
464
        return '<li>' . $indi_html . '</li>';
465
    }
466
467
    /**
468
     * Do two surnames match?
469
     *
470
     * @param string $surname1
471
     * @param string $surname2
472
     * @param bool   $soundex_std
473
     * @param bool   $soundex_dm
474
     *
475
     * @return bool
476
     */
477
    private function surnamesMatch(string $surname1, string $surname2, bool $soundex_std, bool $soundex_dm): bool
478
    {
479
        // One name sounds like another?
480
        if ($soundex_std && Soundex::compare(Soundex::russell($surname1), Soundex::russell($surname2))) {
481
            return true;
482
        }
483
        if ($soundex_dm && Soundex::compare(Soundex::daitchMokotoff($surname1), Soundex::daitchMokotoff($surname2))) {
484
            return true;
485
        }
486
487
        // One is a substring of the other.  e.g. Halen / Van Halen
488
        return stripos($surname1, $surname2) !== false || stripos($surname2, $surname1) !== false;
489
    }
490
491
    /**
492
     * Convert a SOSA number into a generation number. e.g. 8 = great-grandfather = 3 generations
493
     *
494
     * @param int $sosa
495
     *
496
     * @return string
497
     */
498
    private static function sosaGeneration($sosa): string
499
    {
500
        $generation = (int) log($sosa, 2) + 1;
501
502
        return '<sup title="' . I18N::translate('Generation') . '">' . $generation . '</sup>';
503
    }
504
}
505