Passed
Pull Request — 2.0 (#4079)
by
unknown
08:17 queued 01:58
created

SearchService::searchPlaces()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 85
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 69
c 1
b 0
f 0
nc 4
nop 5
dl 0
loc 85
rs 8.6763

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Closure;
23
use Fisharebest\Webtrees\Date;
24
use Fisharebest\Webtrees\Exceptions\HttpServiceUnavailableException;
25
use Fisharebest\Webtrees\Location;
26
use Fisharebest\Webtrees\Registry;
27
use Fisharebest\Webtrees\Family;
28
use Fisharebest\Webtrees\Gedcom;
29
use Fisharebest\Webtrees\GedcomRecord;
30
use Fisharebest\Webtrees\I18N;
31
use Fisharebest\Webtrees\Individual;
32
use Fisharebest\Webtrees\Media;
33
use Fisharebest\Webtrees\Note;
34
use Fisharebest\Webtrees\Place;
35
use Fisharebest\Webtrees\Repository;
36
use Fisharebest\Webtrees\Soundex;
37
use Fisharebest\Webtrees\Source;
38
use Fisharebest\Webtrees\Submission;
39
use Fisharebest\Webtrees\Submitter;
40
use Fisharebest\Webtrees\Tree;
41
use Illuminate\Database\Capsule\Manager as DB;
42
use Illuminate\Database\Query\Builder;
43
use Illuminate\Database\Query\Expression;
44
use Illuminate\Database\Query\JoinClause;
45
use Illuminate\Support\Collection;
46
use stdClass;
47
48
use function addcslashes;
49
use function array_filter;
50
use function array_map;
51
use function array_unique;
52
use function explode;
53
use function implode;
54
use function mb_stripos;
55
use function preg_match;
56
use function preg_quote;
57
use function preg_replace;
58
59
use const PHP_INT_MAX;
60
61
/**
62
 * Search trees for genealogy records.
63
 */
64
class SearchService
65
{
66
    // Do not attempt to show search results larger than this/
67
    protected const MAX_SEARCH_RESULTS = 5000;
68
69
    /** @var TreeService */
70
    private $tree_service;
71
72
    /**
73
     * SearchService constructor.
74
     *
75
     * @param TreeService $tree_service
76
     */
77
    public function __construct(
78
        TreeService $tree_service
79
    ) {
80
        $this->tree_service = $tree_service;
81
    }
82
83
    /**
84
     * @param Tree[]   $trees
85
     * @param string[] $search
86
     *
87
     * @return Collection<Family>
88
     */
89
    public function searchFamilies(array $trees, array $search): Collection
90
    {
91
        $query = DB::table('families');
92
93
        $this->whereTrees($query, 'f_file', $trees);
94
        $this->whereSearch($query, 'f_gedcom', $search);
95
96
        return $query
97
            ->get()
98
            ->each($this->rowLimiter())
99
            ->map($this->familyRowMapper())
100
            ->filter(GedcomRecord::accessFilter())
101
            ->filter($this->rawGedcomFilter($search));
102
    }
103
104
    /**
105
     * Search for families by name.
106
     *
107
     * @param Tree[]   $trees
108
     * @param string[] $search
109
     * @param int      $offset
110
     * @param int      $limit
111
     *
112
     * @return Collection<Family>
113
     */
114
    public function searchFamilyNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
115
    {
116
        $query = DB::table('families')
117
            ->leftJoin('name AS husb_name', static function (JoinClause $join): void {
118
                $join
119
                    ->on('husb_name.n_file', '=', 'families.f_file')
120
                    ->on('husb_name.n_id', '=', 'families.f_husb')
121
                    ->where('husb_name.n_type', '<>', '_MARNM');
122
            })
123
            ->leftJoin('name AS wife_name', static function (JoinClause $join): void {
124
                $join
125
                    ->on('wife_name.n_file', '=', 'families.f_file')
126
                    ->on('wife_name.n_id', '=', 'families.f_wife')
127
                    ->where('wife_name.n_type', '<>', '_MARNM');
128
            });
129
130
        $prefix = DB::connection()->getTablePrefix();
131
        $field  = new Expression('COALESCE(' . $prefix . "husb_name.n_full, '') || COALESCE(" . $prefix . "wife_name.n_full, '')");
132
133
        $this->whereTrees($query, 'f_file', $trees);
134
        $this->whereSearch($query, $field, $search);
135
136
        $query
137
            ->orderBy('husb_name.n_sort')
138
            ->orderBy('wife_name.n_sort')
139
            ->select(['families.*', 'husb_name.n_sort', 'wife_name.n_sort']);
140
141
        return $this->paginateQuery($query, $this->familyRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
142
    }
143
144
    /**
145
     * @param Place $place
146
     *
147
     * @return Collection<Family>
148
     */
149
    public function searchFamiliesInPlace(Place $place): Collection
150
    {
151
        return DB::table('families')
152
            ->join('placelinks', static function (JoinClause $query) {
153
                $query
154
                    ->on('families.f_file', '=', 'placelinks.pl_file')
155
                    ->on('families.f_id', '=', 'placelinks.pl_gid');
156
            })
157
            ->where('f_file', '=', $place->tree()->id())
158
            ->where('pl_p_id', '=', $place->id())
159
            ->select(['families.*'])
160
            ->get()
161
            ->each($this->rowLimiter())
162
            ->map($this->familyRowMapper())
163
            ->filter(GedcomRecord::accessFilter());
164
    }
165
166
    /**
167
     * @param Tree[]   $trees
168
     * @param string[] $search
169
     *
170
     * @return Collection<Individual>
171
     */
172
    public function searchIndividuals(array $trees, array $search): Collection
173
    {
174
        $query = DB::table('individuals');
175
176
        $this->whereTrees($query, 'i_file', $trees);
177
        $this->whereSearch($query, 'i_gedcom', $search);
178
179
        return $query
180
            ->get()
181
            ->each($this->rowLimiter())
182
            ->map($this->individualRowMapper())
183
            ->filter(GedcomRecord::accessFilter())
184
            ->filter($this->rawGedcomFilter($search));
185
    }
186
187
    /**
188
     * Search for individuals by name.
189
     *
190
     * @param Tree[]   $trees
191
     * @param string[] $search
192
     * @param int      $offset
193
     * @param int      $limit
194
     *
195
     * @return Collection<Individual>
196
     */
197
    public function searchIndividualNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
198
    {
199
        $query = DB::table('individuals')
200
            ->join('name', static function (JoinClause $join): void {
201
                $join
202
                    ->on('name.n_file', '=', 'individuals.i_file')
203
                    ->on('name.n_id', '=', 'individuals.i_id');
204
            })
205
            ->orderBy('n_sort')
206
            ->select(['individuals.*', 'n_sort']);
207
208
        $this->whereTrees($query, 'i_file', $trees);
209
        $this->whereSearch($query, 'n_full', $search);
210
211
        return $this->paginateQuery($query, $this->individualRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
212
    }
213
214
    /**
215
     * @param Place $place
216
     *
217
     * @return Collection<Individual>
218
     */
219
    public function searchIndividualsInPlace(Place $place): Collection
220
    {
221
        return DB::table('individuals')
222
            ->join('placelinks', static function (JoinClause $join) {
223
                $join
224
                    ->on('i_file', '=', 'pl_file')
225
                    ->on('i_id', '=', 'pl_gid');
226
            })
227
            ->where('i_file', '=', $place->tree()->id())
228
            ->where('pl_p_id', '=', $place->id())
229
            ->select(['individuals.*'])
230
            ->get()
231
            ->each($this->rowLimiter())
232
            ->map($this->individualRowMapper())
233
            ->filter(GedcomRecord::accessFilter());
234
    }
235
236
    /**
237
     * Search for submissions.
238
     *
239
     * @param Tree[]   $trees
240
     * @param string[] $search
241
     * @param int      $offset
242
     * @param int      $limit
243
     *
244
     * @return Collection<Location>
245
     */
246
    public function searchLocations(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
247
    {
248
        $query = DB::table('other')
249
            ->where('o_type', '=', '_LOC');
250
251
        $this->whereTrees($query, 'o_file', $trees);
252
        $this->whereSearch($query, 'o_gedcom', $search);
253
254
        return $this->paginateQuery($query, $this->locationRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
255
    }
256
257
    /**
258
     * Search for media objects.
259
     *
260
     * @param Tree[]   $trees
261
     * @param string[] $search
262
     * @param int      $offset
263
     * @param int      $limit
264
     *
265
     * @return Collection<Media>
266
     */
267
    public function searchMedia(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
268
    {
269
        $query = DB::table('media');
270
271
        $this->whereTrees($query, 'media.m_file', $trees);
272
        $this->whereSearch($query, 'm_gedcom', $search);
273
274
        return $this->paginateQuery($query, $this->mediaRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
275
    }
276
277
    /**
278
     * Search for notes.
279
     *
280
     * @param Tree[]   $trees
281
     * @param string[] $search
282
     * @param int      $offset
283
     * @param int      $limit
284
     *
285
     * @return Collection<Note>
286
     */
287
    public function searchNotes(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
288
    {
289
        $query = DB::table('other')
290
            ->where('o_type', '=', 'NOTE');
291
292
        $this->whereTrees($query, 'o_file', $trees);
293
        $this->whereSearch($query, 'o_gedcom', $search);
294
295
        return $this->paginateQuery($query, $this->noteRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
296
    }
297
298
    /**
299
     * Search for repositories.
300
     *
301
     * @param Tree[]   $trees
302
     * @param string[] $search
303
     * @param int      $offset
304
     * @param int      $limit
305
     *
306
     * @return Collection<Repository>
307
     */
308
    public function searchRepositories(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
309
    {
310
        $query = DB::table('other')
311
            ->where('o_type', '=', 'REPO');
312
313
        $this->whereTrees($query, 'o_file', $trees);
314
        $this->whereSearch($query, 'o_gedcom', $search);
315
316
        return $this->paginateQuery($query, $this->repositoryRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
317
    }
318
319
    /**
320
     * Search for sources.
321
     *
322
     * @param Tree[]   $trees
323
     * @param string[] $search
324
     * @param int      $offset
325
     * @param int      $limit
326
     *
327
     * @return Collection<Source>
328
     */
329
    public function searchSources(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
330
    {
331
        $query = DB::table('sources');
332
333
        $this->whereTrees($query, 's_file', $trees);
334
        $this->whereSearch($query, 's_gedcom', $search);
335
336
        return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
337
    }
338
339
    /**
340
     * Search for sources by name.
341
     *
342
     * @param Tree[]   $trees
343
     * @param string[] $search
344
     * @param int      $offset
345
     * @param int      $limit
346
     *
347
     * @return Collection<Source>
348
     */
349
    public function searchSourcesByName(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
350
    {
351
        $query = DB::table('sources')
352
            ->orderBy('s_name');
353
354
        $this->whereTrees($query, 's_file', $trees);
355
        $this->whereSearch($query, 's_name', $search);
356
357
        return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
358
    }
359
360
    /**
361
     * Search for sources.
362
     *
363
     * @param Tree[]   $trees
364
     * @param string[] $search
365
     * @param int      $offset
366
     * @param int      $limit
367
     *
368
     * @return Collection<string>
369
     */
370
    public function searchSurnames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
371
    {
372
        $query = DB::table('name');
373
374
        $this->whereTrees($query, 'n_file', $trees);
375
        $this->whereSearch($query, 'n_surname', $search);
376
377
        return $query
378
            ->groupBy(['n_surname'])
379
            ->orderBy('n_surname')
380
            ->skip($offset)
381
            ->take($limit)
382
            ->pluck('n_surname');
383
    }
384
385
    /**
386
     * Search for submissions.
387
     *
388
     * @param Tree[]   $trees
389
     * @param string[] $search
390
     * @param int      $offset
391
     * @param int      $limit
392
     *
393
     * @return Collection<Submission>
394
     */
395
    public function searchSubmissions(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
396
    {
397
        $query = DB::table('other')
398
            ->where('o_type', '=', 'SUBN');
399
400
        $this->whereTrees($query, 'o_file', $trees);
401
        $this->whereSearch($query, 'o_gedcom', $search);
402
403
        return $this->paginateQuery($query, $this->submissionRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
404
    }
405
406
    /**
407
     * Search for submitters.
408
     *
409
     * @param Tree[]   $trees
410
     * @param string[] $search
411
     * @param int      $offset
412
     * @param int      $limit
413
     *
414
     * @return Collection<Submitter>
415
     */
416
    public function searchSubmitters(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
417
    {
418
        $query = DB::table('other')
419
            ->where('o_type', '=', 'SUBM');
420
421
        $this->whereTrees($query, 'o_file', $trees);
422
        $this->whereSearch($query, 'o_gedcom', $search);
423
424
        return $this->paginateQuery($query, $this->submitterRowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
425
    }
426
427
    /**
428
     * Search for places.
429
     *
430
     * @param Tree   $tree
431
     * @param string $search
432
     * @param int    $offset
433
     * @param int    $limit
434
     * @param bool   $searchplaceloc
435
     *
436
     * @return Collection<Place>
437
     */
438
    public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX, bool $searchplaceloc = false): Collection
439
    {
440
        $query = DB::table('places AS p0')
441
            ->where('p0.p_file', '=', $tree->id())
442
            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
443
            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
444
            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
445
            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
446
            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
447
            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
448
            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
449
            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
450
            ->orderBy('p0.p_place')
451
            ->orderBy('p1.p_place')
452
            ->orderBy('p2.p_place')
453
            ->orderBy('p3.p_place')
454
            ->orderBy('p4.p_place')
455
            ->orderBy('p5.p_place')
456
            ->orderBy('p6.p_place')
457
            ->orderBy('p7.p_place')
458
            ->orderBy('p8.p_place')
459
            ->select([
460
                'p0.p_place AS place0',
461
                'p1.p_place AS place1',
462
                'p2.p_place AS place2',
463
                'p3.p_place AS place3',
464
                'p4.p_place AS place4',
465
                'p5.p_place AS place5',
466
                'p6.p_place AS place6',
467
                'p7.p_place AS place7',
468
                'p8.p_place AS place8',
469
            ]);
470
471
        // Filter each level of the hierarchy.
472
        foreach (explode(',', $search, 9) as $level => $string) {
473
            $query->where('p' . $level . '.p_place', 'LIKE', '%' . addcslashes($string, '\\%_') . '%');
474
        }
475
        if ($searchplaceloc){
476
            $geonamesquery = DB::table('place_location AS p0')
477
            ->leftJoin('place_location AS p1', 'p1.id', '=', 'p0.parent_id')
478
            ->leftJoin('place_location AS p2', 'p2.id', '=', 'p1.parent_id')
479
            ->leftJoin('place_location AS p3', 'p3.id', '=', 'p2.parent_id')
480
            ->leftJoin('place_location AS p4', 'p4.id', '=', 'p3.parent_id')
481
            ->leftJoin('place_location AS p5', 'p5.id', '=', 'p4.parent_id')
482
            ->leftJoin('place_location AS p6', 'p6.id', '=', 'p5.parent_id')
483
            ->leftJoin('place_location AS p7', 'p7.id', '=', 'p6.parent_id')
484
            ->leftJoin('place_location AS p8', 'p8.id', '=', 'p7.parent_id')
485
            ->orderBy('p0.place')
486
            ->orderBy('p1.place')
487
            ->orderBy('p2.place')
488
            ->orderBy('p3.place')
489
            ->orderBy('p4.place')
490
            ->orderBy('p5.place')
491
            ->orderBy('p6.place')
492
            ->orderBy('p7.place')
493
            ->orderBy('p8.place')
494
            ->select([
495
                'p0.place AS place0',
496
                'p1.place AS place1',
497
                'p2.place AS place2',
498
                'p3.place AS place3',
499
                'p4.place AS place4',
500
                'p5.place AS place5',
501
                'p6.place AS place6',
502
                'p7.place AS place7',
503
                'p8.place AS place8',
504
            ]);
505
            // Filter each level of the hierarchy.
506
            foreach (explode(',', $search, 9) as $level => $string) {
507
                $geonamesquery->where('p' . $level . '.place', 'LIKE', '%' . addcslashes($string, '\\%_') . '%');
508
            }
509
            $query->union($geonamesquery);
510
        }
511
512
        $row_mapper = static function (stdClass $row) use ($tree): Place {
513
            $place = implode(', ', array_filter((array) $row));
514
515
            return new Place($place, $tree);
516
        };
517
518
        $filter = static function (): bool {
519
            return true;
520
        };
521
522
        return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit);
523
    }
524
525
    /**
526
     * @param Tree[]   $trees
527
     * @param string[] $fields
528
     * @param string[] $modifiers
529
     *
530
     * @return Collection<Individual>
531
     */
532
    public function searchIndividualsAdvanced(array $trees, array $fields, array $modifiers): Collection
533
    {
534
        $fields = array_filter($fields);
535
536
        $query = DB::table('individuals')
537
            ->select(['individuals.*'])
538
            ->distinct();
539
540
        $this->whereTrees($query, 'i_file', $trees);
541
542
        // Join the following tables
543
        $father_name   = false;
544
        $mother_name   = false;
545
        $spouse_family = false;
546
        $indi_name     = false;
547
        $indi_dates    = [];
548
        $fam_dates     = [];
549
        $indi_plac     = false;
550
        $fam_plac      = false;
551
552
        foreach ($fields as $field_name => $field_value) {
553
            if ($field_value !== '') {
554
                // Fields can have up to 4 parts, but we only need the first 3 to identify
555
                // which tables to select
556
                $field_parts = explode(':', $field_name . '::');
557
558
                if ($field_parts[0] === 'FAMC') {
559
                    // Parent name - FAMC:[HUSB|WIFE]:NAME:[GIVN|SURN]
560
                    if ($field_parts[1] === 'HUSB') {
561
                        $father_name = true;
562
                    } else {
563
                        $mother_name = true;
564
                    }
565
                } elseif ($field_parts[0] === 'NAME') {
566
                    // Individual name - NAME:[GIVN|SURN]
567
                    $indi_name = true;
568
                } elseif ($field_parts[0] === 'FAMS') {
569
                    // Family facts - FAMS:NOTE or FAMS:[FACT]:[DATE|PLAC]
570
                    $spouse_family = true;
571
                    if ($field_parts[2] === 'DATE') {
572
                        $fam_dates[] = $field_parts[1];
573
                    } elseif ($field_parts[2] === 'PLAC') {
574
                        $fam_plac = true;
575
                    }
576
                } else {
577
                    // Individual facts - [FACT] or [FACT]:[DATE|PLAC]
578
                    if ($field_parts[1] === 'DATE') {
579
                        $indi_dates[] = $field_parts[0];
580
                    } elseif ($field_parts[1] === 'PLAC') {
581
                        $indi_plac = true;
582
                    }
583
                }
584
            }
585
        }
586
587
        if ($father_name || $mother_name) {
588
            $query->join('link AS l1', static function (JoinClause $join): void {
589
                $join
590
                    ->on('l1.l_file', '=', 'individuals.i_file')
591
                    ->on('l1.l_from', '=', 'individuals.i_id')
592
                    ->where('l1.l_type', '=', 'FAMC');
593
            });
594
595
            if ($father_name) {
596
                $query->join('link AS l2', static function (JoinClause $join): void {
597
                    $join
598
                        ->on('l2.l_file', '=', 'l1.l_file')
599
                        ->on('l2.l_from', '=', 'l1.l_to')
600
                        ->where('l2.l_type', '=', 'HUSB');
601
                });
602
                $query->join('name AS father_name', static function (JoinClause $join): void {
603
                    $join
604
                        ->on('father_name.n_file', '=', 'l2.l_file')
605
                        ->on('father_name.n_id', '=', 'l2.l_to');
606
                });
607
            }
608
609
            if ($mother_name) {
610
                $query->join('link AS l3', static function (JoinClause $join): void {
611
                    $join
612
                        ->on('l3.l_file', '=', 'l1.l_file')
613
                        ->on('l3.l_from', '=', 'l1.l_to')
614
                        ->where('l3.l_type', '=', 'WIFE');
615
                });
616
                $query->join('name AS mother_name', static function (JoinClause $join): void {
617
                    $join
618
                        ->on('mother_name.n_file', '=', 'l3.l_file')
619
                        ->on('mother_name.n_id', '=', 'l3.l_to');
620
                });
621
            }
622
        }
623
624
        if ($spouse_family) {
625
            $query->join('link AS l4', static function (JoinClause $join): void {
626
                $join
627
                    ->on('l4.l_file', '=', 'individuals.i_file')
628
                    ->on('l4.l_from', '=', 'individuals.i_id')
629
                    ->where('l4.l_type', '=', 'FAMS');
630
            });
631
            $query->join('families AS spouse_families', static function (JoinClause $join): void {
632
                $join
633
                    ->on('spouse_families.f_file', '=', 'l4.l_file')
634
                    ->on('spouse_families.f_id', '=', 'l4.l_to');
635
            });
636
        }
637
638
        if ($indi_name) {
639
            $query->join('name AS individual_name', static function (JoinClause $join): void {
640
                $join
641
                    ->on('individual_name.n_file', '=', 'individuals.i_file')
642
                    ->on('individual_name.n_id', '=', 'individuals.i_id');
643
            });
644
        }
645
646
        foreach (array_unique($indi_dates) as $indi_date) {
647
            $query->join('dates AS date_' . $indi_date, static function (JoinClause $join) use ($indi_date): void {
648
                $join
649
                    ->on('date_' . $indi_date . '.d_file', '=', 'individuals.i_file')
650
                    ->on('date_' . $indi_date . '.d_gid', '=', 'individuals.i_id');
651
            });
652
        }
653
654
        foreach (array_unique($fam_dates) as $fam_date) {
655
            $query->join('dates AS date_' . $fam_date, static function (JoinClause $join) use ($fam_date): void {
656
                $join
657
                    ->on('date_' . $fam_date . '.d_file', '=', 'spouse_families.f_file')
658
                    ->on('date_' . $fam_date . '.d_gid', '=', 'spouse_families.f_id');
659
            });
660
        }
661
662
        if ($indi_plac) {
663
            $query->join('placelinks AS individual_placelinks', static function (JoinClause $join): void {
664
                $join
665
                    ->on('individual_placelinks.pl_file', '=', 'individuals.i_file')
666
                    ->on('individual_placelinks.pl_gid', '=', 'individuals.i_id');
667
            });
668
            $query->join('places AS individual_places', static function (JoinClause $join): void {
669
                $join
670
                    ->on('individual_places.p_file', '=', 'individual_placelinks.pl_file')
671
                    ->on('individual_places.p_id', '=', 'individual_placelinks.pl_p_id');
672
            });
673
        }
674
675
        if ($fam_plac) {
676
            $query->join('placelinks AS familyl_placelinks', static function (JoinClause $join): void {
677
                $join
678
                    ->on('familyl_placelinks.pl_file', '=', 'individuals.i_file')
679
                    ->on('familyl_placelinks.pl_gid', '=', 'individuals.i_id');
680
            });
681
            $query->join('places AS family_places', static function (JoinClause $join): void {
682
                $join
683
                    ->on('family_places.p_file', '=', 'familyl_placelinks.pl_file')
684
                    ->on('family_places.p_id', '=', 'familyl_placelinks.pl_p_id');
685
            });
686
        }
687
688
        foreach ($fields as $field_name => $field_value) {
689
            $parts = explode(':', $field_name . ':::');
690
            if ($parts[0] === 'NAME') {
691
                // NAME:*
692
                switch ($parts[1]) {
693
                    case 'GIVN':
694
                        switch ($modifiers[$field_name]) {
695
                            case 'EXACT':
696
                                $query->where('individual_name.n_givn', '=', $field_value);
697
                                break;
698
                            case 'BEGINS':
699
                                $query->where('individual_name.n_givn', 'LIKE', $field_value . '%');
700
                                break;
701
                            case 'CONTAINS':
702
                                $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
703
                                break;
704
                            case 'SDX_STD':
705
                                $sdx = Soundex::russell($field_value);
706
                                if ($sdx !== '') {
707
                                    $this->wherePhonetic($query, 'individual_name.n_soundex_givn_std', $sdx);
708
                                } else {
709
                                    // No phonetic content? Use a substring match
710
                                    $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
711
                                }
712
                                break;
713
                            case 'SDX': // SDX uses DM by default.
714
                            case 'SDX_DM':
715
                                $sdx = Soundex::daitchMokotoff($field_value);
716
                                if ($sdx !== '') {
717
                                    $this->wherePhonetic($query, 'individual_name.n_soundex_givn_dm', $sdx);
718
                                } else {
719
                                    // No phonetic content? Use a substring match
720
                                    $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
721
                                }
722
                                break;
723
                        }
724
                        unset($fields[$field_name]);
725
                        break;
726
                    case 'SURN':
727
                        switch ($modifiers[$field_name]) {
728
                            case 'EXACT':
729
                                $query->where(function (Builder $query) use ($field_value): void {
730
                                    $query
731
                                        ->where('individual_name.n_surn', '=', $field_value)
732
                                        ->orWhere('individual_name.n_surname', '=', $field_value);
733
                                });
734
                                break;
735
                            case 'BEGINS':
736
                                $query->where(function (Builder $query) use ($field_value): void {
737
                                    $query
738
                                        ->where('individual_name.n_surn', 'LIKE', $field_value . '%')
739
                                        ->orWhere('individual_name.n_surname', 'LIKE', $field_value . '%');
740
                                });
741
                                break;
742
                            case 'CONTAINS':
743
                                $query->where(function (Builder $query) use ($field_value): void {
744
                                    $query
745
                                        ->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%')
746
                                        ->orWhere('individual_name.n_surname', 'LIKE', '%' . $field_value . '%');
747
                                });
748
                                break;
749
                            case 'SDX_STD':
750
                                $sdx = Soundex::russell($field_value);
751
                                if ($sdx !== '') {
752
                                    $this->wherePhonetic($query, 'individual_name.n_soundex_surn_std', $sdx);
753
                                } else {
754
                                    // No phonetic content? Use a substring match
755
                                    $query->where(function (Builder $query) use ($field_value): void {
756
                                        $query
757
                                            ->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%')
758
                                            ->orWhere('individual_name.n_surname', 'LIKE', '%' . $field_value . '%');
759
                                    });
760
                                }
761
                                break;
762
                            case 'SDX': // SDX uses DM by default.
763
                            case 'SDX_DM':
764
                                $sdx = Soundex::daitchMokotoff($field_value);
765
                                if ($sdx !== '') {
766
                                    $this->wherePhonetic($query, 'individual_name.n_soundex_surn_dm', $sdx);
767
                                } else {
768
                                    // No phonetic content? Use a substring match
769
                                    $query->where(function (Builder $query) use ($field_value): void {
770
                                        $query
771
                                            ->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%')
772
                                            ->orWhere('individual_name.n_surname', 'LIKE', '%' . $field_value . '%');
773
                                    });
774
                                }
775
                                break;
776
                        }
777
                        unset($fields[$field_name]);
778
                        break;
779
                    case 'NICK':
780
                    case '_MARNM':
781
                    case '_HEB':
782
                    case '_AKA':
783
                        $like = "%\n1 " . $parts[0] . "%\n2 " . $parts[1] . ' %' . preg_quote($field_value, '/') . '%';
784
                        $query->where('individuals.i_gedcom', 'LIKE', $like);
785
                        break;
786
                }
787
            } elseif ($parts[1] === 'DATE') {
788
                // *:DATE
789
                $date = new Date($field_value);
790
                if ($date->isOK()) {
791
                    $delta = 365 * ($modifiers[$field_name] ?? 0);
792
                    $query
793
                        ->where('date_' . $parts[0] . '.d_fact', '=', $parts[0])
794
                        ->where('date_' . $parts[0] . '.d_julianday1', '>=', $date->minimumJulianDay() - $delta)
795
                        ->where('date_' . $parts[0] . '.d_julianday2', '<=', $date->maximumJulianDay() + $delta);
796
                }
797
                unset($fields[$field_name]);
798
            } elseif ($parts[0] === 'FAMS' && $parts[2] === 'DATE') {
799
                // FAMS:*:DATE
800
                $date = new Date($field_value);
801
                if ($date->isOK()) {
802
                    $delta = 365 * $modifiers[$field_name];
803
                    $query
804
                        ->where('date_' . $parts[1] . '.d_fact', '=', $parts[1])
805
                        ->where('date_' . $parts[1] . '.d_julianday1', '>=', $date->minimumJulianDay() - $delta)
806
                        ->where('date_' . $parts[1] . '.d_julianday2', '<=', $date->maximumJulianDay() + $delta);
807
                }
808
                unset($fields[$field_name]);
809
            } elseif ($parts[1] === 'PLAC') {
810
                // *:PLAC
811
                // SQL can only link a place to a person/family, not to an event.
812
                $query->where('individual_places.p_place', 'LIKE', '%' . $field_value . '%');
813
            } elseif ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') {
814
                // FAMS:*:PLAC
815
                // SQL can only link a place to a person/family, not to an event.
816
                $query->where('family_places.p_place', 'LIKE', '%' . $field_value . '%');
817
            } elseif ($parts[0] === 'FAMC' && $parts[2] === 'NAME') {
818
                $table = $parts[1] === 'HUSB' ? 'father_name' : 'mother_name';
819
                // NAME:*
820
                switch ($parts[3]) {
821
                    case 'GIVN':
822
                        switch ($modifiers[$field_name]) {
823
                            case 'EXACT':
824
                                $query->where($table . '.n_givn', '=', $field_value);
825
                                break;
826
                            case 'BEGINS':
827
                                $query->where($table . '.n_givn', 'LIKE', $field_value . '%');
828
                                break;
829
                            case 'CONTAINS':
830
                                $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
831
                                break;
832
                            case 'SDX_STD':
833
                                $sdx = Soundex::russell($field_value);
834
                                if ($sdx !== '') {
835
                                    $this->wherePhonetic($query, $table . '.n_soundex_givn_std', $sdx);
836
                                } else {
837
                                    // No phonetic content? Use a substring match
838
                                    $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
839
                                }
840
                                break;
841
                            case 'SDX': // SDX uses DM by default.
842
                            case 'SDX_DM':
843
                                $sdx = Soundex::daitchMokotoff($field_value);
844
                                if ($sdx !== '') {
845
                                    $this->wherePhonetic($query, $table . '.n_soundex_givn_dm', $sdx);
846
                                } else {
847
                                    // No phonetic content? Use a substring match
848
                                    $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
849
                                }
850
                                break;
851
                        }
852
                        break;
853
                    case 'SURN':
854
                        switch ($modifiers[$field_name]) {
855
                            case 'EXACT':
856
                                $query->where($table . '.n_surn', '=', $field_value);
857
                                break;
858
                            case 'BEGINS':
859
                                $query->where($table . '.n_surn', 'LIKE', $field_value . '%');
860
                                break;
861
                            case 'CONTAINS':
862
                                $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
863
                                break;
864
                            case 'SDX_STD':
865
                                $sdx = Soundex::russell($field_value);
866
                                if ($sdx !== '') {
867
                                    $this->wherePhonetic($query, $table . '.n_soundex_surn_std', $sdx);
868
                                } else {
869
                                    // No phonetic content? Use a substring match
870
                                    $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
871
                                }
872
                                break;
873
                            case 'SDX': // SDX uses DM by default.
874
                            case 'SDX_DM':
875
                                $sdx = Soundex::daitchMokotoff($field_value);
876
                                if ($sdx !== '') {
877
                                    $this->wherePhonetic($query, $table . '.n_soundex_surn_dm', $sdx);
878
                                } else {
879
                                    // No phonetic content? Use a substring match
880
                                    $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
881
                                }
882
                                break;
883
                        }
884
                        break;
885
                }
886
                unset($fields[$field_name]);
887
            } elseif ($parts[0] === 'FAMS') {
888
                // e.g. searches for occupation, religion, note, etc.
889
                // Initial matching only.  Need PHP to apply filter.
890
                $query->where('spouse_families.f_gedcom', 'LIKE', "%\n1 " . $parts[1] . ' %' . $field_value . '%');
891
            } elseif ($parts[1] === 'TYPE') {
892
                // e.g. FACT:TYPE or EVEN:TYPE
893
                // Initial matching only.  Need PHP to apply filter.
894
                $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . "%\n2 TYPE %" . $field_value . '%');
895
            } else {
896
                // e.g. searches for occupation, religion, note, etc.
897
                // Initial matching only.  Need PHP to apply filter.
898
                $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%' . $parts[1] . '%' . $field_value . '%');
899
            }
900
        }
901
902
        return $query
903
            ->get()
904
            ->each($this->rowLimiter())
905
            ->map($this->individualRowMapper())
906
            ->filter(GedcomRecord::accessFilter())
907
            ->filter(static function (Individual $individual) use ($fields): bool {
908
                // Check for searches which were only partially matched by SQL
909
                foreach ($fields as $field_name => $field_value) {
910
                    $parts = explode(':', $field_name . '::::');
911
912
                    // NAME:*
913
                    if ($parts[0] === 'NAME') {
914
                        $regex = '/\n1 NAME.*(?:\n2.*)*\n2 ' . $parts[1] . ' .*' . preg_quote($field_value, '/') . '/i';
915
916
                        if (preg_match($regex, $individual->gedcom())) {
917
                            continue;
918
                        }
919
920
                        return false;
921
                    }
922
923
                    $regex = '/' . preg_quote($field_value, '/') . '/i';
924
925
                    // *:PLAC
926
                    if ($parts[1] === 'PLAC') {
927
                        foreach ($individual->facts([$parts[0]]) as $fact) {
928
                            if (preg_match($regex, $fact->place()->gedcomName())) {
929
                                continue 2;
930
                            }
931
                        }
932
                        return false;
933
                    }
934
935
                    // FAMS:*:PLAC
936
                    if ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') {
937
                        foreach ($individual->spouseFamilies() as $family) {
938
                            foreach ($family->facts([$parts[1]]) as $fact) {
939
                                if (preg_match($regex, $fact->place()->gedcomName())) {
940
                                    continue 3;
941
                                }
942
                            }
943
                        }
944
                        return false;
945
                    }
946
947
                    // e.g. searches for occupation, religion, note, etc.
948
                    if ($parts[0] === 'FAMS') {
949
                        foreach ($individual->spouseFamilies() as $family) {
950
                            foreach ($family->facts([$parts[1]]) as $fact) {
951
                                if (preg_match($regex, $fact->value())) {
952
                                    continue 3;
953
                                }
954
                            }
955
                        }
956
                        return false;
957
                    }
958
959
                    // e.g. FACT:TYPE or EVEN:TYPE
960
                    if ($parts[1] === 'TYPE' || $parts[1] === '_WT_USER') {
961
                        foreach ($individual->facts([$parts[0]]) as $fact) {
962
                            if (preg_match($regex, $fact->attribute($parts[1]))) {
963
                                continue 2;
964
                            }
965
                        }
966
967
                        return false;
968
                    }
969
                }
970
971
                return true;
972
            });
973
    }
974
975
    /**
976
     * @param string $soundex
977
     * @param string $lastname
978
     * @param string $firstname
979
     * @param string $place
980
     * @param Tree[] $search_trees
981
     *
982
     * @return Collection<Individual>
983
     */
984
    public function searchIndividualsPhonetic(string $soundex, string $lastname, string $firstname, string $place, array $search_trees): Collection
985
    {
986
        switch ($soundex) {
987
            default:
988
            case 'Russell':
989
                $givn_sdx   = Soundex::russell($firstname);
990
                $surn_sdx   = Soundex::russell($lastname);
991
                $plac_sdx   = Soundex::russell($place);
992
                $givn_field = 'n_soundex_givn_std';
993
                $surn_field = 'n_soundex_surn_std';
994
                $plac_field = 'p_std_soundex';
995
                break;
996
            case 'DaitchM':
997
                $givn_sdx   = Soundex::daitchMokotoff($firstname);
998
                $surn_sdx   = Soundex::daitchMokotoff($lastname);
999
                $plac_sdx   = Soundex::daitchMokotoff($place);
1000
                $givn_field = 'n_soundex_givn_dm';
1001
                $surn_field = 'n_soundex_surn_dm';
1002
                $plac_field = 'p_dm_soundex';
1003
                break;
1004
        }
1005
1006
        // Nothing to search for? Return nothing.
1007
        if ($givn_sdx === '' && $surn_sdx === '' && $plac_sdx === '') {
1008
            return new Collection();
1009
        }
1010
1011
        $query = DB::table('individuals')
1012
            ->select(['individuals.*'])
1013
            ->distinct();
1014
1015
        $this->whereTrees($query, 'i_file', $search_trees);
1016
1017
        if ($plac_sdx !== '') {
1018
            $query->join('placelinks', static function (JoinClause $join): void {
1019
                $join
1020
                    ->on('placelinks.pl_file', '=', 'individuals.i_file')
1021
                    ->on('placelinks.pl_gid', '=', 'individuals.i_id');
1022
            });
1023
            $query->join('places', static function (JoinClause $join): void {
1024
                $join
1025
                    ->on('places.p_file', '=', 'placelinks.pl_file')
1026
                    ->on('places.p_id', '=', 'placelinks.pl_p_id');
1027
            });
1028
1029
            $this->wherePhonetic($query, $plac_field, $plac_sdx);
1030
        }
1031
1032
        if ($givn_sdx !== '' || $surn_sdx !== '') {
1033
            $query->join('name', static function (JoinClause $join): void {
1034
                $join
1035
                    ->on('name.n_file', '=', 'individuals.i_file')
1036
                    ->on('name.n_id', '=', 'individuals.i_id');
1037
            });
1038
1039
            $this->wherePhonetic($query, $givn_field, $givn_sdx);
1040
            $this->wherePhonetic($query, $surn_field, $surn_sdx);
1041
        }
1042
1043
        return $query
1044
            ->get()
1045
            ->each($this->rowLimiter())
1046
            ->map($this->individualRowMapper())
1047
            ->filter(GedcomRecord::accessFilter());
1048
    }
1049
1050
    /**
1051
     * Paginate a search query.
1052
     *
1053
     * @param Builder $query      Searches the database for the desired records.
1054
     * @param Closure $row_mapper Converts a row from the query into a record.
1055
     * @param Closure $row_filter
1056
     * @param int     $offset     Skip this many rows.
1057
     * @param int     $limit      Take this many rows.
1058
     *
1059
     * @return Collection<mixed>
1060
     */
1061
    private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection
1062
    {
1063
        $collection = new Collection();
1064
1065
        foreach ($query->cursor() as $row) {
1066
            $record = $row_mapper($row);
1067
            // searchIndividualNames() and searchFamilyNames() can return duplicate rows,
1068
            // where individuals have multiple names - and we need to sort results by name.
1069
            if ($collection->containsStrict($record)) {
1070
                continue;
1071
            }
1072
            // If the object has a method "canShow()", then use it to filter for privacy.
1073
            if ($row_filter($record)) {
1074
                if ($offset > 0) {
1075
                    $offset--;
1076
                } else {
1077
                    if ($limit > 0) {
1078
                        $collection->push($record);
1079
                    }
1080
1081
                    $limit--;
1082
1083
                    if ($limit === 0) {
1084
                        break;
1085
                    }
1086
                }
1087
            }
1088
        }
1089
1090
1091
        return $collection;
1092
    }
1093
1094
    /**
1095
     * Apply search filters to a SQL query column.  Apply collation rules to MySQL.
1096
     *
1097
     * @param Builder           $query
1098
     * @param Expression|string $field
1099
     * @param string[]          $search_terms
1100
     */
1101
    private function whereSearch(Builder $query, $field, array $search_terms): void
1102
    {
1103
        if ($field instanceof Expression) {
1104
            $field = $field->getValue();
1105
        }
1106
1107
        foreach ($search_terms as $search_term) {
1108
            $query->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_') . '%');
1109
        }
1110
    }
1111
1112
    /**
1113
     * Apply soundex search filters to a SQL query column.
1114
     *
1115
     * @param Builder           $query
1116
     * @param Expression|string $field
1117
     * @param string            $soundex
1118
     */
1119
    private function wherePhonetic(Builder $query, $field, string $soundex): void
1120
    {
1121
        if ($soundex !== '') {
1122
            $query->where(static function (Builder $query) use ($soundex, $field): void {
1123
                foreach (explode(':', $soundex) as $sdx) {
1124
                    $query->orWhere($field, 'LIKE', '%' . $sdx . '%');
1125
                }
1126
            });
1127
        }
1128
    }
1129
1130
    /**
1131
     * @param Builder $query
1132
     * @param string  $tree_id_field
1133
     * @param Tree[]  $trees
1134
     */
1135
    private function whereTrees(Builder $query, string $tree_id_field, array $trees): void
1136
    {
1137
        $tree_ids = array_map(static function (Tree $tree): int {
1138
            return $tree->id();
1139
        }, $trees);
1140
1141
        $query->whereIn($tree_id_field, $tree_ids);
1142
    }
1143
1144
    /**
1145
     * Find the media object that uses a particular media file.
1146
     *
1147
     * @param string $file
1148
     *
1149
     * @return Media[]
1150
     */
1151
    public function findMediaObjectsForMediaFile(string $file): array
1152
    {
1153
        return DB::table('media')
1154
            ->join('media_file', static function (JoinClause $join): void {
1155
                $join
1156
                    ->on('media_file.m_file', '=', 'media.m_file')
1157
                    ->on('media_file.m_id', '=', 'media.m_id');
1158
            })
1159
            ->join('gedcom_setting', 'media.m_file', '=', 'gedcom_setting.gedcom_id')
1160
            ->where(new Expression('setting_value || multimedia_file_refn'), '=', $file)
1161
            ->select(['media.*'])
1162
            ->distinct()
1163
            ->get()
1164
            ->map($this->mediaRowMapper())
1165
            ->all();
1166
    }
1167
1168
    /**
1169
     * A closure to filter records by privacy-filtered GEDCOM data.
1170
     *
1171
     * @param array<string> $search_terms
1172
     *
1173
     * @return Closure
1174
     */
1175
    private function rawGedcomFilter(array $search_terms): Closure
1176
    {
1177
        return static function (GedcomRecord $record) use ($search_terms): bool {
1178
            // Ignore non-genealogy fields
1179
            $gedcom = preg_replace('/\n\d (?:_UID|_WT_USER) .*/', '', $record->gedcom());
1180
1181
            // Ignore matches in links
1182
            $gedcom = preg_replace('/\n\d ' . Gedcom::REGEX_TAG . '( @' . Gedcom::REGEX_XREF . '@)?/', '', $gedcom);
1183
1184
            // Re-apply the filtering
1185
            foreach ($search_terms as $search_term) {
1186
                if (mb_stripos($gedcom, $search_term) === false) {
1187
                    return false;
1188
                }
1189
            }
1190
1191
            return true;
1192
        };
1193
    }
1194
1195
    /**
1196
     * Searching for short or common text can give more results than the system can process.
1197
     *
1198
     * @param int $limit
1199
     *
1200
     * @return Closure
1201
     */
1202
    private function rowLimiter(int $limit = self::MAX_SEARCH_RESULTS): Closure
1203
    {
1204
        return static function () use ($limit): void {
1205
            static $n = 0;
1206
1207
            if (++$n > $limit) {
1208
                $message = I18N::translate('The search returned too many results.');
1209
1210
                throw new HttpServiceUnavailableException($message);
1211
            }
1212
        };
1213
    }
1214
1215
    /**
1216
     * Convert a row from any tree in the families table into a family object.
1217
     *
1218
     * @return Closure
1219
     */
1220
    private function familyRowMapper(): Closure
1221
    {
1222
        return function (stdClass $row): Family {
1223
            $tree = $this->tree_service->find((int) $row->f_file);
1224
1225
            return Registry::familyFactory()->mapper($tree)($row);
1226
        };
1227
    }
1228
1229
    /**
1230
     * Convert a row from any tree in the individuals table into an individual object.
1231
     *
1232
     * @return Closure
1233
     */
1234
    private function individualRowMapper(): Closure
1235
    {
1236
        return function (stdClass $row): Individual {
1237
            $tree = $this->tree_service->find((int) $row->i_file);
1238
1239
            return Registry::individualFactory()->mapper($tree)($row);
1240
        };
1241
    }
1242
1243
    /**
1244
     * Convert a row from any tree in the media table into a location object.
1245
     *
1246
     * @return Closure
1247
     */
1248
    private function locationRowMapper(): Closure
1249
    {
1250
        return function (stdClass $row): Location {
1251
            $tree = $this->tree_service->find((int) $row->o_file);
1252
1253
            return Registry::locationFactory()->mapper($tree)($row);
1254
        };
1255
    }
1256
1257
    /**
1258
     * Convert a row from any tree in the media table into an media object.
1259
     *
1260
     * @return Closure
1261
     */
1262
    private function mediaRowMapper(): Closure
1263
    {
1264
        return function (stdClass $row): Media {
1265
            $tree = $this->tree_service->find((int) $row->m_file);
1266
1267
            return Registry::mediaFactory()->mapper($tree)($row);
1268
        };
1269
    }
1270
1271
    /**
1272
     * Convert a row from any tree in the other table into a note object.
1273
     *
1274
     * @return Closure
1275
     */
1276
    private function noteRowMapper(): Closure
1277
    {
1278
        return function (stdClass $row): Note {
1279
            $tree = $this->tree_service->find((int) $row->o_file);
1280
1281
            return Registry::noteFactory()->mapper($tree)($row);
1282
        };
1283
    }
1284
1285
    /**
1286
     * Convert a row from any tree in the other table into a repository object.
1287
     *
1288
     * @return Closure
1289
     */
1290
    private function repositoryRowMapper(): Closure
1291
    {
1292
        return function (stdClass $row): Repository {
1293
            $tree = $this->tree_service->find((int) $row->o_file);
1294
1295
            return Registry::repositoryFactory()->mapper($tree)($row);
1296
        };
1297
    }
1298
1299
    /**
1300
     * Convert a row from any tree in the sources table into a source object.
1301
     *
1302
     * @return Closure
1303
     */
1304
    private function sourceRowMapper(): Closure
1305
    {
1306
        return function (stdClass $row): Source {
1307
            $tree = $this->tree_service->find((int) $row->s_file);
1308
1309
            return Registry::sourceFactory()->mapper($tree)($row);
1310
        };
1311
    }
1312
1313
    /**
1314
     * Convert a row from any tree in the other table into a submission object.
1315
     *
1316
     * @return Closure
1317
     */
1318
    private function submissionRowMapper(): Closure
1319
    {
1320
        return function (stdClass $row): Submission {
1321
            $tree = $this->tree_service->find((int) $row->o_file);
1322
1323
            return Registry::submissionFactory()->mapper($tree)($row);
1324
        };
1325
    }
1326
1327
    /**
1328
     * Convert a row from any tree in the other table into a submitter object.
1329
     *
1330
     * @return Closure
1331
     */
1332
    private function submitterRowMapper(): Closure
1333
    {
1334
        return function (stdClass $row): Submitter {
1335
            $tree = $this->tree_service->find((int) $row->o_file);
1336
1337
            return Registry::submitterFactory()->mapper($tree)($row);
1338
        };
1339
    }
1340
}
1341