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