132cd2800SGreg Roach<?php 23976b470SGreg Roach 332cd2800SGreg Roach/** 432cd2800SGreg Roach * webtrees: online genealogy 532cd2800SGreg Roach * Copyright (C) 2019 webtrees development team 632cd2800SGreg Roach * This program is free software: you can redistribute it and/or modify 732cd2800SGreg Roach * it under the terms of the GNU General Public License as published by 832cd2800SGreg Roach * the Free Software Foundation, either version 3 of the License, or 932cd2800SGreg Roach * (at your option) any later version. 1032cd2800SGreg Roach * This program is distributed in the hope that it will be useful, 1132cd2800SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 1232cd2800SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1332cd2800SGreg Roach * GNU General Public License for more details. 1432cd2800SGreg Roach * You should have received a copy of the GNU General Public License 1532cd2800SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 1632cd2800SGreg Roach */ 17fcfa147eSGreg Roach 1832cd2800SGreg Roachdeclare(strict_types=1); 1932cd2800SGreg Roach 2032cd2800SGreg Roachnamespace Fisharebest\Webtrees\Services; 2132cd2800SGreg Roach 2232cd2800SGreg Roachuse Closure; 23dfb2cda2SGreg Roachuse Fisharebest\Webtrees\Date; 24d501c45dSGreg Roachuse Fisharebest\Webtrees\Exceptions\HttpServiceUnavailableException; 2532cd2800SGreg Roachuse Fisharebest\Webtrees\Family; 26a7a24840SGreg Roachuse Fisharebest\Webtrees\Gedcom; 2732cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 2852a8ef61SGreg Roachuse Fisharebest\Webtrees\I18N; 2932cd2800SGreg Roachuse Fisharebest\Webtrees\Individual; 3032cd2800SGreg Roachuse Fisharebest\Webtrees\Media; 3132cd2800SGreg Roachuse Fisharebest\Webtrees\Note; 32b68caec6SGreg Roachuse Fisharebest\Webtrees\Place; 3332cd2800SGreg Roachuse Fisharebest\Webtrees\Repository; 342d686e68SGreg Roachuse Fisharebest\Webtrees\Soundex; 3532cd2800SGreg Roachuse Fisharebest\Webtrees\Source; 36b5c8fd7eSGreg Roachuse Fisharebest\Webtrees\Submitter; 3732cd2800SGreg Roachuse Fisharebest\Webtrees\Tree; 3832cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 3932cd2800SGreg Roachuse Illuminate\Database\Query\Builder; 40a7a24840SGreg Roachuse Illuminate\Database\Query\Expression; 4132cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause; 4232cd2800SGreg Roachuse Illuminate\Support\Collection; 4332cd2800SGreg Roachuse stdClass; 443976b470SGreg Roach 45a7a24840SGreg Roachuse function mb_stripos; 4632cd2800SGreg Roach 4732cd2800SGreg Roach/** 4832cd2800SGreg Roach * Search trees for genealogy records. 4932cd2800SGreg Roach */ 5032cd2800SGreg Roachclass SearchService 5132cd2800SGreg Roach{ 52d5ad3db0SGreg Roach /** @var TreeService */ 53d5ad3db0SGreg Roach private $tree_service; 54d5ad3db0SGreg Roach 55d5ad3db0SGreg Roach /** 56d5ad3db0SGreg Roach * SearchService constructor. 57d5ad3db0SGreg Roach * 58d5ad3db0SGreg Roach * @param TreeService $tree_service 59d5ad3db0SGreg Roach */ 60*3959eeb6SGreg Roach public function __construct( 61*3959eeb6SGreg Roach TreeService $tree_service 62*3959eeb6SGreg Roach ) { 63d5ad3db0SGreg Roach $this->tree_service = $tree_service; 64d5ad3db0SGreg Roach } 65d5ad3db0SGreg Roach 66a7a24840SGreg Roach /** 67a7a24840SGreg Roach * @param Tree[] $trees 68a7a24840SGreg Roach * @param string[] $search 69a7a24840SGreg Roach * 70b5c8fd7eSGreg Roach * @return Collection<Family> 71a7a24840SGreg Roach */ 72a7a24840SGreg Roach public function searchFamilies(array $trees, array $search): Collection 73a7a24840SGreg Roach { 74a7a24840SGreg Roach $query = DB::table('families'); 75a7a24840SGreg Roach 76a7a24840SGreg Roach $this->whereTrees($query, 'f_file', $trees); 77a7a24840SGreg Roach $this->whereSearch($query, 'f_gedcom', $search); 78a7a24840SGreg Roach 79a7a24840SGreg Roach return $query 80a7a24840SGreg Roach ->get() 8152a8ef61SGreg Roach ->each($this->rowLimiter()) 82d5ad3db0SGreg Roach ->map($this->familyRowMapper()) 83a7a24840SGreg Roach ->filter(GedcomRecord::accessFilter()) 847f5fa3c2SGreg Roach ->filter($this->rawGedcomFilter($search)); 85a7a24840SGreg Roach } 86a7a24840SGreg Roach 8732cd2800SGreg Roach /** 8832cd2800SGreg Roach * Search for families by name. 8932cd2800SGreg Roach * 90a7a24840SGreg Roach * @param Tree[] $trees 91a7a24840SGreg Roach * @param string[] $search 9232cd2800SGreg Roach * @param int $offset 9332cd2800SGreg Roach * @param int $limit 9432cd2800SGreg Roach * 95b5c8fd7eSGreg Roach * @return Collection<Family> 9632cd2800SGreg Roach */ 97a7a24840SGreg Roach public function searchFamilyNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 9832cd2800SGreg Roach { 9932cd2800SGreg Roach $query = DB::table('families') 100ac499332SGreg Roach ->leftJoin('name AS husb_name', static function (JoinClause $join): void { 10132cd2800SGreg Roach $join 10232cd2800SGreg Roach ->on('husb_name.n_file', '=', 'families.f_file') 103ac499332SGreg Roach ->on('husb_name.n_id', '=', 'families.f_husb') 104ac499332SGreg Roach ->where('husb_name.n_type', '<>', '_MARNM'); 10532cd2800SGreg Roach }) 106ac499332SGreg Roach ->leftJoin('name AS wife_name', static function (JoinClause $join): void { 10732cd2800SGreg Roach $join 10832cd2800SGreg Roach ->on('wife_name.n_file', '=', 'families.f_file') 109ac499332SGreg Roach ->on('wife_name.n_id', '=', 'families.f_wife') 110ac499332SGreg Roach ->where('wife_name.n_type', '<>', '_MARNM'); 111ac499332SGreg Roach }); 112a7a24840SGreg Roach 113a7a24840SGreg Roach $prefix = DB::connection()->getTablePrefix(); 114a69f5655SGreg Roach $field = new Expression('COALESCE(' . $prefix . "husb_name.n_full, '') || COALESCE(" . $prefix . "wife_name.n_full, '')"); 115a7a24840SGreg Roach 116a7a24840SGreg Roach $this->whereTrees($query, 'f_file', $trees); 117a7a24840SGreg Roach $this->whereSearch($query, $field, $search); 118a7a24840SGreg Roach 119a7a24840SGreg Roach $query 12032cd2800SGreg Roach ->orderBy('husb_name.n_sort') 12132cd2800SGreg Roach ->orderBy('wife_name.n_sort') 122c0804649SGreg Roach ->select(['families.*', 'husb_name.n_sort', 'wife_name.n_sort']) 12332cd2800SGreg Roach ->distinct(); 12432cd2800SGreg Roach 125d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->familyRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 126a7a24840SGreg Roach } 127a7a24840SGreg Roach 128a7a24840SGreg Roach /** 129dbe53437SGreg Roach * @param Place $place 130dbe53437SGreg Roach * 131dbe53437SGreg Roach * @return Collection<Family> 132dbe53437SGreg Roach */ 133dbe53437SGreg Roach public function searchFamiliesInPlace(Place $place): Collection 134dbe53437SGreg Roach { 135dbe53437SGreg Roach return DB::table('families') 136dbe53437SGreg Roach ->join('placelinks', static function (JoinClause $query) { 137dbe53437SGreg Roach $query 138dbe53437SGreg Roach ->on('families.f_file', '=', 'placelinks.pl_file') 139dbe53437SGreg Roach ->on('families.f_id', '=', 'placelinks.pl_gid'); 140dbe53437SGreg Roach }) 141dbe53437SGreg Roach ->where('f_file', '=', $place->tree()->id()) 142dbe53437SGreg Roach ->where('pl_p_id', '=', $place->id()) 143dbe53437SGreg Roach ->select(['families.*']) 144dbe53437SGreg Roach ->get() 145dbe53437SGreg Roach ->each($this->rowLimiter()) 146dbe53437SGreg Roach ->map($this->familyRowMapper()) 147dbe53437SGreg Roach ->filter(GedcomRecord::accessFilter()); 148dbe53437SGreg Roach } 149dbe53437SGreg Roach 150dbe53437SGreg Roach /** 151a7a24840SGreg Roach * @param Tree[] $trees 152a7a24840SGreg Roach * @param string[] $search 153a7a24840SGreg Roach * 154b5c8fd7eSGreg Roach * @return Collection<Individual> 155a7a24840SGreg Roach */ 156a7a24840SGreg Roach public function searchIndividuals(array $trees, array $search): Collection 157a7a24840SGreg Roach { 158a7a24840SGreg Roach $query = DB::table('individuals'); 159a7a24840SGreg Roach 160a7a24840SGreg Roach $this->whereTrees($query, 'i_file', $trees); 161a7a24840SGreg Roach $this->whereSearch($query, 'i_gedcom', $search); 162a7a24840SGreg Roach 163a7a24840SGreg Roach return $query 164a7a24840SGreg Roach ->get() 16552a8ef61SGreg Roach ->each($this->rowLimiter()) 166d5ad3db0SGreg Roach ->map($this->individualRowMapper()) 167a7a24840SGreg Roach ->filter(GedcomRecord::accessFilter()) 1687f5fa3c2SGreg Roach ->filter($this->rawGedcomFilter($search)); 16932cd2800SGreg Roach } 17032cd2800SGreg Roach 17132cd2800SGreg Roach /** 17232cd2800SGreg Roach * Search for individuals by name. 17332cd2800SGreg Roach * 174a7a24840SGreg Roach * @param Tree[] $trees 175a7a24840SGreg Roach * @param string[] $search 17632cd2800SGreg Roach * @param int $offset 17732cd2800SGreg Roach * @param int $limit 17832cd2800SGreg Roach * 179b5c8fd7eSGreg Roach * @return Collection<Individual> 18032cd2800SGreg Roach */ 181a7a24840SGreg Roach public function searchIndividualNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 18232cd2800SGreg Roach { 18332cd2800SGreg Roach $query = DB::table('individuals') 1840b5fd0a6SGreg Roach ->join('name', static function (JoinClause $join): void { 18532cd2800SGreg Roach $join 18632cd2800SGreg Roach ->on('name.n_file', '=', 'individuals.i_file') 187a7a24840SGreg Roach ->on('name.n_id', '=', 'individuals.i_id'); 18832cd2800SGreg Roach }) 189e84cf2deSGreg Roach ->orderBy('n_sort') 190d78d61f7SGreg Roach ->distinct() 191685de081SGreg Roach ->select(['individuals.*', 'n_sort']); 19232cd2800SGreg Roach 193a7a24840SGreg Roach $this->whereTrees($query, 'i_file', $trees); 194a7a24840SGreg Roach $this->whereSearch($query, 'n_full', $search); 195a7a24840SGreg Roach 196d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->individualRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 19732cd2800SGreg Roach } 19832cd2800SGreg Roach 19932cd2800SGreg Roach /** 200dbe53437SGreg Roach * @param Place $place 201dbe53437SGreg Roach * 202dbe53437SGreg Roach * @return Collection<Individual> 203dbe53437SGreg Roach */ 204dbe53437SGreg Roach public function searchIndividualsInPlace(Place $place): Collection 205dbe53437SGreg Roach { 206dbe53437SGreg Roach return DB::table('individuals') 207dbe53437SGreg Roach ->join('placelinks', static function (JoinClause $join) { 208dbe53437SGreg Roach $join 209dbe53437SGreg Roach ->on('i_file', '=', 'pl_file') 210dbe53437SGreg Roach ->on('i_id', '=', 'pl_gid'); 211dbe53437SGreg Roach }) 212dbe53437SGreg Roach ->where('i_file', '=', $place->tree()->id()) 213dbe53437SGreg Roach ->where('pl_p_id', '=', $place->id()) 214dbe53437SGreg Roach ->select(['individuals.*']) 215dbe53437SGreg Roach ->get() 216dbe53437SGreg Roach ->each($this->rowLimiter()) 217dbe53437SGreg Roach ->map($this->individualRowMapper()) 218dbe53437SGreg Roach ->filter(GedcomRecord::accessFilter()); 219dbe53437SGreg Roach } 220dbe53437SGreg Roach 221dbe53437SGreg Roach /** 22232cd2800SGreg Roach * Search for media objects. 22332cd2800SGreg Roach * 224a7a24840SGreg Roach * @param Tree[] $trees 225a7a24840SGreg Roach * @param string[] $search 22632cd2800SGreg Roach * @param int $offset 22732cd2800SGreg Roach * @param int $limit 22832cd2800SGreg Roach * 229b5c8fd7eSGreg Roach * @return Collection<Media> 23032cd2800SGreg Roach */ 231a7a24840SGreg Roach public function searchMedia(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 23232cd2800SGreg Roach { 233a7a24840SGreg Roach $query = DB::table('media'); 23432cd2800SGreg Roach 235a7a24840SGreg Roach $this->whereTrees($query, 'media.m_file', $trees); 236a7a24840SGreg Roach $this->whereSearch($query, 'm_gedcom', $search); 237a7a24840SGreg Roach 238d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->mediaRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 23932cd2800SGreg Roach } 24032cd2800SGreg Roach 24132cd2800SGreg Roach /** 24232cd2800SGreg Roach * Search for notes. 24332cd2800SGreg Roach * 244a7a24840SGreg Roach * @param Tree[] $trees 245a7a24840SGreg Roach * @param string[] $search 24632cd2800SGreg Roach * @param int $offset 24732cd2800SGreg Roach * @param int $limit 24832cd2800SGreg Roach * 249b5c8fd7eSGreg Roach * @return Collection<Note> 25032cd2800SGreg Roach */ 251a7a24840SGreg Roach public function searchNotes(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 25232cd2800SGreg Roach { 25332cd2800SGreg Roach $query = DB::table('other') 254a7a24840SGreg Roach ->where('o_type', '=', 'NOTE'); 25532cd2800SGreg Roach 256a7a24840SGreg Roach $this->whereTrees($query, 'o_file', $trees); 257a7a24840SGreg Roach $this->whereSearch($query, 'o_gedcom', $search); 258a7a24840SGreg Roach 259d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->noteRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 26032cd2800SGreg Roach } 26132cd2800SGreg Roach 26232cd2800SGreg Roach /** 26332cd2800SGreg Roach * Search for repositories. 26432cd2800SGreg Roach * 265a7a24840SGreg Roach * @param Tree[] $trees 266a7a24840SGreg Roach * @param string[] $search 26732cd2800SGreg Roach * @param int $offset 26832cd2800SGreg Roach * @param int $limit 26932cd2800SGreg Roach * 270b5c8fd7eSGreg Roach * @return Collection<Repository> 27132cd2800SGreg Roach */ 272a7a24840SGreg Roach public function searchRepositories(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 27332cd2800SGreg Roach { 27432cd2800SGreg Roach $query = DB::table('other') 275a7a24840SGreg Roach ->where('o_type', '=', 'REPO'); 27632cd2800SGreg Roach 277a7a24840SGreg Roach $this->whereTrees($query, 'o_file', $trees); 278a7a24840SGreg Roach $this->whereSearch($query, 'o_gedcom', $search); 279a7a24840SGreg Roach 280d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->repositoryRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 28132cd2800SGreg Roach } 28232cd2800SGreg Roach 28332cd2800SGreg Roach /** 284a7a24840SGreg Roach * Search for sources. 28532cd2800SGreg Roach * 286a7a24840SGreg Roach * @param Tree[] $trees 287a7a24840SGreg Roach * @param string[] $search 28832cd2800SGreg Roach * @param int $offset 28932cd2800SGreg Roach * @param int $limit 29032cd2800SGreg Roach * 291b5c8fd7eSGreg Roach * @return Collection<Source> 29232cd2800SGreg Roach */ 293a7a24840SGreg Roach public function searchSources(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 294a7a24840SGreg Roach { 295a7a24840SGreg Roach $query = DB::table('sources'); 296a7a24840SGreg Roach 297a7a24840SGreg Roach $this->whereTrees($query, 's_file', $trees); 298a7a24840SGreg Roach $this->whereSearch($query, 's_gedcom', $search); 299a7a24840SGreg Roach 300d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 301a7a24840SGreg Roach } 302a7a24840SGreg Roach 303a7a24840SGreg Roach /** 304a7a24840SGreg Roach * Search for sources by name. 305a7a24840SGreg Roach * 306a7a24840SGreg Roach * @param Tree[] $trees 307a7a24840SGreg Roach * @param string[] $search 308a7a24840SGreg Roach * @param int $offset 309a7a24840SGreg Roach * @param int $limit 310a7a24840SGreg Roach * 311b5c8fd7eSGreg Roach * @return Collection<Source> 312a7a24840SGreg Roach */ 313a7a24840SGreg Roach public function searchSourcesByName(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 31432cd2800SGreg Roach { 31532cd2800SGreg Roach $query = DB::table('sources') 316c0804649SGreg Roach ->orderBy('s_name'); 31732cd2800SGreg Roach 318a7a24840SGreg Roach $this->whereTrees($query, 's_file', $trees); 319a7a24840SGreg Roach $this->whereSearch($query, 's_name', $search); 320a7a24840SGreg Roach 321d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 32232cd2800SGreg Roach } 32332cd2800SGreg Roach 32432cd2800SGreg Roach /** 32532cd2800SGreg Roach * Search for submitters. 32632cd2800SGreg Roach * 327a7a24840SGreg Roach * @param Tree[] $trees 328a7a24840SGreg Roach * @param string[] $search 32932cd2800SGreg Roach * @param int $offset 33032cd2800SGreg Roach * @param int $limit 33132cd2800SGreg Roach * 332b5c8fd7eSGreg Roach * @return Collection<Submitter> 33332cd2800SGreg Roach */ 334a7a24840SGreg Roach public function searchSubmitters(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 33532cd2800SGreg Roach { 33632cd2800SGreg Roach $query = DB::table('other') 337a7a24840SGreg Roach ->where('o_type', '=', 'SUBM'); 33832cd2800SGreg Roach 339a7a24840SGreg Roach $this->whereTrees($query, 'o_file', $trees); 340a7a24840SGreg Roach $this->whereSearch($query, 'o_gedcom', $search); 341a7a24840SGreg Roach 342d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->submitterRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 34332cd2800SGreg Roach } 34432cd2800SGreg Roach 34532cd2800SGreg Roach /** 346b68caec6SGreg Roach * Search for places. 347b68caec6SGreg Roach * 348b68caec6SGreg Roach * @param Tree $tree 349b68caec6SGreg Roach * @param string $search 350b68caec6SGreg Roach * @param int $offset 351b68caec6SGreg Roach * @param int $limit 352b68caec6SGreg Roach * 353b5c8fd7eSGreg Roach * @return Collection<Place> 354b68caec6SGreg Roach */ 355b68caec6SGreg Roach public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 356b68caec6SGreg Roach { 357b68caec6SGreg Roach $query = DB::table('places AS p0') 358b68caec6SGreg Roach ->where('p0.p_file', '=', $tree->id()) 359b68caec6SGreg Roach ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id') 360b68caec6SGreg Roach ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id') 361b68caec6SGreg Roach ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id') 362b68caec6SGreg Roach ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id') 363b68caec6SGreg Roach ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id') 364b68caec6SGreg Roach ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id') 365b68caec6SGreg Roach ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id') 366b68caec6SGreg Roach ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id') 367b68caec6SGreg Roach ->orderBy('p0.p_place') 368b68caec6SGreg Roach ->orderBy('p1.p_place') 369b68caec6SGreg Roach ->orderBy('p2.p_place') 370b68caec6SGreg Roach ->orderBy('p3.p_place') 371b68caec6SGreg Roach ->orderBy('p4.p_place') 372b68caec6SGreg Roach ->orderBy('p5.p_place') 373b68caec6SGreg Roach ->orderBy('p6.p_place') 374b68caec6SGreg Roach ->orderBy('p7.p_place') 375b68caec6SGreg Roach ->orderBy('p8.p_place') 376b68caec6SGreg Roach ->select([ 377b68caec6SGreg Roach 'p0.p_place AS place0', 378b68caec6SGreg Roach 'p1.p_place AS place1', 379b68caec6SGreg Roach 'p2.p_place AS place2', 380b68caec6SGreg Roach 'p3.p_place AS place3', 381b68caec6SGreg Roach 'p4.p_place AS place4', 382b68caec6SGreg Roach 'p5.p_place AS place5', 383b68caec6SGreg Roach 'p6.p_place AS place6', 384b68caec6SGreg Roach 'p7.p_place AS place7', 385b68caec6SGreg Roach 'p8.p_place AS place8', 386b68caec6SGreg Roach ]); 387b68caec6SGreg Roach 388b68caec6SGreg Roach // Filter each level of the hierarchy. 389b68caec6SGreg Roach foreach (explode(',', $search, 9) as $level => $string) { 390b68caec6SGreg Roach $query->whereContains('p' . $level . '.p_place', $string); 391b68caec6SGreg Roach } 392b68caec6SGreg Roach 3936c2179e2SGreg Roach $row_mapper = static function (stdClass $row) use ($tree): Place { 394b68caec6SGreg Roach $place = implode(', ', array_filter((array) $row)); 395b68caec6SGreg Roach 396b68caec6SGreg Roach return new Place($place, $tree); 397b68caec6SGreg Roach }; 398b68caec6SGreg Roach 3996c2179e2SGreg Roach $filter = static function (): bool { 400a7a24840SGreg Roach return true; 401a7a24840SGreg Roach }; 402b68caec6SGreg Roach 403a7a24840SGreg Roach return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit); 404a7a24840SGreg Roach } 405b68caec6SGreg Roach 406b68caec6SGreg Roach /** 407a5dbc5b2SGreg Roach * @param Tree[] $trees 408dfb2cda2SGreg Roach * @param string[] $fields 409dfb2cda2SGreg Roach * @param string[] $modifiers 410dfb2cda2SGreg Roach * 411b5c8fd7eSGreg Roach * @return Collection<Individual> 412dfb2cda2SGreg Roach */ 413dfb2cda2SGreg Roach public function searchIndividualsAdvanced(array $trees, array $fields, array $modifiers): Collection 414dfb2cda2SGreg Roach { 415dfb2cda2SGreg Roach $fields = array_filter($fields); 416dfb2cda2SGreg Roach 417dfb2cda2SGreg Roach $query = DB::table('individuals') 418dfb2cda2SGreg Roach ->select(['individuals.*']) 419dfb2cda2SGreg Roach ->distinct(); 420dfb2cda2SGreg Roach 421dfb2cda2SGreg Roach $this->whereTrees($query, 'i_file', $trees); 422dfb2cda2SGreg Roach 423dfb2cda2SGreg Roach // Join the following tables 424dfb2cda2SGreg Roach $father_name = false; 425dfb2cda2SGreg Roach $mother_name = false; 426dfb2cda2SGreg Roach $spouse_family = false; 427dfb2cda2SGreg Roach $indi_name = false; 428dfb2cda2SGreg Roach $indi_date = false; 429dfb2cda2SGreg Roach $fam_date = false; 430dfb2cda2SGreg Roach $indi_plac = false; 431dfb2cda2SGreg Roach $fam_plac = false; 432dfb2cda2SGreg Roach 433dfb2cda2SGreg Roach foreach ($fields as $field_name => $field_value) { 434dfb2cda2SGreg Roach if ($field_value !== '') { 435dfb2cda2SGreg Roach if (substr($field_name, 0, 14) === 'FAMC:HUSB:NAME') { 436dfb2cda2SGreg Roach $father_name = true; 437dfb2cda2SGreg Roach } elseif (substr($field_name, 0, 14) === 'FAMC:WIFE:NAME') { 438dfb2cda2SGreg Roach $mother_name = true; 439dfb2cda2SGreg Roach } elseif (substr($field_name, 0, 4) === 'NAME') { 440dfb2cda2SGreg Roach $indi_name = true; 441dfb2cda2SGreg Roach } elseif (strpos($field_name, ':DATE') !== false) { 442dfb2cda2SGreg Roach if (substr($field_name, 0, 4) === 'FAMS') { 443dfb2cda2SGreg Roach $fam_date = true; 444dfb2cda2SGreg Roach $spouse_family = true; 445dfb2cda2SGreg Roach } else { 446dfb2cda2SGreg Roach $indi_date = true; 447dfb2cda2SGreg Roach } 448dfb2cda2SGreg Roach } elseif (strpos($field_name, ':PLAC') !== false) { 449dfb2cda2SGreg Roach if (substr($field_name, 0, 4) === 'FAMS') { 450dfb2cda2SGreg Roach $fam_plac = true; 451dfb2cda2SGreg Roach $spouse_family = true; 452dfb2cda2SGreg Roach } else { 453dfb2cda2SGreg Roach $indi_plac = true; 454dfb2cda2SGreg Roach } 455dfb2cda2SGreg Roach } elseif ($field_name === 'FAMS:NOTE') { 456dfb2cda2SGreg Roach $spouse_family = true; 457dfb2cda2SGreg Roach } 458dfb2cda2SGreg Roach } 459dfb2cda2SGreg Roach } 460dfb2cda2SGreg Roach 461dfb2cda2SGreg Roach if ($father_name || $mother_name) { 4620b5fd0a6SGreg Roach $query->join('link AS l1', static function (JoinClause $join): void { 463dfb2cda2SGreg Roach $join 464dfb2cda2SGreg Roach ->on('l1.l_file', '=', 'individuals.i_file') 465dfb2cda2SGreg Roach ->on('l1.l_from', '=', 'individuals.i_id') 466dfb2cda2SGreg Roach ->where('l1.l_type', '=', 'FAMC'); 467dfb2cda2SGreg Roach }); 468dfb2cda2SGreg Roach 469dfb2cda2SGreg Roach if ($father_name) { 4700b5fd0a6SGreg Roach $query->join('link AS l2', static function (JoinClause $join): void { 471dfb2cda2SGreg Roach $join 472dfb2cda2SGreg Roach ->on('l2.l_file', '=', 'l1.l_file') 473dfb2cda2SGreg Roach ->on('l2.l_from', '=', 'l1.l_to') 474dfb2cda2SGreg Roach ->where('l2.l_type', '=', 'HUSB'); 475dfb2cda2SGreg Roach }); 4760b5fd0a6SGreg Roach $query->join('name AS father_name', static function (JoinClause $join): void { 477dfb2cda2SGreg Roach $join 478dfb2cda2SGreg Roach ->on('father_name.n_file', '=', 'l2.l_file') 479dfb2cda2SGreg Roach ->on('father_name.n_id', '=', 'l2.l_to'); 480dfb2cda2SGreg Roach }); 481dfb2cda2SGreg Roach } 482dfb2cda2SGreg Roach 483dfb2cda2SGreg Roach if ($mother_name) { 4840b5fd0a6SGreg Roach $query->join('link AS l3', static function (JoinClause $join): void { 485dfb2cda2SGreg Roach $join 486dfb2cda2SGreg Roach ->on('l3.l_file', '=', 'l1.l_file') 487dfb2cda2SGreg Roach ->on('l3.l_from', '=', 'l1.l_to') 488dfb2cda2SGreg Roach ->where('l3.l_type', '=', 'WIFE'); 489dfb2cda2SGreg Roach }); 4900b5fd0a6SGreg Roach $query->join('name AS mother_name', static function (JoinClause $join): void { 491dfb2cda2SGreg Roach $join 492dfb2cda2SGreg Roach ->on('mother_name.n_file', '=', 'l3.l_file') 493dfb2cda2SGreg Roach ->on('mother_name.n_id', '=', 'l3.l_to'); 494dfb2cda2SGreg Roach }); 495dfb2cda2SGreg Roach } 496dfb2cda2SGreg Roach } 497dfb2cda2SGreg Roach 498dfb2cda2SGreg Roach if ($spouse_family) { 4990b5fd0a6SGreg Roach $query->join('link AS l4', static function (JoinClause $join): void { 500dfb2cda2SGreg Roach $join 501dfb2cda2SGreg Roach ->on('l4.l_file', '=', 'individuals.i_file') 502dfb2cda2SGreg Roach ->on('l4.l_from', '=', 'individuals.i_id') 503dfb2cda2SGreg Roach ->where('l4.l_type', '=', 'FAMS'); 504dfb2cda2SGreg Roach }); 5050b5fd0a6SGreg Roach $query->join('families AS spouse_families', static function (JoinClause $join): void { 506dfb2cda2SGreg Roach $join 507dfb2cda2SGreg Roach ->on('spouse_families.f_file', '=', 'l4.l_file') 508dfb2cda2SGreg Roach ->on('spouse_families.f_id', '=', 'l4.l_to'); 509dfb2cda2SGreg Roach }); 510dfb2cda2SGreg Roach } 511dfb2cda2SGreg Roach 512dfb2cda2SGreg Roach if ($indi_name) { 5130b5fd0a6SGreg Roach $query->join('name AS individual_name', static function (JoinClause $join): void { 514dfb2cda2SGreg Roach $join 515dfb2cda2SGreg Roach ->on('individual_name.n_file', '=', 'individuals.i_file') 516dfb2cda2SGreg Roach ->on('individual_name.n_id', '=', 'individuals.i_id'); 517dfb2cda2SGreg Roach }); 518dfb2cda2SGreg Roach } 519dfb2cda2SGreg Roach 520dfb2cda2SGreg Roach if ($indi_date) { 5210b5fd0a6SGreg Roach $query->join('dates AS individual_dates', static function (JoinClause $join): void { 522dfb2cda2SGreg Roach $join 523dfb2cda2SGreg Roach ->on('individual_dates.d_file', '=', 'individuals.i_file') 524dfb2cda2SGreg Roach ->on('individual_dates.d_gid', '=', 'individuals.i_id'); 525dfb2cda2SGreg Roach }); 526dfb2cda2SGreg Roach } 527dfb2cda2SGreg Roach 528dfb2cda2SGreg Roach if ($fam_date) { 5290b5fd0a6SGreg Roach $query->join('dates AS family_dates', static function (JoinClause $join): void { 530dfb2cda2SGreg Roach $join 531dfb2cda2SGreg Roach ->on('family_dates.d_file', '=', 'spouse_families.f_file') 532dfb2cda2SGreg Roach ->on('family_dates.d_gid', '=', 'spouse_families.f_id'); 533dfb2cda2SGreg Roach }); 534dfb2cda2SGreg Roach } 535dfb2cda2SGreg Roach 536dfb2cda2SGreg Roach if ($indi_plac) { 5370b5fd0a6SGreg Roach $query->join('placelinks AS individual_placelinks', static function (JoinClause $join): void { 538dfb2cda2SGreg Roach $join 539dfb2cda2SGreg Roach ->on('individual_placelinks.pl_file', '=', 'individuals.i_file') 540dfb2cda2SGreg Roach ->on('individual_placelinks.pl_gid', '=', 'individuals.i_id'); 541dfb2cda2SGreg Roach }); 5420b5fd0a6SGreg Roach $query->join('places AS individual_places', static function (JoinClause $join): void { 543dfb2cda2SGreg Roach $join 544dfb2cda2SGreg Roach ->on('individual_places.p_file', '=', 'individual_placelinks.pl_file') 545dfb2cda2SGreg Roach ->on('individual_places.p_id', '=', 'individual_placelinks.pl_p_id'); 546dfb2cda2SGreg Roach }); 547dfb2cda2SGreg Roach } 548dfb2cda2SGreg Roach 549dfb2cda2SGreg Roach if ($fam_plac) { 5500b5fd0a6SGreg Roach $query->join('placelinks AS familyl_placelinks', static function (JoinClause $join): void { 551dfb2cda2SGreg Roach $join 552dfb2cda2SGreg Roach ->on('familyl_placelinks.pl_file', '=', 'individuals.i_file') 553dfb2cda2SGreg Roach ->on('familyl_placelinks.pl_gid', '=', 'individuals.i_id'); 554dfb2cda2SGreg Roach }); 5550b5fd0a6SGreg Roach $query->join('places AS family_places', static function (JoinClause $join): void { 556dfb2cda2SGreg Roach $join 557dfb2cda2SGreg Roach ->on('family_places.p_file', '=', 'familyl_placelinks.pl_file') 558dfb2cda2SGreg Roach ->on('family_places.p_id', '=', 'familyl_placelinks.pl_p_id'); 559dfb2cda2SGreg Roach }); 560dfb2cda2SGreg Roach } 561dfb2cda2SGreg Roach 562dfb2cda2SGreg Roach foreach ($fields as $field_name => $field_value) { 5633cfcc809SGreg Roach $parts = explode(':', $field_name . '::::'); 564dfb2cda2SGreg Roach if ($parts[0] === 'NAME') { 565dfb2cda2SGreg Roach // NAME:* 566dfb2cda2SGreg Roach switch ($parts[1]) { 567dfb2cda2SGreg Roach case 'GIVN': 568dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 569dfb2cda2SGreg Roach case 'EXACT': 570dfb2cda2SGreg Roach $query->where('individual_name.n_givn', '=', $field_value); 571dfb2cda2SGreg Roach break; 572dfb2cda2SGreg Roach case 'BEGINS': 573dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', $field_value . '%'); 574dfb2cda2SGreg Roach break; 575dfb2cda2SGreg Roach case 'CONTAINS': 576dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%'); 577dfb2cda2SGreg Roach break; 578dfb2cda2SGreg Roach case 'SDX_STD': 579dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 580dfb2cda2SGreg Roach if ($sdx !== '') { 581dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_givn_std', $sdx); 582dfb2cda2SGreg Roach } else { 583dfb2cda2SGreg Roach // No phonetic content? Use a substring match 584dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%'); 585dfb2cda2SGreg Roach } 586dfb2cda2SGreg Roach break; 587dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 588dfb2cda2SGreg Roach case 'SDX_DM': 589dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 590dfb2cda2SGreg Roach if ($sdx !== '') { 591dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_givn_dm', $sdx); 592dfb2cda2SGreg Roach } else { 593dfb2cda2SGreg Roach // No phonetic content? Use a substring match 594dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%'); 595dfb2cda2SGreg Roach } 596dfb2cda2SGreg Roach break; 597dfb2cda2SGreg Roach } 598dfb2cda2SGreg Roach break; 599dfb2cda2SGreg Roach case 'SURN': 600dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 601dfb2cda2SGreg Roach case 'EXACT': 602dfb2cda2SGreg Roach $query->where('individual_name.n_surn', '=', $field_value); 603dfb2cda2SGreg Roach break; 604dfb2cda2SGreg Roach case 'BEGINS': 605dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', $field_value . '%'); 606dfb2cda2SGreg Roach break; 607dfb2cda2SGreg Roach case 'CONTAINS': 608dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%'); 609dfb2cda2SGreg Roach break; 610dfb2cda2SGreg Roach case 'SDX_STD': 611dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 612dfb2cda2SGreg Roach if ($sdx !== '') { 613dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_surn_std', $sdx); 614dfb2cda2SGreg Roach } else { 615dfb2cda2SGreg Roach // No phonetic content? Use a substring match 616dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%'); 617dfb2cda2SGreg Roach } 618dfb2cda2SGreg Roach break; 619dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 620dfb2cda2SGreg Roach case 'SDX_DM': 621dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 622dfb2cda2SGreg Roach if ($sdx !== '') { 623dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_surn_dm', $sdx); 624dfb2cda2SGreg Roach } else { 625dfb2cda2SGreg Roach // No phonetic content? Use a substring match 626dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%'); 627dfb2cda2SGreg Roach } 628dfb2cda2SGreg Roach break; 629dfb2cda2SGreg Roach } 630dfb2cda2SGreg Roach break; 631dfb2cda2SGreg Roach case 'NICK': 632dfb2cda2SGreg Roach case '_MARNM': 633dfb2cda2SGreg Roach case '_HEB': 634dfb2cda2SGreg Roach case '_AKA': 635dfb2cda2SGreg Roach $query 636dfb2cda2SGreg Roach ->where('individual_name', '=', $parts[1]) 637dfb2cda2SGreg Roach ->where('individual_name', 'LIKE', '%' . $field_value . '%'); 638dfb2cda2SGreg Roach break; 639dfb2cda2SGreg Roach } 640dfb2cda2SGreg Roach unset($fields[$field_name]); 641dfb2cda2SGreg Roach } elseif ($parts[1] === 'DATE') { 642dfb2cda2SGreg Roach // *:DATE 643dfb2cda2SGreg Roach $date = new Date($field_value); 644dfb2cda2SGreg Roach if ($date->isOK()) { 645dfb2cda2SGreg Roach $delta = 365 * ($modifiers[$field_name] ?? 0); 646dfb2cda2SGreg Roach $query 647dfb2cda2SGreg Roach ->where('individual_dates.d_fact', '=', $parts[0]) 648dfb2cda2SGreg Roach ->where('individual_dates.d_julianday1', '>=', $date->minimumJulianDay() - $delta) 649dfb2cda2SGreg Roach ->where('individual_dates.d_julianday2', '<=', $date->minimumJulianDay() + $delta); 650dfb2cda2SGreg Roach } 651dfb2cda2SGreg Roach unset($fields[$field_name]); 652dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMS' && $parts[2] === 'DATE') { 653dfb2cda2SGreg Roach // FAMS:*:DATE 654dfb2cda2SGreg Roach $date = new Date($field_value); 655dfb2cda2SGreg Roach if ($date->isOK()) { 656dfb2cda2SGreg Roach $delta = 365 * $modifiers[$field_name]; 657dfb2cda2SGreg Roach $query 658dfb2cda2SGreg Roach ->where('family_dates.d_fact', '=', $parts[1]) 659dfb2cda2SGreg Roach ->where('family_dates.d_julianday1', '>=', $date->minimumJulianDay() - $delta) 660dfb2cda2SGreg Roach ->where('family_dates.d_julianday2', '<=', $date->minimumJulianDay() + $delta); 661dfb2cda2SGreg Roach } 662dfb2cda2SGreg Roach unset($fields[$field_name]); 663dfb2cda2SGreg Roach } elseif ($parts[1] === 'PLAC') { 664dfb2cda2SGreg Roach // *:PLAC 665dfb2cda2SGreg Roach // SQL can only link a place to a person/family, not to an event. 666dfb2cda2SGreg Roach $query->where('individual_places.p_place', 'LIKE', '%' . $field_value . '%'); 667dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') { 668dfb2cda2SGreg Roach // FAMS:*:PLAC 669dfb2cda2SGreg Roach // SQL can only link a place to a person/family, not to an event. 670dfb2cda2SGreg Roach $query->where('family_places.p_place', 'LIKE', '%' . $field_value . '%'); 671dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMC' && $parts[2] === 'NAME') { 672dfb2cda2SGreg Roach $table = $parts[1] === 'HUSB' ? 'father_name' : 'mother_name'; 673dfb2cda2SGreg Roach // NAME:* 674dfb2cda2SGreg Roach switch ($parts[3]) { 675dfb2cda2SGreg Roach case 'GIVN': 676dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 677dfb2cda2SGreg Roach case 'EXACT': 678dfb2cda2SGreg Roach $query->where($table . '.n_givn', '=', $field_value); 679dfb2cda2SGreg Roach break; 680dfb2cda2SGreg Roach case 'BEGINS': 681dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', $field_value . '%'); 682dfb2cda2SGreg Roach break; 683dfb2cda2SGreg Roach case 'CONTAINS': 684dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%'); 685dfb2cda2SGreg Roach break; 686dfb2cda2SGreg Roach case 'SDX_STD': 687dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 688dfb2cda2SGreg Roach if ($sdx !== '') { 689dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_givn_std', $sdx); 690dfb2cda2SGreg Roach } else { 691dfb2cda2SGreg Roach // No phonetic content? Use a substring match 692dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%'); 693dfb2cda2SGreg Roach } 694dfb2cda2SGreg Roach break; 695dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 696dfb2cda2SGreg Roach case 'SDX_DM': 697dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 698dfb2cda2SGreg Roach if ($sdx !== '') { 699dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_givn_dm', $sdx); 700dfb2cda2SGreg Roach } else { 701dfb2cda2SGreg Roach // No phonetic content? Use a substring match 702dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%'); 703dfb2cda2SGreg Roach } 704dfb2cda2SGreg Roach break; 705dfb2cda2SGreg Roach } 706dfb2cda2SGreg Roach break; 707dfb2cda2SGreg Roach case 'SURN': 708dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 709dfb2cda2SGreg Roach case 'EXACT': 710dfb2cda2SGreg Roach $query->where($table . '.n_surn', '=', $field_value); 711dfb2cda2SGreg Roach break; 712dfb2cda2SGreg Roach case 'BEGINS': 713dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', $field_value . '%'); 714dfb2cda2SGreg Roach break; 715dfb2cda2SGreg Roach case 'CONTAINS': 716dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%'); 717dfb2cda2SGreg Roach break; 718dfb2cda2SGreg Roach case 'SDX_STD': 719dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 720dfb2cda2SGreg Roach if ($sdx !== '') { 721dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_surn_std', $sdx); 722dfb2cda2SGreg Roach } else { 723dfb2cda2SGreg Roach // No phonetic content? Use a substring match 724dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%'); 725dfb2cda2SGreg Roach } 726dfb2cda2SGreg Roach break; 727dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 728dfb2cda2SGreg Roach case 'SDX_DM': 729dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 730dfb2cda2SGreg Roach if ($sdx !== '') { 731dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_surn_dm', $sdx); 732dfb2cda2SGreg Roach } else { 733dfb2cda2SGreg Roach // No phonetic content? Use a substring match 734dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%'); 735dfb2cda2SGreg Roach } 736dfb2cda2SGreg Roach break; 737dfb2cda2SGreg Roach } 738dfb2cda2SGreg Roach break; 739dfb2cda2SGreg Roach } 740dfb2cda2SGreg Roach unset($fields[$field_name]); 741dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMS') { 742dfb2cda2SGreg Roach // e.g. searches for occupation, religion, note, etc. 743dfb2cda2SGreg Roach // Initial matching only. Need PHP to apply filter. 7443c480c6bSGreg Roach $query->where('spouse_families.f_gedcom', 'LIKE', "%\n1 " . $parts[1] . ' %' . $field_value . '%'); 745dfb2cda2SGreg Roach } elseif ($parts[1] === 'TYPE') { 746dfb2cda2SGreg Roach // e.g. FACT:TYPE or EVEN:TYPE 747dfb2cda2SGreg Roach // Initial matching only. Need PHP to apply filter. 748dfb2cda2SGreg Roach $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%\n2 TYPE %' . $field_value . '%'); 749dfb2cda2SGreg Roach } else { 750dfb2cda2SGreg Roach // e.g. searches for occupation, religion, note, etc. 751dfb2cda2SGreg Roach // Initial matching only. Need PHP to apply filter. 752c5457947SGreg Roach $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%' . $parts[1] . '%' . $field_value . '%'); 753dfb2cda2SGreg Roach } 754dfb2cda2SGreg Roach } 755dfb2cda2SGreg Roach return $query 756dfb2cda2SGreg Roach ->get() 75752a8ef61SGreg Roach ->each($this->rowLimiter()) 758d5ad3db0SGreg Roach ->map($this->individualRowMapper()) 759dfb2cda2SGreg Roach ->filter(GedcomRecord::accessFilter()) 7600b5fd0a6SGreg Roach ->filter(static function (Individual $individual) use ($fields): bool { 761c5457947SGreg Roach // Check for searches which were only partially matched by SQL 762dfb2cda2SGreg Roach foreach ($fields as $field_name => $field_value) { 763a41e65f0SGreg Roach $regex = '/' . preg_quote($field_value, '/') . '/i'; 764dfb2cda2SGreg Roach 765c5457947SGreg Roach $parts = explode(':', $field_name . '::::'); 766dfb2cda2SGreg Roach 767dfb2cda2SGreg Roach // *:PLAC 768c5457947SGreg Roach if ($parts[1] === 'PLAC') { 769a41e65f0SGreg Roach foreach ($individual->facts([$parts[0]]) as $fact) { 770392561bbSGreg Roach if (preg_match($regex, $fact->place()->gedcomName())) { 771c5457947SGreg Roach continue 2; 772dfb2cda2SGreg Roach } 773a41e65f0SGreg Roach } 774c5457947SGreg Roach return false; 775c5457947SGreg Roach } 776c5457947SGreg Roach 777dfb2cda2SGreg Roach // FAMS:*:PLAC 778c5457947SGreg Roach if ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') { 77939ca88baSGreg Roach foreach ($individual->spouseFamilies() as $family) { 780a41e65f0SGreg Roach foreach ($family->facts([$parts[1]]) as $fact) { 781392561bbSGreg Roach if (preg_match($regex, $fact->place()->gedcomName())) { 782c5457947SGreg Roach continue 2; 783a41e65f0SGreg Roach } 784dfb2cda2SGreg Roach } 785dfb2cda2SGreg Roach } 786c5457947SGreg Roach return false; 787c5457947SGreg Roach } 788c5457947SGreg Roach 789dfb2cda2SGreg Roach // e.g. searches for occupation, religion, note, etc. 790c5457947SGreg Roach if ($parts[0] === 'FAMS') { 79139ca88baSGreg Roach foreach ($individual->spouseFamilies() as $family) { 792a41e65f0SGreg Roach foreach ($family->facts([$parts[1]]) as $fact) { 793a41e65f0SGreg Roach if (preg_match($regex, $fact->value())) { 794c5457947SGreg Roach continue 3; 795a41e65f0SGreg Roach } 796dfb2cda2SGreg Roach } 797dfb2cda2SGreg Roach } 798c5457947SGreg Roach return false; 799c5457947SGreg Roach } 800c5457947SGreg Roach 801dfb2cda2SGreg Roach // e.g. FACT:TYPE or EVEN:TYPE 802c5457947SGreg Roach if ($parts[1] === 'TYPE' || $parts[1] === '_WT_USER') { 803a41e65f0SGreg Roach foreach ($individual->facts([$parts[0]]) as $fact) { 804c5457947SGreg Roach if (preg_match($regex, $fact->attribute($parts[1]))) { 805c5457947SGreg Roach continue 2; 806dfb2cda2SGreg Roach } 807dfb2cda2SGreg Roach } 808dfb2cda2SGreg Roach 809dfb2cda2SGreg Roach return false; 810dfb2cda2SGreg Roach } 811c5457947SGreg Roach } 812dfb2cda2SGreg Roach 813dfb2cda2SGreg Roach return true; 814dfb2cda2SGreg Roach }); 815dfb2cda2SGreg Roach } 816dfb2cda2SGreg Roach 817dfb2cda2SGreg Roach /** 818dfb2cda2SGreg Roach * @param string $soundex 819dfb2cda2SGreg Roach * @param string $lastname 820dfb2cda2SGreg Roach * @param string $firstname 821dfb2cda2SGreg Roach * @param string $place 822dfb2cda2SGreg Roach * @param Tree[] $search_trees 823dfb2cda2SGreg Roach * 824b5c8fd7eSGreg Roach * @return Collection<Individual> 825dfb2cda2SGreg Roach */ 826dfb2cda2SGreg Roach public function searchIndividualsPhonetic(string $soundex, string $lastname, string $firstname, string $place, array $search_trees): Collection 827dfb2cda2SGreg Roach { 828dfb2cda2SGreg Roach switch ($soundex) { 829dfb2cda2SGreg Roach default: 830dfb2cda2SGreg Roach case 'Russell': 831dfb2cda2SGreg Roach $givn_sdx = Soundex::russell($firstname); 832dfb2cda2SGreg Roach $surn_sdx = Soundex::russell($lastname); 833dfb2cda2SGreg Roach $plac_sdx = Soundex::russell($place); 834dfb2cda2SGreg Roach $givn_field = 'n_soundex_givn_std'; 835dfb2cda2SGreg Roach $surn_field = 'n_soundex_surn_std'; 836dfb2cda2SGreg Roach $plac_field = 'p_std_soundex'; 837dfb2cda2SGreg Roach break; 838dfb2cda2SGreg Roach case 'DaitchM': 839dfb2cda2SGreg Roach $givn_sdx = Soundex::daitchMokotoff($firstname); 840dfb2cda2SGreg Roach $surn_sdx = Soundex::daitchMokotoff($lastname); 841dfb2cda2SGreg Roach $plac_sdx = Soundex::daitchMokotoff($place); 842dfb2cda2SGreg Roach $givn_field = 'n_soundex_givn_dm'; 843dfb2cda2SGreg Roach $surn_field = 'n_soundex_surn_dm'; 844dfb2cda2SGreg Roach $plac_field = 'p_dm_soundex'; 845dfb2cda2SGreg Roach break; 846dfb2cda2SGreg Roach } 847dfb2cda2SGreg Roach 848dfb2cda2SGreg Roach // Nothing to search for? Return nothing. 849dfb2cda2SGreg Roach if ($givn_sdx === '' && $surn_sdx === '' && $plac_sdx === '') { 8503fda39a7SGreg Roach return new Collection(); 851dfb2cda2SGreg Roach } 852dfb2cda2SGreg Roach 853dfb2cda2SGreg Roach $query = DB::table('individuals') 854dfb2cda2SGreg Roach ->select(['individuals.*']) 855dfb2cda2SGreg Roach ->distinct(); 856dfb2cda2SGreg Roach 857dfb2cda2SGreg Roach $this->whereTrees($query, 'i_file', $search_trees); 858dfb2cda2SGreg Roach 859dfb2cda2SGreg Roach if ($plac_sdx !== '') { 8600b5fd0a6SGreg Roach $query->join('placelinks', static function (JoinClause $join): void { 861dfb2cda2SGreg Roach $join 862dfb2cda2SGreg Roach ->on('placelinks.pl_file', '=', 'individuals.i_file') 863dfb2cda2SGreg Roach ->on('placelinks.pl_gid', '=', 'individuals.i_id'); 864dfb2cda2SGreg Roach }); 8650b5fd0a6SGreg Roach $query->join('places', static function (JoinClause $join): void { 866dfb2cda2SGreg Roach $join 867dfb2cda2SGreg Roach ->on('places.p_file', '=', 'placelinks.pl_file') 868dfb2cda2SGreg Roach ->on('places.p_id', '=', 'placelinks.pl_p_id'); 869dfb2cda2SGreg Roach }); 870dfb2cda2SGreg Roach 871dfb2cda2SGreg Roach $this->wherePhonetic($query, $plac_field, $plac_sdx); 872dfb2cda2SGreg Roach } 873dfb2cda2SGreg Roach 874dfb2cda2SGreg Roach if ($givn_sdx !== '' || $surn_sdx !== '') { 8750b5fd0a6SGreg Roach $query->join('name', static function (JoinClause $join): void { 876dfb2cda2SGreg Roach $join 877dfb2cda2SGreg Roach ->on('name.n_file', '=', 'individuals.i_file') 878dfb2cda2SGreg Roach ->on('name.n_id', '=', 'individuals.i_id'); 879dfb2cda2SGreg Roach }); 880dfb2cda2SGreg Roach 881dfb2cda2SGreg Roach $this->wherePhonetic($query, $givn_field, $givn_sdx); 882dfb2cda2SGreg Roach $this->wherePhonetic($query, $surn_field, $surn_sdx); 883dfb2cda2SGreg Roach } 884dfb2cda2SGreg Roach 885dfb2cda2SGreg Roach return $query 886dfb2cda2SGreg Roach ->get() 88752a8ef61SGreg Roach ->each($this->rowLimiter()) 888d5ad3db0SGreg Roach ->map($this->individualRowMapper()) 889dfb2cda2SGreg Roach ->filter(GedcomRecord::accessFilter()); 890dfb2cda2SGreg Roach } 891dfb2cda2SGreg Roach 892dfb2cda2SGreg Roach /** 89332cd2800SGreg Roach * Paginate a search query. 89432cd2800SGreg Roach * 89532cd2800SGreg Roach * @param Builder $query Searches the database for the desired records. 89632cd2800SGreg Roach * @param Closure $row_mapper Converts a row from the query into a record. 897a7a24840SGreg Roach * @param Closure $row_filter 89832cd2800SGreg Roach * @param int $offset Skip this many rows. 89932cd2800SGreg Roach * @param int $limit Take this many rows. 90032cd2800SGreg Roach * 901b5c8fd7eSGreg Roach * @return Collection<mixed> 90232cd2800SGreg Roach */ 903a7a24840SGreg Roach private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection 90432cd2800SGreg Roach { 90532cd2800SGreg Roach $collection = new Collection(); 90632cd2800SGreg Roach 90732cd2800SGreg Roach foreach ($query->cursor() as $row) { 90832cd2800SGreg Roach $record = $row_mapper($row); 909b68caec6SGreg Roach // If the object has a method "canShow()", then use it to filter for privacy. 910a7a24840SGreg Roach if ($row_filter($record)) { 91132cd2800SGreg Roach if ($offset > 0) { 91232cd2800SGreg Roach $offset--; 91332cd2800SGreg Roach } else { 91432cd2800SGreg Roach if ($limit > 0) { 91532cd2800SGreg Roach $collection->push($record); 91632cd2800SGreg Roach } 91732cd2800SGreg Roach 91832cd2800SGreg Roach $limit--; 91932cd2800SGreg Roach 92032cd2800SGreg Roach if ($limit === 0) { 92132cd2800SGreg Roach break; 92232cd2800SGreg Roach } 92332cd2800SGreg Roach } 92432cd2800SGreg Roach } 92532cd2800SGreg Roach } 92632cd2800SGreg Roach 927e0458bdcSGreg Roach 92832cd2800SGreg Roach return $collection; 92932cd2800SGreg Roach } 930a7a24840SGreg Roach 931a7a24840SGreg Roach /** 932a7a24840SGreg Roach * Apply search filters to a SQL query column. Apply collation rules to MySQL. 933a7a24840SGreg Roach * 934a7a24840SGreg Roach * @param Builder $query 935a7a24840SGreg Roach * @param Expression|string $field 936a7a24840SGreg Roach * @param string[] $search_terms 937a7a24840SGreg Roach */ 938a7a24840SGreg Roach private function whereSearch(Builder $query, $field, array $search_terms): void 939a7a24840SGreg Roach { 940a7a24840SGreg Roach if ($field instanceof Expression) { 941a7a24840SGreg Roach $field = $field->getValue(); 942a7a24840SGreg Roach } 943a7a24840SGreg Roach 944a7a24840SGreg Roach foreach ($search_terms as $search_term) { 945a69f5655SGreg Roach $query->whereContains(new Expression($field), $search_term); 946a7a24840SGreg Roach } 947a7a24840SGreg Roach } 948a7a24840SGreg Roach 949a7a24840SGreg Roach /** 9502d686e68SGreg Roach * Apply soundex search filters to a SQL query column. 9512d686e68SGreg Roach * 9522d686e68SGreg Roach * @param Builder $query 9532d686e68SGreg Roach * @param Expression|string $field 9542d686e68SGreg Roach * @param string $soundex 9552d686e68SGreg Roach */ 9562d686e68SGreg Roach private function wherePhonetic(Builder $query, $field, string $soundex): void 9572d686e68SGreg Roach { 9582d686e68SGreg Roach if ($soundex !== '') { 9590b5fd0a6SGreg Roach $query->where(static function (Builder $query) use ($soundex, $field): void { 9602d686e68SGreg Roach foreach (explode(':', $soundex) as $sdx) { 9612d686e68SGreg Roach $query->orWhere($field, 'LIKE', '%' . $sdx . '%'); 9622d686e68SGreg Roach } 9632d686e68SGreg Roach }); 9642d686e68SGreg Roach } 9652d686e68SGreg Roach } 9662d686e68SGreg Roach 9672d686e68SGreg Roach /** 968a7a24840SGreg Roach * @param Builder $query 969a7a24840SGreg Roach * @param string $tree_id_field 970a7a24840SGreg Roach * @param Tree[] $trees 971a7a24840SGreg Roach */ 972a7a24840SGreg Roach private function whereTrees(Builder $query, string $tree_id_field, array $trees): void 973a7a24840SGreg Roach { 9740b5fd0a6SGreg Roach $tree_ids = array_map(static function (Tree $tree): int { 975a7a24840SGreg Roach return $tree->id(); 976a7a24840SGreg Roach }, $trees); 977a7a24840SGreg Roach 978a7a24840SGreg Roach $query->whereIn($tree_id_field, $tree_ids); 979a7a24840SGreg Roach } 980a7a24840SGreg Roach 981d5ad3db0SGreg Roach /** 982d5ad3db0SGreg Roach * Find the media object that uses a particular media file. 983d5ad3db0SGreg Roach * 984d5ad3db0SGreg Roach * @param string $file 985d5ad3db0SGreg Roach * 986d5ad3db0SGreg Roach * @return Media[] 987d5ad3db0SGreg Roach */ 988d5ad3db0SGreg Roach public function findMediaObjectsForMediaFile(string $file): array 989d5ad3db0SGreg Roach { 990d5ad3db0SGreg Roach return DB::table('media') 991d5ad3db0SGreg Roach ->join('media_file', static function (JoinClause $join): void { 992d5ad3db0SGreg Roach $join 993d5ad3db0SGreg Roach ->on('media_file.m_file', '=', 'media.m_file') 994d5ad3db0SGreg Roach ->on('media_file.m_id', '=', 'media.m_id'); 995d5ad3db0SGreg Roach }) 996d5ad3db0SGreg Roach ->join('gedcom_setting', 'media.m_file', '=', 'gedcom_setting.gedcom_id') 997d5ad3db0SGreg Roach ->where(new Expression('setting_value || multimedia_file_refn'), '=', $file) 998d5ad3db0SGreg Roach ->select(['media.*']) 999d5ad3db0SGreg Roach ->distinct() 1000d5ad3db0SGreg Roach ->get() 1001d5ad3db0SGreg Roach ->map($this->mediaRowMapper()) 1002d5ad3db0SGreg Roach ->all(); 1003d5ad3db0SGreg Roach } 1004d5ad3db0SGreg Roach 1005a7a24840SGreg Roach /** 1006a7a24840SGreg Roach * A closure to filter records by privacy-filtered GEDCOM data. 1007a7a24840SGreg Roach * 1008a7a24840SGreg Roach * @param array $search_terms 1009a7a24840SGreg Roach * 1010a7a24840SGreg Roach * @return Closure 1011a7a24840SGreg Roach */ 1012a7a24840SGreg Roach private function rawGedcomFilter(array $search_terms): Closure 1013a7a24840SGreg Roach { 10146c2179e2SGreg Roach return static function (GedcomRecord $record) use ($search_terms): bool { 1015a7a24840SGreg Roach // Ignore non-genealogy fields 1016faa5e163SGreg Roach $gedcom = preg_replace('/\n\d (?:_UID|_WT_USER) .*/', '', $record->gedcom()); 1017a7a24840SGreg Roach 1018a7a24840SGreg Roach // Ignore matches in links 1019a7a24840SGreg Roach $gedcom = preg_replace('/\n\d ' . Gedcom::REGEX_TAG . '( @' . Gedcom::REGEX_XREF . '@)?/', '', $gedcom); 1020a7a24840SGreg Roach 1021a7a24840SGreg Roach // Re-apply the filtering 1022a7a24840SGreg Roach foreach ($search_terms as $search_term) { 1023a7a24840SGreg Roach if (mb_stripos($gedcom, $search_term) === false) { 1024a7a24840SGreg Roach return false; 1025a7a24840SGreg Roach } 1026a7a24840SGreg Roach } 1027a7a24840SGreg Roach 1028a7a24840SGreg Roach return true; 1029a7a24840SGreg Roach }; 1030a7a24840SGreg Roach } 103152a8ef61SGreg Roach 103252a8ef61SGreg Roach /** 103352a8ef61SGreg Roach * Searching for short or common text can give more results than the system can process. 103452a8ef61SGreg Roach * 103552a8ef61SGreg Roach * @param int $limit 103652a8ef61SGreg Roach * 103752a8ef61SGreg Roach * @return Closure 103852a8ef61SGreg Roach */ 103952a8ef61SGreg Roach private function rowLimiter(int $limit = 1000): Closure 104052a8ef61SGreg Roach { 10416c2179e2SGreg Roach return static function () use ($limit): void { 104252a8ef61SGreg Roach static $n = 0; 104352a8ef61SGreg Roach 104452a8ef61SGreg Roach if (++$n > $limit) { 104552a8ef61SGreg Roach $message = I18N::translate('The search returned too many results.'); 104652a8ef61SGreg Roach 1047d501c45dSGreg Roach throw new HttpServiceUnavailableException($message); 104852a8ef61SGreg Roach } 104952a8ef61SGreg Roach }; 105052a8ef61SGreg Roach } 1051d5ad3db0SGreg Roach 1052d5ad3db0SGreg Roach /** 1053d5ad3db0SGreg Roach * Convert a row from any tree in the families table into a family object. 1054d5ad3db0SGreg Roach * 1055d5ad3db0SGreg Roach * @return Closure 1056d5ad3db0SGreg Roach */ 1057d5ad3db0SGreg Roach private function familyRowMapper(): Closure 1058d5ad3db0SGreg Roach { 1059d5ad3db0SGreg Roach return function (stdClass $row): Family { 1060d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->f_file); 1061d5ad3db0SGreg Roach 1062*3959eeb6SGreg Roach return Family::rowMapper($tree)($row); 1063d5ad3db0SGreg Roach }; 1064d5ad3db0SGreg Roach } 1065d5ad3db0SGreg Roach 1066d5ad3db0SGreg Roach /** 1067d5ad3db0SGreg Roach * Convert a row from any tree in the individuals table into an individual object. 1068d5ad3db0SGreg Roach * 1069d5ad3db0SGreg Roach * @return Closure 1070d5ad3db0SGreg Roach */ 1071d5ad3db0SGreg Roach private function individualRowMapper(): Closure 1072d5ad3db0SGreg Roach { 1073d5ad3db0SGreg Roach return function (stdClass $row): Individual { 1074d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->i_file); 1075d5ad3db0SGreg Roach 1076*3959eeb6SGreg Roach return Individual::rowMapper($tree)($row); 1077d5ad3db0SGreg Roach }; 1078d5ad3db0SGreg Roach } 1079d5ad3db0SGreg Roach 1080d5ad3db0SGreg Roach /** 1081d5ad3db0SGreg Roach * Convert a row from any tree in the media table into an media object. 1082d5ad3db0SGreg Roach * 1083d5ad3db0SGreg Roach * @return Closure 1084d5ad3db0SGreg Roach */ 1085d5ad3db0SGreg Roach private function mediaRowMapper(): Closure 1086d5ad3db0SGreg Roach { 1087d5ad3db0SGreg Roach return function (stdClass $row): Media { 1088d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->m_file); 1089d5ad3db0SGreg Roach 1090*3959eeb6SGreg Roach return Media::rowMapper($tree)($row); 1091d5ad3db0SGreg Roach }; 1092d5ad3db0SGreg Roach } 1093d5ad3db0SGreg Roach 1094d5ad3db0SGreg Roach /** 1095d5ad3db0SGreg Roach * Convert a row from any tree in the other table into a note object. 1096d5ad3db0SGreg Roach * 1097d5ad3db0SGreg Roach * @return Closure 1098d5ad3db0SGreg Roach */ 1099d5ad3db0SGreg Roach private function noteRowMapper(): Closure 1100d5ad3db0SGreg Roach { 1101d5ad3db0SGreg Roach return function (stdClass $row): Note { 1102d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->o_file); 1103d5ad3db0SGreg Roach 1104*3959eeb6SGreg Roach return Note::rowMapper($tree)($row); 1105d5ad3db0SGreg Roach }; 1106d5ad3db0SGreg Roach } 1107d5ad3db0SGreg Roach 1108d5ad3db0SGreg Roach /** 1109d5ad3db0SGreg Roach * Convert a row from any tree in the other table into a repository object. 1110d5ad3db0SGreg Roach * 1111d5ad3db0SGreg Roach * @return Closure 1112d5ad3db0SGreg Roach */ 1113d5ad3db0SGreg Roach private function repositoryRowMapper(): Closure 1114d5ad3db0SGreg Roach { 1115d5ad3db0SGreg Roach return function (stdClass $row): Repository { 1116d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->o_file); 1117d5ad3db0SGreg Roach 1118*3959eeb6SGreg Roach return Repository::rowMapper($tree)($row); 1119d5ad3db0SGreg Roach }; 1120d5ad3db0SGreg Roach } 1121d5ad3db0SGreg Roach 1122d5ad3db0SGreg Roach /** 1123d5ad3db0SGreg Roach * Convert a row from any tree in the sources table into a source object. 1124d5ad3db0SGreg Roach * 1125d5ad3db0SGreg Roach * @return Closure 1126d5ad3db0SGreg Roach */ 1127d5ad3db0SGreg Roach private function sourceRowMapper(): Closure 1128d5ad3db0SGreg Roach { 1129d5ad3db0SGreg Roach return function (stdClass $row): Source { 1130d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->s_file); 1131d5ad3db0SGreg Roach 1132*3959eeb6SGreg Roach return Source::rowMapper($tree)($row); 1133d5ad3db0SGreg Roach }; 1134d5ad3db0SGreg Roach } 1135d5ad3db0SGreg Roach 1136d5ad3db0SGreg Roach /** 1137d5ad3db0SGreg Roach * Convert a row from any tree in the other table into a submitter object. 1138d5ad3db0SGreg Roach * 1139d5ad3db0SGreg Roach * @return Closure 1140d5ad3db0SGreg Roach */ 1141d5ad3db0SGreg Roach private function submitterRowMapper(): Closure 1142d5ad3db0SGreg Roach { 1143*3959eeb6SGreg Roach return function (stdClass $row): Submitter { 1144d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->o_file); 1145d5ad3db0SGreg Roach 1146*3959eeb6SGreg Roach return Submitter::rowMapper($tree)($row); 1147d5ad3db0SGreg Roach }; 1148d5ad3db0SGreg Roach } 114932cd2800SGreg Roach} 1150