xref: /webtrees/app/Services/SearchService.php (revision b68caec60900b4d11ce1bad44ffcce337629d086)
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;
21*b68caec6SGreg Roachuse Fisharebest\Webtrees\Database;
2232cd2800SGreg Roachuse Fisharebest\Webtrees\Family;
2332cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
24*b68caec6SGreg Roachuse Fisharebest\Webtrees\I18N;
2532cd2800SGreg Roachuse Fisharebest\Webtrees\Individual;
2632cd2800SGreg Roachuse Fisharebest\Webtrees\Media;
2732cd2800SGreg Roachuse Fisharebest\Webtrees\Note;
28*b68caec6SGreg Roachuse Fisharebest\Webtrees\Place;
2932cd2800SGreg Roachuse Fisharebest\Webtrees\Repository;
3032cd2800SGreg Roachuse Fisharebest\Webtrees\Source;
3132cd2800SGreg Roachuse Fisharebest\Webtrees\Tree;
3232cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
3332cd2800SGreg Roachuse Illuminate\Database\Query\Builder;
3432cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause;
3532cd2800SGreg Roachuse Illuminate\Support\Collection;
36*b68caec6SGreg Roachuse function method_exists;
3732cd2800SGreg Roachuse stdClass;
3832cd2800SGreg Roach
3932cd2800SGreg Roach/**
4032cd2800SGreg Roach * Search trees for genealogy records.
4132cd2800SGreg Roach */
4232cd2800SGreg Roachclass SearchService
4332cd2800SGreg Roach{
4432cd2800SGreg Roach    /**
4532cd2800SGreg Roach     * Search for families by name.
4632cd2800SGreg Roach     *
4732cd2800SGreg Roach     * @param Tree   $tree
4832cd2800SGreg Roach     * @param string $search
4932cd2800SGreg Roach     * @param int    $offset
5032cd2800SGreg Roach     * @param int    $limit
5132cd2800SGreg Roach     *
5232cd2800SGreg Roach     * @return Collection|Family[]
5332cd2800SGreg Roach     */
5432cd2800SGreg Roach    public function searchFamiliesByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
5532cd2800SGreg Roach    {
5632cd2800SGreg Roach        $prefix = DB::connection()->getTablePrefix();
5732cd2800SGreg Roach
5832cd2800SGreg Roach        $query = DB::table('families')
5932cd2800SGreg Roach            ->where('f_file', '=', $tree->id())
6032cd2800SGreg Roach            ->join('name AS husb_name', function (JoinClause $join) use ($search): void {
6132cd2800SGreg Roach                $join
6232cd2800SGreg Roach                    ->on('husb_name.n_file', '=', 'families.f_file')
6332cd2800SGreg Roach                    ->on('husb_name.n_id', '=', 'families.f_husb')
6432cd2800SGreg Roach                    ->where('husb_name.n_type', '<>', '_MARNM');
6532cd2800SGreg Roach            })
6632cd2800SGreg Roach            ->join('name AS wife_name', function (JoinClause $join) use ($search): void {
6732cd2800SGreg Roach                $join
6832cd2800SGreg Roach                    ->on('wife_name.n_file', '=', 'families.f_file')
6932cd2800SGreg Roach                    ->on('wife_name.n_id', '=', 'families.f_wife')
7032cd2800SGreg Roach                    ->where('wife_name.n_type', '<>', '_MARNM');
7132cd2800SGreg Roach            })
7232cd2800SGreg Roach            ->whereContains(DB::raw("CONCAT(" . $prefix . "husb_name.n_full, ' ', " . $prefix . "wife_name.n_full)"), $search)
7332cd2800SGreg Roach            ->orderBy('husb_name.n_sort')
7432cd2800SGreg Roach            ->orderBy('wife_name.n_sort')
7532cd2800SGreg Roach            ->select(['families.f_id', 'families.f_gedcom', 'husb_name.n_sort', 'wife_name.n_sort'])
7632cd2800SGreg Roach            ->distinct();
7732cd2800SGreg Roach
78886b77daSGreg Roach        $row_mapper = Family::rowMapper($tree);
7932cd2800SGreg Roach
8032cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
8132cd2800SGreg Roach    }
8232cd2800SGreg Roach
8332cd2800SGreg Roach    /**
8432cd2800SGreg Roach     * Search for individuals by name.
8532cd2800SGreg Roach     *
8632cd2800SGreg Roach     * @param Tree   $tree
8732cd2800SGreg Roach     * @param string $search
8832cd2800SGreg Roach     * @param int    $offset
8932cd2800SGreg Roach     * @param int    $limit
9032cd2800SGreg Roach     *
9132cd2800SGreg Roach     * @return Collection|Individual[]
9232cd2800SGreg Roach     */
9332cd2800SGreg Roach    public function searchIndividualsByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
9432cd2800SGreg Roach    {
9532cd2800SGreg Roach        $query = DB::table('individuals')
9632cd2800SGreg Roach            ->where('i_file', '=', $tree->id())
9732cd2800SGreg Roach            ->join('name', function (JoinClause $join) use ($search): void {
9832cd2800SGreg Roach                $join
9932cd2800SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
10032cd2800SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id')
10132cd2800SGreg Roach                    ->whereContains('n_full', $search);
10232cd2800SGreg Roach            })
10332cd2800SGreg Roach            ->select(['individuals.i_id', 'individuals.i_gedcom', 'n_sort'])
10432cd2800SGreg Roach            ->distinct();
10532cd2800SGreg Roach
106886b77daSGreg Roach        $row_mapper = Individual::rowMapper($tree);
10732cd2800SGreg Roach
10832cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
10932cd2800SGreg Roach    }
11032cd2800SGreg Roach
11132cd2800SGreg Roach    /**
11232cd2800SGreg Roach     * Search for media objects.
11332cd2800SGreg Roach     *
11432cd2800SGreg Roach     * @param Tree   $tree
11532cd2800SGreg Roach     * @param string $search
11632cd2800SGreg Roach     * @param int    $offset
11732cd2800SGreg Roach     * @param int    $limit
11832cd2800SGreg Roach     *
11932cd2800SGreg Roach     * @return Collection|Media[]
12032cd2800SGreg Roach     */
12132cd2800SGreg Roach    public function searchMedia(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
12232cd2800SGreg Roach    {
12332cd2800SGreg Roach        $query = DB::table('media')
12432cd2800SGreg Roach            ->where('media.m_file', '=', $tree->id())
12532cd2800SGreg Roach            ->join('media_file', function (JoinClause $join) use ($search): void {
12632cd2800SGreg Roach                $join
12732cd2800SGreg Roach                    ->on('media_file.m_file', '=', 'media.m_file')
12832cd2800SGreg Roach                    ->on('media_file.m_id', '=', 'media.m_id');
12932cd2800SGreg Roach            })
13032cd2800SGreg Roach            ->where(function (Builder $query) use ($search): void {
13132cd2800SGreg Roach                $query
13232cd2800SGreg Roach                    ->whereContains('multimedia_file_refn', $search)
13332cd2800SGreg Roach                    ->whereContains('descriptive_title', $search, 'or');
13432cd2800SGreg Roach            })
13532cd2800SGreg Roach            ->select(['media.m_id', 'media.m_gedcom'])
13632cd2800SGreg Roach            ->distinct();
13732cd2800SGreg Roach
138886b77daSGreg Roach        $row_mapper = Media::rowMapper($tree);
13932cd2800SGreg Roach
14032cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
14132cd2800SGreg Roach    }
14232cd2800SGreg Roach
14332cd2800SGreg Roach    /**
14432cd2800SGreg Roach     * Search for notes.
14532cd2800SGreg Roach     *
14632cd2800SGreg Roach     * @param Tree   $tree
14732cd2800SGreg Roach     * @param string $search
14832cd2800SGreg Roach     * @param int    $offset
14932cd2800SGreg Roach     * @param int    $limit
15032cd2800SGreg Roach     *
15132cd2800SGreg Roach     * @return Collection|Note[]
15232cd2800SGreg Roach     */
15332cd2800SGreg Roach    public function searchNotes(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
15432cd2800SGreg Roach    {
15532cd2800SGreg Roach        $query = DB::table('other')
15632cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
15732cd2800SGreg Roach            ->where('o_type', '=', 'NOTE')
15832cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
15932cd2800SGreg Roach            ->orderBy('o_id')
16032cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
16132cd2800SGreg Roach
162886b77daSGreg Roach        $row_mapper = Note::rowMapper($tree);
16332cd2800SGreg Roach
16432cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
16532cd2800SGreg Roach    }
16632cd2800SGreg Roach
16732cd2800SGreg Roach    /**
16832cd2800SGreg Roach     * Search for repositories.
16932cd2800SGreg Roach     *
17032cd2800SGreg Roach     * @param Tree   $tree
17132cd2800SGreg Roach     * @param string $search
17232cd2800SGreg Roach     * @param int    $offset
17332cd2800SGreg Roach     * @param int    $limit
17432cd2800SGreg Roach     *
17532cd2800SGreg Roach     * @return Collection|Repository[]
17632cd2800SGreg Roach     */
17732cd2800SGreg Roach    public function searchRepositories(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
17832cd2800SGreg Roach    {
17932cd2800SGreg Roach        $query = DB::table('other')
18032cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
18132cd2800SGreg Roach            ->where('o_type', '=', 'REPO')
18232cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
18332cd2800SGreg Roach            ->orderBy('o_id')
18432cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
18532cd2800SGreg Roach
186886b77daSGreg Roach        $row_mapper = Repository::rowMapper($tree);
18732cd2800SGreg Roach
18832cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
18932cd2800SGreg Roach    }
19032cd2800SGreg Roach
19132cd2800SGreg Roach    /**
19232cd2800SGreg Roach    * Search for sources by name.
19332cd2800SGreg Roach    *
19432cd2800SGreg Roach    * @param Tree   $tree
19532cd2800SGreg Roach    * @param string $search
19632cd2800SGreg Roach    * @param int    $offset
19732cd2800SGreg Roach    * @param int    $limit
19832cd2800SGreg Roach    *
19932cd2800SGreg Roach    * @return Collection|Source[]
20032cd2800SGreg Roach    */
20132cd2800SGreg Roach    public function searchSourcesByName(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
20232cd2800SGreg Roach    {
20332cd2800SGreg Roach        $query = DB::table('sources')
20432cd2800SGreg Roach            ->where('s_file', '=', $tree->id())
20532cd2800SGreg Roach            ->whereContains('s_name', $search)
20632cd2800SGreg Roach            ->orderBy('s_name')
20732cd2800SGreg Roach            ->select(['s_id', 's_gedcom']);
20832cd2800SGreg Roach
209886b77daSGreg Roach        $row_mapper = Source::rowMapper($tree);
21032cd2800SGreg Roach
21132cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
21232cd2800SGreg Roach    }
21332cd2800SGreg Roach
21432cd2800SGreg Roach    /**
21532cd2800SGreg Roach     * Search for submitters.
21632cd2800SGreg Roach     *
21732cd2800SGreg Roach     * @param Tree   $tree
21832cd2800SGreg Roach     * @param string $search
21932cd2800SGreg Roach     * @param int    $offset
22032cd2800SGreg Roach     * @param int    $limit
22132cd2800SGreg Roach     *
22232cd2800SGreg Roach     * @return Collection|GedcomRecord[]
22332cd2800SGreg Roach     */
22432cd2800SGreg Roach    public function searchSubmitters(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
22532cd2800SGreg Roach    {
22632cd2800SGreg Roach        $query = DB::table('other')
22732cd2800SGreg Roach            ->where('o_file', '=', $tree->id())
22832cd2800SGreg Roach            ->where('o_type', '=', 'SUBM')
22932cd2800SGreg Roach            ->whereContains('o_gedcom', $search)
23032cd2800SGreg Roach            ->orderBy('o_id')
23132cd2800SGreg Roach            ->select(['o_id', 'o_gedcom']);
23232cd2800SGreg Roach
233886b77daSGreg Roach        $row_mapper = GedcomRecord::rowMapper($tree);
23432cd2800SGreg Roach
23532cd2800SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
23632cd2800SGreg Roach    }
23732cd2800SGreg Roach
23832cd2800SGreg Roach    /**
239*b68caec6SGreg Roach     * Search for places.
240*b68caec6SGreg Roach     *
241*b68caec6SGreg Roach     * @param Tree   $tree
242*b68caec6SGreg Roach     * @param string $search
243*b68caec6SGreg Roach     * @param int    $offset
244*b68caec6SGreg Roach     * @param int    $limit
245*b68caec6SGreg Roach     *
246*b68caec6SGreg Roach     * @return Collection|Place[]
247*b68caec6SGreg Roach     */
248*b68caec6SGreg Roach    public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
249*b68caec6SGreg Roach    {
250*b68caec6SGreg Roach        $searches = explode(',', $search);
251*b68caec6SGreg Roach
252*b68caec6SGreg Roach        $query = DB::table('places AS p0')
253*b68caec6SGreg Roach            ->where('p0.p_file', '=', $tree->id())
254*b68caec6SGreg Roach            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
255*b68caec6SGreg Roach            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
256*b68caec6SGreg Roach            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
257*b68caec6SGreg Roach            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
258*b68caec6SGreg Roach            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
259*b68caec6SGreg Roach            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
260*b68caec6SGreg Roach            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
261*b68caec6SGreg Roach            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
262*b68caec6SGreg Roach            ->orderBy('p0.p_place')
263*b68caec6SGreg Roach            ->orderBy('p1.p_place')
264*b68caec6SGreg Roach            ->orderBy('p2.p_place')
265*b68caec6SGreg Roach            ->orderBy('p3.p_place')
266*b68caec6SGreg Roach            ->orderBy('p4.p_place')
267*b68caec6SGreg Roach            ->orderBy('p5.p_place')
268*b68caec6SGreg Roach            ->orderBy('p6.p_place')
269*b68caec6SGreg Roach            ->orderBy('p7.p_place')
270*b68caec6SGreg Roach            ->orderBy('p8.p_place')
271*b68caec6SGreg Roach            ->select([
272*b68caec6SGreg Roach                'p0.p_place AS place0',
273*b68caec6SGreg Roach                'p1.p_place AS place1',
274*b68caec6SGreg Roach                'p2.p_place AS place2',
275*b68caec6SGreg Roach                'p3.p_place AS place3',
276*b68caec6SGreg Roach                'p4.p_place AS place4',
277*b68caec6SGreg Roach                'p5.p_place AS place5',
278*b68caec6SGreg Roach                'p6.p_place AS place6',
279*b68caec6SGreg Roach                'p7.p_place AS place7',
280*b68caec6SGreg Roach                'p8.p_place AS place8',
281*b68caec6SGreg Roach            ]);
282*b68caec6SGreg Roach
283*b68caec6SGreg Roach        // Filter each level of the hierarchy.
284*b68caec6SGreg Roach        foreach (explode(',', $search, 9) as $level => $string) {
285*b68caec6SGreg Roach            $query->whereContains('p' . $level . '.p_place', $string);
286*b68caec6SGreg Roach        }
287*b68caec6SGreg Roach
288*b68caec6SGreg Roach        $row_mapper = function (stdClass $row) use ($tree): Place {
289*b68caec6SGreg Roach            $place = implode(', ', array_filter((array) $row));
290*b68caec6SGreg Roach
291*b68caec6SGreg Roach            return new Place($place, $tree);
292*b68caec6SGreg Roach        };
293*b68caec6SGreg Roach
294*b68caec6SGreg Roach        return $this->paginateQuery($query, $row_mapper, $offset, $limit);
295*b68caec6SGreg Roach    }
296*b68caec6SGreg Roach
297*b68caec6SGreg Roach
298*b68caec6SGreg Roach    /**
29932cd2800SGreg Roach     * Paginate a search query.
30032cd2800SGreg Roach     *
30132cd2800SGreg Roach     * @param Builder $query      Searches the database for the desired records.
30232cd2800SGreg Roach     * @param Closure $row_mapper Converts a row from the query into a record.
30332cd2800SGreg Roach     * @param int     $offset     Skip this many rows.
30432cd2800SGreg Roach     * @param int     $limit      Take this many rows.
30532cd2800SGreg Roach     *
30632cd2800SGreg Roach     * @return Collection
30732cd2800SGreg Roach     */
30832cd2800SGreg Roach    private function paginateQuery(Builder $query, Closure $row_mapper, int $offset, int $limit): Collection
30932cd2800SGreg Roach    {
31032cd2800SGreg Roach        $collection = new Collection();
31132cd2800SGreg Roach
31232cd2800SGreg Roach        foreach ($query->cursor() as $row) {
31332cd2800SGreg Roach            $record = $row_mapper($row);
31432cd2800SGreg Roach
315*b68caec6SGreg Roach            // If the object has a method "canShow()", then use it to filter for privacy.
316*b68caec6SGreg Roach            if (!method_exists($record, 'canShow') || $record->canShow()) {
31732cd2800SGreg Roach                if ($offset > 0) {
31832cd2800SGreg Roach                    $offset--;
31932cd2800SGreg Roach                } else {
32032cd2800SGreg Roach                    if ($limit > 0) {
32132cd2800SGreg Roach                        $collection->push($record);
32232cd2800SGreg Roach                    }
32332cd2800SGreg Roach
32432cd2800SGreg Roach                    $limit--;
32532cd2800SGreg Roach
32632cd2800SGreg Roach                    if ($limit === 0) {
32732cd2800SGreg Roach                        break;
32832cd2800SGreg Roach                    }
32932cd2800SGreg Roach                }
33032cd2800SGreg Roach            }
33132cd2800SGreg Roach        }
33232cd2800SGreg Roach
33332cd2800SGreg Roach        return $collection;
33432cd2800SGreg Roach    }
33532cd2800SGreg Roach}
336