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