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