169c05a6eSGreg Roach<?php 269c05a6eSGreg Roach 369c05a6eSGreg Roach/** 469c05a6eSGreg Roach * webtrees: online genealogy 5*5bfc6897SGreg Roach * Copyright (C) 2022 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 7236779af1SGreg 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