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