xref: /webtrees/app/Services/GedcomExportService.php (revision 36779af1bd0601de7819554b13a393f6edb92507)
169c05a6eSGreg Roach<?php
269c05a6eSGreg Roach
369c05a6eSGreg Roach/**
469c05a6eSGreg Roach * webtrees: online genealogy
589f7189bSGreg Roach * Copyright (C) 2021 webtrees development team
669c05a6eSGreg Roach * This program is free software: you can redistribute it and/or modify
769c05a6eSGreg Roach * it under the terms of the GNU General Public License as published by
869c05a6eSGreg Roach * the Free Software Foundation, either version 3 of the License, or
969c05a6eSGreg Roach * (at your option) any later version.
1069c05a6eSGreg Roach * This program is distributed in the hope that it will be useful,
1169c05a6eSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1269c05a6eSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1369c05a6eSGreg Roach * GNU General Public License for more details.
1469c05a6eSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
1669c05a6eSGreg Roach */
1769c05a6eSGreg Roach
1869c05a6eSGreg Roachdeclare(strict_types=1);
1969c05a6eSGreg Roach
2069c05a6eSGreg Roachnamespace Fisharebest\Webtrees\Services;
2169c05a6eSGreg Roach
2269c05a6eSGreg Roachuse Fisharebest\Webtrees\Auth;
231c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF16BE;
241c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF16LE;
251c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF8;
261c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\Windows1252;
2769c05a6eSGreg Roachuse Fisharebest\Webtrees\Factories\AbstractGedcomRecordFactory;
2869c05a6eSGreg Roachuse Fisharebest\Webtrees\Gedcom;
291c6adce8SGreg Roachuse Fisharebest\Webtrees\GedcomFilters\GedcomEncodingFilter;
3069c05a6eSGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
3169c05a6eSGreg Roachuse Fisharebest\Webtrees\Header;
321c6adce8SGreg Roachuse Fisharebest\Webtrees\Registry;
3369c05a6eSGreg Roachuse Fisharebest\Webtrees\Tree;
3469c05a6eSGreg Roachuse Fisharebest\Webtrees\Webtrees;
3569c05a6eSGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
3669c05a6eSGreg Roachuse Illuminate\Database\Query\Builder;
3769c05a6eSGreg Roachuse Illuminate\Database\Query\Expression;
3869c05a6eSGreg Roachuse Illuminate\Support\Collection;
39783b32e3SGreg Roachuse RuntimeException;
4069c05a6eSGreg Roach
4169c05a6eSGreg Roachuse function date;
4269c05a6eSGreg Roachuse function explode;
43ea517a3bSGreg Roachuse function fopen;
4469c05a6eSGreg Roachuse function fwrite;
4569c05a6eSGreg Roachuse function pathinfo;
46ea517a3bSGreg Roachuse function rewind;
47dec352c1SGreg Roachuse function str_contains;
48dec352c1SGreg Roachuse function str_starts_with;
491c6adce8SGreg Roachuse function stream_filter_append;
50783b32e3SGreg Roachuse function strlen;
5169c05a6eSGreg Roachuse function strpos;
5269c05a6eSGreg Roachuse function strtolower;
5369c05a6eSGreg Roachuse function strtoupper;
5469c05a6eSGreg Roach
5569c05a6eSGreg Roachuse const PATHINFO_EXTENSION;
561c6adce8SGreg Roachuse const STREAM_FILTER_WRITE;
5769c05a6eSGreg Roach
5869c05a6eSGreg Roach/**
5969c05a6eSGreg Roach * Export data in GEDCOM format
6069c05a6eSGreg Roach */
6169c05a6eSGreg Roachclass GedcomExportService
6269c05a6eSGreg Roach{
6369c05a6eSGreg Roach    /**
6469c05a6eSGreg Roach     * Write GEDCOM data to a stream.
6569c05a6eSGreg Roach     *
6669c05a6eSGreg Roach     * @param Tree                        $tree         - Export data from this tree
6769c05a6eSGreg Roach     * @param bool                        $sort_by_xref - Write GEDCOM records in XREF order
6869c05a6eSGreg Roach     * @param string                      $encoding     - Convert from UTF-8 to other encoding
6969c05a6eSGreg Roach     * @param int                         $access_level - Apply privacy filtering
7069c05a6eSGreg Roach     * @param string                      $media_path   - Prepend path to media filenames
711c6adce8SGreg Roach     * @param string                      $line_endings - CRLF or LF
72*36779af1SGreg Roach     * @param Collection<int,string>|null $records      - Just export these records
73ea517a3bSGreg Roach     *
74ea517a3bSGreg Roach     * @return resource
7569c05a6eSGreg Roach     */
7669c05a6eSGreg Roach    public function export(
7769c05a6eSGreg Roach        Tree $tree,
7869c05a6eSGreg Roach        bool $sort_by_xref = false,
791c6adce8SGreg Roach        string $encoding = UTF8::NAME,
8069c05a6eSGreg Roach        int $access_level = Auth::PRIV_HIDE,
8169c05a6eSGreg Roach        string $media_path = '',
821c6adce8SGreg Roach        string $line_endings = 'CRLF',
8369c05a6eSGreg Roach        Collection $records = null
84ea517a3bSGreg Roach    ) {
85ea517a3bSGreg Roach        $stream = fopen('php://memory', 'wb+');
86ea517a3bSGreg Roach
87ea517a3bSGreg Roach        if ($stream === false) {
88ea517a3bSGreg Roach            throw new RuntimeException('Failed to create temporary stream');
89ea517a3bSGreg Roach        }
90ea517a3bSGreg Roach
911c6adce8SGreg Roach        stream_filter_append($stream, GedcomEncodingFilter::class, STREAM_FILTER_WRITE, ['src_encoding' => UTF8::NAME, 'dst_encoding' => $encoding]);
921c6adce8SGreg Roach
9369c05a6eSGreg Roach        if ($records instanceof Collection) {
9469c05a6eSGreg Roach            // Export just these records - e.g. from clippings cart.
9569c05a6eSGreg Roach            $data = [
9669c05a6eSGreg Roach                new Collection([$this->createHeader($tree, $encoding, false)]),
9769c05a6eSGreg Roach                $records,
9869c05a6eSGreg Roach                new Collection(['0 TRLR']),
9969c05a6eSGreg Roach            ];
10069c05a6eSGreg Roach        } elseif ($access_level === Auth::PRIV_HIDE) {
10169c05a6eSGreg Roach            // If we will be applying privacy filters, then we will need the GEDCOM record objects.
10269c05a6eSGreg Roach            $data = [
10369c05a6eSGreg Roach                new Collection([$this->createHeader($tree, $encoding, true)]),
10469c05a6eSGreg Roach                $this->individualQuery($tree, $sort_by_xref)->cursor(),
10569c05a6eSGreg Roach                $this->familyQuery($tree, $sort_by_xref)->cursor(),
10669c05a6eSGreg Roach                $this->sourceQuery($tree, $sort_by_xref)->cursor(),
10769c05a6eSGreg Roach                $this->otherQuery($tree, $sort_by_xref)->cursor(),
10869c05a6eSGreg Roach                $this->mediaQuery($tree, $sort_by_xref)->cursor(),
10969c05a6eSGreg Roach                new Collection(['0 TRLR']),
11069c05a6eSGreg Roach            ];
11169c05a6eSGreg Roach        } else {
11269c05a6eSGreg Roach            // Disable the pending changes before creating GEDCOM records.
1136b9cb339SGreg Roach            Registry::cache()->array()->remember(AbstractGedcomRecordFactory::class . $tree->id(), static function (): Collection {
11469c05a6eSGreg Roach                return new Collection();
11569c05a6eSGreg Roach            });
11669c05a6eSGreg Roach
11769c05a6eSGreg Roach            $data = [
11869c05a6eSGreg Roach                new Collection([$this->createHeader($tree, $encoding, true)]),
1196b9cb339SGreg Roach                $this->individualQuery($tree, $sort_by_xref)->get()->map(Registry::individualFactory()->mapper($tree)),
1206b9cb339SGreg Roach                $this->familyQuery($tree, $sort_by_xref)->get()->map(Registry::familyFactory()->mapper($tree)),
1216b9cb339SGreg Roach                $this->sourceQuery($tree, $sort_by_xref)->get()->map(Registry::sourceFactory()->mapper($tree)),
1226b9cb339SGreg Roach                $this->otherQuery($tree, $sort_by_xref)->get()->map(Registry::gedcomRecordFactory()->mapper($tree)),
1236b9cb339SGreg Roach                $this->mediaQuery($tree, $sort_by_xref)->get()->map(Registry::mediaFactory()->mapper($tree)),
12469c05a6eSGreg Roach                new Collection(['0 TRLR']),
12569c05a6eSGreg Roach            ];
12669c05a6eSGreg Roach        }
12769c05a6eSGreg Roach
12869c05a6eSGreg Roach        foreach ($data as $rows) {
12969c05a6eSGreg Roach            foreach ($rows as $datum) {
13069c05a6eSGreg Roach                if (is_string($datum)) {
13169c05a6eSGreg Roach                    $gedcom = $datum;
13269c05a6eSGreg Roach                } elseif ($datum instanceof GedcomRecord) {
13369c05a6eSGreg Roach                    $gedcom = $datum->privatizeGedcom($access_level);
13469c05a6eSGreg Roach                } else {
135813bb733SGreg Roach                    $gedcom =
136813bb733SGreg Roach                        $datum->i_gedcom ??
137813bb733SGreg Roach                        $datum->f_gedcom ??
138813bb733SGreg Roach                        $datum->s_gedcom ??
139813bb733SGreg Roach                        $datum->m_gedcom ??
140813bb733SGreg Roach                        $datum->o_gedcom;
14169c05a6eSGreg Roach                }
14269c05a6eSGreg Roach
14369c05a6eSGreg Roach                if ($media_path !== '') {
14469c05a6eSGreg Roach                    $gedcom = $this->convertMediaPath($gedcom, $media_path);
14569c05a6eSGreg Roach                }
14669c05a6eSGreg Roach
1471c6adce8SGreg Roach                $gedcom = $this->wrapLongLines($gedcom, Gedcom::LINE_LENGTH) . "\n";
1481c6adce8SGreg Roach
1491c6adce8SGreg Roach                if ($line_endings === 'CRLF') {
1501c6adce8SGreg Roach                    $gedcom = strtr($gedcom, ["\n" => "\r\n"]);
1511c6adce8SGreg Roach                }
15269c05a6eSGreg Roach
153783b32e3SGreg Roach                $bytes_written = fwrite($stream, $gedcom);
154783b32e3SGreg Roach
155783b32e3SGreg Roach                if ($bytes_written !== strlen($gedcom)) {
156783b32e3SGreg Roach                    throw new RuntimeException('Unable to write to stream.  Perhaps the disk is full?');
157783b32e3SGreg Roach                }
15869c05a6eSGreg Roach            }
15969c05a6eSGreg Roach        }
160ea517a3bSGreg Roach
161ea517a3bSGreg Roach        if (rewind($stream) === false) {
162ea517a3bSGreg Roach            throw new RuntimeException('Cannot rewind temporary stream');
163ea517a3bSGreg Roach        }
164ea517a3bSGreg Roach
165ea517a3bSGreg Roach        return $stream;
16669c05a6eSGreg Roach    }
16769c05a6eSGreg Roach
16869c05a6eSGreg Roach    /**
16969c05a6eSGreg Roach     * Create a header record for a gedcom file.
17069c05a6eSGreg Roach     *
17169c05a6eSGreg Roach     * @param Tree   $tree
17269c05a6eSGreg Roach     * @param string $encoding
17369c05a6eSGreg Roach     * @param bool   $include_sub
17469c05a6eSGreg Roach     *
17569c05a6eSGreg Roach     * @return string
17669c05a6eSGreg Roach     */
17769c05a6eSGreg Roach    public function createHeader(Tree $tree, string $encoding, bool $include_sub): string
17869c05a6eSGreg Roach    {
17969c05a6eSGreg Roach        // Force a ".ged" suffix
18069c05a6eSGreg Roach        $filename = $tree->name();
18169c05a6eSGreg Roach
18269c05a6eSGreg Roach        if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') {
18369c05a6eSGreg Roach            $filename .= '.ged';
18469c05a6eSGreg Roach        }
18569c05a6eSGreg Roach
1861c6adce8SGreg Roach        $gedcom_encodings = [
1871c6adce8SGreg Roach            UTF16BE::NAME     => 'UNICODE',
1881c6adce8SGreg Roach            UTF16LE::NAME     => 'UNICODE',
1891c6adce8SGreg Roach            Windows1252::NAME => 'ANSI',
1901c6adce8SGreg Roach        ];
1911c6adce8SGreg Roach
1921c6adce8SGreg Roach        $encoding = $gedcom_encodings[$encoding] ?? $encoding;
1931c6adce8SGreg Roach
19469c05a6eSGreg Roach        // Build a new header record
19569c05a6eSGreg Roach        $gedcom = '0 HEAD';
19669c05a6eSGreg Roach        $gedcom .= "\n1 SOUR " . Webtrees::NAME;
19769c05a6eSGreg Roach        $gedcom .= "\n2 NAME " . Webtrees::NAME;
19869c05a6eSGreg Roach        $gedcom .= "\n2 VERS " . Webtrees::VERSION;
19969c05a6eSGreg Roach        $gedcom .= "\n1 DEST DISKETTE";
20069c05a6eSGreg Roach        $gedcom .= "\n1 DATE " . strtoupper(date('d M Y'));
20169c05a6eSGreg Roach        $gedcom .= "\n2 TIME " . date('H:i:s');
20288a91440SGreg Roach        $gedcom .= "\n1 GEDC\n2 VERS 5.5.1\n2 FORM LINEAGE-LINKED";
20369c05a6eSGreg Roach        $gedcom .= "\n1 CHAR " . $encoding;
20469c05a6eSGreg Roach        $gedcom .= "\n1 FILE " . $filename;
20569c05a6eSGreg Roach
20669c05a6eSGreg Roach        // Preserve some values from the original header
2076b9cb339SGreg Roach        $header = Registry::headerFactory()->make('HEAD', $tree) ?? Registry::headerFactory()->new('HEAD', '0 HEAD', null, $tree);
20869c05a6eSGreg Roach
20969c05a6eSGreg Roach        foreach ($header->facts(['COPR', 'LANG', 'PLAC', 'NOTE']) as $fact) {
21069c05a6eSGreg Roach            $gedcom .= "\n" . $fact->gedcom();
21169c05a6eSGreg Roach        }
21269c05a6eSGreg Roach
21369c05a6eSGreg Roach        if ($include_sub) {
21469c05a6eSGreg Roach            foreach ($header->facts(['SUBM', 'SUBN']) as $fact) {
21569c05a6eSGreg Roach                $gedcom .= "\n" . $fact->gedcom();
21669c05a6eSGreg Roach            }
21769c05a6eSGreg Roach        }
21869c05a6eSGreg Roach
21969c05a6eSGreg Roach        return $gedcom;
22069c05a6eSGreg Roach    }
22169c05a6eSGreg Roach
22269c05a6eSGreg Roach    /**
22369c05a6eSGreg Roach     * Prepend a media path, such as might have been removed during import.
22469c05a6eSGreg Roach     *
22569c05a6eSGreg Roach     * @param string $gedcom
22669c05a6eSGreg Roach     * @param string $media_path
22769c05a6eSGreg Roach     *
22869c05a6eSGreg Roach     * @return string
22969c05a6eSGreg Roach     */
23069c05a6eSGreg Roach    private function convertMediaPath(string $gedcom, string $media_path): string
23169c05a6eSGreg Roach    {
23269c05a6eSGreg Roach        if (preg_match('/^0 @[^@]+@ OBJE/', $gedcom)) {
23369c05a6eSGreg Roach            return preg_replace_callback('/\n1 FILE (.+)/', static function (array $match) use ($media_path): string {
23469c05a6eSGreg Roach                $filename = $match[1];
23569c05a6eSGreg Roach
236290708e9SGreg Roach                // Don’t modify external links
237290708e9SGreg Roach                if (!str_contains($filename, '://')) {
23869c05a6eSGreg Roach                    // Convert separators to match new path.
239dec352c1SGreg Roach                    if (str_contains($media_path, '\\')) {
24069c05a6eSGreg Roach                        $filename = strtr($filename, ['/' => '\\']);
24169c05a6eSGreg Roach                    }
24269c05a6eSGreg Roach
243dec352c1SGreg Roach                    if (!str_starts_with($filename, $media_path)) {
244290708e9SGreg Roach                        $filename = $media_path . $filename;
245290708e9SGreg Roach                    }
24669c05a6eSGreg Roach                }
24769c05a6eSGreg Roach
248290708e9SGreg Roach                return "\n1 FILE " . $filename;
24969c05a6eSGreg Roach            }, $gedcom);
25069c05a6eSGreg Roach        }
25169c05a6eSGreg Roach
25269c05a6eSGreg Roach        return $gedcom;
25369c05a6eSGreg Roach    }
25469c05a6eSGreg Roach
25569c05a6eSGreg Roach    /**
25669c05a6eSGreg Roach     * Wrap long lines using concatenation records.
25769c05a6eSGreg Roach     *
25869c05a6eSGreg Roach     * @param string $gedcom
25969c05a6eSGreg Roach     * @param int    $max_line_length
26069c05a6eSGreg Roach     *
26169c05a6eSGreg Roach     * @return string
26269c05a6eSGreg Roach     */
26369c05a6eSGreg Roach    public function wrapLongLines(string $gedcom, int $max_line_length): string
26469c05a6eSGreg Roach    {
26569c05a6eSGreg Roach        $lines = [];
26669c05a6eSGreg Roach
26769c05a6eSGreg Roach        foreach (explode("\n", $gedcom) as $line) {
26869c05a6eSGreg Roach            // Split long lines
26969c05a6eSGreg Roach            // The total length of a GEDCOM line, including level number, cross-reference number,
27069c05a6eSGreg Roach            // tag, value, delimiters, and terminator, must not exceed 255 (wide) characters.
27169c05a6eSGreg Roach            if (mb_strlen($line) > $max_line_length) {
27269c05a6eSGreg Roach                [$level, $tag] = explode(' ', $line, 3);
27369c05a6eSGreg Roach                if ($tag !== 'CONT') {
27469c05a6eSGreg Roach                    $level++;
27569c05a6eSGreg Roach                }
27669c05a6eSGreg Roach                do {
27769c05a6eSGreg Roach                    // Split after $pos chars
27869c05a6eSGreg Roach                    $pos = $max_line_length;
27969c05a6eSGreg Roach                    // Split on a non-space (standard gedcom behavior)
28069c05a6eSGreg Roach                    while (mb_substr($line, $pos - 1, 1) === ' ') {
28169c05a6eSGreg Roach                        --$pos;
28269c05a6eSGreg Roach                    }
28369c05a6eSGreg Roach                    if ($pos === strpos($line, ' ', 3)) {
28469c05a6eSGreg Roach                        // No non-spaces in the data! Can’t split it :-(
28569c05a6eSGreg Roach                        break;
28669c05a6eSGreg Roach                    }
28769c05a6eSGreg Roach                    $lines[] = mb_substr($line, 0, $pos);
28869c05a6eSGreg Roach                    $line    = $level . ' CONC ' . mb_substr($line, $pos);
28969c05a6eSGreg Roach                } while (mb_strlen($line) > $max_line_length);
29069c05a6eSGreg Roach            }
29169c05a6eSGreg Roach            $lines[] = $line;
29269c05a6eSGreg Roach        }
29369c05a6eSGreg Roach
2941c6adce8SGreg Roach        return implode("\n", $lines);
29569c05a6eSGreg Roach    }
29669c05a6eSGreg Roach
29769c05a6eSGreg Roach    /**
29869c05a6eSGreg Roach     * @param Tree $tree
29969c05a6eSGreg Roach     * @param bool $sort_by_xref
30069c05a6eSGreg Roach     *
30169c05a6eSGreg Roach     * @return Builder
30269c05a6eSGreg Roach     */
30369c05a6eSGreg Roach    private function familyQuery(Tree $tree, bool $sort_by_xref): Builder
30469c05a6eSGreg Roach    {
30569c05a6eSGreg Roach        $query = DB::table('families')
30669c05a6eSGreg Roach            ->where('f_file', '=', $tree->id())
307813bb733SGreg Roach            ->select(['f_gedcom', 'f_id']);
30869c05a6eSGreg Roach
30969c05a6eSGreg Roach
31069c05a6eSGreg Roach        if ($sort_by_xref) {
31169c05a6eSGreg Roach            $query
31269c05a6eSGreg Roach                ->orderBy(new Expression('LENGTH(f_id)'))
31369c05a6eSGreg Roach                ->orderBy('f_id');
31469c05a6eSGreg Roach        }
31569c05a6eSGreg Roach
31669c05a6eSGreg Roach        return $query;
31769c05a6eSGreg Roach    }
31869c05a6eSGreg Roach
31969c05a6eSGreg Roach    /**
32069c05a6eSGreg Roach     * @param Tree $tree
32169c05a6eSGreg Roach     * @param bool $sort_by_xref
32269c05a6eSGreg Roach     *
32369c05a6eSGreg Roach     * @return Builder
32469c05a6eSGreg Roach     */
32569c05a6eSGreg Roach    private function individualQuery(Tree $tree, bool $sort_by_xref): Builder
32669c05a6eSGreg Roach    {
32769c05a6eSGreg Roach        $query = DB::table('individuals')
32869c05a6eSGreg Roach            ->where('i_file', '=', $tree->id())
329813bb733SGreg Roach            ->select(['i_gedcom', 'i_id']);
33069c05a6eSGreg Roach
33169c05a6eSGreg Roach        if ($sort_by_xref) {
33269c05a6eSGreg Roach            $query
33369c05a6eSGreg Roach                ->orderBy(new Expression('LENGTH(i_id)'))
33469c05a6eSGreg Roach                ->orderBy('i_id');
33569c05a6eSGreg Roach        }
33669c05a6eSGreg Roach
33769c05a6eSGreg Roach        return $query;
33869c05a6eSGreg Roach    }
33969c05a6eSGreg Roach
34069c05a6eSGreg Roach    /**
34169c05a6eSGreg Roach     * @param Tree $tree
34269c05a6eSGreg Roach     * @param bool $sort_by_xref
34369c05a6eSGreg Roach     *
34469c05a6eSGreg Roach     * @return Builder
34569c05a6eSGreg Roach     */
34669c05a6eSGreg Roach    private function sourceQuery(Tree $tree, bool $sort_by_xref): Builder
34769c05a6eSGreg Roach    {
34869c05a6eSGreg Roach        $query = DB::table('sources')
34969c05a6eSGreg Roach            ->where('s_file', '=', $tree->id())
350813bb733SGreg Roach            ->select(['s_gedcom', 's_id']);
35169c05a6eSGreg Roach
35269c05a6eSGreg Roach        if ($sort_by_xref) {
35369c05a6eSGreg Roach            $query
35469c05a6eSGreg Roach                ->orderBy(new Expression('LENGTH(s_id)'))
35569c05a6eSGreg Roach                ->orderBy('s_id');
35669c05a6eSGreg Roach        }
35769c05a6eSGreg Roach
35869c05a6eSGreg Roach        return $query;
35969c05a6eSGreg Roach    }
36069c05a6eSGreg Roach
36169c05a6eSGreg Roach    /**
36269c05a6eSGreg Roach     * @param Tree $tree
36369c05a6eSGreg Roach     * @param bool $sort_by_xref
36469c05a6eSGreg Roach     *
36569c05a6eSGreg Roach     * @return Builder
36669c05a6eSGreg Roach     */
36769c05a6eSGreg Roach    private function mediaQuery(Tree $tree, bool $sort_by_xref): Builder
36869c05a6eSGreg Roach    {
36969c05a6eSGreg Roach        $query = DB::table('media')
37069c05a6eSGreg Roach            ->where('m_file', '=', $tree->id())
371813bb733SGreg Roach            ->select(['m_gedcom', 'm_id']);
37269c05a6eSGreg Roach
37369c05a6eSGreg Roach        if ($sort_by_xref) {
37469c05a6eSGreg Roach            $query
37569c05a6eSGreg Roach                ->orderBy(new Expression('LENGTH(m_id)'))
37669c05a6eSGreg Roach                ->orderBy('m_id');
37769c05a6eSGreg Roach        }
37869c05a6eSGreg Roach
37969c05a6eSGreg Roach        return $query;
38069c05a6eSGreg Roach    }
38169c05a6eSGreg Roach
38269c05a6eSGreg Roach    /**
38369c05a6eSGreg Roach     * @param Tree $tree
38469c05a6eSGreg Roach     * @param bool $sort_by_xref
38569c05a6eSGreg Roach     *
38669c05a6eSGreg Roach     * @return Builder
38769c05a6eSGreg Roach     */
38869c05a6eSGreg Roach    private function otherQuery(Tree $tree, bool $sort_by_xref): Builder
38969c05a6eSGreg Roach    {
39069c05a6eSGreg Roach        $query = DB::table('other')
39169c05a6eSGreg Roach            ->where('o_file', '=', $tree->id())
39269c05a6eSGreg Roach            ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
393813bb733SGreg Roach            ->select(['o_gedcom', 'o_id']);
39469c05a6eSGreg Roach
39569c05a6eSGreg Roach        if ($sort_by_xref) {
39669c05a6eSGreg Roach            $query
39769c05a6eSGreg Roach                ->orderBy('o_type')
39869c05a6eSGreg Roach                ->orderBy(new Expression('LENGTH(o_id)'))
39969c05a6eSGreg Roach                ->orderBy('o_id');
40069c05a6eSGreg Roach        }
40169c05a6eSGreg Roach
40269c05a6eSGreg Roach        return $query;
40369c05a6eSGreg Roach    }
40469c05a6eSGreg Roach}
405