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

BranchesListModule::getDescendantsHtml()   F

Complexity

Conditions 19
Paths 675

Size

Total Lines 90
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 55
nc 675
nop 8
dl 0
loc 90
rs 0.8013
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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