Passed
Push — master ( aabcb6...83d280 )
by Greg
05:15
created

BranchesController::getDescendantsHtml()   F

Complexity

Conditions 19
Paths 675

Size

Total Lines 90
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 55
nc 675
nop 8
dl 0
loc 90
rs 0.8013
c 1
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) 2019 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
declare(strict_types=1);
18
19
namespace Fisharebest\Webtrees\Http\Controllers;
20
21
use Fisharebest\Webtrees\Auth;
22
use Fisharebest\Webtrees\Family;
23
use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi;
24
use Fisharebest\Webtrees\GedcomRecord;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Module\ModuleChartInterface;
28
use Fisharebest\Webtrees\Module\ModuleInterface;
29
use Fisharebest\Webtrees\Module\RelationshipsChartModule;
30
use Fisharebest\Webtrees\Services\ModuleService;
31
use Fisharebest\Webtrees\Soundex;
32
use Fisharebest\Webtrees\Tree;
33
use Illuminate\Database\Capsule\Manager as DB;
34
use Illuminate\Database\Query\Builder;
35
use Illuminate\Database\Query\JoinClause;
36
use Psr\Http\Message\ResponseInterface;
37
use Psr\Http\Message\ServerRequestInterface;
38
39
use function stripos;
40
use function view;
41
42
/**
43
 * Find all branches of families with a given surname.
44
 */
45
class BranchesController extends AbstractBaseController
46
{
47
    /** @var ModuleService */
48
    protected $module_service;
49
50
    /**
51
     * BranchesController constructor.
52
     *
53
     * @param ModuleService $module_service
54
     */
55
    public function __construct(ModuleService $module_service)
56
    {
57
        $this->module_service = $module_service;
58
    }
59
60
    /**
61
     * A form to request the page parameters.
62
     *
63
     * @param ServerRequestInterface $request
64
     *
65
     * @return ResponseInterface
66
     */
67
    public function page(ServerRequestInterface $request): ResponseInterface
68
    {
69
        $module = $request->getAttribute('module');
70
        $action = $request->getAttribute('action');
71
72
        $surname     = $request->getQueryParams()['surname'] ?? '';
73
        $soundex_std = (bool) ($request->getQueryParams()['soundex_std'] ?? false);
74
        $soundex_dm  = (bool) ($request->getQueryParams()['soundex_dm'] ?? false);
75
76
        if ($surname !== '') {
77
            /* I18N: %s is a surname */
78
            $title = I18N::translate('Branches of the %s family', e($surname));
79
        } else {
80
            /* I18N: Branches of a family tree */
81
            $title = I18N::translate('Branches');
82
        }
83
84
        return $this->viewResponse('branches-page', [
85
            'soundex_dm'  => $soundex_dm,
86
            'soundex_std' => $soundex_std,
87
            'surname'     => $surname,
88
            'title'       => $title,
89
            'module'      => $module,
90
            'action'      => $action,
91
        ]);
92
    }
93
94
    /**
95
     * @param ServerRequestInterface $request
96
     *
97
     * @return ResponseInterface
98
     */
99
    public function list(ServerRequestInterface $request): ResponseInterface
100
    {
101
        $tree        = $request->getAttribute('tree');
102
        $user        = $request->getAttribute('user');
103
        $params      = $request->getQueryParams();
104
        $surname     = $params['surname'];
105
        $soundex_std = (bool) ($params['soundex_std'] ?? false);
106
        $soundex_dm  = (bool) ($params['soundex_dm'] ?? false);
107
108
        // Highlight direct-line ancestors of this individual.
109
        $self = Individual::getInstance($tree->getUserPreference($user, 'gedcomid'), $tree);
110
111
        if ($surname !== '') {
112
            $individuals = $this->loadIndividuals($tree, $surname, $soundex_dm, $soundex_std);
113
        } else {
114
            $individuals = [];
115
        }
116
117
        if ($self !== null) {
118
            $ancestors = $this->allAncestors($self);
119
        } else {
120
            $ancestors = [];
121
        }
122
123
        // @TODO - convert this to use views
124
        $html = view('branches-list', [
125
            'branches' => $this->getPatriarchsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std),
126
        ]);
127
128
        return response($html);
129
    }
130
131
    /**
132
     * Find all ancestors of an individual, indexed by the Sosa-Stradonitz number.
133
     *
134
     * @param Individual $individual
135
     *
136
     * @return Individual[]
137
     */
138
    protected function allAncestors(Individual $individual): array
139
    {
140
        /** @var Individual[] $ancestors */
141
        $ancestors = [
142
            1 => $individual,
143
        ];
144
145
        do {
146
            $sosa = key($ancestors);
147
148
            $family = $ancestors[$sosa]->primaryChildFamily();
149
150
            if ($family !== null) {
151
                if ($family->husband() !== null) {
152
                    $ancestors[$sosa * 2] = $family->husband();
153
                }
154
                if ($family->wife() !== null) {
155
                    $ancestors[$sosa * 2 + 1] = $family->wife();
156
                }
157
            }
158
        } while (next($ancestors));
159
160
        return $ancestors;
161
    }
162
163
    /**
164
     * Fetch all individuals with a matching surname
165
     *
166
     * @param Tree   $tree
167
     * @param string $surname
168
     * @param bool   $soundex_dm
169
     * @param bool   $soundex_std
170
     *
171
     * @return Individual[]
172
     */
173
    private function loadIndividuals(Tree $tree, string $surname, bool $soundex_dm, bool $soundex_std): array
174
    {
175
        $individuals = DB::table('individuals')
176
            ->join('name', static function (JoinClause $join): void {
177
                $join
178
                    ->on('name.n_file', '=', 'individuals.i_file')
179
                    ->on('name.n_id', '=', 'individuals.i_id');
180
            })
181
            ->where('i_file', '=', $tree->id())
182
            ->where('n_type', '<>', '_MARNM')
183
            ->where(static function (Builder $query) use ($surname, $soundex_dm, $soundex_std): void {
184
                $query
185
                    ->where('n_surn', '=', $surname)
186
                    ->orWhere('n_surname', '=', $surname);
187
188
                if ($soundex_std) {
189
                    $sdx = Soundex::russell($surname);
190
                    if ($sdx !== '') {
191
                        foreach (explode(':', $sdx) as $value) {
192
                            $query->whereContains('n_soundex_surn_std', $value, 'or');
193
                        }
194
                    }
195
                }
196
197
                if ($soundex_dm) {
198
                    $sdx = Soundex::daitchMokotoff($surname);
199
                    if ($sdx !== '') {
200
                        foreach (explode(':', $sdx) as $value) {
201
                            $query->whereContains('n_soundex_surn_dm', $value, 'or');
202
                        }
203
                    }
204
                }
205
            })
206
            ->select(['individuals.*'])
207
            ->distinct()
208
            ->get()
209
            ->map(Individual::rowMapper())
210
            ->filter(GedcomRecord::accessFilter())
211
            ->all();
212
213
        usort($individuals, Individual::birthDateComparator());
214
215
        return $individuals;
216
    }
217
218
    /**
219
     * For each individual with no ancestors, list their descendants.
220
     *
221
     * @param Tree         $tree
222
     * @param Individual[] $individuals
223
     * @param Individual[] $ancestors
224
     * @param string       $surname
225
     * @param bool         $soundex_dm
226
     * @param bool         $soundex_std
227
     *
228
     * @return string
229
     */
230
    public function getPatriarchsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std): string
231
    {
232
        $html = '';
233
        foreach ($individuals as $individual) {
234
            foreach ($individual->childFamilies() as $family) {
235
                foreach ($family->spouses() as $parent) {
236
                    if (in_array($parent, $individuals, true)) {
237
                        continue 3;
238
                    }
239
                }
240
            }
241
            $html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $individual, null);
242
        }
243
244
        return $html;
245
    }
246
247
    /**
248
     * Generate a recursive list of descendants of an individual.
249
     * If parents are specified, we can also show the pedigree (adopted, etc.).
250
     *
251
     * @param Tree         $tree
252
     * @param Individual[] $individuals
253
     * @param Individual[] $ancestors
254
     * @param string       $surname
255
     * @param bool         $soundex_dm
256
     * @param bool         $soundex_std
257
     * @param Individual   $individual
258
     * @param Family|null  $parents
259
     *
260
     * @return string
261
     */
262
    private function getDescendantsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std, Individual $individual, Family $parents = null): string
263
    {
264
        $module = $this->module_service->findByComponent(ModuleChartInterface::class, $tree, Auth::user())->first(static function (ModuleInterface $module) {
265
            return $module instanceof RelationshipsChartModule;
266
        });
267
268
        // A person has many names. Select the one that matches the searched surname
269
        $person_name = '';
270
        foreach ($individual->getAllNames() as $name) {
271
            [$surn1] = explode(',', $name['sort']);
272
            if ($this->surnamesMatch($surn1, $surname, $soundex_std, $soundex_dm)) {
273
                $person_name = $name['full'];
274
                break;
275
            }
276
        }
277
278
        // No matching name? Typically children with a different surname. The branch stops here.
279
        if (!$person_name) {
280
            return '<li title="' . strip_tags($individual->fullName()) . '"><small>' . view('icons/sex-' . $individual->sex()) . '…</small></li>';
281
        }
282
283
        // Is this individual one of our ancestors?
284
        $sosa = array_search($individual, $ancestors, true);
285
        if (is_int($sosa) && $module instanceof RelationshipsChartModule) {
286
            $sosa_class = 'search_hit';
287
            $sosa_html  = '<a class="details1 ' . $individual->getBoxStyle() . '" href="' . e($module->chartUrl($individual, ['xref2' => $individuals[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationships') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa);
288
        } else {
289
            $sosa_class = '';
290
            $sosa_html  = '';
291
        }
292
293
        // Generate HTML for this individual, and all their descendants
294
        $indi_html = '<small>' . view('icons/sex-' . $individual->sex()) . '</small><a class="' . $sosa_class . '" href="' . e($individual->url()) . '">' . $person_name . '</a> ' . $individual->getLifeSpan() . $sosa_html;
295
296
        // If this is not a birth pedigree (e.g. an adoption), highlight it
297
        if ($parents) {
298
            $pedi = '';
299
            foreach ($individual->facts(['FAMC']) as $fact) {
300
                if ($fact->target() === $parents) {
301
                    $pedi = $fact->attribute('PEDI');
302
                    break;
303
                }
304
            }
305
            if ($pedi !== '' && $pedi !== 'birth') {
306
                $indi_html = '<span class="red">' . GedcomCodePedi::getValue($pedi, $individual) . '</span> ' . $indi_html;
307
            }
308
        }
309
310
        // spouses and children
311
        $spouse_families = $individual->spouseFamilies()
312
            ->sort(Family::marriageDateComparator());
313
314
        if ($spouse_families->isNotEmpty()) {
315
            $fam_html = '';
316
            foreach ($spouse_families as $family) {
317
                $fam_html .= $indi_html; // Repeat the individual details for each spouse.
318
319
                $spouse = $family->spouse($individual);
320
                if ($spouse instanceof Individual) {
321
                    $sosa = array_search($spouse, $ancestors, true);
322
                    if (is_int($sosa) && $module instanceof RelationshipsChartModule) {
323
                        $sosa_class = 'search_hit';
324
                        $sosa_html  = '<a class="details1 ' . $spouse->getBoxStyle() . '" href="' . e($module->chartUrl($individual, ['xref2' => $individuals[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationships') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa);
325
                    } else {
326
                        $sosa_class = '';
327
                        $sosa_html  = '';
328
                    }
329
                    $marriage_year = $family->getMarriageYear();
330
                    if ($marriage_year) {
331
                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . strip_tags($family->getMarriageDate()->display()) . '"><i class="icon-rings"></i>' . $marriage_year . '</a>';
332
                    } elseif ($family->facts(['MARR'])->first()) {
333
                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Marriage') . '"><i class="icon-rings"></i></a>';
334
                    } else {
335
                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Not married') . '"><i class="icon-rings"></i></a>';
336
                    }
337
                    $fam_html .= ' <small>' . view('icons/sex-' . $spouse->sex()) . '</small><a class="' . $sosa_class . '" href="' . e($spouse->url()) . '">' . $spouse->fullName() . '</a> ' . $spouse->getLifeSpan() . ' ' . $sosa_html;
338
                }
339
340
                $fam_html .= '<ol>';
341
                foreach ($family->children() as $child) {
342
                    $fam_html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $child, $family);
343
                }
344
                $fam_html .= '</ol>';
345
            }
346
347
            return '<li>' . $fam_html . '</li>';
348
        }
349
350
        // No spouses - just show the individual
351
        return '<li>' . $indi_html . '</li>';
352
    }
353
354
    /**
355
     * Do two surnames match?
356
     *
357
     * @param string $surname1
358
     * @param string $surname2
359
     * @param bool   $soundex_std
360
     * @param bool   $soundex_dm
361
     *
362
     * @return bool
363
     */
364
    private function surnamesMatch(string $surname1, string $surname2, bool $soundex_std, bool $soundex_dm): bool
365
    {
366
        // One name sounds like another?
367
        if ($soundex_std && Soundex::compare(Soundex::russell($surname1), Soundex::russell($surname2))) {
368
            return true;
369
        }
370
        if ($soundex_dm && Soundex::compare(Soundex::daitchMokotoff($surname1), Soundex::daitchMokotoff($surname2))) {
371
            return true;
372
        }
373
374
        // One is a substring of the other.  e.g. Halen / Van Halen
375
        return stripos($surname1, $surname2) !== false || stripos($surname2, $surname1) !== false;
376
    }
377
378
    /**
379
     * Convert a SOSA number into a generation number. e.g. 8 = great-grandfather = 3 generations
380
     *
381
     * @param int $sosa
382
     *
383
     * @return string
384
     */
385
    private static function sosaGeneration($sosa): string
386
    {
387
        $generation = (int) log($sosa, 2) + 1;
388
389
        return '<sup title="' . I18N::translate('Generation') . '">' . $generation . '</sup>';
390
    }
391
}
392