Passed
Push — master ( f1d4b4...ee4364 )
by Greg
05:58
created

IndividualController   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 173
dl 0
loc 354
rs 8.96
c 3
b 0
f 0
wmc 43

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B formatSexRecord() 0 34 6
A significant() 0 10 1
C show() 0 73 13
A getTabs() 0 5 1
A getSidebars() 0 5 1
A tab() 0 21 2
F formatNameRecord() 0 88 15
A countFacts() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like IndividualController 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 IndividualController, and based on these observations, apply Extract Interface, too.

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\Date;
23
use Fisharebest\Webtrees\Fact;
24
use Fisharebest\Webtrees\Functions\FunctionsDate;
25
use Fisharebest\Webtrees\Functions\FunctionsPrint;
26
use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
27
use Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
28
use Fisharebest\Webtrees\GedcomTag;
29
use Fisharebest\Webtrees\I18N;
30
use Fisharebest\Webtrees\Individual;
31
use Fisharebest\Webtrees\Media;
32
use Fisharebest\Webtrees\MediaFile;
33
use Fisharebest\Webtrees\Module\ModuleSidebarInterface;
34
use Fisharebest\Webtrees\Module\ModuleTabInterface;
35
use Fisharebest\Webtrees\Services\ClipboardService;
36
use Fisharebest\Webtrees\Services\ModuleService;
37
use Fisharebest\Webtrees\Services\UserService;
38
use Fisharebest\Webtrees\Tree;
39
use Illuminate\Support\Collection;
40
use Psr\Http\Message\ResponseInterface;
41
use Psr\Http\Message\ServerRequestInterface;
42
use stdClass;
43
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
44
use function explode;
45
use function ob_get_clean;
46
use function ob_start;
47
use function preg_match_all;
48
use function preg_replace;
49
use function redirect;
50
use function str_replace;
51
use function strpos;
52
53
/**
54
 * Controller for the individual page.
55
 */
56
class IndividualController extends AbstractBaseController
57
{
58
    /** @var ClipboardService */
59
    private $clipboard_service;
60
61
    /** @var ModuleService */
62
    private $module_service;
63
64
    /** @var UserService */
65
    private $user_service;
66
67
    /**
68
     * IndividualController constructor.
69
     *
70
     * @param ClipboardService $clipboard_service
71
     * @param ModuleService    $module_service
72
     * @param UserService      $user_service
73
     */
74
    public function __construct(ClipboardService $clipboard_service, ModuleService $module_service, UserService $user_service)
75
    {
76
        $this->clipboard_service = $clipboard_service;
77
        $this->module_service    = $module_service;
78
        $this->user_service      = $user_service;
79
    }
80
81
    /**
82
     * Show a individual's page.
83
     *
84
     * @param ServerRequestInterface $request
85
     *
86
     * @return ResponseInterface
87
     */
88
    public function show(ServerRequestInterface $request): ResponseInterface
89
    {
90
        $slug       = $request->getAttribute('slug');
91
        $tree       = $request->getAttribute('tree');
92
        $xref       = $request->getAttribute('xref');
93
        $individual = Individual::getInstance($xref, $tree);
94
95
        Auth::checkIndividualAccess($individual);
96
97
        if ($slug !== $individual->slug()) {
98
            return redirect($individual->url());
99
        }
100
101
        // What is (was) the age of the individual
102
        $bdate = $individual->getBirthDate();
103
        $ddate = $individual->getDeathDate();
104
        if ($bdate->isOK() && !$individual->isDead()) {
105
            // If living display age
106
            $age = ' (' . I18N::translate('age') . ' ' . FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($bdate, new Date(strtoupper(date('d M Y'))))) . ')';
107
        } elseif ($bdate->isOK() && $ddate->isOK()) {
108
            // If dead, show age at death
109
            $age = ' (' . I18N::translate('age') . ' ' . FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($bdate, $ddate)) . ')';
110
        } else {
111
            $age = '';
112
        }
113
114
        // What images are linked to this individual
115
        $individual_media = new Collection();
116
        foreach ($individual->facts(['OBJE']) as $fact) {
117
            $media_object = $fact->target();
118
            if ($media_object instanceof Media) {
119
                $media_file = $media_object->firstImageFile();
120
                if ($media_file instanceof MediaFile) {
121
                    $individual_media->add($media_file);
122
                }
123
            }
124
        }
125
126
        $name_records = new Collection();
127
        foreach ($individual->facts(['NAME']) as $n => $name_fact) {
128
            $name_records->add($this->formatNameRecord($tree, $n, $name_fact));
129
        }
130
131
        $sex_records = new Collection();
132
        foreach ($individual->facts(['SEX']) as $n => $sex_fact) {
133
            $sex_records->add($this->formatSexRecord($sex_fact));
134
        }
135
136
        // If this individual is linked to a user account, show the link
137
        $user_link = '';
138
        if (Auth::isAdmin()) {
139
            $users = $this->user_service->findByIndividual($individual);
140
            foreach ($users as $user) {
141
                $user_link = ' —  <a href="' . e(route('admin-users', ['filter' => $user->email()])) . '">' . e($user->userName()) . '</a>';
142
            }
143
        }
144
145
        return $this->viewResponse('individual-page', [
146
            'age'              => $age,
147
            'clipboard_facts'  => $this->clipboard_service->pastableFacts($individual, new Collection()),
148
            'count_media'      => $this->countFacts($individual, ['OBJE']),
149
            'count_names'      => $this->countFacts($individual, ['NAME']),
150
            'count_sex'        => $this->countFacts($individual, ['SEX']),
151
            'individual'       => $individual,
152
            'individual_media' => $individual_media,
153
            'meta_robots'      => 'index,follow',
154
            'name_records'     => $name_records,
155
            'sex_records'      => $sex_records,
156
            'sidebars'         => $this->getSidebars($individual),
157
            'tabs'             => $this->getTabs($individual),
158
            'significant'      => $this->significant($individual),
159
            'title'            => $individual->fullName() . ' ' . $individual->getLifeSpan(),
160
            'user_link'        => $user_link,
161
        ]);
162
    }
163
164
    /**
165
     * @param ServerRequestInterface $request
166
     *
167
     * @return ResponseInterface
168
     */
169
    public function tab(ServerRequestInterface $request): ResponseInterface
170
    {
171
        $tree        = $request->getAttribute('tree');
172
        $user        = $request->getAttribute('user');
173
        $xref        = $request->getQueryParams()['xref'];
174
        $record      = Individual::getInstance($xref, $tree);
175
        $module_name = $request->getQueryParams()['module'];
176
        $module      = $this->module_service->findByName($module_name);
177
178
        if (!$module instanceof ModuleTabInterface) {
179
            throw new NotFoundHttpException('No such tab: ' . $module_name);
180
        }
181
182
        Auth::checkIndividualAccess($record);
183
        Auth::checkComponentAccess($module, 'tab', $tree, $user);
184
185
        $layout = view('layouts/ajax', [
186
            'content' => $module->getTabContent($record),
187
        ]);
188
189
        return response($layout);
190
    }
191
192
    /**
193
     * Count the (non-pending-delete) name records for an individual.
194
     *
195
     * @param Individual $individual
196
     * @param string[]   $tags
197
     *
198
     * @return int
199
     */
200
    private function countFacts(Individual $individual, array $tags): int
201
    {
202
        $count = 0;
203
204
        foreach ($individual->facts($tags) as $fact) {
205
            if (!$fact->isPendingDeletion()) {
206
                $count++;
207
            }
208
        }
209
210
        return $count;
211
    }
212
213
    /**
214
     * Format a name record
215
     *
216
     * @param Tree $tree
217
     * @param int  $n
218
     * @param Fact $fact
219
     *
220
     * @return string
221
     */
222
    private function formatNameRecord(Tree $tree, $n, Fact $fact): string
223
    {
224
        $individual = $fact->record();
225
226
        // Create a dummy record, so we can extract the formatted NAME value from it.
227
        $dummy = new Individual(
228
            'xref',
229
            "0 @xref@ INDI\n1 DEAT Y\n" . $fact->gedcom(),
230
            null,
231
            $individual->tree()
232
        );
233
        $dummy->setPrimaryName(0); // Make sure we use the name from "1 NAME"
234
235
        $container_class = 'card';
236
        $content_class   = 'collapse';
237
        $aria            = 'false';
238
239
        if ($n === 0) {
240
            $content_class = 'collapse show';
241
            $aria          = 'true';
242
        }
243
        if ($fact->isPendingDeletion()) {
244
            $container_class .= ' wt-old';
245
        } elseif ($fact->isPendingAddition()) {
246
            $container_class .= ' wt-new';
247
        }
248
249
        ob_start();
250
        echo '<dl><dt class="label">', I18N::translate('Name'), '</dt>';
251
        echo '<dd class="field">', $dummy->fullName(), '</dd>';
252
        $ct = preg_match_all('/\n2 (\w+) (.*)/', $fact->gedcom(), $nmatch, PREG_SET_ORDER);
253
        for ($i = 0; $i < $ct; $i++) {
254
            $tag = $nmatch[$i][1];
255
            if ($tag !== 'SOUR' && $tag !== 'NOTE' && $tag !== 'SPFX') {
256
                echo '<dt class="label">', GedcomTag::getLabel($tag, $individual), '</dt>';
257
                echo '<dd class="field">'; // Before using dir="auto" on this field, note that Gecko treats this as an inline element but WebKit treats it as a block element
258
                if (isset($nmatch[$i][2])) {
259
                    $name = e($nmatch[$i][2]);
260
                    $name = str_replace('/', '', $name);
261
                    $name = preg_replace('/(\S*)\*/', '<span class="starredname">\\1</span>', $name);
262
                    switch ($tag) {
263
                        case 'TYPE':
264
                            echo GedcomCodeName::getValue($name, $individual);
265
                            break;
266
                        case 'SURN':
267
                            // The SURN field is not necessarily the surname.
268
                            // Where it is not a substring of the real surname, show it after the real surname.
269
                            $surname = e($dummy->getAllNames()[0]['surname']);
270
                            $surns   = preg_replace('/, */', ' ', $nmatch[$i][2]);
271
                            if (strpos($dummy->getAllNames()[0]['surname'], $surns) !== false) {
272
                                echo '<span dir="auto">' . $surname . '</span>';
273
                            } else {
274
                                echo I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $surname . '</span>', '<span dir="auto">' . $name . '</span>');
275
                            }
276
                            break;
277
                        default:
278
                            echo '<span dir="auto">' . $name . '</span>';
279
                            break;
280
                    }
281
                }
282
                echo '</dd>';
283
            }
284
        }
285
        echo '</dl>';
286
        if (strpos($fact->gedcom(), "\n2 SOUR") !== false) {
287
            echo '<div id="indi_sour" class="clearfix">', FunctionsPrintFacts::printFactSources($tree, $fact->gedcom(), 2), '</div>';
288
        }
289
        if (strpos($fact->gedcom(), "\n2 NOTE") !== false) {
290
            echo '<div id="indi_note" class="clearfix">', FunctionsPrint::printFactNotes($tree, $fact->gedcom(), 2), '</div>';
291
        }
292
        $content = ob_get_clean();
293
294
        if ($fact->canEdit()) {
295
            $edit_links =
296
                '<a class="btn btn-link" href="#" data-confirm="' . I18N::translate('Are you sure you want to delete this fact?') . '" onclick="return delete_fact(this.dataset.confirm, \'' . e($individual->tree()->name()) . '\', \'' . e($individual->xref()) . '\', \'' . $fact->id() . '\');" title="' . I18N::translate('Delete this name') . '">' . view('icons/delete') . '<span class="sr-only">' . I18N::translate('Delete this name') . '</span></a>' .
297
                '<a class="btn btn-link" href="' . e(route('edit-name', ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'ged' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the name') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the name') . '</span></a>';
298
        } else {
299
            $edit_links = '';
300
        }
301
302
        return '
303
			<div class="' . $container_class . '">
304
        <div class="card-header" role="tab" id="name-header-' . $n . '">
305
		        <a data-toggle="collapse" data-parent="#individual-names" href="#name-content-' . $n . '" aria-expanded="' . $aria . '" aria-controls="name-content-' . $n . '">' . $dummy->fullName() . '</a>
306
		      ' . $edit_links . '
307
        </div>
308
		    <div id="name-content-' . $n . '" class="' . $content_class . '" role="tabpanel" aria-labelledby="name-header-' . $n . '">
309
		      <div class="card-body">' . $content . '</div>
310
        </div>
311
      </div>';
312
    }
313
314
    /**
315
     * print information for a sex record
316
     *
317
     * @param Fact $fact
318
     *
319
     * @return string
320
     */
321
    private function formatSexRecord(Fact $fact): string
322
    {
323
        $individual = $fact->record();
324
325
        switch ($fact->value()) {
326
            case 'M':
327
                $sex = I18N::translate('Male');
328
                break;
329
            case 'F':
330
                $sex = I18N::translate('Female');
331
                break;
332
            default:
333
                $sex = I18N::translateContext('unknown gender', 'Unknown');
334
                break;
335
        }
336
337
        $container_class = 'card';
338
        if ($fact->isPendingDeletion()) {
339
            $container_class .= ' wt-old';
340
        } elseif ($fact->isPendingAddition()) {
341
            $container_class .= ' wt-new';
342
        }
343
344
        if ($individual->canEdit()) {
345
            $edit_links = '<a class="btn btn-link" href="' . e(route('edit-fact', ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'ged' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the gender') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the gender') . '</span></a>';
346
        } else {
347
            $edit_links = '';
348
        }
349
350
        return '
351
		<div class="' . $container_class . '">
352
			<div class="card-header" role="tab" id="name-header-add">
353
				<div class="card-title mb-0">
354
					<b>' . I18N::translate('Gender') . '</b> ' . $sex . $edit_links . '
355
				</div>
356
			</div>
357
		</div>';
358
    }
359
360
    /**
361
     * Which tabs should we show on this individual's page.
362
     * We don't show empty tabs.
363
     *
364
     * @param Individual $individual
365
     *
366
     * @return Collection
367
     */
368
    public function getSidebars(Individual $individual): Collection
369
    {
370
        return $this->module_service->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
371
            ->filter(static function (ModuleSidebarInterface $sidebar) use ($individual): bool {
372
                return $sidebar->hasSidebarContent($individual);
373
            });
374
    }
375
376
    /**
377
     * Which tabs should we show on this individual's page.
378
     * We don't show empty tabs.
379
     *
380
     * @param Individual $individual
381
     *
382
     * @return Collection
383
     */
384
    public function getTabs(Individual $individual): Collection
385
    {
386
        return $this->module_service->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
387
            ->filter(static function (ModuleTabInterface $tab) use ($individual): bool {
388
                return $tab->hasTabContent($individual);
389
            });
390
    }
391
392
    /**
393
     * What are the significant elements of this page?
394
     * The layout will need them to generate URLs for charts and reports.
395
     *
396
     * @param Individual $individual
397
     *
398
     * @return stdClass
399
     */
400
    private function significant(Individual $individual): stdClass
401
    {
402
        [$surname] = explode(',', $individual->sortName());
403
404
        $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first();
405
406
        return (object) [
407
            'family'     => $family,
408
            'individual' => $individual,
409
            'surname'    => $surname,
410
        ];
411
    }
412
}
413