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