xref: /webtrees/app/Services/SearchService.php (revision 32cd280058cdb3b5f1807bfaaeffb8d79d5ebf3f)
1*32cd2800SGreg Roach<?php
2*32cd2800SGreg Roach/**
3*32cd2800SGreg Roach * webtrees: online genealogy
4*32cd2800SGreg Roach * Copyright (C) 2019 webtrees development team
5*32cd2800SGreg Roach * This program is free software: you can redistribute it and/or modify
6*32cd2800SGreg Roach * it under the terms of the GNU General Public License as published by
7*32cd2800SGreg Roach * the Free Software Foundation, either version 3 of the License, or
8*32cd2800SGreg Roach * (at your option) any later version.
9*32cd2800SGreg Roach * This program is distributed in the hope that it will be useful,
10*32cd2800SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
11*32cd2800SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12*32cd2800SGreg Roach * GNU General Public License for more details.
13*32cd2800SGreg Roach * You should have received a copy of the GNU General Public License
14*32cd2800SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
15*32cd2800SGreg Roach */
16*32cd2800SGreg Roachdeclare(strict_types=1);
17*32cd2800SGreg Roach
18*32cd2800SGreg Roachnamespace Fisharebest\Webtrees\Services;
19*32cd2800SGreg Roach
20*32cd2800SGreg Roachuse Closure;
21*32cd2800SGreg Roachuse Fisharebest\Webtrees\Family;
22*32cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
23*32cd2800SGreg Roachuse Fisharebest\Webtrees\Individual;
24*32cd2800SGreg Roachuse Fisharebest\Webtrees\Media;
25*32cd2800SGreg Roachuse Fisharebest\Webtrees\Note;
26*32cd2800SGreg Roachuse Fisharebest\Webtrees\Repository;
27*32cd2800SGreg Roachuse Fisharebest\Webtrees\Source;
28*32cd2800SGreg Roachuse Fisharebest\Webtrees\Tree;
29*32cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
30*32cd2800SGreg Roachuse Illuminate\Database\Query\Builder;
31*32cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause;
32*32cd2800SGreg Roachuse Illuminate\Support\Collection;
33*32cd2800SGreg Roachuse stdClass;
34*32cd2800SGreg Roach
35*32cd2800SGreg Roach/**
36*32cd2800SGreg Roach * Search trees for genealogy records.
37*32cd2800SGreg Roach */
38*32cd2800SGreg Roachclass SearchService
39*32cd2800SGreg Roach{
40*32cd2800SGreg Roach    /**
41*32cd2800SGreg Roach     * Search for families by name.
42*32cd2800SGreg Roach     *
43*32cd2800SGreg Roach     * @param Tree   $tree
44*32cd2800SGreg Roach     * @param string $search
45*32cd2800SGreg Roach     * @param int    $offset
46*32cd2800SGreg Roach     * @param int    $limit
47*32cd2800SGreg Roach     *
48*32cd2800SGreg Roach     * @return Collection|Family[]
49*32cd2800SGreg Roach     */
50*32cd2800SGreg Roach    public function searchFamiliesByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
51*32cd2800SGreg Roach    {
52*32cd2800SGreg Roach        $prefix = DB::connection()->getTablePrefix();
53*32cd2800SGreg Roach
54*32cd2800SGreg Roach        $query = DB::table('families')
55*32cd2800SGreg Roach            ->where('f_file', '=', $tree->id())
56*32cd2800SGreg Roach            ->join('name AS husb_name', function (JoinClause $join) use ($search): void {
57*32cd2800SGreg Roach                $join
58*32cd2800SGreg Roach                    ->on('husb_name.n_file', '=', 'families.f_file')
59*32cd2800SGreg Roach                    ->on('husb_name.n_id', '=', 'families.f_husb')
60*32cd2800SGreg Roach                    ->where('husb_name.n_type', '<>', '_MARNM');
61*32cd2800SGreg Roach            })
62*32cd2800SGreg Roach            ->join('name AS wife_name', function (JoinClause $join) use ($search): void {
63*32cd2800SGreg Roach                $join
64*32cd2800SGreg Roach                    ->on('wife_name.n_file', '=', 'families.f_file')
65*32cd2800SGreg Roach                    ->on('wife_name.n_id', '=', 'families.f_wife')
66*32cd2800SGreg Roach                    ->where('wife_name.n_type', '<>', '_MARNM');
67*32cd2800SGreg Roach            })
68*32cd2800SGreg Roach            ->whereContains(DB::raw("CONCAT(" . $prefix . "husb_name.n_full, ' ', " . $prefix . "wife_name.n_full)"), $search)
69*32cd2800SGreg Roach            ->orderBy('husb_name.n_sort')
70*32cd2800SGreg Roach            ->orderBy('wife_name.n_sort')
71*32cd2800SGreg Roach            ->select(['families.f_id', 'families.f_gedcom', 'husb_name.n_sort', 'wife_name.n_sort'])
72*32cd2800SGreg Roach            ->distinct();
73*32cd2800SGreg Roach
74*32cd2800SGreg Roach        $row_mapper = $this->familyRowMapper($tree);
75*32cd2800SGreg Roach
76*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
77*32cd2800SGreg Roach    }
78*32cd2800SGreg Roach
79*32cd2800SGreg Roach    /**
80*32cd2800SGreg Roach     * Search for individuals by name.
81*32cd2800SGreg Roach     *
82*32cd2800SGreg Roach     * @param Tree   $tree
83*32cd2800SGreg Roach     * @param string $search
84*32cd2800SGreg Roach     * @param int    $offset
85*32cd2800SGreg Roach     * @param int    $limit
86*32cd2800SGreg Roach     *
87*32cd2800SGreg Roach     * @return Collection|Individual[]
88*32cd2800SGreg Roach     */
89*32cd2800SGreg Roach    public function searchIndividualsByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
90*32cd2800SGreg Roach    {
91*32cd2800SGreg Roach        $query = DB::table('individuals')
92*32cd2800SGreg Roach            ->where('i_file', '=', $tree->id())
93*32cd2800SGreg Roach            ->join('name', function (JoinClause $join) use ($search): void {
94*32cd2800SGreg Roach                $join
95*32cd2800SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
96*32cd2800SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id')
97*32cd2800SGreg Roach                    ->whereContains('n_full', $search);
98*32cd2800SGreg Roach            })
99*32cd2800SGreg Roach            ->select(['individuals.i_id', 'individuals.i_gedcom', 'n_sort'])
100*32cd2800SGreg Roach            ->distinct();
101*32cd2800SGreg Roach
102*32cd2800SGreg Roach        $row_mapper = $this->individualRowMapper($tree);
103*32cd2800SGreg Roach
104*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
105*32cd2800SGreg Roach    }
106*32cd2800SGreg Roach
107*32cd2800SGreg Roach    /**
108*32cd2800SGreg Roach     * Search for media objects.
109*32cd2800SGreg Roach     *
110*32cd2800SGreg Roach     * @param Tree   $tree
111*32cd2800SGreg Roach     * @param string $search
112*32cd2800SGreg Roach     * @param int    $offset
113*32cd2800SGreg Roach     * @param int    $limit
114*32cd2800SGreg Roach     *
115*32cd2800SGreg Roach     * @return Collection|Media[]
116*32cd2800SGreg Roach     */
117*32cd2800SGreg Roach    public function searchMedia(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
118*32cd2800SGreg Roach    {
119*32cd2800SGreg Roach        $query = DB::table('media')
120*32cd2800SGreg Roach            ->where('media.m_file', '=', $tree->id())
121*32cd2800SGreg Roach            ->join('media_file', function (JoinClause $join) use ($search): void {
122*32cd2800SGreg Roach                $join
123*32cd2800SGreg Roach                    ->on('media_file.m_file', '=', 'media.m_file')
124*32cd2800SGreg Roach                    ->on('media_file.m_id', '=', 'media.m_id');
125*32cd2800SGreg Roach            })
126*32cd2800SGreg Roach            ->where(function (Builder $query) use ($search): void {
127*32cd2800SGreg Roach                $query
128*32cd2800SGreg Roach                    ->whereContains('multimedia_file_refn', $search)
129*32cd2800SGreg Roach                    ->whereContains('descriptive_title', $search, 'or');
130*32cd2800SGreg Roach            })
131*32cd2800SGreg Roach            ->select(['media.m_id', 'media.m_gedcom'])
132*32cd2800SGreg Roach            ->distinct();
133*32cd2800SGreg Roach
134*32cd2800SGreg Roach        $row_mapper = $this->mediaRowMapper($tree);
135*32cd2800SGreg Roach
136*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
137*32cd2800SGreg Roach    }
138*32cd2800SGreg Roach
139*32cd2800SGreg Roach    /**
140*32cd2800SGreg Roach     * Search for notes.
141*32cd2800SGreg Roach     *
142*32cd2800SGreg Roach     * @param Tree   $tree
143*32cd2800SGreg Roach     * @param string $search
144*32cd2800SGreg Roach     * @param int    $offset
145*32cd2800SGreg Roach     * @param int    $limit
146*32cd2800SGreg Roach     *
147*32cd2800SGreg Roach     * @return Collection|Note[]
148*32cd2800SGreg Roach     */
149*32cd2800SGreg Roach    public function searchNotes(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
150*32cd2800SGreg Roach    {
151*32cd2800SGreg Roach        $query = DB::table('other')
152*32cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
153*32cd2800SGreg Roach            ->where('o_type', '=', 'NOTE')
154*32cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
155*32cd2800SGreg Roach            ->orderBy('o_id')
156*32cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
157*32cd2800SGreg Roach
158*32cd2800SGreg Roach        $row_mapper = $this->noteRowMapper($tree);
159*32cd2800SGreg Roach
160*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
161*32cd2800SGreg Roach    }
162*32cd2800SGreg Roach
163*32cd2800SGreg Roach    /**
164*32cd2800SGreg Roach     * Search for repositories.
165*32cd2800SGreg Roach     *
166*32cd2800SGreg Roach     * @param Tree   $tree
167*32cd2800SGreg Roach     * @param string $search
168*32cd2800SGreg Roach     * @param int    $offset
169*32cd2800SGreg Roach     * @param int    $limit
170*32cd2800SGreg Roach     *
171*32cd2800SGreg Roach     * @return Collection|Repository[]
172*32cd2800SGreg Roach     */
173*32cd2800SGreg Roach    public function searchRepositories(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
174*32cd2800SGreg Roach    {
175*32cd2800SGreg Roach        $query = DB::table('other')
176*32cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
177*32cd2800SGreg Roach            ->where('o_type', '=', 'REPO')
178*32cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
179*32cd2800SGreg Roach            ->orderBy('o_id')
180*32cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
181*32cd2800SGreg Roach
182*32cd2800SGreg Roach        $row_mapper = $this->repositoryRowMapper($tree);
183*32cd2800SGreg Roach
184*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
185*32cd2800SGreg Roach    }
186*32cd2800SGreg Roach
187*32cd2800SGreg Roach     /**
188*32cd2800SGreg Roach     * Search for sources by name.
189*32cd2800SGreg Roach     *
190*32cd2800SGreg Roach     * @param Tree   $tree
191*32cd2800SGreg Roach     * @param string $search
192*32cd2800SGreg Roach     * @param int    $offset
193*32cd2800SGreg Roach     * @param int    $limit
194*32cd2800SGreg Roach     *
195*32cd2800SGreg Roach     * @return Collection|Source[]
196*32cd2800SGreg Roach     */
197*32cd2800SGreg Roach    public function searchSourcesByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
198*32cd2800SGreg Roach    {
199*32cd2800SGreg Roach        $query = DB::table('sources')
200*32cd2800SGreg Roach            ->where('s_file', '=', $tree->id())
201*32cd2800SGreg Roach            ->whereContains('s_name', $search)
202*32cd2800SGreg Roach            ->orderBy('s_name')
203*32cd2800SGreg Roach            ->select(['s_id', 's_gedcom']);
204*32cd2800SGreg Roach
205*32cd2800SGreg Roach        $row_mapper = $this->sourceRowMapper($tree);
206*32cd2800SGreg Roach
207*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
208*32cd2800SGreg Roach    }
209*32cd2800SGreg Roach
210*32cd2800SGreg Roach    /**
211*32cd2800SGreg Roach     * Search for submitters.
212*32cd2800SGreg Roach     *
213*32cd2800SGreg Roach     * @param Tree   $tree
214*32cd2800SGreg Roach     * @param string $search
215*32cd2800SGreg Roach     * @param int    $offset
216*32cd2800SGreg Roach     * @param int    $limit
217*32cd2800SGreg Roach     *
218*32cd2800SGreg Roach     * @return Collection|GedcomRecord[]
219*32cd2800SGreg Roach     */
220*32cd2800SGreg Roach    public function searchSubmitters(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
221*32cd2800SGreg Roach    {
222*32cd2800SGreg Roach        $query = DB::table('other')
223*32cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
224*32cd2800SGreg Roach            ->where('o_type', '=', 'SUBM')
225*32cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
226*32cd2800SGreg Roach            ->orderBy('o_id')
227*32cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
228*32cd2800SGreg Roach
229*32cd2800SGreg Roach        $row_mapper = $this->submitterRowMapper($tree);
230*32cd2800SGreg Roach
231*32cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
232*32cd2800SGreg Roach    }
233*32cd2800SGreg Roach
234*32cd2800SGreg Roach    /**
235*32cd2800SGreg Roach     * Paginate a search query.
236*32cd2800SGreg Roach     *
237*32cd2800SGreg Roach     * @param Builder $query      Searches the database for the desired records.
238*32cd2800SGreg Roach     * @param Closure $row_mapper Converts a row from the query into a record.
239*32cd2800SGreg Roach     * @param int     $offset     Skip this many rows.
240*32cd2800SGreg Roach     * @param int     $limit      Take this many rows.
241*32cd2800SGreg Roach     *
242*32cd2800SGreg Roach     * @return Collection
243*32cd2800SGreg Roach     */
244*32cd2800SGreg Roach    private function paginateQuery(Builder $query, Closure $row_mapper, int $offset, int $limit): Collection
245*32cd2800SGreg Roach    {
246*32cd2800SGreg Roach        $collection = new Collection();
247*32cd2800SGreg Roach
248*32cd2800SGreg Roach        foreach ($query->cursor() as $row) {
249*32cd2800SGreg Roach            $record = $row_mapper($row);
250*32cd2800SGreg Roach
251*32cd2800SGreg Roach            if ($record->canShow()) {
252*32cd2800SGreg Roach                if ($offset > 0) {
253*32cd2800SGreg Roach                    $offset--;
254*32cd2800SGreg Roach                } else {
255*32cd2800SGreg Roach                    if ($limit > 0) {
256*32cd2800SGreg Roach                        $collection->push($record);
257*32cd2800SGreg Roach                    }
258*32cd2800SGreg Roach
259*32cd2800SGreg Roach                    $limit--;
260*32cd2800SGreg Roach
261*32cd2800SGreg Roach                    if ($limit === 0) {
262*32cd2800SGreg Roach                        break;
263*32cd2800SGreg Roach                    }
264*32cd2800SGreg Roach                }
265*32cd2800SGreg Roach            }
266*32cd2800SGreg Roach        }
267*32cd2800SGreg Roach
268*32cd2800SGreg Roach        return $collection;
269*32cd2800SGreg Roach    }
270*32cd2800SGreg Roach
271*32cd2800SGreg Roach    /**
272*32cd2800SGreg Roach     * A closure to map a database row to a family.
273*32cd2800SGreg Roach     *
274*32cd2800SGreg Roach     * @param Tree $tree
275*32cd2800SGreg Roach     *
276*32cd2800SGreg Roach     * @return Closure
277*32cd2800SGreg Roach     */
278*32cd2800SGreg Roach    private function familyRowMapper(Tree $tree): Closure
279*32cd2800SGreg Roach    {
280*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): Family {
281*32cd2800SGreg Roach            return Family::getInstance($row->f_id, $tree, $row->f_gedcom);
282*32cd2800SGreg Roach        };
283*32cd2800SGreg Roach    }
284*32cd2800SGreg Roach
285*32cd2800SGreg Roach    /**
286*32cd2800SGreg Roach     * A closure to map a database row to an individual.
287*32cd2800SGreg Roach     *
288*32cd2800SGreg Roach     * @param Tree $tree
289*32cd2800SGreg Roach     *
290*32cd2800SGreg Roach     * @return Closure
291*32cd2800SGreg Roach     */
292*32cd2800SGreg Roach    private function individualRowMapper(Tree $tree): Closure
293*32cd2800SGreg Roach    {
294*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): Individual {
295*32cd2800SGreg Roach            return Individual::getInstance($row->i_id, $tree, $row->i_gedcom);
296*32cd2800SGreg Roach        };
297*32cd2800SGreg Roach    }
298*32cd2800SGreg Roach
299*32cd2800SGreg Roach    /**
300*32cd2800SGreg Roach     * A closure to map a database row to a media object.
301*32cd2800SGreg Roach     *
302*32cd2800SGreg Roach     * @param Tree $tree
303*32cd2800SGreg Roach     *
304*32cd2800SGreg Roach     * @return Closure
305*32cd2800SGreg Roach     */
306*32cd2800SGreg Roach    private function mediaRowMapper(Tree $tree): Closure
307*32cd2800SGreg Roach    {
308*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): Media {
309*32cd2800SGreg Roach            return Media::getInstance($row->m_id, $tree, $row->m_gedcom);
310*32cd2800SGreg Roach        };
311*32cd2800SGreg Roach    }
312*32cd2800SGreg Roach
313*32cd2800SGreg Roach    /**
314*32cd2800SGreg Roach     * A closure to map a database row to a note.
315*32cd2800SGreg Roach     *
316*32cd2800SGreg Roach     * @param Tree $tree
317*32cd2800SGreg Roach     *
318*32cd2800SGreg Roach     * @return Closure
319*32cd2800SGreg Roach     */
320*32cd2800SGreg Roach    private function noteRowMapper(Tree $tree): Closure
321*32cd2800SGreg Roach    {
322*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): GedcomRecord {
323*32cd2800SGreg Roach            return Note::getInstance($row->o_id, $tree, $row->o_gedcom);
324*32cd2800SGreg Roach        };
325*32cd2800SGreg Roach    }
326*32cd2800SGreg Roach
327*32cd2800SGreg Roach    /**
328*32cd2800SGreg Roach     * A closure to map a database row to a repository.
329*32cd2800SGreg Roach     *
330*32cd2800SGreg Roach     * @param Tree $tree
331*32cd2800SGreg Roach     *
332*32cd2800SGreg Roach     * @return Closure
333*32cd2800SGreg Roach     */
334*32cd2800SGreg Roach    private function repositoryRowMapper(Tree $tree): Closure
335*32cd2800SGreg Roach    {
336*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): GedcomRecord {
337*32cd2800SGreg Roach            return Repository::getInstance($row->o_id, $tree, $row->o_gedcom);
338*32cd2800SGreg Roach        };
339*32cd2800SGreg Roach    }
340*32cd2800SGreg Roach
341*32cd2800SGreg Roach    /**
342*32cd2800SGreg Roach     * A closure to map a database row to an source.
343*32cd2800SGreg Roach     *
344*32cd2800SGreg Roach     * @param Tree $tree
345*32cd2800SGreg Roach     *
346*32cd2800SGreg Roach     * @return Closure
347*32cd2800SGreg Roach     */
348*32cd2800SGreg Roach    private function sourceRowMapper(Tree $tree): Closure
349*32cd2800SGreg Roach    {
350*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): Source {
351*32cd2800SGreg Roach            return Source::getInstance($row->s_id, $tree, $row->s_gedcom);
352*32cd2800SGreg Roach        };
353*32cd2800SGreg Roach    }
354*32cd2800SGreg Roach
355*32cd2800SGreg Roach    /**
356*32cd2800SGreg Roach     * A closure to map a database row to a submitter.
357*32cd2800SGreg Roach     *
358*32cd2800SGreg Roach     * @param Tree $tree
359*32cd2800SGreg Roach     *
360*32cd2800SGreg Roach     * @return Closure
361*32cd2800SGreg Roach     */
362*32cd2800SGreg Roach    private function submitterRowMapper(Tree $tree): Closure
363*32cd2800SGreg Roach    {
364*32cd2800SGreg Roach        return function (stdClass $row) use ($tree): GedcomRecord {
365*32cd2800SGreg Roach            return GedcomRecord::getInstance($row->o_id, $tree, $row->o_gedcom);
366*32cd2800SGreg Roach        };
367*32cd2800SGreg Roach    }
368*32cd2800SGreg Roach}
369