xref: /webtrees/app/Services/SearchService.php (revision a091ac74647eab281b25090b737835eeea14ae10)
132cd2800SGreg Roach<?php
23976b470SGreg Roach
332cd2800SGreg Roach/**
432cd2800SGreg Roach * webtrees: online genealogy
5b5961194SGreg Roach * Copyright (C) 2020 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;
25*a091ac74SGreg Roachuse Fisharebest\Webtrees\Factory;
2632cd2800SGreg Roachuse Fisharebest\Webtrees\Family;
27a7a24840SGreg Roachuse Fisharebest\Webtrees\Gedcom;
2832cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2952a8ef61SGreg Roachuse Fisharebest\Webtrees\I18N;
3032cd2800SGreg Roachuse Fisharebest\Webtrees\Individual;
3132cd2800SGreg Roachuse Fisharebest\Webtrees\Media;
3232cd2800SGreg Roachuse Fisharebest\Webtrees\Note;
33b68caec6SGreg Roachuse Fisharebest\Webtrees\Place;
3432cd2800SGreg Roachuse Fisharebest\Webtrees\Repository;
352d686e68SGreg Roachuse Fisharebest\Webtrees\Soundex;
3632cd2800SGreg Roachuse Fisharebest\Webtrees\Source;
37b5c8fd7eSGreg Roachuse Fisharebest\Webtrees\Submitter;
3832cd2800SGreg Roachuse Fisharebest\Webtrees\Tree;
3932cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
4032cd2800SGreg Roachuse Illuminate\Database\Query\Builder;
41a7a24840SGreg Roachuse Illuminate\Database\Query\Expression;
4232cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause;
4332cd2800SGreg Roachuse Illuminate\Support\Collection;
4432cd2800SGreg Roachuse stdClass;
453976b470SGreg Roach
46b5961194SGreg Roachuse function addcslashes;
4756946b7aSGreg Roachuse function array_unique;
4856946b7aSGreg Roachuse function explode;
49a7a24840SGreg Roachuse function mb_stripos;
5032cd2800SGreg Roach
5132cd2800SGreg Roach/**
5232cd2800SGreg Roach * Search trees for genealogy records.
5332cd2800SGreg Roach */
5432cd2800SGreg Roachclass SearchService
5532cd2800SGreg Roach{
56d5ad3db0SGreg Roach    /** @var TreeService */
57d5ad3db0SGreg Roach    private $tree_service;
58d5ad3db0SGreg Roach
59d5ad3db0SGreg Roach    /**
60d5ad3db0SGreg Roach     * SearchService constructor.
61d5ad3db0SGreg Roach     *
62d5ad3db0SGreg Roach     * @param TreeService $tree_service
63d5ad3db0SGreg Roach     */
643959eeb6SGreg Roach    public function __construct(
653959eeb6SGreg Roach        TreeService $tree_service
663959eeb6SGreg Roach    ) {
67d5ad3db0SGreg Roach        $this->tree_service = $tree_service;
68d5ad3db0SGreg Roach    }
69d5ad3db0SGreg Roach
70a7a24840SGreg Roach    /**
71a7a24840SGreg Roach     * @param Tree[]   $trees
72a7a24840SGreg Roach     * @param string[] $search
73a7a24840SGreg Roach     *
74b5c8fd7eSGreg Roach     * @return Collection<Family>
75a7a24840SGreg Roach     */
76a7a24840SGreg Roach    public function searchFamilies(array $trees, array $search): Collection
77a7a24840SGreg Roach    {
78a7a24840SGreg Roach        $query = DB::table('families');
79a7a24840SGreg Roach
80a7a24840SGreg Roach        $this->whereTrees($query, 'f_file', $trees);
81a7a24840SGreg Roach        $this->whereSearch($query, 'f_gedcom', $search);
82a7a24840SGreg Roach
83a7a24840SGreg Roach        return $query
84a7a24840SGreg Roach            ->get()
8552a8ef61SGreg Roach            ->each($this->rowLimiter())
86d5ad3db0SGreg Roach            ->map($this->familyRowMapper())
87a7a24840SGreg Roach            ->filter(GedcomRecord::accessFilter())
887f5fa3c2SGreg Roach            ->filter($this->rawGedcomFilter($search));
89a7a24840SGreg Roach    }
90a7a24840SGreg Roach
9132cd2800SGreg Roach    /**
9232cd2800SGreg Roach     * Search for families by name.
9332cd2800SGreg Roach     *
94a7a24840SGreg Roach     * @param Tree[]   $trees
95a7a24840SGreg Roach     * @param string[] $search
9632cd2800SGreg Roach     * @param int      $offset
9732cd2800SGreg Roach     * @param int      $limit
9832cd2800SGreg Roach     *
99b5c8fd7eSGreg Roach     * @return Collection<Family>
10032cd2800SGreg Roach     */
101a7a24840SGreg Roach    public function searchFamilyNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
10232cd2800SGreg Roach    {
10332cd2800SGreg Roach        $query = DB::table('families')
104ac499332SGreg Roach            ->leftJoin('name AS husb_name', static function (JoinClause $join): void {
10532cd2800SGreg Roach                $join
10632cd2800SGreg Roach                    ->on('husb_name.n_file', '=', 'families.f_file')
107ac499332SGreg Roach                    ->on('husb_name.n_id', '=', 'families.f_husb')
108ac499332SGreg Roach                    ->where('husb_name.n_type', '<>', '_MARNM');
10932cd2800SGreg Roach            })
110ac499332SGreg Roach            ->leftJoin('name AS wife_name', static function (JoinClause $join): void {
11132cd2800SGreg Roach                $join
11232cd2800SGreg Roach                    ->on('wife_name.n_file', '=', 'families.f_file')
113ac499332SGreg Roach                    ->on('wife_name.n_id', '=', 'families.f_wife')
114ac499332SGreg Roach                    ->where('wife_name.n_type', '<>', '_MARNM');
115ac499332SGreg Roach            });
116a7a24840SGreg Roach
117a7a24840SGreg Roach        $prefix = DB::connection()->getTablePrefix();
118a69f5655SGreg Roach        $field  = new Expression('COALESCE(' . $prefix . "husb_name.n_full, '') || COALESCE(" . $prefix . "wife_name.n_full, '')");
119a7a24840SGreg Roach
120a7a24840SGreg Roach        $this->whereTrees($query, 'f_file', $trees);
121a7a24840SGreg Roach        $this->whereSearch($query, $field, $search);
122a7a24840SGreg Roach
123a7a24840SGreg Roach        $query
12432cd2800SGreg Roach            ->orderBy('husb_name.n_sort')
12532cd2800SGreg Roach            ->orderBy('wife_name.n_sort')
126c0804649SGreg Roach            ->select(['families.*', 'husb_name.n_sort', 'wife_name.n_sort'])
12732cd2800SGreg Roach            ->distinct();
12832cd2800SGreg Roach
129d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->familyRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
130a7a24840SGreg Roach    }
131a7a24840SGreg Roach
132a7a24840SGreg Roach    /**
133dbe53437SGreg Roach     * @param Place $place
134dbe53437SGreg Roach     *
135dbe53437SGreg Roach     * @return Collection<Family>
136dbe53437SGreg Roach     */
137dbe53437SGreg Roach    public function searchFamiliesInPlace(Place $place): Collection
138dbe53437SGreg Roach    {
139dbe53437SGreg Roach        return DB::table('families')
140dbe53437SGreg Roach            ->join('placelinks', static function (JoinClause $query) {
141dbe53437SGreg Roach                $query
142dbe53437SGreg Roach                    ->on('families.f_file', '=', 'placelinks.pl_file')
143dbe53437SGreg Roach                    ->on('families.f_id', '=', 'placelinks.pl_gid');
144dbe53437SGreg Roach            })
145dbe53437SGreg Roach            ->where('f_file', '=', $place->tree()->id())
146dbe53437SGreg Roach            ->where('pl_p_id', '=', $place->id())
147dbe53437SGreg Roach            ->select(['families.*'])
148dbe53437SGreg Roach            ->get()
149dbe53437SGreg Roach            ->each($this->rowLimiter())
150dbe53437SGreg Roach            ->map($this->familyRowMapper())
151dbe53437SGreg Roach            ->filter(GedcomRecord::accessFilter());
152dbe53437SGreg Roach    }
153dbe53437SGreg Roach
154dbe53437SGreg Roach    /**
155a7a24840SGreg Roach     * @param Tree[]   $trees
156a7a24840SGreg Roach     * @param string[] $search
157a7a24840SGreg Roach     *
158b5c8fd7eSGreg Roach     * @return Collection<Individual>
159a7a24840SGreg Roach     */
160a7a24840SGreg Roach    public function searchIndividuals(array $trees, array $search): Collection
161a7a24840SGreg Roach    {
162a7a24840SGreg Roach        $query = DB::table('individuals');
163a7a24840SGreg Roach
164a7a24840SGreg Roach        $this->whereTrees($query, 'i_file', $trees);
165a7a24840SGreg Roach        $this->whereSearch($query, 'i_gedcom', $search);
166a7a24840SGreg Roach
167a7a24840SGreg Roach        return $query
168a7a24840SGreg Roach            ->get()
16952a8ef61SGreg Roach            ->each($this->rowLimiter())
170d5ad3db0SGreg Roach            ->map($this->individualRowMapper())
171a7a24840SGreg Roach            ->filter(GedcomRecord::accessFilter())
1727f5fa3c2SGreg Roach            ->filter($this->rawGedcomFilter($search));
17332cd2800SGreg Roach    }
17432cd2800SGreg Roach
17532cd2800SGreg Roach    /**
17632cd2800SGreg Roach     * Search for individuals by name.
17732cd2800SGreg Roach     *
178a7a24840SGreg Roach     * @param Tree[]   $trees
179a7a24840SGreg Roach     * @param string[] $search
18032cd2800SGreg Roach     * @param int      $offset
18132cd2800SGreg Roach     * @param int      $limit
18232cd2800SGreg Roach     *
183b5c8fd7eSGreg Roach     * @return Collection<Individual>
18432cd2800SGreg Roach     */
185a7a24840SGreg Roach    public function searchIndividualNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
18632cd2800SGreg Roach    {
18732cd2800SGreg Roach        $query = DB::table('individuals')
1880b5fd0a6SGreg Roach            ->join('name', static function (JoinClause $join): void {
18932cd2800SGreg Roach                $join
19032cd2800SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
191a7a24840SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id');
19232cd2800SGreg Roach            })
193e84cf2deSGreg Roach            ->orderBy('n_sort')
194d78d61f7SGreg Roach            ->distinct()
195685de081SGreg Roach            ->select(['individuals.*', 'n_sort']);
19632cd2800SGreg Roach
197a7a24840SGreg Roach        $this->whereTrees($query, 'i_file', $trees);
198a7a24840SGreg Roach        $this->whereSearch($query, 'n_full', $search);
199a7a24840SGreg Roach
200d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->individualRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
20132cd2800SGreg Roach    }
20232cd2800SGreg Roach
20332cd2800SGreg Roach    /**
204dbe53437SGreg Roach     * @param Place $place
205dbe53437SGreg Roach     *
206dbe53437SGreg Roach     * @return Collection<Individual>
207dbe53437SGreg Roach     */
208dbe53437SGreg Roach    public function searchIndividualsInPlace(Place $place): Collection
209dbe53437SGreg Roach    {
210dbe53437SGreg Roach        return DB::table('individuals')
211dbe53437SGreg Roach            ->join('placelinks', static function (JoinClause $join) {
212dbe53437SGreg Roach                $join
213dbe53437SGreg Roach                    ->on('i_file', '=', 'pl_file')
214dbe53437SGreg Roach                    ->on('i_id', '=', 'pl_gid');
215dbe53437SGreg Roach            })
216dbe53437SGreg Roach            ->where('i_file', '=', $place->tree()->id())
217dbe53437SGreg Roach            ->where('pl_p_id', '=', $place->id())
218dbe53437SGreg Roach            ->select(['individuals.*'])
219dbe53437SGreg Roach            ->get()
220dbe53437SGreg Roach            ->each($this->rowLimiter())
221dbe53437SGreg Roach            ->map($this->individualRowMapper())
222dbe53437SGreg Roach            ->filter(GedcomRecord::accessFilter());
223dbe53437SGreg Roach    }
224dbe53437SGreg Roach
225dbe53437SGreg Roach    /**
22632cd2800SGreg Roach     * Search for media objects.
22732cd2800SGreg Roach     *
228a7a24840SGreg Roach     * @param Tree[]   $trees
229a7a24840SGreg Roach     * @param string[] $search
23032cd2800SGreg Roach     * @param int      $offset
23132cd2800SGreg Roach     * @param int      $limit
23232cd2800SGreg Roach     *
233b5c8fd7eSGreg Roach     * @return Collection<Media>
23432cd2800SGreg Roach     */
235a7a24840SGreg Roach    public function searchMedia(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
23632cd2800SGreg Roach    {
237a7a24840SGreg Roach        $query = DB::table('media');
23832cd2800SGreg Roach
239a7a24840SGreg Roach        $this->whereTrees($query, 'media.m_file', $trees);
240a7a24840SGreg Roach        $this->whereSearch($query, 'm_gedcom', $search);
241a7a24840SGreg Roach
242d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->mediaRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
24332cd2800SGreg Roach    }
24432cd2800SGreg Roach
24532cd2800SGreg Roach    /**
24632cd2800SGreg Roach     * Search for notes.
24732cd2800SGreg Roach     *
248a7a24840SGreg Roach     * @param Tree[]   $trees
249a7a24840SGreg Roach     * @param string[] $search
25032cd2800SGreg Roach     * @param int      $offset
25132cd2800SGreg Roach     * @param int      $limit
25232cd2800SGreg Roach     *
253b5c8fd7eSGreg Roach     * @return Collection<Note>
25432cd2800SGreg Roach     */
255a7a24840SGreg Roach    public function searchNotes(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
25632cd2800SGreg Roach    {
25732cd2800SGreg Roach        $query = DB::table('other')
258a7a24840SGreg Roach            ->where('o_type', '=', 'NOTE');
25932cd2800SGreg Roach
260a7a24840SGreg Roach        $this->whereTrees($query, 'o_file', $trees);
261a7a24840SGreg Roach        $this->whereSearch($query, 'o_gedcom', $search);
262a7a24840SGreg Roach
263d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->noteRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
26432cd2800SGreg Roach    }
26532cd2800SGreg Roach
26632cd2800SGreg Roach    /**
26732cd2800SGreg Roach     * Search for repositories.
26832cd2800SGreg Roach     *
269a7a24840SGreg Roach     * @param Tree[]   $trees
270a7a24840SGreg Roach     * @param string[] $search
27132cd2800SGreg Roach     * @param int      $offset
27232cd2800SGreg Roach     * @param int      $limit
27332cd2800SGreg Roach     *
274b5c8fd7eSGreg Roach     * @return Collection<Repository>
27532cd2800SGreg Roach     */
276a7a24840SGreg Roach    public function searchRepositories(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
27732cd2800SGreg Roach    {
27832cd2800SGreg Roach        $query = DB::table('other')
279a7a24840SGreg Roach            ->where('o_type', '=', 'REPO');
28032cd2800SGreg Roach
281a7a24840SGreg Roach        $this->whereTrees($query, 'o_file', $trees);
282a7a24840SGreg Roach        $this->whereSearch($query, 'o_gedcom', $search);
283a7a24840SGreg Roach
284d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->repositoryRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
28532cd2800SGreg Roach    }
28632cd2800SGreg Roach
28732cd2800SGreg Roach    /**
288a7a24840SGreg Roach     * Search for sources.
28932cd2800SGreg Roach     *
290a7a24840SGreg Roach     * @param Tree[]   $trees
291a7a24840SGreg Roach     * @param string[] $search
29232cd2800SGreg Roach     * @param int      $offset
29332cd2800SGreg Roach     * @param int      $limit
29432cd2800SGreg Roach     *
295b5c8fd7eSGreg Roach     * @return Collection<Source>
29632cd2800SGreg Roach     */
297a7a24840SGreg Roach    public function searchSources(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
298a7a24840SGreg Roach    {
299a7a24840SGreg Roach        $query = DB::table('sources');
300a7a24840SGreg Roach
301a7a24840SGreg Roach        $this->whereTrees($query, 's_file', $trees);
302a7a24840SGreg Roach        $this->whereSearch($query, 's_gedcom', $search);
303a7a24840SGreg Roach
304d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
305a7a24840SGreg Roach    }
306a7a24840SGreg Roach
307a7a24840SGreg Roach    /**
308a7a24840SGreg Roach     * Search for sources by name.
309a7a24840SGreg Roach     *
310a7a24840SGreg Roach     * @param Tree[]   $trees
311a7a24840SGreg Roach     * @param string[] $search
312a7a24840SGreg Roach     * @param int      $offset
313a7a24840SGreg Roach     * @param int      $limit
314a7a24840SGreg Roach     *
315b5c8fd7eSGreg Roach     * @return Collection<Source>
316a7a24840SGreg Roach     */
317a7a24840SGreg Roach    public function searchSourcesByName(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
31832cd2800SGreg Roach    {
31932cd2800SGreg Roach        $query = DB::table('sources')
320c0804649SGreg Roach            ->orderBy('s_name');
32132cd2800SGreg Roach
322a7a24840SGreg Roach        $this->whereTrees($query, 's_file', $trees);
323a7a24840SGreg Roach        $this->whereSearch($query, 's_name', $search);
324a7a24840SGreg Roach
325d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
32632cd2800SGreg Roach    }
32732cd2800SGreg Roach
32832cd2800SGreg Roach    /**
32932cd2800SGreg Roach     * Search for submitters.
33032cd2800SGreg Roach     *
331a7a24840SGreg Roach     * @param Tree[]   $trees
332a7a24840SGreg Roach     * @param string[] $search
33332cd2800SGreg Roach     * @param int      $offset
33432cd2800SGreg Roach     * @param int      $limit
33532cd2800SGreg Roach     *
336b5c8fd7eSGreg Roach     * @return Collection<Submitter>
33732cd2800SGreg Roach     */
338a7a24840SGreg Roach    public function searchSubmitters(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
33932cd2800SGreg Roach    {
34032cd2800SGreg Roach        $query = DB::table('other')
341a7a24840SGreg Roach            ->where('o_type', '=', 'SUBM');
34232cd2800SGreg Roach
343a7a24840SGreg Roach        $this->whereTrees($query, 'o_file', $trees);
344a7a24840SGreg Roach        $this->whereSearch($query, 'o_gedcom', $search);
345a7a24840SGreg Roach
346d5ad3db0SGreg Roach        return $this->paginateQuery($query, $this->submitterRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
34732cd2800SGreg Roach    }
34832cd2800SGreg Roach
34932cd2800SGreg Roach    /**
350b68caec6SGreg Roach     * Search for places.
351b68caec6SGreg Roach     *
352b68caec6SGreg Roach     * @param Tree   $tree
353b68caec6SGreg Roach     * @param string $search
354b68caec6SGreg Roach     * @param int    $offset
355b68caec6SGreg Roach     * @param int    $limit
356b68caec6SGreg Roach     *
357b5c8fd7eSGreg Roach     * @return Collection<Place>
358b68caec6SGreg Roach     */
359b68caec6SGreg Roach    public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
360b68caec6SGreg Roach    {
361b68caec6SGreg Roach        $query = DB::table('places AS p0')
362b68caec6SGreg Roach            ->where('p0.p_file', '=', $tree->id())
363b68caec6SGreg Roach            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
364b68caec6SGreg Roach            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
365b68caec6SGreg Roach            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
366b68caec6SGreg Roach            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
367b68caec6SGreg Roach            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
368b68caec6SGreg Roach            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
369b68caec6SGreg Roach            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
370b68caec6SGreg Roach            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
371b68caec6SGreg Roach            ->orderBy('p0.p_place')
372b68caec6SGreg Roach            ->orderBy('p1.p_place')
373b68caec6SGreg Roach            ->orderBy('p2.p_place')
374b68caec6SGreg Roach            ->orderBy('p3.p_place')
375b68caec6SGreg Roach            ->orderBy('p4.p_place')
376b68caec6SGreg Roach            ->orderBy('p5.p_place')
377b68caec6SGreg Roach            ->orderBy('p6.p_place')
378b68caec6SGreg Roach            ->orderBy('p7.p_place')
379b68caec6SGreg Roach            ->orderBy('p8.p_place')
380b68caec6SGreg Roach            ->select([
381b68caec6SGreg Roach                'p0.p_place AS place0',
382b68caec6SGreg Roach                'p1.p_place AS place1',
383b68caec6SGreg Roach                'p2.p_place AS place2',
384b68caec6SGreg Roach                'p3.p_place AS place3',
385b68caec6SGreg Roach                'p4.p_place AS place4',
386b68caec6SGreg Roach                'p5.p_place AS place5',
387b68caec6SGreg Roach                'p6.p_place AS place6',
388b68caec6SGreg Roach                'p7.p_place AS place7',
389b68caec6SGreg Roach                'p8.p_place AS place8',
390b68caec6SGreg Roach            ]);
391b68caec6SGreg Roach
392b68caec6SGreg Roach        // Filter each level of the hierarchy.
393b68caec6SGreg Roach        foreach (explode(',', $search, 9) as $level => $string) {
394b5961194SGreg Roach            $query->where('p' . $level . '.p_place', 'LIKE', '%' . addcslashes($string, '\\%_') . '%');
395b68caec6SGreg Roach        }
396b68caec6SGreg Roach
3976c2179e2SGreg Roach        $row_mapper = static function (stdClass $row) use ($tree): Place {
398b68caec6SGreg Roach            $place = implode(', ', array_filter((array) $row));
399b68caec6SGreg Roach
400b68caec6SGreg Roach            return new Place($place, $tree);
401b68caec6SGreg Roach        };
402b68caec6SGreg Roach
4036c2179e2SGreg Roach        $filter = static function (): bool {
404a7a24840SGreg Roach            return true;
405a7a24840SGreg Roach        };
406b68caec6SGreg Roach
407a7a24840SGreg Roach        return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit);
408a7a24840SGreg Roach    }
409b68caec6SGreg Roach
410b68caec6SGreg Roach    /**
411a5dbc5b2SGreg Roach     * @param Tree[]   $trees
412dfb2cda2SGreg Roach     * @param string[] $fields
413dfb2cda2SGreg Roach     * @param string[] $modifiers
414dfb2cda2SGreg Roach     *
415b5c8fd7eSGreg Roach     * @return Collection<Individual>
416dfb2cda2SGreg Roach     */
417dfb2cda2SGreg Roach    public function searchIndividualsAdvanced(array $trees, array $fields, array $modifiers): Collection
418dfb2cda2SGreg Roach    {
419dfb2cda2SGreg Roach        $fields = array_filter($fields);
420dfb2cda2SGreg Roach
421dfb2cda2SGreg Roach        $query = DB::table('individuals')
422dfb2cda2SGreg Roach            ->select(['individuals.*'])
423dfb2cda2SGreg Roach            ->distinct();
424dfb2cda2SGreg Roach
425dfb2cda2SGreg Roach        $this->whereTrees($query, 'i_file', $trees);
426dfb2cda2SGreg Roach
427dfb2cda2SGreg Roach        // Join the following tables
428dfb2cda2SGreg Roach        $father_name   = false;
429dfb2cda2SGreg Roach        $mother_name   = false;
430dfb2cda2SGreg Roach        $spouse_family = false;
431dfb2cda2SGreg Roach        $indi_name     = false;
43256946b7aSGreg Roach        $indi_dates    = [];
43356946b7aSGreg Roach        $fam_dates     = [];
434dfb2cda2SGreg Roach        $indi_plac     = false;
435dfb2cda2SGreg Roach        $fam_plac      = false;
436dfb2cda2SGreg Roach
437dfb2cda2SGreg Roach        foreach ($fields as $field_name => $field_value) {
438dfb2cda2SGreg Roach            if ($field_value !== '') {
43956946b7aSGreg Roach                // Fields can have up to 4 parts, but we only need the first 3 to identify
44056946b7aSGreg Roach                // which tables to select
44156946b7aSGreg Roach                $field_parts = explode(':', $field_name . '::');
44256946b7aSGreg Roach
44356946b7aSGreg Roach                if ($field_parts[0] === 'FAMC') {
44456946b7aSGreg Roach                    // Parent name - FAMC:[HUSB|WIFE]:NAME:[GIVN|SURN]
44556946b7aSGreg Roach                    if ($field_parts[1] === 'HUSB') {
446dfb2cda2SGreg Roach                        $father_name = true;
44731dd1910SGreg Roach                    } else {
448dfb2cda2SGreg Roach                        $mother_name = true;
449dfb2cda2SGreg Roach                    }
45056946b7aSGreg Roach                } elseif ($field_parts[0] === 'NAME') {
45156946b7aSGreg Roach                    // Individual name - NAME:[GIVN|SURN]
45256946b7aSGreg Roach                    $indi_name = true;
45356946b7aSGreg Roach                } elseif ($field_parts[0] === 'FAMS') {
45456946b7aSGreg Roach                    // Family facts - FAMS:NOTE or FAMS:[FACT]:[DATE|PLAC]
455dfb2cda2SGreg Roach                    $spouse_family = true;
45656946b7aSGreg Roach                    if ($field_parts[2] === 'DATE') {
45756946b7aSGreg Roach                        $fam_dates[] = $field_parts[1];
45856946b7aSGreg Roach                    } elseif ($field_parts[2] === 'PLAC') {
45956946b7aSGreg Roach                        $fam_plac = true;
46056946b7aSGreg Roach                    }
461dfb2cda2SGreg Roach                } else {
46256946b7aSGreg Roach                    // Individual facts - [FACT] or [FACT]:[DATE|PLAC]
46356946b7aSGreg Roach                    if ($field_parts[1] === 'DATE') {
46456946b7aSGreg Roach                        $indi_dates[] = $field_parts[0];
46556946b7aSGreg Roach                    } elseif ($field_parts[1] === 'PLAC') {
466dfb2cda2SGreg Roach                        $indi_plac = true;
467dfb2cda2SGreg Roach                    }
468dfb2cda2SGreg Roach                }
469dfb2cda2SGreg Roach            }
470dfb2cda2SGreg Roach        }
471dfb2cda2SGreg Roach
472dfb2cda2SGreg Roach        if ($father_name || $mother_name) {
4730b5fd0a6SGreg Roach            $query->join('link AS l1', static function (JoinClause $join): void {
474dfb2cda2SGreg Roach                $join
475dfb2cda2SGreg Roach                    ->on('l1.l_file', '=', 'individuals.i_file')
476dfb2cda2SGreg Roach                    ->on('l1.l_from', '=', 'individuals.i_id')
477dfb2cda2SGreg Roach                    ->where('l1.l_type', '=', 'FAMC');
478dfb2cda2SGreg Roach            });
479dfb2cda2SGreg Roach
480dfb2cda2SGreg Roach            if ($father_name) {
4810b5fd0a6SGreg Roach                $query->join('link AS l2', static function (JoinClause $join): void {
482dfb2cda2SGreg Roach                    $join
483dfb2cda2SGreg Roach                        ->on('l2.l_file', '=', 'l1.l_file')
484dfb2cda2SGreg Roach                        ->on('l2.l_from', '=', 'l1.l_to')
485dfb2cda2SGreg Roach                        ->where('l2.l_type', '=', 'HUSB');
486dfb2cda2SGreg Roach                });
4870b5fd0a6SGreg Roach                $query->join('name AS father_name', static function (JoinClause $join): void {
488dfb2cda2SGreg Roach                    $join
489dfb2cda2SGreg Roach                        ->on('father_name.n_file', '=', 'l2.l_file')
490dfb2cda2SGreg Roach                        ->on('father_name.n_id', '=', 'l2.l_to');
491dfb2cda2SGreg Roach                });
492dfb2cda2SGreg Roach            }
493dfb2cda2SGreg Roach
494dfb2cda2SGreg Roach            if ($mother_name) {
4950b5fd0a6SGreg Roach                $query->join('link AS l3', static function (JoinClause $join): void {
496dfb2cda2SGreg Roach                    $join
497dfb2cda2SGreg Roach                        ->on('l3.l_file', '=', 'l1.l_file')
498dfb2cda2SGreg Roach                        ->on('l3.l_from', '=', 'l1.l_to')
499dfb2cda2SGreg Roach                        ->where('l3.l_type', '=', 'WIFE');
500dfb2cda2SGreg Roach                });
5010b5fd0a6SGreg Roach                $query->join('name AS mother_name', static function (JoinClause $join): void {
502dfb2cda2SGreg Roach                    $join
503dfb2cda2SGreg Roach                        ->on('mother_name.n_file', '=', 'l3.l_file')
504dfb2cda2SGreg Roach                        ->on('mother_name.n_id', '=', 'l3.l_to');
505dfb2cda2SGreg Roach                });
506dfb2cda2SGreg Roach            }
507dfb2cda2SGreg Roach        }
508dfb2cda2SGreg Roach
509dfb2cda2SGreg Roach        if ($spouse_family) {
5100b5fd0a6SGreg Roach            $query->join('link AS l4', static function (JoinClause $join): void {
511dfb2cda2SGreg Roach                $join
512dfb2cda2SGreg Roach                    ->on('l4.l_file', '=', 'individuals.i_file')
513dfb2cda2SGreg Roach                    ->on('l4.l_from', '=', 'individuals.i_id')
514dfb2cda2SGreg Roach                    ->where('l4.l_type', '=', 'FAMS');
515dfb2cda2SGreg Roach            });
5160b5fd0a6SGreg Roach            $query->join('families AS spouse_families', static function (JoinClause $join): void {
517dfb2cda2SGreg Roach                $join
518dfb2cda2SGreg Roach                    ->on('spouse_families.f_file', '=', 'l4.l_file')
519dfb2cda2SGreg Roach                    ->on('spouse_families.f_id', '=', 'l4.l_to');
520dfb2cda2SGreg Roach            });
521dfb2cda2SGreg Roach        }
522dfb2cda2SGreg Roach
523dfb2cda2SGreg Roach        if ($indi_name) {
5240b5fd0a6SGreg Roach            $query->join('name AS individual_name', static function (JoinClause $join): void {
525dfb2cda2SGreg Roach                $join
526dfb2cda2SGreg Roach                    ->on('individual_name.n_file', '=', 'individuals.i_file')
527dfb2cda2SGreg Roach                    ->on('individual_name.n_id', '=', 'individuals.i_id');
528dfb2cda2SGreg Roach            });
529dfb2cda2SGreg Roach        }
530dfb2cda2SGreg Roach
53156946b7aSGreg Roach        foreach (array_unique($indi_dates) as $indi_date) {
53256946b7aSGreg Roach            $query->join('dates AS date_' . $indi_date, static function (JoinClause $join) use ($indi_date): void {
533dfb2cda2SGreg Roach                $join
53456946b7aSGreg Roach                    ->on('date_' . $indi_date . '.d_file', '=', 'individuals.i_file')
53556946b7aSGreg Roach                    ->on('date_' . $indi_date . '.d_gid', '=', 'individuals.i_id');
536dfb2cda2SGreg Roach            });
537dfb2cda2SGreg Roach        }
538dfb2cda2SGreg Roach
53956946b7aSGreg Roach        foreach (array_unique($fam_dates) as $fam_date) {
54056946b7aSGreg Roach            $query->join('dates AS date_' . $fam_date, static function (JoinClause $join) use ($fam_date): void {
541dfb2cda2SGreg Roach                $join
54256946b7aSGreg Roach                    ->on('date_' . $fam_date . '.d_file', '=', 'spouse_families.i_file')
54356946b7aSGreg Roach                    ->on('date_' . $fam_date . '.d_gid', '=', 'spouse_families.i_id');
544dfb2cda2SGreg Roach            });
545dfb2cda2SGreg Roach        }
546dfb2cda2SGreg Roach
547dfb2cda2SGreg Roach        if ($indi_plac) {
5480b5fd0a6SGreg Roach            $query->join('placelinks AS individual_placelinks', static function (JoinClause $join): void {
549dfb2cda2SGreg Roach                $join
550dfb2cda2SGreg Roach                    ->on('individual_placelinks.pl_file', '=', 'individuals.i_file')
551dfb2cda2SGreg Roach                    ->on('individual_placelinks.pl_gid', '=', 'individuals.i_id');
552dfb2cda2SGreg Roach            });
5530b5fd0a6SGreg Roach            $query->join('places AS individual_places', static function (JoinClause $join): void {
554dfb2cda2SGreg Roach                $join
555dfb2cda2SGreg Roach                    ->on('individual_places.p_file', '=', 'individual_placelinks.pl_file')
556dfb2cda2SGreg Roach                    ->on('individual_places.p_id', '=', 'individual_placelinks.pl_p_id');
557dfb2cda2SGreg Roach            });
558dfb2cda2SGreg Roach        }
559dfb2cda2SGreg Roach
560dfb2cda2SGreg Roach        if ($fam_plac) {
5610b5fd0a6SGreg Roach            $query->join('placelinks AS familyl_placelinks', static function (JoinClause $join): void {
562dfb2cda2SGreg Roach                $join
563dfb2cda2SGreg Roach                    ->on('familyl_placelinks.pl_file', '=', 'individuals.i_file')
564dfb2cda2SGreg Roach                    ->on('familyl_placelinks.pl_gid', '=', 'individuals.i_id');
565dfb2cda2SGreg Roach            });
5660b5fd0a6SGreg Roach            $query->join('places AS family_places', static function (JoinClause $join): void {
567dfb2cda2SGreg Roach                $join
568dfb2cda2SGreg Roach                    ->on('family_places.p_file', '=', 'familyl_placelinks.pl_file')
569dfb2cda2SGreg Roach                    ->on('family_places.p_id', '=', 'familyl_placelinks.pl_p_id');
570dfb2cda2SGreg Roach            });
571dfb2cda2SGreg Roach        }
572dfb2cda2SGreg Roach
573dfb2cda2SGreg Roach        foreach ($fields as $field_name => $field_value) {
57456946b7aSGreg Roach            $parts = explode(':', $field_name . ':::');
575dfb2cda2SGreg Roach            if ($parts[0] === 'NAME') {
576dfb2cda2SGreg Roach                // NAME:*
577dfb2cda2SGreg Roach                switch ($parts[1]) {
578dfb2cda2SGreg Roach                    case 'GIVN':
579dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
580dfb2cda2SGreg Roach                            case 'EXACT':
581dfb2cda2SGreg Roach                                $query->where('individual_name.n_givn', '=', $field_value);
582dfb2cda2SGreg Roach                                break;
583dfb2cda2SGreg Roach                            case 'BEGINS':
584dfb2cda2SGreg Roach                                $query->where('individual_name.n_givn', 'LIKE', $field_value . '%');
585dfb2cda2SGreg Roach                                break;
586dfb2cda2SGreg Roach                            case 'CONTAINS':
587dfb2cda2SGreg Roach                                $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
588dfb2cda2SGreg Roach                                break;
589dfb2cda2SGreg Roach                            case 'SDX_STD':
590dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
591dfb2cda2SGreg Roach                                if ($sdx !== '') {
592dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_givn_std', $sdx);
593dfb2cda2SGreg Roach                                } else {
594dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
595dfb2cda2SGreg Roach                                    $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
596dfb2cda2SGreg Roach                                }
597dfb2cda2SGreg Roach                                break;
598dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
599dfb2cda2SGreg Roach                            case 'SDX_DM':
600dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
601dfb2cda2SGreg Roach                                if ($sdx !== '') {
602dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_givn_dm', $sdx);
603dfb2cda2SGreg Roach                                } else {
604dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
605dfb2cda2SGreg Roach                                    $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
606dfb2cda2SGreg Roach                                }
607dfb2cda2SGreg Roach                                break;
608dfb2cda2SGreg Roach                        }
609dfb2cda2SGreg Roach                        break;
610dfb2cda2SGreg Roach                    case 'SURN':
611dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
612dfb2cda2SGreg Roach                            case 'EXACT':
613fc6d23eeSGreg Roach                                $query->where(function (Builder $query) use ($field_value): void {
614fc6d23eeSGreg Roach                                    $query
615fc6d23eeSGreg Roach                                        ->where('individual_name.n_surn', '=', $field_value)
616fc6d23eeSGreg Roach                                        ->orWhere('individual_name.n_surname', '=', $field_value);
617fc6d23eeSGreg Roach                                });
618dfb2cda2SGreg Roach                                break;
619dfb2cda2SGreg Roach                            case 'BEGINS':
620fc6d23eeSGreg Roach                                $query->where(function (Builder $query) use ($field_value): void {
621fc6d23eeSGreg Roach                                    $query
622fc6d23eeSGreg Roach                                        ->where('individual_name.n_surn', 'LIKE', $field_value . '%')
6239875d474SGreg Roach                                        ->orWhere('individual_name.n_surname', 'LIKE', $field_value . '%');
624fc6d23eeSGreg Roach                                });
625dfb2cda2SGreg Roach                                break;
626dfb2cda2SGreg Roach                            case 'CONTAINS':
627fc6d23eeSGreg Roach                                $query->where(function (Builder $query) use ($field_value): void {
628fc6d23eeSGreg Roach                                    $query
629fc6d23eeSGreg Roach                                        ->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%')
6309875d474SGreg Roach                                        ->orWhere('individual_name.n_surname', 'LIKE', '%' . $field_value . '%');
631fc6d23eeSGreg Roach                                });
632dfb2cda2SGreg Roach                                break;
633dfb2cda2SGreg Roach                            case 'SDX_STD':
634dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
635dfb2cda2SGreg Roach                                if ($sdx !== '') {
636dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_surn_std', $sdx);
637dfb2cda2SGreg Roach                                } else {
638dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
639fc6d23eeSGreg Roach                                    $query->where(function (Builder $query) use ($field_value): void {
640fc6d23eeSGreg Roach                                        $query
641fc6d23eeSGreg Roach                                            ->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%')
6429875d474SGreg Roach                                            ->orWhere('individual_name.n_surname', 'LIKE', '%' . $field_value . '%');
643fc6d23eeSGreg Roach                                    });
644dfb2cda2SGreg Roach                                }
645dfb2cda2SGreg Roach                                break;
646dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
647dfb2cda2SGreg Roach                            case 'SDX_DM':
648dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
649dfb2cda2SGreg Roach                                if ($sdx !== '') {
650dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_surn_dm', $sdx);
651dfb2cda2SGreg Roach                                } else {
652dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
6539875d474SGreg Roach                                    $query->where(function (Builder $query) use ($field_value): void {
6549875d474SGreg Roach                                        $query
6559875d474SGreg Roach                                            ->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%')
6569875d474SGreg Roach                                            ->orWhere('individual_name.n_surname', 'LIKE', '%' . $field_value . '%');
6579875d474SGreg Roach                                    });
658dfb2cda2SGreg Roach                                }
659dfb2cda2SGreg Roach                                break;
660dfb2cda2SGreg Roach                        }
661dfb2cda2SGreg Roach                        break;
662dfb2cda2SGreg Roach                    case 'NICK':
663dfb2cda2SGreg Roach                    case '_MARNM':
664dfb2cda2SGreg Roach                    case '_HEB':
665dfb2cda2SGreg Roach                    case '_AKA':
666dfb2cda2SGreg Roach                        $query
667dfb2cda2SGreg Roach                            ->where('individual_name', '=', $parts[1])
668dfb2cda2SGreg Roach                            ->where('individual_name', 'LIKE', '%' . $field_value . '%');
669dfb2cda2SGreg Roach                        break;
670dfb2cda2SGreg Roach                }
671dfb2cda2SGreg Roach                unset($fields[$field_name]);
672dfb2cda2SGreg Roach            } elseif ($parts[1] === 'DATE') {
673dfb2cda2SGreg Roach                // *:DATE
674dfb2cda2SGreg Roach                $date = new Date($field_value);
675dfb2cda2SGreg Roach                if ($date->isOK()) {
676dfb2cda2SGreg Roach                    $delta = 365 * ($modifiers[$field_name] ?? 0);
677dfb2cda2SGreg Roach                    $query
67856946b7aSGreg Roach                        ->where('date_' . $parts[0] . '.d_fact', '=', $parts[0])
67956946b7aSGreg Roach                        ->where('date_' . $parts[0] . '.d_julianday1', '>=', $date->minimumJulianDay() - $delta)
68056946b7aSGreg Roach                        ->where('date_' . $parts[0] . '.d_julianday2', '<=', $date->maximumJulianDay() + $delta);
681dfb2cda2SGreg Roach                }
682dfb2cda2SGreg Roach                unset($fields[$field_name]);
683dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMS' && $parts[2] === 'DATE') {
684dfb2cda2SGreg Roach                // FAMS:*:DATE
685dfb2cda2SGreg Roach                $date = new Date($field_value);
686dfb2cda2SGreg Roach                if ($date->isOK()) {
687dfb2cda2SGreg Roach                    $delta = 365 * $modifiers[$field_name];
688dfb2cda2SGreg Roach                    $query
68956946b7aSGreg Roach                        ->where('date_' . $parts[1] . '.d_fact', '=', $parts[1])
69056946b7aSGreg Roach                        ->where('date_' . $parts[1] . '.d_julianday1', '>=', $date->minimumJulianDay() - $delta)
69156946b7aSGreg Roach                        ->where('date_' . $parts[1] . '.d_julianday2', '<=', $date->maximumJulianDay() + $delta);
692dfb2cda2SGreg Roach                }
693dfb2cda2SGreg Roach                unset($fields[$field_name]);
694dfb2cda2SGreg Roach            } elseif ($parts[1] === 'PLAC') {
695dfb2cda2SGreg Roach                // *:PLAC
696dfb2cda2SGreg Roach                // SQL can only link a place to a person/family, not to an event.
697dfb2cda2SGreg Roach                $query->where('individual_places.p_place', 'LIKE', '%' . $field_value . '%');
698dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') {
699dfb2cda2SGreg Roach                // FAMS:*:PLAC
700dfb2cda2SGreg Roach                // SQL can only link a place to a person/family, not to an event.
701dfb2cda2SGreg Roach                $query->where('family_places.p_place', 'LIKE', '%' . $field_value . '%');
702dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMC' && $parts[2] === 'NAME') {
703dfb2cda2SGreg Roach                $table = $parts[1] === 'HUSB' ? 'father_name' : 'mother_name';
704dfb2cda2SGreg Roach                // NAME:*
705dfb2cda2SGreg Roach                switch ($parts[3]) {
706dfb2cda2SGreg Roach                    case 'GIVN':
707dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
708dfb2cda2SGreg Roach                            case 'EXACT':
709dfb2cda2SGreg Roach                                $query->where($table . '.n_givn', '=', $field_value);
710dfb2cda2SGreg Roach                                break;
711dfb2cda2SGreg Roach                            case 'BEGINS':
712dfb2cda2SGreg Roach                                $query->where($table . '.n_givn', 'LIKE', $field_value . '%');
713dfb2cda2SGreg Roach                                break;
714dfb2cda2SGreg Roach                            case 'CONTAINS':
715dfb2cda2SGreg Roach                                $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
716dfb2cda2SGreg Roach                                break;
717dfb2cda2SGreg Roach                            case 'SDX_STD':
718dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
719dfb2cda2SGreg Roach                                if ($sdx !== '') {
720dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_givn_std', $sdx);
721dfb2cda2SGreg Roach                                } else {
722dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
723dfb2cda2SGreg Roach                                    $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
724dfb2cda2SGreg Roach                                }
725dfb2cda2SGreg Roach                                break;
726dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
727dfb2cda2SGreg Roach                            case 'SDX_DM':
728dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
729dfb2cda2SGreg Roach                                if ($sdx !== '') {
730dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_givn_dm', $sdx);
731dfb2cda2SGreg Roach                                } else {
732dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
733dfb2cda2SGreg Roach                                    $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
734dfb2cda2SGreg Roach                                }
735dfb2cda2SGreg Roach                                break;
736dfb2cda2SGreg Roach                        }
737dfb2cda2SGreg Roach                        break;
738dfb2cda2SGreg Roach                    case 'SURN':
739dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
740dfb2cda2SGreg Roach                            case 'EXACT':
741dfb2cda2SGreg Roach                                $query->where($table . '.n_surn', '=', $field_value);
742dfb2cda2SGreg Roach                                break;
743dfb2cda2SGreg Roach                            case 'BEGINS':
744dfb2cda2SGreg Roach                                $query->where($table . '.n_surn', 'LIKE', $field_value . '%');
745dfb2cda2SGreg Roach                                break;
746dfb2cda2SGreg Roach                            case 'CONTAINS':
747dfb2cda2SGreg Roach                                $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
748dfb2cda2SGreg Roach                                break;
749dfb2cda2SGreg Roach                            case 'SDX_STD':
750dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
751dfb2cda2SGreg Roach                                if ($sdx !== '') {
752dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_surn_std', $sdx);
753dfb2cda2SGreg Roach                                } else {
754dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
755dfb2cda2SGreg Roach                                    $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
756dfb2cda2SGreg Roach                                }
757dfb2cda2SGreg Roach                                break;
758dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
759dfb2cda2SGreg Roach                            case 'SDX_DM':
760dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
761dfb2cda2SGreg Roach                                if ($sdx !== '') {
762dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_surn_dm', $sdx);
763dfb2cda2SGreg Roach                                } else {
764dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
765dfb2cda2SGreg Roach                                    $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
766dfb2cda2SGreg Roach                                }
767dfb2cda2SGreg Roach                                break;
768dfb2cda2SGreg Roach                        }
769dfb2cda2SGreg Roach                        break;
770dfb2cda2SGreg Roach                }
771dfb2cda2SGreg Roach                unset($fields[$field_name]);
772dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMS') {
773dfb2cda2SGreg Roach                // e.g. searches for occupation, religion, note, etc.
774dfb2cda2SGreg Roach                // Initial matching only.  Need PHP to apply filter.
7753c480c6bSGreg Roach                $query->where('spouse_families.f_gedcom', 'LIKE', "%\n1 " . $parts[1] . ' %' . $field_value . '%');
776dfb2cda2SGreg Roach            } elseif ($parts[1] === 'TYPE') {
777dfb2cda2SGreg Roach                // e.g. FACT:TYPE or EVEN:TYPE
778dfb2cda2SGreg Roach                // Initial matching only.  Need PHP to apply filter.
779dfb2cda2SGreg Roach                $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%\n2 TYPE %' . $field_value . '%');
780dfb2cda2SGreg Roach            } else {
781dfb2cda2SGreg Roach                // e.g. searches for occupation, religion, note, etc.
782dfb2cda2SGreg Roach                // Initial matching only.  Need PHP to apply filter.
783c5457947SGreg Roach                $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%' . $parts[1] . '%' . $field_value . '%');
784dfb2cda2SGreg Roach            }
785dfb2cda2SGreg Roach        }
786dfb2cda2SGreg Roach        return $query
787dfb2cda2SGreg Roach            ->get()
78852a8ef61SGreg Roach            ->each($this->rowLimiter())
789d5ad3db0SGreg Roach            ->map($this->individualRowMapper())
790dfb2cda2SGreg Roach            ->filter(GedcomRecord::accessFilter())
7910b5fd0a6SGreg Roach            ->filter(static function (Individual $individual) use ($fields): bool {
792c5457947SGreg Roach                // Check for searches which were only partially matched by SQL
793dfb2cda2SGreg Roach                foreach ($fields as $field_name => $field_value) {
794a41e65f0SGreg Roach                    $regex = '/' . preg_quote($field_value, '/') . '/i';
795dfb2cda2SGreg Roach
796c5457947SGreg Roach                    $parts = explode(':', $field_name . '::::');
797dfb2cda2SGreg Roach
798dfb2cda2SGreg Roach                    // *:PLAC
799c5457947SGreg Roach                    if ($parts[1] === 'PLAC') {
800a41e65f0SGreg Roach                        foreach ($individual->facts([$parts[0]]) as $fact) {
801392561bbSGreg Roach                            if (preg_match($regex, $fact->place()->gedcomName())) {
802c5457947SGreg Roach                                continue 2;
803dfb2cda2SGreg Roach                            }
804a41e65f0SGreg Roach                        }
805c5457947SGreg Roach                        return false;
806c5457947SGreg Roach                    }
807c5457947SGreg Roach
808dfb2cda2SGreg Roach                    // FAMS:*:PLAC
809c5457947SGreg Roach                    if ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') {
81039ca88baSGreg Roach                        foreach ($individual->spouseFamilies() as $family) {
811a41e65f0SGreg Roach                            foreach ($family->facts([$parts[1]]) as $fact) {
812392561bbSGreg Roach                                if (preg_match($regex, $fact->place()->gedcomName())) {
813c5457947SGreg Roach                                    continue 2;
814a41e65f0SGreg Roach                                }
815dfb2cda2SGreg Roach                            }
816dfb2cda2SGreg Roach                        }
817c5457947SGreg Roach                        return false;
818c5457947SGreg Roach                    }
819c5457947SGreg Roach
820dfb2cda2SGreg Roach                    // e.g. searches for occupation, religion, note, etc.
821c5457947SGreg Roach                    if ($parts[0] === 'FAMS') {
82239ca88baSGreg Roach                        foreach ($individual->spouseFamilies() as $family) {
823a41e65f0SGreg Roach                            foreach ($family->facts([$parts[1]]) as $fact) {
824a41e65f0SGreg Roach                                if (preg_match($regex, $fact->value())) {
825c5457947SGreg Roach                                    continue 3;
826a41e65f0SGreg Roach                                }
827dfb2cda2SGreg Roach                            }
828dfb2cda2SGreg Roach                        }
829c5457947SGreg Roach                        return false;
830c5457947SGreg Roach                    }
831c5457947SGreg Roach
832dfb2cda2SGreg Roach                    // e.g. FACT:TYPE or EVEN:TYPE
833c5457947SGreg Roach                    if ($parts[1] === 'TYPE' || $parts[1] === '_WT_USER') {
834a41e65f0SGreg Roach                        foreach ($individual->facts([$parts[0]]) as $fact) {
835c5457947SGreg Roach                            if (preg_match($regex, $fact->attribute($parts[1]))) {
836c5457947SGreg Roach                                continue 2;
837dfb2cda2SGreg Roach                            }
838dfb2cda2SGreg Roach                        }
839dfb2cda2SGreg Roach
840dfb2cda2SGreg Roach                        return false;
841dfb2cda2SGreg Roach                    }
842c5457947SGreg Roach                }
843dfb2cda2SGreg Roach
844dfb2cda2SGreg Roach                return true;
845dfb2cda2SGreg Roach            });
846dfb2cda2SGreg Roach    }
847dfb2cda2SGreg Roach
848dfb2cda2SGreg Roach    /**
849dfb2cda2SGreg Roach     * @param string $soundex
850dfb2cda2SGreg Roach     * @param string $lastname
851dfb2cda2SGreg Roach     * @param string $firstname
852dfb2cda2SGreg Roach     * @param string $place
853dfb2cda2SGreg Roach     * @param Tree[] $search_trees
854dfb2cda2SGreg Roach     *
855b5c8fd7eSGreg Roach     * @return Collection<Individual>
856dfb2cda2SGreg Roach     */
857dfb2cda2SGreg Roach    public function searchIndividualsPhonetic(string $soundex, string $lastname, string $firstname, string $place, array $search_trees): Collection
858dfb2cda2SGreg Roach    {
859dfb2cda2SGreg Roach        switch ($soundex) {
860dfb2cda2SGreg Roach            default:
861dfb2cda2SGreg Roach            case 'Russell':
862dfb2cda2SGreg Roach                $givn_sdx   = Soundex::russell($firstname);
863dfb2cda2SGreg Roach                $surn_sdx   = Soundex::russell($lastname);
864dfb2cda2SGreg Roach                $plac_sdx   = Soundex::russell($place);
865dfb2cda2SGreg Roach                $givn_field = 'n_soundex_givn_std';
866dfb2cda2SGreg Roach                $surn_field = 'n_soundex_surn_std';
867dfb2cda2SGreg Roach                $plac_field = 'p_std_soundex';
868dfb2cda2SGreg Roach                break;
869dfb2cda2SGreg Roach            case 'DaitchM':
870dfb2cda2SGreg Roach                $givn_sdx   = Soundex::daitchMokotoff($firstname);
871dfb2cda2SGreg Roach                $surn_sdx   = Soundex::daitchMokotoff($lastname);
872dfb2cda2SGreg Roach                $plac_sdx   = Soundex::daitchMokotoff($place);
873dfb2cda2SGreg Roach                $givn_field = 'n_soundex_givn_dm';
874dfb2cda2SGreg Roach                $surn_field = 'n_soundex_surn_dm';
875dfb2cda2SGreg Roach                $plac_field = 'p_dm_soundex';
876dfb2cda2SGreg Roach                break;
877dfb2cda2SGreg Roach        }
878dfb2cda2SGreg Roach
879dfb2cda2SGreg Roach        // Nothing to search for? Return nothing.
880dfb2cda2SGreg Roach        if ($givn_sdx === '' && $surn_sdx === '' && $plac_sdx === '') {
8813fda39a7SGreg Roach            return new Collection();
882dfb2cda2SGreg Roach        }
883dfb2cda2SGreg Roach
884dfb2cda2SGreg Roach        $query = DB::table('individuals')
885dfb2cda2SGreg Roach            ->select(['individuals.*'])
886dfb2cda2SGreg Roach            ->distinct();
887dfb2cda2SGreg Roach
888dfb2cda2SGreg Roach        $this->whereTrees($query, 'i_file', $search_trees);
889dfb2cda2SGreg Roach
890dfb2cda2SGreg Roach        if ($plac_sdx !== '') {
8910b5fd0a6SGreg Roach            $query->join('placelinks', static function (JoinClause $join): void {
892dfb2cda2SGreg Roach                $join
893dfb2cda2SGreg Roach                    ->on('placelinks.pl_file', '=', 'individuals.i_file')
894dfb2cda2SGreg Roach                    ->on('placelinks.pl_gid', '=', 'individuals.i_id');
895dfb2cda2SGreg Roach            });
8960b5fd0a6SGreg Roach            $query->join('places', static function (JoinClause $join): void {
897dfb2cda2SGreg Roach                $join
898dfb2cda2SGreg Roach                    ->on('places.p_file', '=', 'placelinks.pl_file')
899dfb2cda2SGreg Roach                    ->on('places.p_id', '=', 'placelinks.pl_p_id');
900dfb2cda2SGreg Roach            });
901dfb2cda2SGreg Roach
902dfb2cda2SGreg Roach            $this->wherePhonetic($query, $plac_field, $plac_sdx);
903dfb2cda2SGreg Roach        }
904dfb2cda2SGreg Roach
905dfb2cda2SGreg Roach        if ($givn_sdx !== '' || $surn_sdx !== '') {
9060b5fd0a6SGreg Roach            $query->join('name', static function (JoinClause $join): void {
907dfb2cda2SGreg Roach                $join
908dfb2cda2SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
909dfb2cda2SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id');
910dfb2cda2SGreg Roach            });
911dfb2cda2SGreg Roach
912dfb2cda2SGreg Roach            $this->wherePhonetic($query, $givn_field, $givn_sdx);
913dfb2cda2SGreg Roach            $this->wherePhonetic($query, $surn_field, $surn_sdx);
914dfb2cda2SGreg Roach        }
915dfb2cda2SGreg Roach
916dfb2cda2SGreg Roach        return $query
917dfb2cda2SGreg Roach            ->get()
91852a8ef61SGreg Roach            ->each($this->rowLimiter())
919d5ad3db0SGreg Roach            ->map($this->individualRowMapper())
920dfb2cda2SGreg Roach            ->filter(GedcomRecord::accessFilter());
921dfb2cda2SGreg Roach    }
922dfb2cda2SGreg Roach
923dfb2cda2SGreg Roach    /**
92432cd2800SGreg Roach     * Paginate a search query.
92532cd2800SGreg Roach     *
92632cd2800SGreg Roach     * @param Builder $query      Searches the database for the desired records.
92732cd2800SGreg Roach     * @param Closure $row_mapper Converts a row from the query into a record.
928a7a24840SGreg Roach     * @param Closure $row_filter
92932cd2800SGreg Roach     * @param int     $offset     Skip this many rows.
93032cd2800SGreg Roach     * @param int     $limit      Take this many rows.
93132cd2800SGreg Roach     *
932b5c8fd7eSGreg Roach     * @return Collection<mixed>
93332cd2800SGreg Roach     */
934a7a24840SGreg Roach    private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection
93532cd2800SGreg Roach    {
93632cd2800SGreg Roach        $collection = new Collection();
93732cd2800SGreg Roach
93832cd2800SGreg Roach        foreach ($query->cursor() as $row) {
93932cd2800SGreg Roach            $record = $row_mapper($row);
940b68caec6SGreg Roach            // If the object has a method "canShow()", then use it to filter for privacy.
941a7a24840SGreg Roach            if ($row_filter($record)) {
94232cd2800SGreg Roach                if ($offset > 0) {
94332cd2800SGreg Roach                    $offset--;
94432cd2800SGreg Roach                } else {
94532cd2800SGreg Roach                    if ($limit > 0) {
94632cd2800SGreg Roach                        $collection->push($record);
94732cd2800SGreg Roach                    }
94832cd2800SGreg Roach
94932cd2800SGreg Roach                    $limit--;
95032cd2800SGreg Roach
95132cd2800SGreg Roach                    if ($limit === 0) {
95232cd2800SGreg Roach                        break;
95332cd2800SGreg Roach                    }
95432cd2800SGreg Roach                }
95532cd2800SGreg Roach            }
95632cd2800SGreg Roach        }
95732cd2800SGreg Roach
958e0458bdcSGreg Roach
95932cd2800SGreg Roach        return $collection;
96032cd2800SGreg Roach    }
961a7a24840SGreg Roach
962a7a24840SGreg Roach    /**
963a7a24840SGreg Roach     * Apply search filters to a SQL query column.  Apply collation rules to MySQL.
964a7a24840SGreg Roach     *
965a7a24840SGreg Roach     * @param Builder           $query
966a7a24840SGreg Roach     * @param Expression|string $field
967a7a24840SGreg Roach     * @param string[]          $search_terms
968a7a24840SGreg Roach     */
969a7a24840SGreg Roach    private function whereSearch(Builder $query, $field, array $search_terms): void
970a7a24840SGreg Roach    {
971a7a24840SGreg Roach        if ($field instanceof Expression) {
972a7a24840SGreg Roach            $field = $field->getValue();
973a7a24840SGreg Roach        }
974a7a24840SGreg Roach
975a7a24840SGreg Roach        foreach ($search_terms as $search_term) {
976b5961194SGreg Roach            $query->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_') . '%');
977a7a24840SGreg Roach        }
978a7a24840SGreg Roach    }
979a7a24840SGreg Roach
980a7a24840SGreg Roach    /**
9812d686e68SGreg Roach     * Apply soundex search filters to a SQL query column.
9822d686e68SGreg Roach     *
9832d686e68SGreg Roach     * @param Builder           $query
9842d686e68SGreg Roach     * @param Expression|string $field
9852d686e68SGreg Roach     * @param string            $soundex
9862d686e68SGreg Roach     */
9872d686e68SGreg Roach    private function wherePhonetic(Builder $query, $field, string $soundex): void
9882d686e68SGreg Roach    {
9892d686e68SGreg Roach        if ($soundex !== '') {
9900b5fd0a6SGreg Roach            $query->where(static function (Builder $query) use ($soundex, $field): void {
9912d686e68SGreg Roach                foreach (explode(':', $soundex) as $sdx) {
9922d686e68SGreg Roach                    $query->orWhere($field, 'LIKE', '%' . $sdx . '%');
9932d686e68SGreg Roach                }
9942d686e68SGreg Roach            });
9952d686e68SGreg Roach        }
9962d686e68SGreg Roach    }
9972d686e68SGreg Roach
9982d686e68SGreg Roach    /**
999a7a24840SGreg Roach     * @param Builder $query
1000a7a24840SGreg Roach     * @param string  $tree_id_field
1001a7a24840SGreg Roach     * @param Tree[]  $trees
1002a7a24840SGreg Roach     */
1003a7a24840SGreg Roach    private function whereTrees(Builder $query, string $tree_id_field, array $trees): void
1004a7a24840SGreg Roach    {
10050b5fd0a6SGreg Roach        $tree_ids = array_map(static function (Tree $tree): int {
1006a7a24840SGreg Roach            return $tree->id();
1007a7a24840SGreg Roach        }, $trees);
1008a7a24840SGreg Roach
1009a7a24840SGreg Roach        $query->whereIn($tree_id_field, $tree_ids);
1010a7a24840SGreg Roach    }
1011a7a24840SGreg Roach
1012d5ad3db0SGreg Roach    /**
1013d5ad3db0SGreg Roach     * Find the media object that uses a particular media file.
1014d5ad3db0SGreg Roach     *
1015d5ad3db0SGreg Roach     * @param string $file
1016d5ad3db0SGreg Roach     *
1017d5ad3db0SGreg Roach     * @return Media[]
1018d5ad3db0SGreg Roach     */
1019d5ad3db0SGreg Roach    public function findMediaObjectsForMediaFile(string $file): array
1020d5ad3db0SGreg Roach    {
1021d5ad3db0SGreg Roach        return DB::table('media')
1022d5ad3db0SGreg Roach            ->join('media_file', static function (JoinClause $join): void {
1023d5ad3db0SGreg Roach                $join
1024d5ad3db0SGreg Roach                    ->on('media_file.m_file', '=', 'media.m_file')
1025d5ad3db0SGreg Roach                    ->on('media_file.m_id', '=', 'media.m_id');
1026d5ad3db0SGreg Roach            })
1027d5ad3db0SGreg Roach            ->join('gedcom_setting', 'media.m_file', '=', 'gedcom_setting.gedcom_id')
1028d5ad3db0SGreg Roach            ->where(new Expression('setting_value || multimedia_file_refn'), '=', $file)
1029d5ad3db0SGreg Roach            ->select(['media.*'])
1030d5ad3db0SGreg Roach            ->distinct()
1031d5ad3db0SGreg Roach            ->get()
1032d5ad3db0SGreg Roach            ->map($this->mediaRowMapper())
1033d5ad3db0SGreg Roach            ->all();
1034d5ad3db0SGreg Roach    }
1035d5ad3db0SGreg Roach
1036a7a24840SGreg Roach    /**
1037a7a24840SGreg Roach     * A closure to filter records by privacy-filtered GEDCOM data.
1038a7a24840SGreg Roach     *
1039a7a24840SGreg Roach     * @param array $search_terms
1040a7a24840SGreg Roach     *
1041a7a24840SGreg Roach     * @return Closure
1042a7a24840SGreg Roach     */
1043a7a24840SGreg Roach    private function rawGedcomFilter(array $search_terms): Closure
1044a7a24840SGreg Roach    {
10456c2179e2SGreg Roach        return static function (GedcomRecord $record) use ($search_terms): bool {
1046a7a24840SGreg Roach            // Ignore non-genealogy fields
1047faa5e163SGreg Roach            $gedcom = preg_replace('/\n\d (?:_UID|_WT_USER) .*/', '', $record->gedcom());
1048a7a24840SGreg Roach
1049a7a24840SGreg Roach            // Ignore matches in links
1050a7a24840SGreg Roach            $gedcom = preg_replace('/\n\d ' . Gedcom::REGEX_TAG . '( @' . Gedcom::REGEX_XREF . '@)?/', '', $gedcom);
1051a7a24840SGreg Roach
1052a7a24840SGreg Roach            // Re-apply the filtering
1053a7a24840SGreg Roach            foreach ($search_terms as $search_term) {
1054a7a24840SGreg Roach                if (mb_stripos($gedcom, $search_term) === false) {
1055a7a24840SGreg Roach                    return false;
1056a7a24840SGreg Roach                }
1057a7a24840SGreg Roach            }
1058a7a24840SGreg Roach
1059a7a24840SGreg Roach            return true;
1060a7a24840SGreg Roach        };
1061a7a24840SGreg Roach    }
106252a8ef61SGreg Roach
106352a8ef61SGreg Roach    /**
106452a8ef61SGreg Roach     * Searching for short or common text can give more results than the system can process.
106552a8ef61SGreg Roach     *
106652a8ef61SGreg Roach     * @param int $limit
106752a8ef61SGreg Roach     *
106852a8ef61SGreg Roach     * @return Closure
106952a8ef61SGreg Roach     */
107052a8ef61SGreg Roach    private function rowLimiter(int $limit = 1000): Closure
107152a8ef61SGreg Roach    {
10726c2179e2SGreg Roach        return static function () use ($limit): void {
107352a8ef61SGreg Roach            static $n = 0;
107452a8ef61SGreg Roach
107552a8ef61SGreg Roach            if (++$n > $limit) {
107652a8ef61SGreg Roach                $message = I18N::translate('The search returned too many results.');
107752a8ef61SGreg Roach
1078d501c45dSGreg Roach                throw new HttpServiceUnavailableException($message);
107952a8ef61SGreg Roach            }
108052a8ef61SGreg Roach        };
108152a8ef61SGreg Roach    }
1082d5ad3db0SGreg Roach
1083d5ad3db0SGreg Roach    /**
1084d5ad3db0SGreg Roach     * Convert a row from any tree in the families table into a family object.
1085d5ad3db0SGreg Roach     *
1086d5ad3db0SGreg Roach     * @return Closure
1087d5ad3db0SGreg Roach     */
1088d5ad3db0SGreg Roach    private function familyRowMapper(): Closure
1089d5ad3db0SGreg Roach    {
1090d5ad3db0SGreg Roach        return function (stdClass $row): Family {
1091d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->f_file);
1092d5ad3db0SGreg Roach
1093*a091ac74SGreg Roach            return Factory::family()->mapper($tree)($row);
1094d5ad3db0SGreg Roach        };
1095d5ad3db0SGreg Roach    }
1096d5ad3db0SGreg Roach
1097d5ad3db0SGreg Roach    /**
1098d5ad3db0SGreg Roach     * Convert a row from any tree in the individuals table into an individual object.
1099d5ad3db0SGreg Roach     *
1100d5ad3db0SGreg Roach     * @return Closure
1101d5ad3db0SGreg Roach     */
1102d5ad3db0SGreg Roach    private function individualRowMapper(): Closure
1103d5ad3db0SGreg Roach    {
1104d5ad3db0SGreg Roach        return function (stdClass $row): Individual {
1105d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->i_file);
1106d5ad3db0SGreg Roach
1107*a091ac74SGreg Roach            return Factory::individual()->mapper($tree)($row);
1108d5ad3db0SGreg Roach        };
1109d5ad3db0SGreg Roach    }
1110d5ad3db0SGreg Roach
1111d5ad3db0SGreg Roach    /**
1112d5ad3db0SGreg Roach     * Convert a row from any tree in the media table into an media object.
1113d5ad3db0SGreg Roach     *
1114d5ad3db0SGreg Roach     * @return Closure
1115d5ad3db0SGreg Roach     */
1116d5ad3db0SGreg Roach    private function mediaRowMapper(): Closure
1117d5ad3db0SGreg Roach    {
1118d5ad3db0SGreg Roach        return function (stdClass $row): Media {
1119d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->m_file);
1120d5ad3db0SGreg Roach
1121*a091ac74SGreg Roach            return Factory::media()->mapper($tree)($row);
1122d5ad3db0SGreg Roach        };
1123d5ad3db0SGreg Roach    }
1124d5ad3db0SGreg Roach
1125d5ad3db0SGreg Roach    /**
1126d5ad3db0SGreg Roach     * Convert a row from any tree in the other table into a note object.
1127d5ad3db0SGreg Roach     *
1128d5ad3db0SGreg Roach     * @return Closure
1129d5ad3db0SGreg Roach     */
1130d5ad3db0SGreg Roach    private function noteRowMapper(): Closure
1131d5ad3db0SGreg Roach    {
1132d5ad3db0SGreg Roach        return function (stdClass $row): Note {
1133d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->o_file);
1134d5ad3db0SGreg Roach
1135*a091ac74SGreg Roach            return Factory::note()->mapper($tree)($row);
1136d5ad3db0SGreg Roach        };
1137d5ad3db0SGreg Roach    }
1138d5ad3db0SGreg Roach
1139d5ad3db0SGreg Roach    /**
1140d5ad3db0SGreg Roach     * Convert a row from any tree in the other table into a repository object.
1141d5ad3db0SGreg Roach     *
1142d5ad3db0SGreg Roach     * @return Closure
1143d5ad3db0SGreg Roach     */
1144d5ad3db0SGreg Roach    private function repositoryRowMapper(): Closure
1145d5ad3db0SGreg Roach    {
1146d5ad3db0SGreg Roach        return function (stdClass $row): Repository {
1147d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->o_file);
1148d5ad3db0SGreg Roach
1149*a091ac74SGreg Roach            return Factory::repository()->mapper($tree)($row);
1150d5ad3db0SGreg Roach        };
1151d5ad3db0SGreg Roach    }
1152d5ad3db0SGreg Roach
1153d5ad3db0SGreg Roach    /**
1154d5ad3db0SGreg Roach     * Convert a row from any tree in the sources table into a source object.
1155d5ad3db0SGreg Roach     *
1156d5ad3db0SGreg Roach     * @return Closure
1157d5ad3db0SGreg Roach     */
1158d5ad3db0SGreg Roach    private function sourceRowMapper(): Closure
1159d5ad3db0SGreg Roach    {
1160d5ad3db0SGreg Roach        return function (stdClass $row): Source {
1161d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->s_file);
1162d5ad3db0SGreg Roach
1163*a091ac74SGreg Roach            return Factory::source()->mapper($tree)($row);
1164d5ad3db0SGreg Roach        };
1165d5ad3db0SGreg Roach    }
1166d5ad3db0SGreg Roach
1167d5ad3db0SGreg Roach    /**
1168d5ad3db0SGreg Roach     * Convert a row from any tree in the other table into a submitter object.
1169d5ad3db0SGreg Roach     *
1170d5ad3db0SGreg Roach     * @return Closure
1171d5ad3db0SGreg Roach     */
1172d5ad3db0SGreg Roach    private function submitterRowMapper(): Closure
1173d5ad3db0SGreg Roach    {
11743959eeb6SGreg Roach        return function (stdClass $row): Submitter {
1175d5ad3db0SGreg Roach            $tree = $this->tree_service->find((int) $row->o_file);
1176d5ad3db0SGreg Roach
1177*a091ac74SGreg Roach            return Factory::submitter()->mapper($tree)($row);
1178d5ad3db0SGreg Roach        };
1179d5ad3db0SGreg Roach    }
118032cd2800SGreg Roach}
1181