Completed
Push — develop ( dd8217...f3d6a4 )
by Greg
16:04 queued 07:43
created

Family   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 445
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 127
dl 0
loc 445
rs 3.44
c 0
b 0
f 0
wmc 62

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getMarriage() 0 3 1
A getMarriageYear() 0 3 1
A getMarriageDate() 0 8 2
A marriageDateComparator() 0 4 1
A canShowName() 0 5 1
A spouse() 0 7 2
A spouses() 0 8 1
C getAllNames() 0 70 14
A numberOfChildren() 0 9 2
A formatListDetails() 0 5 1
A lock() 0 7 1
A children() 0 19 5
A __construct() 0 12 3
A createPrivateGedcomRecord() 0 17 5
A getInstance() 0 3 1
A canShowByType() 0 12 4
A wife() 0 11 4
A husband() 0 11 4
A getMarriagePlace() 0 9 2
A rowMapper() 0 3 1
A getAllMarriagePlaces() 0 10 3
A getAllMarriageDates() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like Family 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 Family, 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;
21
22
use Closure;
23
use Fisharebest\Webtrees\Http\RequestHandlers\FamilyPage;
24
use Illuminate\Database\Capsule\Manager as DB;
25
use Illuminate\Support\Collection;
26
27
/**
28
 * A GEDCOM family (FAM) object.
29
 */
30
class Family extends GedcomRecord
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\GedcomRecord was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
{
32
    public const RECORD_TYPE = 'FAM';
33
34
    protected const ROUTE_NAME = FamilyPage::class;
35
36
    /** @var Individual|null The husband (or first spouse for same-sex couples) */
37
    private $husb;
38
39
    /** @var Individual|null The wife (or second spouse for same-sex couples) */
40
    private $wife;
41
42
    /**
43
     * Create a GedcomRecord object from raw GEDCOM data.
44
     *
45
     * @param string      $xref
46
     * @param string      $gedcom  an empty string for new/pending records
47
     * @param string|null $pending null for a record with no pending edits,
48
     *                             empty string for records with pending deletions
49
     * @param Tree        $tree
50
     */
51
    public function __construct(string $xref, string $gedcom, ?string $pending, Tree $tree)
52
    {
53
        parent::__construct($xref, $gedcom, $pending, $tree);
54
55
        // Make sure we find records in pending records.
56
        $gedcom_pending = $gedcom . "\n" . $pending;
57
58
        if (preg_match('/\n1 HUSB @(.+)@/', $gedcom_pending, $match)) {
59
            $this->husb = Registry::individualFactory()->make($match[1], $tree);
60
        }
61
        if (preg_match('/\n1 WIFE @(.+)@/', $gedcom_pending, $match)) {
62
            $this->wife = Registry::individualFactory()->make($match[1], $tree);
63
        }
64
    }
65
66
    /**
67
     * A closure which will create a record from a database row.
68
     *
69
     * @deprecated since 2.0.4.  Will be removed in 2.1.0 - Use Registry::familyFactory()
70
     *
71
     * @param Tree $tree
72
     *
73
     * @return Closure
74
     */
75
    public static function rowMapper(Tree $tree): Closure
76
    {
77
        return Registry::familyFactory()->mapper($tree);
78
    }
79
80
    /**
81
     * A closure which will compare families by marriage date.
82
     *
83
     * @return Closure
84
     */
85
    public static function marriageDateComparator(): Closure
86
    {
87
        return static function (Family $x, Family $y): int {
88
            return Date::compare($x->getMarriageDate(), $y->getMarriageDate());
89
        };
90
    }
91
92
    /**
93
     * Get an instance of a family object. For single records,
94
     * we just receive the XREF. For bulk records (such as lists
95
     * and search results) we can receive the GEDCOM data as well.
96
     *
97
     * @deprecated since 2.0.4.  Will be removed in 2.1.0 - Use Registry::familyFactory()
98
     *
99
     * @param string      $xref
100
     * @param Tree        $tree
101
     * @param string|null $gedcom
102
     *
103
     * @return Family|null
104
     */
105
    public static function getInstance(string $xref, Tree $tree, string $gedcom = null): ?Family
106
    {
107
        return Registry::familyFactory()->make($xref, $tree, $gedcom);
108
    }
109
110
    /**
111
     * Generate a private version of this record
112
     *
113
     * @param int $access_level
114
     *
115
     * @return string
116
     */
117
    protected function createPrivateGedcomRecord(int $access_level): string
118
    {
119
        if ($this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1') {
120
            $access_level = Auth::PRIV_HIDE;
121
        }
122
123
        $rec = '0 @' . $this->xref . '@ FAM';
124
        // Just show the 1 CHIL/HUSB/WIFE tag, not any subtags, which may contain private data
125
        preg_match_all('/\n1 (?:CHIL|HUSB|WIFE) @(' . Gedcom::REGEX_XREF . ')@/', $this->gedcom, $matches, PREG_SET_ORDER);
126
        foreach ($matches as $match) {
127
            $rela = Registry::individualFactory()->make($match[1], $this->tree);
128
            if ($rela instanceof Individual && $rela->canShow($access_level)) {
129
                $rec .= $match[0];
130
            }
131
        }
132
133
        return $rec;
134
    }
135
136
    /**
137
     * Get the male (or first female) partner of the family
138
     *
139
     * @param int|null $access_level
140
     *
141
     * @return Individual|null
142
     */
143
    public function husband($access_level = null): ?Individual
144
    {
145
        if ($this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1') {
146
            $access_level = Auth::PRIV_HIDE;
147
        }
148
149
        if ($this->husb instanceof Individual && $this->husb->canShowName($access_level)) {
150
            return $this->husb;
151
        }
152
153
        return null;
154
    }
155
156
    /**
157
     * Get the female (or second male) partner of the family
158
     *
159
     * @param int|null $access_level
160
     *
161
     * @return Individual|null
162
     */
163
    public function wife($access_level = null): ?Individual
164
    {
165
        if ($this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1') {
166
            $access_level = Auth::PRIV_HIDE;
167
        }
168
169
        if ($this->wife instanceof Individual && $this->wife->canShowName($access_level)) {
170
            return $this->wife;
171
        }
172
173
        return null;
174
    }
175
176
    /**
177
     * Each object type may have its own special rules, and re-implement this function.
178
     *
179
     * @param int $access_level
180
     *
181
     * @return bool
182
     */
183
    protected function canShowByType(int $access_level): bool
184
    {
185
        // Hide a family if any member is private
186
        preg_match_all('/\n1 (?:CHIL|HUSB|WIFE) @(' . Gedcom::REGEX_XREF . ')@/', $this->gedcom, $matches);
187
        foreach ($matches[1] as $match) {
188
            $person = Registry::individualFactory()->make($match, $this->tree);
189
            if ($person && !$person->canShow($access_level)) {
190
                return false;
191
            }
192
        }
193
194
        return true;
195
    }
196
197
    /**
198
     * Can the name of this record be shown?
199
     *
200
     * @param int|null $access_level
201
     *
202
     * @return bool
203
     */
204
    public function canShowName(int $access_level = null): bool
205
    {
206
        // We can always see the name (Husband-name + Wife-name), however,
207
        // the name will often be "private + private"
208
        return true;
209
    }
210
211
    /**
212
     * Find the spouse of a person.
213
     *
214
     * @param Individual $person
215
     * @param int|null   $access_level
216
     *
217
     * @return Individual|null
218
     */
219
    public function spouse(Individual $person, $access_level = null): ?Individual
220
    {
221
        if ($person === $this->wife) {
222
            return $this->husband($access_level);
223
        }
224
225
        return $this->wife($access_level);
226
    }
227
228
    /**
229
     * Get the (zero, one or two) spouses from this family.
230
     *
231
     * @param int|null $access_level
232
     *
233
     * @return Collection<Individual>
234
     */
235
    public function spouses($access_level = null): Collection
236
    {
237
        $spouses = new Collection([
238
            $this->husband($access_level),
239
            $this->wife($access_level),
240
        ]);
241
242
        return $spouses->filter();
243
    }
244
245
    /**
246
     * Get a list of this family’s children.
247
     *
248
     * @param int|null $access_level
249
     *
250
     * @return Collection<Individual>
251
     */
252
    public function children($access_level = null): Collection
253
    {
254
        $access_level = $access_level ?? Auth::accessLevel($this->tree);
255
256
        if ($this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1') {
257
            $access_level = Auth::PRIV_HIDE;
258
        }
259
260
        $children = new Collection();
261
262
        foreach ($this->facts(['CHIL'], false, $access_level) as $fact) {
263
            $child = $fact->target();
264
265
            if ($child instanceof Individual && $child->canShowName($access_level)) {
266
                $children->push($child);
267
            }
268
        }
269
270
        return $children;
271
    }
272
273
    /**
274
     * Number of children - for the individual list
275
     *
276
     * @return int
277
     */
278
    public function numberOfChildren(): int
279
    {
280
        $nchi = $this->children()->count();
281
282
        foreach ($this->facts(['NCHI']) as $fact) {
283
            $nchi = max($nchi, (int) $fact->value());
284
        }
285
286
        return $nchi;
287
    }
288
289
    /**
290
     * get the marriage event
291
     *
292
     * @return Fact|null
293
     */
294
    public function getMarriage(): ?Fact
295
    {
296
        return $this->facts(['MARR'])->first();
297
    }
298
299
    /**
300
     * Get marriage date
301
     *
302
     * @return Date
303
     */
304
    public function getMarriageDate(): Date
305
    {
306
        $marriage = $this->getMarriage();
307
        if ($marriage) {
308
            return $marriage->date();
309
        }
310
311
        return new Date('');
312
    }
313
314
    /**
315
     * Get the marriage year - displayed on lists of families
316
     *
317
     * @return int
318
     */
319
    public function getMarriageYear(): int
320
    {
321
        return $this->getMarriageDate()->minimumDate()->year;
322
    }
323
324
    /**
325
     * Get the marriage place
326
     *
327
     * @return Place
328
     */
329
    public function getMarriagePlace(): Place
330
    {
331
        $marriage = $this->getMarriage();
332
333
        if ($marriage instanceof Fact) {
334
            return $marriage->place();
335
        }
336
337
        return new Place('', $this->tree);
338
    }
339
340
    /**
341
     * Get a list of all marriage dates - for the family lists.
342
     *
343
     * @return Date[]
344
     */
345
    public function getAllMarriageDates(): array
346
    {
347
        foreach (Gedcom::MARRIAGE_EVENTS as $event) {
348
            $array = $this->getAllEventDates([$event]);
349
350
            if ($array !== []) {
351
                return $array;
352
            }
353
        }
354
355
        return [];
356
    }
357
358
    /**
359
     * Get a list of all marriage places - for the family lists.
360
     *
361
     * @return Place[]
362
     */
363
    public function getAllMarriagePlaces(): array
364
    {
365
        foreach (Gedcom::MARRIAGE_EVENTS as $event) {
366
            $places = $this->getAllEventPlaces([$event]);
367
            if ($places !== []) {
368
                return $places;
369
            }
370
        }
371
372
        return [];
373
    }
374
375
    /**
376
     * Derived classes should redefine this function, otherwise the object will have no name
377
     *
378
     * @return string[][]
379
     */
380
    public function getAllNames(): array
381
    {
382
        if ($this->getAllNames === null) {
383
            // Check the script used by each name, so we can match cyrillic with cyrillic, greek with greek, etc.
384
            $husb_names = [];
385
            if ($this->husb) {
386
                $husb_names = array_filter($this->husb->getAllNames(), static function (array $x): bool {
387
                    return $x['type'] !== '_MARNM';
388
                });
389
            }
390
            // If the individual only has married names, create a fake birth name.
391
            if ($husb_names === []) {
392
                $husb_names[] = [
393
                    'type' => 'BIRT',
394
                    'sort' => Individual::NOMEN_NESCIO,
395
                    'full' => I18N::translateContext('Unknown given name', '…') . ' ' . I18N::translateContext('Unknown surname', '…'),
396
                ];
397
            }
398
            foreach ($husb_names as $n => $husb_name) {
399
                $husb_names[$n]['script'] = I18N::textScript($husb_name['full']);
400
            }
401
402
            $wife_names = [];
403
            if ($this->wife) {
404
                $wife_names = array_filter($this->wife->getAllNames(), static function (array $x): bool {
405
                    return $x['type'] !== '_MARNM';
406
                });
407
            }
408
            // If the individual only has married names, create a fake birth name.
409
            if ($wife_names === []) {
410
                $wife_names[] = [
411
                    'type' => 'BIRT',
412
                    'sort' => Individual::NOMEN_NESCIO,
413
                    'full' => I18N::translateContext('Unknown given name', '…') . ' ' . I18N::translateContext('Unknown surname', '…'),
414
                ];
415
            }
416
            foreach ($wife_names as $n => $wife_name) {
417
                $wife_names[$n]['script'] = I18N::textScript($wife_name['full']);
418
            }
419
420
            // Add the matched names first
421
            foreach ($husb_names as $husb_name) {
422
                foreach ($wife_names as $wife_name) {
423
                    if ($husb_name['script'] === $wife_name['script']) {
424
                        $this->getAllNames[] = [
0 ignored issues
show
Bug Best Practice introduced by
The property getAllNames does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
425
                            'type' => $husb_name['type'],
426
                            'sort' => $husb_name['sort'] . ' + ' . $wife_name['sort'],
427
                            'full' => $husb_name['full'] . ' + ' . $wife_name['full'],
428
                            // No need for a fullNN entry - we do not currently store FAM names in the database
429
                        ];
430
                    }
431
                }
432
            }
433
434
            // Add the unmatched names second (there may be no matched names)
435
            foreach ($husb_names as $husb_name) {
436
                foreach ($wife_names as $wife_name) {
437
                    if ($husb_name['script'] !== $wife_name['script']) {
438
                        $this->getAllNames[] = [
439
                            'type' => $husb_name['type'],
440
                            'sort' => $husb_name['sort'] . ' + ' . $wife_name['sort'],
441
                            'full' => $husb_name['full'] . ' + ' . $wife_name['full'],
442
                            // No need for a fullNN entry - we do not currently store FAM names in the database
443
                        ];
444
                    }
445
                }
446
            }
447
        }
448
449
        return $this->getAllNames;
450
    }
451
452
    /**
453
     * This function should be redefined in derived classes to show any major
454
     * identifying characteristics of this record.
455
     *
456
     * @return string
457
     */
458
    public function formatListDetails(): string
459
    {
460
        return
461
            $this->formatFirstMajorFact(Gedcom::MARRIAGE_EVENTS, 1) .
462
            $this->formatFirstMajorFact(Gedcom::DIVORCE_EVENTS, 1);
463
    }
464
465
    /**
466
     * Lock the database row, to prevent concurrent edits.
467
     */
468
    public function lock(): void
469
    {
470
        DB::table('families')
471
            ->where('f_file', '=', $this->tree->id())
472
            ->where('f_id', '=', $this->xref())
473
            ->lockForUpdate()
474
            ->get();
475
    }
476
}
477