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