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