Relationship   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 502
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 139
dl 0
loc 502
rs 3.04
c 0
b 0
f 0
wmc 67

37 Methods

Rating   Name   Duplication   Size   Complexity  
A brother() 0 3 1
A engaged() 0 3 1
A cousin() 0 3 1
A ancestor() 0 3 1
A dynamic() 0 3 1
A daughter() 0 3 1
A sibling() 0 3 1
A child() 0 3 1
B marriageStatus() 0 26 7
A adoptive() 0 7 2
A descendant() 0 3 1
A father() 0 3 1
A __construct() 0 4 1
A fixed() 0 3 1
A divorced() 0 3 1
A female() 0 3 1
A repeatedRelationship() 0 23 4
A relation() 0 14 2
A adopted() 0 7 3
A match() 0 15 4
A wife() 0 3 1
A spouse() 0 3 1
A married() 0 3 1
A fostered() 0 7 3
A mother() 0 3 1
A younger() 0 10 1
A parent() 0 3 1
A partner() 0 3 1
A twin() 0 15 5
A son() 0 3 1
A male() 0 3 1
A older() 0 10 1
A fostering() 0 7 2
A sister() 0 3 1
A sex() 0 5 1
B symmetricCousin() 0 37 8
A husband() 0 3 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Closure;
23
use Fisharebest\Webtrees\Elements\PedigreeLinkageType;
24
25
use function abs;
26
use function array_slice;
27
use function count;
28
use function in_array;
29
use function intdiv;
30
use function min;
31
32
/**
33
 * Class Relationship - define a relationship for a language.
34
 */
35
class Relationship
36
{
37
    // The basic components of a relationship.
38
    // These strings are needed for compatibility with the legacy algorithm.
39
    // Once that has been replaced, it may be more efficient to use integers here.
40
    public const SISTER   = 'sis';
41
    public const BROTHER  = 'bro';
42
    public const SIBLING  = 'sib';
43
    public const MOTHER   = 'mot';
44
    public const FATHER   = 'fat';
45
    public const PARENT   = 'par';
46
    public const DAUGHTER = 'dau';
47
    public const SON      = 'son';
48
    public const CHILD    = 'chi';
49
    public const WIFE     = 'wif';
50
    public const HUSBAND  = 'hus';
51
    public const SPOUSE   = 'spo';
52
53
    public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING];
54
    public const PARENTS  = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT];
55
    public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD];
56
    public const SPOUSES  = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE];
57
58
    // Generates a name from the matched relationship.
59
    private Closure $callback;
60
61
    /** @var array<Closure> List of rules that need to match */
62
    private array $matchers;
63
64
    /**
65
     * @param Closure $callback
66
     */
67
    private function __construct(Closure $callback)
68
    {
69
        $this->callback = $callback;
70
        $this->matchers = [];
71
    }
72
73
    /**
74
     * Allow fluent constructor.
75
     *
76
     * @param string $nominative
77
     * @param string $genitive
78
     *
79
     * @return Relationship
80
     */
81
    public static function fixed(string $nominative, string $genitive): Relationship
82
    {
83
        return new self(fn () => [$nominative, $genitive]);
84
    }
85
86
    /**
87
     * Allow fluent constructor.
88
     *
89
     * @param Closure $callback
90
     *
91
     * @return Relationship
92
     */
93
    public static function dynamic(Closure $callback): Relationship
94
    {
95
        return new self($callback);
96
    }
97
98
    /**
99
     * Does this relationship match the pattern?
100
     *
101
     * @param array<Individual|Family> $nodes
102
     * @param array<string>            $patterns
103
     *
104
     * @return array<string>|null [nominative, genitive] or null
105
     */
106
    public function match(array $nodes, array $patterns): array|null
107
    {
108
        $captures = [];
109
110
        foreach ($this->matchers as $matcher) {
111
            if (!$matcher($nodes, $patterns, $captures)) {
112
                return null;
113
            }
114
        }
115
116
        if ($patterns === []) {
117
            return ($this->callback)(...$captures);
118
        }
119
120
        return null;
121
    }
122
123
    /**
124
     * @return Relationship
125
     */
126
    public function adopted(): Relationship
127
    {
128
        $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
129
                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
130
                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_ADOPTED);
131
132
        return $this;
133
    }
134
135
    /**
136
     * @return Relationship
137
     */
138
    public function adoptive(): Relationship
139
    {
140
        $this->matchers[] = static fn (array $nodes): bool => $nodes[0]
141
            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
142
            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_ADOPTED);
143
144
        return $this;
145
    }
146
147
    /**
148
     * @return Relationship
149
     */
150
    public function brother(): Relationship
151
    {
152
        return $this->relation([self::BROTHER]);
153
    }
154
155
    /**
156
     * Match the next relationship in the path.
157
     *
158
     * @param array<string> $relationships
159
     *
160
     * @return Relationship
161
     */
162
    protected function relation(array $relationships): Relationship
163
    {
164
        $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool {
165
            if (in_array($patterns[0] ?? '', $relationships, true)) {
166
                $nodes    = array_slice($nodes, 2);
167
                $patterns = array_slice($patterns, 1);
168
169
                return true;
170
            }
171
172
            return false;
173
        };
174
175
        return $this;
176
    }
177
178
    /**
179
     * The number of ancestors may be different to the number of descendants
180
     *
181
     * @return Relationship
182
     */
183
    public function cousin(): Relationship
184
    {
185
        return $this->ancestor()->sibling()->descendant();
186
    }
187
188
    /**
189
     * @return Relationship
190
     */
191
    public function descendant(): Relationship
192
    {
193
        return $this->repeatedRelationship(self::CHILDREN);
194
    }
195
196
    /**
197
     * Match a repeated number of the same type of component
198
     *
199
     * @param array<string> $relationships
200
     *
201
     * @return Relationship
202
     */
203
    protected function repeatedRelationship(array $relationships): Relationship
204
    {
205
        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool {
206
            $limit = min(intdiv(count($nodes), 2), count($patterns));
207
208
            for ($generations = 0; $generations < $limit; ++$generations) {
209
                if (!in_array($patterns[$generations], $relationships, true)) {
210
                    break;
211
                }
212
            }
213
214
            if ($generations > 0) {
215
                $nodes      = array_slice($nodes, 2 * $generations);
216
                $patterns   = array_slice($patterns, $generations);
217
                $captures[] = $generations;
218
219
                return true;
220
            }
221
222
            return false;
223
        };
224
225
        return $this;
226
    }
227
228
    /**
229
     * @return Relationship
230
     */
231
    public function sibling(): Relationship
232
    {
233
        return $this->relation(self::SIBLINGS);
234
    }
235
236
    /**
237
     * @return Relationship
238
     */
239
    public function ancestor(): Relationship
240
    {
241
        return $this->repeatedRelationship(self::PARENTS);
242
    }
243
244
    /**
245
     * @return Relationship
246
     */
247
    public function child(): Relationship
248
    {
249
        return $this->relation(self::CHILDREN);
250
    }
251
252
    /**
253
     * @return Relationship
254
     */
255
    public function daughter(): Relationship
256
    {
257
        return $this->relation([self::DAUGHTER]);
258
    }
259
260
    /**
261
     * @return Relationship
262
     */
263
    public function divorced(): Relationship
264
    {
265
        return $this->marriageStatus('DIV');
266
    }
267
268
    /**
269
     * Match a marriage status
270
     *
271
     * @param string $status
272
     *
273
     * @return Relationship
274
     */
275
    protected function marriageStatus(string $status): Relationship
276
    {
277
        $this->matchers[] = static function (array $nodes) use ($status): bool {
278
            $family = $nodes[1] ?? null;
279
280
            if ($family instanceof Family) {
281
                $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last();
282
283
                if ($fact instanceof Fact) {
284
                    switch ($status) {
285
                        case 'MARR':
286
                            return $fact->tag() === 'FAM:MARR';
287
288
                        case 'DIV':
289
                            return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL';
290
291
                        case 'ENGA':
292
                            return $fact->tag() === 'FAM:ENGA';
293
                    }
294
                }
295
            }
296
297
            return false;
298
        };
299
300
        return $this;
301
    }
302
303
    /**
304
     * @return Relationship
305
     */
306
    public function engaged(): Relationship
307
    {
308
        return $this->marriageStatus('ENGA');
309
    }
310
311
    /**
312
     * @return Relationship
313
     */
314
    public function father(): Relationship
315
    {
316
        return $this->relation([self::FATHER]);
317
    }
318
319
    /**
320
     * @return Relationship
321
     */
322
    public function female(): Relationship
323
    {
324
        return $this->sex('F');
325
    }
326
327
    /**
328
     * Match the sex of the current individual
329
     *
330
     * @param string $sex
331
     *
332
     * @return Relationship
333
     */
334
    protected function sex(string $sex): Relationship
335
    {
336
        $this->matchers[] = static fn (array $nodes): bool => $nodes[0]->sex() === $sex;
337
338
        return $this;
339
    }
340
341
    /**
342
     * @return Relationship
343
     */
344
    public function fostered(): Relationship
345
    {
346
        $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
347
                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
348
                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER);
349
350
        return $this;
351
    }
352
353
    /**
354
     * @return Relationship
355
     */
356
    public function fostering(): Relationship
357
    {
358
        $this->matchers[] = static fn (array $nodes): bool => $nodes[0]
359
            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
360
            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER);
361
362
        return $this;
363
    }
364
365
    /**
366
     * @return Relationship
367
     */
368
    public function husband(): Relationship
369
    {
370
        return $this->married()->relation([self::HUSBAND]);
371
    }
372
373
    /**
374
     * @return Relationship
375
     */
376
    public function married(): Relationship
377
    {
378
        return $this->marriageStatus('MARR');
379
    }
380
381
    /**
382
     * @return Relationship
383
     */
384
    public function male(): Relationship
385
    {
386
        return $this->sex('M');
387
    }
388
389
    /**
390
     * @return Relationship
391
     */
392
    public function mother(): Relationship
393
    {
394
        return $this->relation([self::MOTHER]);
395
    }
396
397
    /**
398
     * @return Relationship
399
     */
400
    public function older(): Relationship
401
    {
402
        $this->matchers[] = static function (array $nodes): bool {
403
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
404
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
405
406
            return Date::compare($date1, $date2) > 0;
407
        };
408
409
        return $this;
410
    }
411
412
    /**
413
     * @return Relationship
414
     */
415
    public function parent(): Relationship
416
    {
417
        return $this->relation(self::PARENTS);
418
    }
419
420
    /**
421
     * @return Relationship
422
     */
423
    public function sister(): Relationship
424
    {
425
        return $this->relation([self::SISTER]);
426
    }
427
428
    /**
429
     * @return Relationship
430
     */
431
    public function son(): Relationship
432
    {
433
        return $this->relation([self::SON]);
434
    }
435
436
    /**
437
     * @return Relationship
438
     */
439
    public function spouse(): Relationship
440
    {
441
        return $this->married()->partner();
442
    }
443
444
    /**
445
     * @return Relationship
446
     */
447
    public function partner(): Relationship
448
    {
449
        return $this->relation(self::SPOUSES);
450
    }
451
452
    /**
453
     * The number of ancestors must be the same as the number of descendants
454
     *
455
     * @return Relationship
456
     */
457
    public function symmetricCousin(): Relationship
458
    {
459
        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
460
            $count = count($patterns);
461
462
            $n = 0;
463
464
            // Ancestors
465
            while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
466
                $n++;
467
            }
468
469
            // No ancestors?  Not enough path left for descendants?
470
            if ($n === 0 || $n * 2 + 1 !== $count) {
471
                return false;
472
            }
473
474
            // Siblings
475
            if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
476
                return false;
477
            }
478
479
            // Descendants
480
            for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
481
                if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
482
                    return false;
483
                }
484
            }
485
486
            $nodes      = array_slice($nodes, 2 * (2 * $n + 1));
487
            $patterns   = [];
488
            $captures[] = $n;
489
490
            return true;
491
        };
492
493
        return $this;
494
    }
495
496
    /**
497
     * @return Relationship
498
     */
499
    public function twin(): Relationship
500
    {
501
        $this->matchers[] = static function (array $nodes): bool {
502
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
503
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
504
505
            return
506
                $date1->isOK() &&
507
                $date2->isOK() &&
508
                abs($date1->julianDay() - $date2->julianDay()) < 2 &&
509
                $date1->minimumDate()->day > 0 &&
510
                $date2->minimumDate()->day > 0;
511
        };
512
513
        return $this;
514
    }
515
516
    /**
517
     * @return Relationship
518
     */
519
    public function wife(): Relationship
520
    {
521
        return $this->married()->relation([self::WIFE]);
522
    }
523
524
    /**
525
     * @return Relationship
526
     */
527
    public function younger(): Relationship
528
    {
529
        $this->matchers[] = static function (array $nodes): bool {
530
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
531
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
532
533
            return Date::compare($date1, $date2) < 0;
534
        };
535
536
        return $this;
537
    }
538
}
539