xref: /webtrees/app/Services/SearchService.php (revision e84cf2de36d77a7261bd4ca037be8a19531f842a)
132cd2800SGreg Roach<?php
232cd2800SGreg Roach/**
332cd2800SGreg Roach * webtrees: online genealogy
432cd2800SGreg Roach * Copyright (C) 2019 webtrees development team
532cd2800SGreg Roach * This program is free software: you can redistribute it and/or modify
632cd2800SGreg Roach * it under the terms of the GNU General Public License as published by
732cd2800SGreg Roach * the Free Software Foundation, either version 3 of the License, or
832cd2800SGreg Roach * (at your option) any later version.
932cd2800SGreg Roach * This program is distributed in the hope that it will be useful,
1032cd2800SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1132cd2800SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1232cd2800SGreg Roach * GNU General Public License for more details.
1332cd2800SGreg Roach * You should have received a copy of the GNU General Public License
1432cd2800SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
1532cd2800SGreg Roach */
1632cd2800SGreg Roachdeclare(strict_types=1);
1732cd2800SGreg Roach
1832cd2800SGreg Roachnamespace Fisharebest\Webtrees\Services;
1932cd2800SGreg Roach
2032cd2800SGreg Roachuse Closure;
2132cd2800SGreg Roachuse Fisharebest\Webtrees\Family;
2232cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2332cd2800SGreg Roachuse Fisharebest\Webtrees\Individual;
2432cd2800SGreg Roachuse Fisharebest\Webtrees\Media;
2532cd2800SGreg Roachuse Fisharebest\Webtrees\Note;
26b68caec6SGreg Roachuse Fisharebest\Webtrees\Place;
2732cd2800SGreg Roachuse Fisharebest\Webtrees\Repository;
2832cd2800SGreg Roachuse Fisharebest\Webtrees\Source;
2932cd2800SGreg Roachuse Fisharebest\Webtrees\Tree;
3032cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
3132cd2800SGreg Roachuse Illuminate\Database\Query\Builder;
3232cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause;
3332cd2800SGreg Roachuse Illuminate\Support\Collection;
34b68caec6SGreg Roachuse function method_exists;
3532cd2800SGreg Roachuse stdClass;
3632cd2800SGreg Roach
3732cd2800SGreg Roach/**
3832cd2800SGreg Roach * Search trees for genealogy records.
3932cd2800SGreg Roach */
4032cd2800SGreg Roachclass SearchService
4132cd2800SGreg Roach{
4232cd2800SGreg Roach    /**
4332cd2800SGreg Roach     * Search for families by name.
4432cd2800SGreg Roach     *
4532cd2800SGreg Roach     * @param Tree   $tree
4632cd2800SGreg Roach     * @param string $search
4732cd2800SGreg Roach     * @param int    $offset
4832cd2800SGreg Roach     * @param int    $limit
4932cd2800SGreg Roach     *
5032cd2800SGreg Roach     * @return Collection|Family[]
5132cd2800SGreg Roach     */
5232cd2800SGreg Roach    public function searchFamiliesByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
5332cd2800SGreg Roach    {
5432cd2800SGreg Roach        $prefix = DB::connection()->getTablePrefix();
5532cd2800SGreg Roach
5632cd2800SGreg Roach        $query = DB::table('families')
5732cd2800SGreg Roach            ->where('f_file', '=', $tree->id())
5832cd2800SGreg Roach            ->join('name AS husb_name', function (JoinClause $join) use ($search): void {
5932cd2800SGreg Roach                $join
6032cd2800SGreg Roach                    ->on('husb_name.n_file', '=', 'families.f_file')
6132cd2800SGreg Roach                    ->on('husb_name.n_id', '=', 'families.f_husb')
6232cd2800SGreg Roach                    ->where('husb_name.n_type', '<>', '_MARNM');
6332cd2800SGreg Roach            })
6432cd2800SGreg Roach            ->join('name AS wife_name', function (JoinClause $join) use ($search): void {
6532cd2800SGreg Roach                $join
6632cd2800SGreg Roach                    ->on('wife_name.n_file', '=', 'families.f_file')
6732cd2800SGreg Roach                    ->on('wife_name.n_id', '=', 'families.f_wife')
6832cd2800SGreg Roach                    ->where('wife_name.n_type', '<>', '_MARNM');
6932cd2800SGreg Roach            })
7032cd2800SGreg Roach            ->whereContains(DB::raw("CONCAT(" . $prefix . "husb_name.n_full, ' ', " . $prefix . "wife_name.n_full)"), $search)
7132cd2800SGreg Roach            ->orderBy('husb_name.n_sort')
7232cd2800SGreg Roach            ->orderBy('wife_name.n_sort')
7332cd2800SGreg Roach            ->select(['families.f_id', 'families.f_gedcom', 'husb_name.n_sort', 'wife_name.n_sort'])
7432cd2800SGreg Roach            ->distinct();
7532cd2800SGreg Roach
76886b77daSGreg Roach        $row_mapper = Family::rowMapper($tree);
7732cd2800SGreg Roach
7832cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
7932cd2800SGreg Roach    }
8032cd2800SGreg Roach
8132cd2800SGreg Roach    /**
8232cd2800SGreg Roach     * Search for individuals by name.
8332cd2800SGreg Roach     *
8432cd2800SGreg Roach     * @param Tree   $tree
8532cd2800SGreg Roach     * @param string $search
8632cd2800SGreg Roach     * @param int    $offset
8732cd2800SGreg Roach     * @param int    $limit
8832cd2800SGreg Roach     *
8932cd2800SGreg Roach     * @return Collection|Individual[]
9032cd2800SGreg Roach     */
9132cd2800SGreg Roach    public function searchIndividualsByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
9232cd2800SGreg Roach    {
9332cd2800SGreg Roach        $query = DB::table('individuals')
9432cd2800SGreg Roach            ->where('i_file', '=', $tree->id())
9532cd2800SGreg Roach            ->join('name', function (JoinClause $join) use ($search): void {
9632cd2800SGreg Roach                $join
9732cd2800SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
9832cd2800SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id')
9932cd2800SGreg Roach                    ->whereContains('n_full', $search);
10032cd2800SGreg Roach            })
101*e84cf2deSGreg Roach            ->orderBy('n_sort')
102*e84cf2deSGreg Roach            ->select(['individuals.i_id', 'individuals.i_gedcom', 'n_sort', 'n_num'])
10332cd2800SGreg Roach            ->distinct();
10432cd2800SGreg Roach
105886b77daSGreg Roach        $row_mapper = Individual::rowMapper($tree);
10632cd2800SGreg Roach
10732cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
10832cd2800SGreg Roach    }
10932cd2800SGreg Roach
11032cd2800SGreg Roach    /**
11132cd2800SGreg Roach     * Search for media objects.
11232cd2800SGreg Roach     *
11332cd2800SGreg Roach     * @param Tree   $tree
11432cd2800SGreg Roach     * @param string $search
11532cd2800SGreg Roach     * @param int    $offset
11632cd2800SGreg Roach     * @param int    $limit
11732cd2800SGreg Roach     *
11832cd2800SGreg Roach     * @return Collection|Media[]
11932cd2800SGreg Roach     */
12032cd2800SGreg Roach    public function searchMedia(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
12132cd2800SGreg Roach    {
12232cd2800SGreg Roach        $query = DB::table('media')
12332cd2800SGreg Roach            ->where('media.m_file', '=', $tree->id())
12432cd2800SGreg Roach            ->join('media_file', function (JoinClause $join) use ($search): void {
12532cd2800SGreg Roach                $join
12632cd2800SGreg Roach                    ->on('media_file.m_file', '=', 'media.m_file')
12732cd2800SGreg Roach                    ->on('media_file.m_id', '=', 'media.m_id');
12832cd2800SGreg Roach            })
12932cd2800SGreg Roach            ->where(function (Builder $query) use ($search): void {
13032cd2800SGreg Roach                $query
13132cd2800SGreg Roach                    ->whereContains('multimedia_file_refn', $search)
13232cd2800SGreg Roach                    ->whereContains('descriptive_title', $search, 'or');
13332cd2800SGreg Roach            })
13432cd2800SGreg Roach            ->select(['media.m_id', 'media.m_gedcom'])
13532cd2800SGreg Roach            ->distinct();
13632cd2800SGreg Roach
137886b77daSGreg Roach        $row_mapper = Media::rowMapper($tree);
13832cd2800SGreg Roach
13932cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
14032cd2800SGreg Roach    }
14132cd2800SGreg Roach
14232cd2800SGreg Roach    /**
14332cd2800SGreg Roach     * Search for notes.
14432cd2800SGreg Roach     *
14532cd2800SGreg Roach     * @param Tree   $tree
14632cd2800SGreg Roach     * @param string $search
14732cd2800SGreg Roach     * @param int    $offset
14832cd2800SGreg Roach     * @param int    $limit
14932cd2800SGreg Roach     *
15032cd2800SGreg Roach     * @return Collection|Note[]
15132cd2800SGreg Roach     */
15232cd2800SGreg Roach    public function searchNotes(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
15332cd2800SGreg Roach    {
15432cd2800SGreg Roach        $query = DB::table('other')
15532cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
15632cd2800SGreg Roach            ->where('o_type', '=', 'NOTE')
15732cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
15832cd2800SGreg Roach            ->orderBy('o_id')
15932cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
16032cd2800SGreg Roach
161886b77daSGreg Roach        $row_mapper = Note::rowMapper($tree);
16232cd2800SGreg Roach
16332cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
16432cd2800SGreg Roach    }
16532cd2800SGreg Roach
16632cd2800SGreg Roach    /**
16732cd2800SGreg Roach     * Search for repositories.
16832cd2800SGreg Roach     *
16932cd2800SGreg Roach     * @param Tree   $tree
17032cd2800SGreg Roach     * @param string $search
17132cd2800SGreg Roach     * @param int    $offset
17232cd2800SGreg Roach     * @param int    $limit
17332cd2800SGreg Roach     *
17432cd2800SGreg Roach     * @return Collection|Repository[]
17532cd2800SGreg Roach     */
17632cd2800SGreg Roach    public function searchRepositories(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
17732cd2800SGreg Roach    {
17832cd2800SGreg Roach        $query = DB::table('other')
17932cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
18032cd2800SGreg Roach            ->where('o_type', '=', 'REPO')
18132cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
18232cd2800SGreg Roach            ->orderBy('o_id')
18332cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
18432cd2800SGreg Roach
185886b77daSGreg Roach        $row_mapper = Repository::rowMapper($tree);
18632cd2800SGreg Roach
18732cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
18832cd2800SGreg Roach    }
18932cd2800SGreg Roach
19032cd2800SGreg Roach    /**
19132cd2800SGreg Roach    * Search for sources by name.
19232cd2800SGreg Roach    *
19332cd2800SGreg Roach    * @param Tree   $tree
19432cd2800SGreg Roach    * @param string $search
19532cd2800SGreg Roach    * @param int    $offset
19632cd2800SGreg Roach    * @param int    $limit
19732cd2800SGreg Roach    *
19832cd2800SGreg Roach    * @return Collection|Source[]
19932cd2800SGreg Roach    */
20032cd2800SGreg Roach    public function searchSourcesByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
20132cd2800SGreg Roach    {
20232cd2800SGreg Roach        $query = DB::table('sources')
20332cd2800SGreg Roach            ->where('s_file', '=', $tree->id())
20432cd2800SGreg Roach            ->whereContains('s_name', $search)
20532cd2800SGreg Roach            ->orderBy('s_name')
20632cd2800SGreg Roach            ->select(['s_id', 's_gedcom']);
20732cd2800SGreg Roach
208886b77daSGreg Roach        $row_mapper = Source::rowMapper($tree);
20932cd2800SGreg Roach
21032cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
21132cd2800SGreg Roach    }
21232cd2800SGreg Roach
21332cd2800SGreg Roach    /**
21432cd2800SGreg Roach     * Search for submitters.
21532cd2800SGreg Roach     *
21632cd2800SGreg Roach     * @param Tree   $tree
21732cd2800SGreg Roach     * @param string $search
21832cd2800SGreg Roach     * @param int    $offset
21932cd2800SGreg Roach     * @param int    $limit
22032cd2800SGreg Roach     *
22132cd2800SGreg Roach     * @return Collection|GedcomRecord[]
22232cd2800SGreg Roach     */
22332cd2800SGreg Roach    public function searchSubmitters(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
22432cd2800SGreg Roach    {
22532cd2800SGreg Roach        $query = DB::table('other')
22632cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
22732cd2800SGreg Roach            ->where('o_type', '=', 'SUBM')
22832cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
22932cd2800SGreg Roach            ->orderBy('o_id')
23032cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
23132cd2800SGreg Roach
232886b77daSGreg Roach        $row_mapper = GedcomRecord::rowMapper($tree);
23332cd2800SGreg Roach
23432cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
23532cd2800SGreg Roach    }
23632cd2800SGreg Roach
23732cd2800SGreg Roach    /**
238b68caec6SGreg Roach     * Search for places.
239b68caec6SGreg Roach     *
240b68caec6SGreg Roach     * @param Tree   $tree
241b68caec6SGreg Roach     * @param string $search
242b68caec6SGreg Roach     * @param int    $offset
243b68caec6SGreg Roach     * @param int    $limit
244b68caec6SGreg Roach     *
245b68caec6SGreg Roach     * @return Collection|Place[]
246b68caec6SGreg Roach     */
247b68caec6SGreg Roach    public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
248b68caec6SGreg Roach    {
249b68caec6SGreg Roach        $query = DB::table('places AS p0')
250b68caec6SGreg Roach            ->where('p0.p_file', '=', $tree->id())
251b68caec6SGreg Roach            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
252b68caec6SGreg Roach            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
253b68caec6SGreg Roach            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
254b68caec6SGreg Roach            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
255b68caec6SGreg Roach            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
256b68caec6SGreg Roach            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
257b68caec6SGreg Roach            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
258b68caec6SGreg Roach            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
259b68caec6SGreg Roach            ->orderBy('p0.p_place')
260b68caec6SGreg Roach            ->orderBy('p1.p_place')
261b68caec6SGreg Roach            ->orderBy('p2.p_place')
262b68caec6SGreg Roach            ->orderBy('p3.p_place')
263b68caec6SGreg Roach            ->orderBy('p4.p_place')
264b68caec6SGreg Roach            ->orderBy('p5.p_place')
265b68caec6SGreg Roach            ->orderBy('p6.p_place')
266b68caec6SGreg Roach            ->orderBy('p7.p_place')
267b68caec6SGreg Roach            ->orderBy('p8.p_place')
268b68caec6SGreg Roach            ->select([
269b68caec6SGreg Roach                'p0.p_place AS place0',
270b68caec6SGreg Roach                'p1.p_place AS place1',
271b68caec6SGreg Roach                'p2.p_place AS place2',
272b68caec6SGreg Roach                'p3.p_place AS place3',
273b68caec6SGreg Roach                'p4.p_place AS place4',
274b68caec6SGreg Roach                'p5.p_place AS place5',
275b68caec6SGreg Roach                'p6.p_place AS place6',
276b68caec6SGreg Roach                'p7.p_place AS place7',
277b68caec6SGreg Roach                'p8.p_place AS place8',
278b68caec6SGreg Roach            ]);
279b68caec6SGreg Roach
280b68caec6SGreg Roach        // Filter each level of the hierarchy.
281b68caec6SGreg Roach        foreach (explode(',', $search, 9) as $level => $string) {
282b68caec6SGreg Roach            $query->whereContains('p' . $level . '.p_place', $string);
283b68caec6SGreg Roach        }
284b68caec6SGreg Roach
285b68caec6SGreg Roach        $row_mapper = function (stdClass $row) use ($tree): Place {
286b68caec6SGreg Roach            $place = implode(', ', array_filter((array) $row));
287b68caec6SGreg Roach
288b68caec6SGreg Roach            return new Place($place, $tree);
289b68caec6SGreg Roach        };
290b68caec6SGreg Roach
291b68caec6SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
292b68caec6SGreg Roach    }
293b68caec6SGreg Roach
294b68caec6SGreg Roach
295b68caec6SGreg Roach    /**
29632cd2800SGreg Roach     * Paginate a search query.
29732cd2800SGreg Roach     *
29832cd2800SGreg Roach     * @param Builder $query      Searches the database for the desired records.
29932cd2800SGreg Roach     * @param Closure $row_mapper Converts a row from the query into a record.
30032cd2800SGreg Roach     * @param int     $offset     Skip this many rows.
30132cd2800SGreg Roach     * @param int     $limit      Take this many rows.
30232cd2800SGreg Roach     *
30332cd2800SGreg Roach     * @return Collection
30432cd2800SGreg Roach     */
30532cd2800SGreg Roach    private function paginateQuery(Builder $query, Closure $row_mapper, int $offset, int $limit): Collection
30632cd2800SGreg Roach    {
30732cd2800SGreg Roach        $collection = new Collection();
30832cd2800SGreg Roach
30932cd2800SGreg Roach        foreach ($query->cursor() as $row) {
31032cd2800SGreg Roach            $record = $row_mapper($row);
31132cd2800SGreg Roach
312b68caec6SGreg Roach            // If the object has a method "canShow()", then use it to filter for privacy.
313b68caec6SGreg Roach            if (!method_exists($record, 'canShow') || $record->canShow()) {
31432cd2800SGreg Roach                if ($offset > 0) {
31532cd2800SGreg Roach                    $offset--;
31632cd2800SGreg Roach                } else {
31732cd2800SGreg Roach                    if ($limit > 0) {
31832cd2800SGreg Roach                        $collection->push($record);
31932cd2800SGreg Roach                    }
32032cd2800SGreg Roach
32132cd2800SGreg Roach                    $limit--;
32232cd2800SGreg Roach
32332cd2800SGreg Roach                    if ($limit === 0) {
32432cd2800SGreg Roach                        break;
32532cd2800SGreg Roach                    }
32632cd2800SGreg Roach                }
32732cd2800SGreg Roach            }
32832cd2800SGreg Roach        }
32932cd2800SGreg Roach
33032cd2800SGreg Roach        return $collection;
33132cd2800SGreg Roach    }
33232cd2800SGreg Roach}
333