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