Passed
Push — develop ( 4529c8...dd8217 )
by Greg
23:51 queued 16:41
created

IndividualPage::ageString()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 6
nop 1
dl 0
loc 33
rs 9.1111
c 0
b 0
f 0
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\Http\RequestHandlers;
21
22
use Fig\Http\Message\StatusCodeInterface;
23
use Fisharebest\Webtrees\Age;
24
use Fisharebest\Webtrees\Auth;
25
use Fisharebest\Webtrees\Date;
26
use Fisharebest\Webtrees\Fact;
27
use Fisharebest\Webtrees\Http\ViewResponseTrait;
28
use Fisharebest\Webtrees\I18N;
29
use Fisharebest\Webtrees\Individual;
30
use Fisharebest\Webtrees\Media;
31
use Fisharebest\Webtrees\MediaFile;
32
use Fisharebest\Webtrees\Module\ModuleSidebarInterface;
33
use Fisharebest\Webtrees\Module\ModuleTabInterface;
34
use Fisharebest\Webtrees\Registry;
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 Psr\Http\Server\RequestHandlerInterface;
43
use stdClass;
44
45
use function array_map;
46
use function assert;
47
use function date;
48
use function e;
49
use function explode;
50
use function implode;
51
use function is_string;
52
use function redirect;
53
use function route;
54
use function strtoupper;
55
use function view;
56
57
/**
58
 * Show an individual's page.
59
 */
60
class IndividualPage implements RequestHandlerInterface
61
{
62
    use ViewResponseTrait;
63
64
    /** @var ClipboardService */
65
    private $clipboard_service;
66
67
    /** @var ModuleService */
68
    private $module_service;
69
70
    /** @var UserService */
71
    private $user_service;
72
73
    /**
74
     * IndividualPage constructor.
75
     *
76
     * @param ClipboardService $clipboard_service
77
     * @param ModuleService    $module_service
78
     * @param UserService      $user_service
79
     */
80
    public function __construct(ClipboardService $clipboard_service, ModuleService $module_service, UserService $user_service)
81
    {
82
        $this->clipboard_service = $clipboard_service;
83
        $this->module_service    = $module_service;
84
        $this->user_service      = $user_service;
85
    }
86
87
    /**
88
     * @param ServerRequestInterface $request
89
     *
90
     * @return ResponseInterface
91
     */
92
    public function handle(ServerRequestInterface $request): ResponseInterface
93
    {
94
        $tree = $request->getAttribute('tree');
95
        assert($tree instanceof Tree);
96
97
        $xref = $request->getAttribute('xref');
98
        assert(is_string($xref));
99
100
        $individual = Registry::individualFactory()->make($xref, $tree);
101
        $individual = Auth::checkIndividualAccess($individual);
102
103
        // Redirect to correct xref/slug
104
        if ($individual->xref() !== $xref || $request->getAttribute('slug') !== $individual->slug()) {
105
            return redirect($individual->url(), StatusCodeInterface::STATUS_MOVED_PERMANENTLY);
106
        }
107
108
        // What images are linked to this individual
109
        $individual_media = new Collection();
110
        foreach ($individual->facts(['OBJE']) as $fact) {
111
            $media_object = $fact->target();
112
            if ($media_object instanceof Media) {
113
                $media_file = $media_object->firstImageFile();
114
                if ($media_file instanceof MediaFile) {
115
                    $individual_media->add($media_file);
116
                }
117
            }
118
        }
119
120
        $name_records = $individual->facts(['NAME'])->map(static function (Fact $fact): string {
121
            return view('individual-name', ['fact' => $fact]);
122
        });
123
124
        $sex_records = $individual->facts(['SEX'])->map(static function (Fact $fact): string {
125
            return view('individual-sex', ['fact' => $fact]);
126
        });
127
128
        // If this individual is linked to a user account, show the link
129
        $user_link = '';
130
        if (Auth::isAdmin()) {
131
            $users = $this->user_service->findByIndividual($individual);
132
            foreach ($users as $user) {
133
                $user_link = ' —  <a href="' . e(route('admin-users', ['filter' => $user->email()])) . '">' . e($user->userName()) . '</a>';
134
            }
135
        }
136
137
        return $this->viewResponse('individual-page', [
138
            'age'              => $this->ageString($individual),
139
            'clipboard_facts'  => $this->clipboard_service->pastableFacts($individual, new Collection()),
140
            'individual'       => $individual,
141
            'individual_media' => $individual_media,
142
            'meta_description' => $this->metaDescription($individual),
143
            'meta_robots'      => 'index,follow',
144
            'name_records'     => $name_records,
145
            'sex_records'      => $sex_records,
146
            'sidebars'         => $this->getSidebars($individual),
147
            'tabs'             => $this->getTabs($individual),
148
            'significant'      => $this->significant($individual),
149
            'title'            => $individual->fullName() . ' ' . $individual->lifespan(),
150
            'tree'             => $tree,
151
            'user_link'        => $user_link,
152
        ]);
153
    }
154
155
    /**
156
     * @param Individual $individual
157
     *
158
     * @return string
159
     */
160
    private function ageString(Individual $individual): string
161
    {
162
        if ($individual->isDead()) {
163
            // If dead, show age at death
164
            $age = (string) new Age($individual->getBirthDate(), $individual->getDeathDate());
165
166
            if ($age === '') {
167
                return '';
168
            }
169
170
            switch ($individual->sex()) {
171
                case 'M':
172
                    /* I18N: The age of an individual at a given date */
173
                    return I18N::translateContext('Male', '(aged %s)', $age);
174
                case 'F':
175
                    /* I18N: The age of an individual at a given date */
176
                    return I18N::translateContext('Female', '(aged %s)', $age);
177
                default:
178
                    /* I18N: The age of an individual at a given date */
179
                    return I18N::translate('(aged %s)', $age);
180
            }
181
        }
182
183
        // If living, show age today
184
        $today = new Date(strtoupper(date('d M Y')));
185
        $age   = (string) new Age($individual->getBirthDate(), $today);
186
187
        if ($age === '') {
188
            return '';
189
        }
190
191
        /* I18N: The current age of a living individual */
192
        return I18N::translate('(age %s)', $age);
193
    }
194
195
    /**
196
     * @param Individual $individual
197
     *
198
     * @return string
199
     */
200
    private function metaDescription(Individual $individual): string
201
    {
202
        $meta_facts = [];
203
204
        $birth_date  = $individual->getBirthDate();
205
        $birth_place = $individual->getBirthPlace();
206
207
        if ($birth_date->isOK() || $birth_place->id() !== 0) {
208
            $meta_facts[] = I18N::translate('Birth') . ' ' .
209
                $birth_date->display(false, null, false) . ' ' .
210
                $birth_place->placeName();
211
        }
212
213
        $death_date  = $individual->getDeathDate();
214
        $death_place = $individual->getDeathPlace();
215
216
        if ($death_date->isOK() || $death_place->id() !== 0) {
217
            $meta_facts[] = I18N::translate('Death') . ' ' .
218
                $death_date->display(false, null, false) . ' ' .
219
                $death_place->placeName();
220
        }
221
222
        foreach ($individual->childFamilies() as $family) {
223
            $meta_facts[] = I18N::translate('Parents') . ' ' . $family->fullName();
224
        }
225
226
        foreach ($individual->spouseFamilies() as $family) {
227
            $spouse = $family->spouse($individual);
228
            if ($spouse instanceof Individual) {
229
                $meta_facts[] = I18N::translate('Spouse') . ' ' . $spouse->fullName();
230
            }
231
232
            $child_names = $family->children()->map(static function (Individual $individual): string {
233
                return e($individual->getAllNames()[0]['givn']);
234
            })->implode(', ');
235
236
237
            if ($child_names !== '') {
238
                $meta_facts[] = I18N::translate('Children') . ' ' . $child_names;
239
            }
240
        }
241
242
        $meta_facts = array_map('strip_tags', $meta_facts);
243
        $meta_facts = array_map('trim', $meta_facts);
244
245
        return implode(', ', $meta_facts);
246
    }
247
248
    /**
249
     * Which tabs should we show on this individual's page.
250
     * We don't show empty tabs.
251
     *
252
     * @param Individual $individual
253
     *
254
     * @return Collection<ModuleSidebarInterface>
255
     */
256
    public function getSidebars(Individual $individual): Collection
257
    {
258
        return $this->module_service->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
259
            ->filter(static function (ModuleSidebarInterface $sidebar) use ($individual): bool {
260
                return $sidebar->hasSidebarContent($individual);
261
            });
262
    }
263
264
    /**
265
     * Which tabs should we show on this individual's page.
266
     * We don't show empty tabs.
267
     *
268
     * @param Individual $individual
269
     *
270
     * @return Collection<ModuleTabInterface>
271
     */
272
    public function getTabs(Individual $individual): Collection
273
    {
274
        return $this->module_service->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
275
            ->filter(static function (ModuleTabInterface $tab) use ($individual): bool {
276
                return $tab->hasTabContent($individual);
277
            });
278
    }
279
280
    /**
281
     * What are the significant elements of this page?
282
     * The layout will need them to generate URLs for charts and reports.
283
     *
284
     * @param Individual $individual
285
     *
286
     * @return stdClass
287
     */
288
    private function significant(Individual $individual): stdClass
289
    {
290
        [$surname] = explode(',', $individual->sortName());
291
292
        $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first();
293
294
        return (object) [
295
            'family'     => $family,
296
            'individual' => $individual,
297
            'surname'    => $surname,
298
        ];
299
    }
300
}
301