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