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